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;