You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
229 lines
7.8 KiB
TypeScript
229 lines
7.8 KiB
TypeScript
import express, { Router, Response } from "express";
|
|
import Joi from "joi"
|
|
import dayjs, { Dayjs } from "dayjs"
|
|
import isoWeek from "dayjs/plugin/isoWeek"
|
|
import utc from "dayjs/plugin/utc"
|
|
import LocalizedFormat from "dayjs/plugin/localizedFormat"
|
|
import { } from "dayjs/locale/da";
|
|
|
|
import { client as pool } from "../db";
|
|
import { DatabaseError } from "pg";
|
|
import { idSchema } from "../schemas";
|
|
import { UserAuth } from "../middlewares/auth";
|
|
import { AuthedRequest } from "../interfaces/auth";
|
|
import Stripe from 'stripe';
|
|
import { stripe } from "../stripe";
|
|
import Trainer from "../interfaces/trainer";
|
|
import { baseURL } from "../environment";
|
|
|
|
dayjs.extend(isoWeek)
|
|
dayjs.extend(utc)
|
|
dayjs.extend(LocalizedFormat);
|
|
|
|
const router: Router = express.Router();
|
|
|
|
const orderSchema = Joi.object({
|
|
trainer: idSchema.required(),
|
|
startDate: Joi.date().min("now").required(),
|
|
endDate: Joi.date().min(Joi.ref("startDate")).required()
|
|
});
|
|
|
|
interface OrderBody {
|
|
trainer: number
|
|
startDate: Date
|
|
endDate: Date
|
|
}
|
|
|
|
interface TimeslotValidQueryResult {
|
|
weekly_timeslot_available: boolean
|
|
time_already_reserved: boolean
|
|
}
|
|
|
|
function formatDate(date: Dayjs): string {
|
|
let output: string = dayjs(date).locale("da").format("dddd D. MMM YYYY");
|
|
let outputStringArray = output.split("");
|
|
outputStringArray[0] = outputStringArray[0].toLocaleUpperCase();
|
|
output = outputStringArray.join("");
|
|
|
|
return output;
|
|
}
|
|
|
|
function formatTime(date: Dayjs): string {
|
|
let output: string = dayjs(date).locale("da").format("HH:mm");
|
|
|
|
return output;
|
|
}
|
|
|
|
router.post("/order", UserAuth, async (req: AuthedRequest, res: Response) => {
|
|
try {
|
|
const client = await pool.connect();
|
|
try {
|
|
const validation = orderSchema.validate(req.body, { abortEarly: false });
|
|
if (validation.error !== undefined) {
|
|
return res.status(400).send(validation.error.details);
|
|
}
|
|
|
|
const orderBody: OrderBody = validation.value;
|
|
|
|
const startDate: dayjs.Dayjs = dayjs(orderBody.startDate).utcOffset(0);
|
|
const endDate: dayjs.Dayjs = dayjs(orderBody.endDate).utcOffset(0);
|
|
|
|
const startTime: string = `${startDate.hour()}:${startDate.minute()}`;
|
|
const endTime: string = `${endDate.hour()}:${endDate.minute()}`;
|
|
|
|
const weekday = startDate.isoWeekday();
|
|
|
|
await client.query("BEGIN");
|
|
|
|
const trainerLookup = await client.query(`
|
|
SELECT trainers.id, first_name, last_name, center_id, centers.name as center_name FROM trainers
|
|
JOIN users ON trainers.user_id = users.id
|
|
JOIN centers on trainers.center_id = centers.id
|
|
WHERE trainers.id = $1;
|
|
`, [
|
|
orderBody.trainer
|
|
]);
|
|
|
|
if (trainerLookup.rows.length !== 1)
|
|
return res.status(400).send([{
|
|
message: "\"trainer\" was not found",
|
|
path: [
|
|
"trainer"
|
|
],
|
|
type: "trainer.not_found"
|
|
}]);
|
|
|
|
const trainer: Trainer = trainerLookup.rows[0];
|
|
|
|
const timeslotValidQuery = await client.query(`
|
|
select
|
|
EXISTS(
|
|
SELECT 1 FROM weekly_timeslots
|
|
WHERE
|
|
trainer_id = $1
|
|
AND day_of_week = $2
|
|
AND start_time = $3
|
|
AND end_time = $4
|
|
) as weekly_timeslot_available,
|
|
EXISTS(
|
|
SELECT 1 FROM public.reserved_timeslots
|
|
JOIN orders ON reserved_timeslots.id = orders.timeslot_id
|
|
WHERE
|
|
((start_time >= $5 AND start_time < $6)
|
|
OR (end_time > $5 AND end_time <= $6))
|
|
AND trainer_id = $1
|
|
AND (
|
|
orders.order_status = 'Confirmed'
|
|
OR orders.order_status = 'Created'
|
|
)
|
|
) as time_already_reserved;
|
|
`, [
|
|
orderBody.trainer,
|
|
weekday,
|
|
startTime,
|
|
endTime,
|
|
startDate.toISOString(),
|
|
endDate.toISOString()
|
|
]);
|
|
|
|
const timeslotValidQueryResult: TimeslotValidQueryResult = timeslotValidQuery.rows[0];
|
|
|
|
if (timeslotValidQueryResult.time_already_reserved || !timeslotValidQueryResult.weekly_timeslot_available)
|
|
return res.status(400).send([{
|
|
message: "timeslot was not found",
|
|
path: [
|
|
"startDate"
|
|
],
|
|
type: "startDate.invalid"
|
|
}]);
|
|
|
|
const emailQuery = await client.query(`
|
|
SELECT email FROM users WHERE id = $1;
|
|
`, [
|
|
req.user?.userId
|
|
]);
|
|
|
|
const email: string = emailQuery.rows[0].email;
|
|
|
|
const priceQuery = await client.query(`
|
|
SELECT hourly_price FROM trainers WHERE id = $1;
|
|
`, [
|
|
orderBody.trainer
|
|
]);
|
|
|
|
const hourlyPrice = priceQuery.rows[0].hourly_price;
|
|
|
|
const hours = endDate.diff(startDate, "hours", true);
|
|
const price = Math.ceil(hours * hourlyPrice);
|
|
|
|
const productName: string = `Personlig træningstime ${trainer.center_name}`;
|
|
const productDescription: string = `Personlig træningstime med ${trainer.first_name} ${trainer.last_name} - ${formatDate(startDate)} ${formatTime(startDate)} - ${formatTime(endDate)}`;
|
|
|
|
const checkoutSession: Stripe.Checkout.Session = await stripe.checkout.sessions.create({
|
|
line_items: [
|
|
{
|
|
price_data: {
|
|
currency: "DKK",
|
|
unit_amount: price,
|
|
product_data: {
|
|
name: productName,
|
|
description: productDescription,
|
|
}
|
|
},
|
|
quantity: 1
|
|
}
|
|
],
|
|
expires_at: Math.floor(Date.now()/1000 + 60*31),
|
|
mode: 'payment',
|
|
success_url: `${baseURL}/user/orders`,
|
|
cancel_url: `${baseURL}`,
|
|
customer_email: email
|
|
});
|
|
|
|
const insertQuery = await client.query(`
|
|
WITH inserted_reserved_timeslot AS (
|
|
INSERT INTO reserved_timeslots (trainer_id, start_time, end_time)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING *
|
|
)
|
|
|
|
INSERT INTO orders (timeslot_id, user_id, price, checkout_session)
|
|
select id, $4, $5, $6
|
|
FROM inserted_reserved_timeslot
|
|
RETURNING id;
|
|
`, [
|
|
orderBody.trainer,
|
|
startDate,
|
|
endDate,
|
|
req.user?.userId,
|
|
price,
|
|
checkoutSession.id
|
|
]);
|
|
|
|
const insertedData = insertQuery.rows[0];
|
|
|
|
await client.query("COMMIT");
|
|
|
|
return res.status(200).send({
|
|
id: insertedData.id,
|
|
trainerId: orderBody.trainer,
|
|
startDate: orderBody.startDate,
|
|
endDate: orderBody.endDate,
|
|
price,
|
|
url: checkoutSession.url
|
|
});
|
|
} catch (error: DatabaseError | Error | any) {
|
|
await client.query("ROLLBACK");
|
|
console.error(error);
|
|
return res.sendStatus(500);
|
|
}
|
|
finally {
|
|
client.release();
|
|
}
|
|
} catch (error: DatabaseError | Error | any) {
|
|
console.error(error);
|
|
return res.sendStatus(500);
|
|
}
|
|
})
|
|
|
|
export default router; |