stripe payment
parent
f56246a524
commit
e2d6f23676
11
server.ts
11
server.ts
|
@ -23,6 +23,7 @@ import logger, { initLogHandler } from "./src/logger/logger";
|
||||||
import passport from "passport";
|
import passport from "passport";
|
||||||
import rabbitmq from "./src/rabbitmq/rabbitmq";
|
import rabbitmq from "./src/rabbitmq/rabbitmq";
|
||||||
import { GOOGLE_CALLBACK_URL } from "./src/utils/constants";
|
import { GOOGLE_CALLBACK_URL } from "./src/utils/constants";
|
||||||
|
import { StripeWebhook } from "./src/controllers/paymentController";
|
||||||
const app: Express = express();
|
const app: Express = express();
|
||||||
const host = process.env.HOST || "localhost";
|
const host = process.env.HOST || "localhost";
|
||||||
const port = Number(process.env.PORT) || 3000;
|
const port = Number(process.env.PORT) || 3000;
|
||||||
|
@ -110,6 +111,14 @@ app.use(cors());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(useragent.express());
|
app.use(useragent.express());
|
||||||
|
|
||||||
|
// we need to parse the body of the request to get the stripe signature
|
||||||
|
// it is important that the webhook is registered before the body parser
|
||||||
|
// see https://github.com/stripe/stripe-node/issues/341
|
||||||
|
app.use(
|
||||||
|
"/api/v1/webhooks/stripe",
|
||||||
|
bodyParser.raw({ type: "application/json" }),
|
||||||
|
StripeWebhook
|
||||||
|
);
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json());
|
||||||
app.use("/api/v1/calendar", calendarRoutes);
|
app.use("/api/v1/calendar", calendarRoutes);
|
||||||
app.use("/api/v1/payment", paymentRoutes);
|
app.use("/api/v1/payment", paymentRoutes);
|
||||||
|
@ -122,7 +131,7 @@ const specs = swaggerJsDoc(options);
|
||||||
app.use("/api-docs", swaggerUI.serve, swaggerUI.setup(specs));
|
app.use("/api-docs", swaggerUI.serve, swaggerUI.setup(specs));
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
logger.warn(`reqnot found, path: ${req.path}`);
|
logger.warn(`req not found, path: ${req.path}`);
|
||||||
res.status(404).send("not found");
|
res.status(404).send("not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,340 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
|
import logger from "../logger/logger";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
import {
|
||||||
|
ACCOUNT_STATE,
|
||||||
|
DASHBOARD_URL,
|
||||||
|
PAYMENT_PLAN_SETTINGS,
|
||||||
|
} from "../utils/constants";
|
||||||
|
import UserPendingPayment from "../models/userPendingPayment";
|
||||||
|
import User from "../models/user";
|
||||||
|
import { getUserSession } from "../utils/utils";
|
||||||
|
import Store from "../models/store";
|
||||||
|
import Session from "../models/session";
|
||||||
|
|
||||||
|
const ZeitAdlerBasicMonthly = "zeitadler-basic-monthly";
|
||||||
|
const ZeitAdlerBasicYearly = "zeitadler-basic-yearly";
|
||||||
|
const ZeitAdlerPremiumMonthly = "zeitadler-premium-monthly";
|
||||||
|
const ZeitAdlerPremiumYearly = "zeitadler-premium-yearly";
|
||||||
|
|
||||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string);
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string);
|
||||||
|
let cachedPrices = [] as any[];
|
||||||
|
|
||||||
export async function CreatePaymentIntent(req: Request, res: Response) {
|
export async function loadPrices() {
|
||||||
const paymentIntent = await stripe.paymentIntents.create({
|
if (cachedPrices.length > 0) {
|
||||||
amount: 1,
|
return;
|
||||||
currency: "eur",
|
}
|
||||||
|
|
||||||
|
logger.info("Loading prices from Stripe API");
|
||||||
|
|
||||||
|
// load prices from stripe
|
||||||
|
const prices = await stripe.prices.list({
|
||||||
|
lookup_keys: [
|
||||||
|
ZeitAdlerBasicMonthly,
|
||||||
|
ZeitAdlerBasicYearly,
|
||||||
|
ZeitAdlerPremiumMonthly,
|
||||||
|
ZeitAdlerPremiumYearly,
|
||||||
|
],
|
||||||
|
expand: ["data.product"],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({
|
for (const price of prices.data) {
|
||||||
clientSecret: paymentIntent.client_secret,
|
cachedPrices.push({
|
||||||
});
|
id: price.id,
|
||||||
|
unit_amount: price.unit_amount,
|
||||||
|
lookup_key: price.lookup_key,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPriceId(paymentPlan: number, interval: number) {
|
||||||
|
await loadPrices();
|
||||||
|
|
||||||
|
const val = `zeitadler-${PAYMENT_PLAN_SETTINGS[paymentPlan].id}-${
|
||||||
|
interval === 1 ? "yearly" : "monthly"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const found = cachedPrices.find((price) => price.lookup_key === val);
|
||||||
|
|
||||||
|
if (found === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return found.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to get the prices for the frontend
|
||||||
|
export async function GetPrices(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { productId } = req.params;
|
||||||
|
|
||||||
|
if (productId === undefined || (productId !== "1" && productId !== "2")) {
|
||||||
|
return res.status(400).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadPrices();
|
||||||
|
|
||||||
|
let lookupKey: any[] = [];
|
||||||
|
|
||||||
|
if (productId === "1") {
|
||||||
|
lookupKey = [ZeitAdlerBasicMonthly, ZeitAdlerBasicYearly];
|
||||||
|
} else if (productId === "2") {
|
||||||
|
lookupKey = [ZeitAdlerPremiumMonthly, ZeitAdlerPremiumYearly];
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
prices: cachedPrices
|
||||||
|
.filter(
|
||||||
|
(price) =>
|
||||||
|
price.lookup_key === lookupKey[0] ||
|
||||||
|
price.lookup_key === lookupKey[1]
|
||||||
|
)
|
||||||
|
.map((price) => {
|
||||||
|
return {
|
||||||
|
lookup_key: price.lookup_key,
|
||||||
|
unit_amount: price.unit_amount,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function CreateCheckoutSession(priceId: string, userId: string) {
|
||||||
|
try {
|
||||||
|
if (priceId === undefined || userId === undefined) {
|
||||||
|
logger.error("CreateCheckoutSession: invalid request");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedPrices.length === 0) {
|
||||||
|
logger.info("Loading prices from Stripe API");
|
||||||
|
await loadPrices();
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await stripe.checkout.sessions.create({
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price: priceId,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customer_email: "hello@roese.dev",
|
||||||
|
mode: "subscription",
|
||||||
|
success_url: `${DASHBOARD_URL}/checkout/success/{CHECKOUT_SESSION_ID}`,
|
||||||
|
cancel_url: `${DASHBOARD_URL}/checkout/canceled/{CHECKOUT_SESSION_ID}`,
|
||||||
|
metadata: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserPendingPayment.create({
|
||||||
|
payment_session_id: session.id,
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return session.url;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
try {
|
||||||
|
const { sessionId } = req.body;
|
||||||
|
|
||||||
|
logger.info("CheckoutSuccess", sessionId);
|
||||||
|
|
||||||
|
if (sessionId === undefined) {
|
||||||
|
return res.status(400).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPendingPayment = await UserPendingPayment.findOne({
|
||||||
|
where: {
|
||||||
|
payment_session_id: sessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userPendingPayment === null) {
|
||||||
|
logger.info("CheckoutSuccess payment successful");
|
||||||
|
return res.status(200).send({ status: 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("CheckoutSuccess pending payment");
|
||||||
|
|
||||||
|
res.status(200).send({ status: 0 });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function CheckoutCanceled(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { sessionId } = req.body;
|
||||||
|
|
||||||
|
if (sessionId === undefined) {
|
||||||
|
logger.error("CheckoutCanceled: invalid request");
|
||||||
|
return res.status(400).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userPendingPayment = await UserPendingPayment.findOne({
|
||||||
|
where: {
|
||||||
|
payment_session_id: sessionId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userPendingPayment === null) {
|
||||||
|
console.log("CheckoutCanceled not found");
|
||||||
|
return res.status(400).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete user
|
||||||
|
|
||||||
|
await User.destroy({
|
||||||
|
where: {
|
||||||
|
user_id: userPendingPayment.user_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete store
|
||||||
|
|
||||||
|
await Store.destroy({
|
||||||
|
where: {
|
||||||
|
owner_user_id: userPendingPayment.user_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete all user sessions
|
||||||
|
|
||||||
|
await Session.destroy({
|
||||||
|
where: {
|
||||||
|
user_id: userPendingPayment.user_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete user pending payment
|
||||||
|
|
||||||
|
await userPendingPayment.destroy();
|
||||||
|
|
||||||
|
res.status(200).send({ status: 1 });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function StripeWebhook(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
logger.error("StripeWebhook: invalid reques. Method", req.method);
|
||||||
|
return res.status(405).end(); // Method Not Allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = req.body;
|
||||||
|
const sig = req.headers["stripe-signature"] as string;
|
||||||
|
|
||||||
|
let event: Stripe.Event;
|
||||||
|
|
||||||
|
try {
|
||||||
|
event = stripe.webhooks.constructEvent(
|
||||||
|
payload,
|
||||||
|
sig,
|
||||||
|
process.env.STRIPE_WEBHOOK_ENDPOINT_SECRET as string
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Webhook-Fehler", err);
|
||||||
|
return res.status(400).send(`Webhook Error: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
logger.warn("Ignoring unknown event:", event.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(200);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetBillingPortal(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const userSession = await getUserSession(req);
|
||||||
|
|
||||||
|
if (userSession === null) {
|
||||||
|
return res.status(401).send({ err: "unauthorized" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
where: {
|
||||||
|
user_id: userSession.user_id,
|
||||||
|
},
|
||||||
|
attributes: ["stripe_customer_id"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user === null || user.stripe_customer_id === null) {
|
||||||
|
return res.status(400).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: user.stripe_customer_id,
|
||||||
|
return_url: `${DASHBOARD_URL}/payment-plan`,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
url: session.url,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Request, Response } from "express";
|
||||||
import Store from "../models/store";
|
import Store from "../models/store";
|
||||||
import { getUserSession } from "../utils/utils";
|
import { getUserSession } from "../utils/utils";
|
||||||
import { isCompanyNameValid } from "../validator/validator";
|
import { isCompanyNameValid } from "../validator/validator";
|
||||||
import { storeLogger, userLogger } from "../logger/logger";
|
import logger, { storeLogger, userLogger } from "../logger/logger";
|
||||||
|
|
||||||
export async function GetStore(req: Request, res: Response) {
|
export async function GetStore(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
|
@ -46,6 +46,7 @@ export async function GetStore(req: Request, res: Response) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,6 +113,7 @@ export async function UpdateStore(req: Request, res: Response) {
|
||||||
|
|
||||||
res.status(200).send({ msg: "success" });
|
res.status(200).send({ msg: "success" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ export async function GetStoreServices(req: Request, res: Response) {
|
||||||
|
|
||||||
res.status(200).send({ services, users });
|
res.status(200).send({ services, users });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ export async function CreateStoreService(req: Request, res: Response) {
|
||||||
|
|
||||||
res.status(200).send({ service });
|
res.status(200).send({ service });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ export async function UpdateStoreService(req: Request, res: Response) {
|
||||||
|
|
||||||
res.status(200).send({ msg: "success" });
|
res.status(200).send({ msg: "success" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -248,7 +248,7 @@ export async function CreateStoreServiceActivity(req: Request, res: Response) {
|
||||||
|
|
||||||
res.status(200).send({ activity });
|
res.status(200).send({ activity });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -282,7 +282,7 @@ export async function GetStoreServiceActivities(req: Request, res: Response) {
|
||||||
|
|
||||||
res.status(200).send({ activities: activities });
|
res.status(200).send({ activities: activities });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -418,7 +418,7 @@ export async function UpdateStoreServiceActivity(req: Request, res: Response) {
|
||||||
|
|
||||||
res.status(200).send({ msg: "success" });
|
res.status(200).send({ msg: "success" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -497,7 +497,7 @@ export async function DeleteStoreServiceActivity(req: Request, res: Response) {
|
||||||
|
|
||||||
res.status(200).send({ msg: "success" });
|
res.status(200).send({ msg: "success" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -583,7 +583,7 @@ export async function DeleteStoreService(req: Request, res: Response) {
|
||||||
|
|
||||||
res.status(200).send({ msg: "success" });
|
res.status(200).send({ msg: "success" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,8 @@ import {
|
||||||
isEmailValid,
|
isEmailValid,
|
||||||
isLanguageCodeValid,
|
isLanguageCodeValid,
|
||||||
isPasswordValid,
|
isPasswordValid,
|
||||||
|
isPaymentIntervalValid,
|
||||||
|
isPaymentPlanValid,
|
||||||
isUsernameValid,
|
isUsernameValid,
|
||||||
} from "../validator/validator";
|
} from "../validator/validator";
|
||||||
import {
|
import {
|
||||||
|
@ -14,7 +16,6 @@ import {
|
||||||
CALENDAR_MAX_SERVICE_DURATION,
|
CALENDAR_MAX_SERVICE_DURATION,
|
||||||
CALENDAR_MIN_EARLIEST_BOOKING_TIME,
|
CALENDAR_MIN_EARLIEST_BOOKING_TIME,
|
||||||
EMAIL_VERIFICATION_STATE,
|
EMAIL_VERIFICATION_STATE,
|
||||||
PAYMENT_PLAN,
|
|
||||||
PAYMENT_PLAN_SETTINGS,
|
PAYMENT_PLAN_SETTINGS,
|
||||||
Roles,
|
Roles,
|
||||||
USER_ANALYTICS_ENABLED_DEFAULT,
|
USER_ANALYTICS_ENABLED_DEFAULT,
|
||||||
|
@ -41,6 +42,7 @@ import rabbitmq from "../rabbitmq/rabbitmq";
|
||||||
import verifyCaptcha from "../utils/recaptcha";
|
import verifyCaptcha from "../utils/recaptcha";
|
||||||
import EmailVerification from "../models/emailVerification";
|
import EmailVerification from "../models/emailVerification";
|
||||||
import UserPendingEmailChange from "../models/userPendingEmailChange";
|
import UserPendingEmailChange from "../models/userPendingEmailChange";
|
||||||
|
import { CreateCheckoutSession, getPriceId } from "./paymentController";
|
||||||
|
|
||||||
export async function SignUp(req: Request, res: Response) {
|
export async function SignUp(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
|
@ -52,6 +54,8 @@ export async function SignUp(req: Request, res: Response) {
|
||||||
language,
|
language,
|
||||||
rememberMe,
|
rememberMe,
|
||||||
recaptcha,
|
recaptcha,
|
||||||
|
paymentPlan,
|
||||||
|
paymentInterval,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// validate request
|
// validate request
|
||||||
|
@ -67,7 +71,11 @@ export async function SignUp(req: Request, res: Response) {
|
||||||
!(await isEmailValid(email)) ||
|
!(await isEmailValid(email)) ||
|
||||||
!isLanguageCodeValid(language) ||
|
!isLanguageCodeValid(language) ||
|
||||||
rememberMe === undefined ||
|
rememberMe === undefined ||
|
||||||
!recaptcha
|
!recaptcha ||
|
||||||
|
paymentPlan === undefined ||
|
||||||
|
!isPaymentPlanValid(paymentPlan) ||
|
||||||
|
paymentInterval === undefined ||
|
||||||
|
!isPaymentIntervalValid(paymentInterval)
|
||||||
) {
|
) {
|
||||||
return res.status(400).send({ err: "invalid request" });
|
return res.status(400).send({ err: "invalid request" });
|
||||||
}
|
}
|
||||||
|
@ -118,14 +126,14 @@ export async function SignUp(req: Request, res: Response) {
|
||||||
owner_user_id: userId,
|
owner_user_id: userId,
|
||||||
name: companyName,
|
name: companyName,
|
||||||
calendar_max_future_booking_days:
|
calendar_max_future_booking_days:
|
||||||
PAYMENT_PLAN_SETTINGS[PAYMENT_PLAN.DEMO].calendarMaxFutureBookingDays,
|
PAYMENT_PLAN_SETTINGS[paymentPlan].calendarMaxFutureBookingDays,
|
||||||
calendar_min_earliest_booking_time: CALENDAR_MIN_EARLIEST_BOOKING_TIME,
|
calendar_min_earliest_booking_time: CALENDAR_MIN_EARLIEST_BOOKING_TIME,
|
||||||
calendar_max_service_duration: CALENDAR_MAX_SERVICE_DURATION,
|
calendar_max_service_duration: CALENDAR_MAX_SERVICE_DURATION,
|
||||||
address: "",
|
address: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// create email verification
|
// create email verification
|
||||||
|
/*
|
||||||
const emailVerificationId = newEmailVerificationId();
|
const emailVerificationId = newEmailVerificationId();
|
||||||
const state = EMAIL_VERIFICATION_STATE.PENDING_EMAIL_VERIFICATION;
|
const state = EMAIL_VERIFICATION_STATE.PENDING_EMAIL_VERIFICATION;
|
||||||
|
|
||||||
|
@ -137,7 +145,7 @@ export async function SignUp(req: Request, res: Response) {
|
||||||
|
|
||||||
rabbitmq.sendEmail(email, "dashboardSignUpEmailVerification", language, {
|
rabbitmq.sendEmail(email, "dashboardSignUpEmailVerification", language, {
|
||||||
emailVerificationUrl: getEmailVerificationUrl(state, emailVerificationId),
|
emailVerificationUrl: getEmailVerificationUrl(state, emailVerificationId),
|
||||||
});
|
}); */
|
||||||
|
|
||||||
// create user
|
// create user
|
||||||
|
|
||||||
|
@ -150,15 +158,26 @@ export async function SignUp(req: Request, res: Response) {
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
language: language,
|
language: language,
|
||||||
analytics_enabled: USER_ANALYTICS_ENABLED_DEFAULT,
|
analytics_enabled: USER_ANALYTICS_ENABLED_DEFAULT,
|
||||||
state: ACCOUNT_STATE.PENDING_EMAIL_VERIFICATION,
|
state: ACCOUNT_STATE.INIT_PAYMENT,
|
||||||
payment_plan: PAYMENT_PLAN.DEMO,
|
payment_plan: paymentPlan,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const checkoutSessionUrl = await CreateCheckoutSession(
|
||||||
|
await getPriceId(paymentPlan, paymentInterval),
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!checkoutSessionUrl) {
|
||||||
|
return res.status(500).send({ err: "invalid request" });
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`new user signed up: user_id: ${userId} email: ${email} language: ${language} company: ${companyName} username: ${username}`
|
`new user signed up: user_id: ${userId} email: ${email} language: ${language} company: ${companyName} username: ${username}`
|
||||||
);
|
);
|
||||||
|
|
||||||
res.status(200).send({ msg: "success" });
|
saveSession(req, res, userId, true, {
|
||||||
|
redirectUrl: checkoutSessionUrl,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
@ -272,7 +291,7 @@ export async function Login(req: Request, res: Response) {
|
||||||
userLogger.info(user.user_id, "User logged in");
|
userLogger.info(user.user_id, "User logged in");
|
||||||
|
|
||||||
// create session
|
// create session
|
||||||
saveSession(req, res, user.user_id, user.username, rememberMe);
|
saveSession(req, res, user.user_id, rememberMe);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
@ -539,7 +558,7 @@ export async function GetUserProfileSettings(req: Request, res: Response) {
|
||||||
|
|
||||||
userLogger.info(session.user_id, "GetUserProfileSettings");
|
userLogger.info(session.user_id, "GetUserProfileSettings");
|
||||||
|
|
||||||
res.status(200).json(user);
|
res.status(200).send(user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
@ -705,7 +724,7 @@ export async function GetUserProfileSessions(req: Request, res: Response) {
|
||||||
|
|
||||||
userLogger.info(session.user_id, "GetUserProfileSessions");
|
userLogger.info(session.user_id, "GetUserProfileSessions");
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).send({
|
||||||
sessions: sessionsList,
|
sessions: sessionsList,
|
||||||
currentSession: currentSession,
|
currentSession: currentSession,
|
||||||
});
|
});
|
||||||
|
@ -899,7 +918,7 @@ export async function ExportUserAccount(req: Request, res: Response) {
|
||||||
|
|
||||||
userLogger.info(session.user_id, "User requested account export");
|
userLogger.info(session.user_id, "User requested account export");
|
||||||
|
|
||||||
res.status(200).json({});
|
res.status(200).send({});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
|
|
@ -7,6 +7,7 @@ import StoreServiceActivity from "./storeServiceActivity";
|
||||||
import StoreServiceActivityUsers from "./storeServiceActivityUsers";
|
import StoreServiceActivityUsers from "./storeServiceActivityUsers";
|
||||||
import User from "./user";
|
import User from "./user";
|
||||||
import UserPendingEmailChange from "./userPendingEmailChange";
|
import UserPendingEmailChange from "./userPendingEmailChange";
|
||||||
|
import UserPendingPayment from "./userPendingPayment";
|
||||||
|
|
||||||
function syncModels() {
|
function syncModels() {
|
||||||
EmailVerification.sync();
|
EmailVerification.sync();
|
||||||
|
@ -18,6 +19,7 @@ function syncModels() {
|
||||||
StoreServiceActivityUsers.sync();
|
StoreServiceActivityUsers.sync();
|
||||||
Feedback.sync();
|
Feedback.sync();
|
||||||
UserPendingEmailChange.sync();
|
UserPendingEmailChange.sync();
|
||||||
|
UserPendingPayment.sync();
|
||||||
|
|
||||||
// UserGoogleTokens.sync(); not needed as it is created by the calendar backend
|
// UserGoogleTokens.sync(); not needed as it is created by the calendar backend
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ interface UserAttributes {
|
||||||
google_account_picture?: string;
|
google_account_picture?: string;
|
||||||
analytics_enabled: boolean;
|
analytics_enabled: boolean;
|
||||||
payment_plan: number;
|
payment_plan: number;
|
||||||
|
stripe_customer_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class User extends Model<UserAttributes> implements UserAttributes {
|
class User extends Model<UserAttributes> implements UserAttributes {
|
||||||
|
@ -36,6 +37,7 @@ 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 stripe_customer_id: string;
|
||||||
declare created_at: Date;
|
declare created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +105,10 @@ User.init(
|
||||||
type: DataTypes.TINYINT,
|
type: DataTypes.TINYINT,
|
||||||
allowNull: false,
|
allowNull: false,
|
||||||
},
|
},
|
||||||
|
stripe_customer_id: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tableName: "users",
|
tableName: "users",
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { DataTypes, Model } from "sequelize";
|
||||||
|
import sequelize from "../database/database";
|
||||||
|
|
||||||
|
interface UserPendingPaymentAttributes {
|
||||||
|
payment_session_id: string;
|
||||||
|
user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserPendingPayment
|
||||||
|
extends Model<UserPendingPaymentAttributes>
|
||||||
|
implements UserPendingPaymentAttributes
|
||||||
|
{
|
||||||
|
declare payment_session_id: string;
|
||||||
|
declare user_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPendingPayment.init(
|
||||||
|
{
|
||||||
|
payment_session_id: {
|
||||||
|
primaryKey: true,
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: "user_pending_payment",
|
||||||
|
createdAt: "created_at",
|
||||||
|
updatedAt: "updated_at",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UserPendingPayment;
|
|
@ -3,6 +3,11 @@ const router = express.Router();
|
||||||
|
|
||||||
import * as paymentController from "../controllers/paymentController";
|
import * as paymentController from "../controllers/paymentController";
|
||||||
|
|
||||||
router.post("/create-payment-intent", paymentController.CreatePaymentIntent);
|
router.get("/prices/:productId", paymentController.GetPrices);
|
||||||
|
// webhook is registered in server.ts as it needs registered before the body parser
|
||||||
|
|
||||||
|
router.post("/checkout/success", paymentController.CheckoutSuccess);
|
||||||
|
router.post("/checkout/canceled", paymentController.CheckoutCanceled);
|
||||||
|
router.get("/portal", paymentController.GetBillingPortal);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
@ -53,6 +53,7 @@ export enum ACCOUNT_STATE {
|
||||||
INIT_LOGIN = 2, // account is created but password is not set yet
|
INIT_LOGIN = 2, // account is created but password is not set yet
|
||||||
BANNED = 3, // account is banned, cannot login
|
BANNED = 3, // account is banned, cannot login
|
||||||
PENDING_EMAIL_VERIFICATION = 4, // account is created but email is not verified yet
|
PENDING_EMAIL_VERIFICATION = 4, // account is created but email is not verified yet
|
||||||
|
INIT_PAYMENT = 5, // account is created but payment is not set yet
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FEEDBACK_MIN_LENGTH = 3;
|
export const FEEDBACK_MIN_LENGTH = 3;
|
||||||
|
@ -88,15 +89,25 @@ export const TERMIN_PLANNER_URL = process.env.TERMIN_PLANNER_URL;
|
||||||
export enum PAYMENT_PLAN {
|
export enum PAYMENT_PLAN {
|
||||||
DEMO = 0,
|
DEMO = 0,
|
||||||
BASIC = 1,
|
BASIC = 1,
|
||||||
|
PREMIUM = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PAYMENT_PLAN_SETTINGS = [
|
export const PAYMENT_PLAN_SETTINGS = [
|
||||||
{
|
{
|
||||||
|
// demo
|
||||||
maxEmployees: 5,
|
maxEmployees: 5,
|
||||||
calendarMaxFutureBookingDays: 14,
|
calendarMaxFutureBookingDays: 14,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// basic
|
||||||
|
id: "basic",
|
||||||
maxEmployees: 15,
|
maxEmployees: 15,
|
||||||
calendarMaxFutureBookingDays: 60,
|
calendarMaxFutureBookingDays: 60,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// premium
|
||||||
|
id: "premium",
|
||||||
|
maxEmployees: 20,
|
||||||
|
calendarMaxFutureBookingDays: 90,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -67,8 +67,8 @@ export async function saveSession(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
userId: string,
|
userId: string,
|
||||||
username: string,
|
rememberMe: boolean,
|
||||||
rememberMe: boolean
|
data?: any
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const userSession = newUserSession();
|
const userSession = newUserSession();
|
||||||
|
@ -88,7 +88,7 @@ export async function saveSession(
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
XAuthorization: userSession,
|
XAuthorization: userSession,
|
||||||
Username: username,
|
...data,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).send({ err: "invalid request" });
|
res.status(500).send({ err: "invalid request" });
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
PAYMENT_PLAN_SETTINGS,
|
PAYMENT_PLAN_SETTINGS,
|
||||||
CALENDAR_MIN_EARLIEST_BOOKING_TIME,
|
CALENDAR_MIN_EARLIEST_BOOKING_TIME,
|
||||||
CALENDAR_MAX_EARLIEST_BOOKING_TIME,
|
CALENDAR_MAX_EARLIEST_BOOKING_TIME,
|
||||||
|
PAYMENT_PLAN,
|
||||||
} from "../utils/constants";
|
} from "../utils/constants";
|
||||||
import User from "../models/user";
|
import User from "../models/user";
|
||||||
import UserPendingEmailChange from "../models/userPendingEmailChange";
|
import UserPendingEmailChange from "../models/userPendingEmailChange";
|
||||||
|
@ -183,3 +184,13 @@ export function isCalendarMinEarliestBookingTimeValid(
|
||||||
calendarMinEarliestBookingTime <= CALENDAR_MAX_EARLIEST_BOOKING_TIME
|
calendarMinEarliestBookingTime <= CALENDAR_MAX_EARLIEST_BOOKING_TIME
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPaymentPlanValid(paymentPlan: number) {
|
||||||
|
return (
|
||||||
|
paymentPlan !== PAYMENT_PLAN.DEMO && paymentPlan <= PAYMENT_PLAN.PREMIUM
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPaymentIntervalValid(paymentInterval: number) {
|
||||||
|
return paymentInterval === 0 || paymentInterval === 1;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue