Added order new time and time proposals endpoint

main
Filip Borum Poulsen 3 years ago
parent 37e9ad7489
commit fbb0f60a23

@ -1,33 +1,33 @@
# Endpoints
| Done | Method | Path | Description | Authentication |
|------|----------|--------------------------|---------------------------------------------------|----------------|
| [x] | POST | /login | Login | |
| [x] | POST | /register | Create account | |
| [x] | GET | /center | Get list of center | |
| [x] | POST | /center | Admin can create center | Admin |
| [x] | PUT | /center/:id | Admin can change center | Admin |
| [ ] | DELETE | /center | Admin can delete center | Admin |
| [x] | GET | /trainer | Get list of trainers | |
| [x] | POST | /trainer | Admin can create trainer | Admin |
| [ ] | PUT | /trainer | Trainer can change profile information | Trainer |
| [x] | PUT | /trainer/:id | Admin can change trainer information | Admin |
| [ ] | DELETE | /trainer/:id | Admin can delete trainer | Admin |
| [x] | GET | /timeslot | Filter for available timeslots | |
| [x] | GET | /trainer/timeslot | Trainer can get weekly timeslots | Trainer |
| [x] | POST | /trainer/timeslot | Trainer can create weekly timeslots | Trainer |
| [x] | DELETE | /trainer/timeslot/:id | Trainer can delete weekly timeslots | Trainer |
| [ ] | GET | /trainer/order | Trainer can get reserved timeslots | Trainer |
| [ ] | PUT | /trainer/order/:id | Trainer can change reserved timeslot | Trainer |
| [ ] | DELTE | /trainer/order/:id | Trainer can delete reserved timeslot | Trainer |
| [ ] | GET | /order | User can get list of orders | User |
| [x] | POST | /order | User can request an order | User |
| [ ] | GET | /order/:id | User can get order details | User |
| [ ] | POST | /order/:id/confirm | User can confirm the order | User |
| [ ] | PUT | /order/:id | User can move order | User |
| [ ] | DELETE | /order/:id | User can cancel order | User |
| [ ] | GET | /user | User can get profile information | User |
| [ ] | PUT | /user | User can change profile information | User |
| [ ] | GET | /verify_email | Verify email | |
| [ ] | POST | /reset_password | Request password reset | |
| [ ] | POST | /new_password | Set new password | |
| Done | Method | Path | Description | Authentication |
|------|----------|---------------------------------|---------------------------------------------------|----------------|
| [x] | POST | /login | Login | |
| [x] | POST | /register | Create account | |
| [x] | GET | /center | Get list of center | |
| [x] | POST | /center | Admin can create center | Admin |
| [x] | PUT | /center/:id | Admin can change center | Admin |
| [ ] | DELETE | /center | Admin can delete center | Admin |
| [x] | GET | /trainer | Get list of trainers | |
| [x] | POST | /trainer | Admin can create trainer | Admin |
| [ ] | PUT | /trainer | Trainer can change profile information | Trainer |
| [x] | PUT | /trainer/:id | Admin can change trainer information | Admin |
| [ ] | DELETE | /trainer/:id | Admin can delete trainer | Admin |
| [x] | GET | /timeslot | Filter for available timeslots | |
| [x] | GET | /trainer/timeslot | Trainer can get weekly timeslots | Trainer |
| [x] | POST | /trainer/timeslot | Trainer can create weekly timeslots | Trainer |
| [x] | DELETE | /trainer/timeslot/:id | Trainer can delete weekly timeslots | Trainer |
| [ ] | GET | /trainer/order | Trainer can get reserved timeslots | Trainer |
| [ ] | PUT | /trainer/order/:id | Trainer can change reserved timeslot | Trainer |
| [x] | GET | /trainer/order/:id/newtimeslots | Trainer can change reserved timeslot | Trainer |
| [ ] | DELTE | /trainer/order/:id | Trainer can delete reserved timeslot | Trainer |
| [x] | GET | /order | User can get list of orders | User |
| [x] | POST | /order | User can request an order | User |
| [x] | PUT | /order/:id | User can propose new time | User |
| [x] | GET | /order/:id/newtimeslots | User can can get list of new timeslots | User |
| [x] | POST | /order/:id/cancel | User can cancel order | User |
| [x] | GET | /user | User can get profile information | User |
| [ ] | PUT | /user | User can change profile information | User |
| [ ] | GET | /verify_email | Verify email | |
| [ ] | POST | /reset_password | Request password reset | |
| [ ] | POST | /new_password | Set new password | |

@ -0,0 +1,16 @@
import Trainer from "./trainer";
export type OrderStatus = "Created" | "Confirmed" | "Failed" | "CancelledByTrainer" | "CancelledByUser";
export type PaymentIntentStatus = "Created" | "Successful" | "Failed";
export interface OrderObject {
id: number
trainer: Trainer
startDate: string
endDate: string
status: OrderObjectStatus
price: number
created_at: Date
}
export type OrderObjectStatus = "Successful" | "Processing" | "CancelledByTrainer" | "CancelledByUser";

@ -12,6 +12,6 @@ export interface WeeklyTimeslot {
export interface ReservedTimeslots {
id: number
start_time: string | Date
end_time: string | Date
start_time: Date
end_time: Date
}

@ -63,7 +63,7 @@ RETURNING id;
INSERT INTO reserved_timeslots (trainer_id, start_time, end_time) VALUES
($1, '2023-04-24 11:00:00+2', '2023-04-24 12:00:00+2'),
($1, '2023-04-24 12:00:00+2', '2023-04-24 13:00:00+2'),
($1, '2023-05-01 11:00:00+2', '2023-05-01 12:00:00+2'),
($1, '2023-04-01 11:00:00+2', '2023-04-01 12:00:00+2'),
($2, '2023-04-17 11:00:00+2', '2023-04-17 12:00:00+2'),
($2, '2023-04-10 12:00:00+2', '2023-04-10 13:00:00+2')
RETURNING id, trainer_id;

@ -0,0 +1,214 @@
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";
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
WHERE
((start_time >= $5 AND start_time < $6)
OR (end_time > $5 AND end_time <= $6))
AND trainer_id = $1
) 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 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: `http://localhost:5173/orders`,
cancel_url: `http://localhost:5173`
});
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;

@ -7,6 +7,9 @@ import register from "./register"
import center from "./center";
import trainer from "./trainer/index";
import timeslot from "./timeslot";
import createOrder from "./createOrder";
import orderNewTimeProposal from "./orderNewTimeProposal";
import orderNewTime from "./orderNewTime";
import order from "./order";
import stripeWebhook from "./stripeWebhook";
import { bodyParserErrorHandler } from "../middlewares/bodyParserErrorHandler";
@ -18,6 +21,9 @@ router.use(register);
router.use(center);
router.use(trainer);
router.use(timeslot);
router.use(createOrder);
router.use(orderNewTimeProposal);
router.use(orderNewTime);
router.use(order);
export default router;

@ -1,235 +1,123 @@
import express, { Router, Request, Response, NextFunction } 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 express, { Router, Request, Response } from "express";
import Joi from "joi";
import dayjs from "dayjs";
import { client as pool } from "../db";
import { client } 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 { stripeWebhookSecret } from "../environment";
import { bodyParserErrorHandler } from "../middlewares/bodyParserErrorHandler";
import Trainer from "../interfaces/trainer";
dayjs.extend(isoWeek)
dayjs.extend(utc)
dayjs.extend(LocalizedFormat);
import { OrderObject, OrderObjectStatus, OrderStatus, PaymentIntentStatus } from "../interfaces/order";
const router: Router = express.Router();
const orderSchema = Joi.object({
trainer: idSchema.required(),
startDate: Joi.date().required(),
endDate: Joi.date().required()
});
interface OrderBody {
trainer: number
startDate: Date
endDate: Date
}
interface TimeslotValidQueryResult {
weekly_timeslot_available: boolean
time_already_reserved: boolean
interface CancelOrderLookup {
order_status: OrderStatus
payment_intent_status: PaymentIntentStatus
start_time: Date
}
router.post("/createTestOrder", async (req: AuthedRequest, res: Response) => {
const checkoutSession: Stripe.Checkout.Session = await stripe.checkout.sessions.create({
line_items: [
{
price_data: {
currency: "DKK",
unit_amount: 1000,
product_data: {
name: "Træningstime",
description: "Personlig træningstime med ...",
}
},
quantity: 1
}
],
mode: 'payment',
success_url: `http://localhost:5173/success`,
cancel_url: `http://localhost:5173/cancel`
});
res.send(checkoutSession);
});
router.post("/order/:id/cancel", UserAuth, async (req: AuthedRequest, res: Response) => {
try {
const lookupResult = await client.query(`
SELECT
order_status,
payment_intents.status as payment_intent_status,
start_time
FROM orders
LEFT JOIN payment_intents ON payment_intents.id = orders.payment_intent
LEFT JOIN reserved_timeslots ON reserved_timeslots.id = orders.timeslot_id
WHERE orders.id = $1
AND user_id = $2
AND order_status = 'Confirmed';
`, [
req.params.id,
req.user?.userId
]);
if (lookupResult.rows.length !== 1) {
return res.sendStatus(404);
}
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("");
const order: CancelOrderLookup = lookupResult.rows[0];
return output;
}
const dateValidation = Joi.date().min(Date.now()).validate(order.start_time)
if (dateValidation.error !== undefined) {
return res.status(400).send({ message: "Timeslot has already occurred" });
}
function formatTime(date: Dayjs): string {
let output: string = dayjs(date).locale("da").format("HH:mm");
await client.query(`
UPDATE orders SET
order_status = 'CancelledByUser'
WHERE id = $1
RETURNING *;
`, [
req.params.id
]);
return output;
}
return res.sendStatus(204);
} catch (error: DatabaseError | Error | any) {
console.error(error);
return res.sendStatus(500);
}
});
router.post("/order", UserAuth, async (req: AuthedRequest, res: Response) => {
router.get("/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 databaseResult = await client.query(`
SELECT
orders.id,
order_status,
price,
created_at,
payment_intents.status as payment_intent_status,
start_time,
end_time,
json_build_object(
'id',trainers.id,
'first_name',users.first_name,
'last_name',users.last_name,
'center_id',trainers.center_id,
'center_name',centers.name
) as trainer
FROM orders
LEFT JOIN reserved_timeslots ON reserved_timeslots.id = orders.timeslot_id
LEFT JOIN payment_intents ON payment_intents.id = orders.payment_intent
LEFT JOIN trainers ON trainers.id = reserved_timeslots.trainer_id
LEFT JOIN centers on trainers.center_id = centers.id
LEFT JOIN users on users.id = trainers.user_id
WHERE
orders.user_id = $1
AND (
orders.order_status = 'Confirmed'
OR orders.order_status = 'CancelledByTrainer'
OR orders.order_status = 'CancelledByUser'
);
`, [
req.user?.userId
]);
const orders: OrderObject[] = databaseResult.rows.map((order): OrderObject => {
const paymentStatus = order.payment_intent_status === "Successful" ? "Successful" : "Processing"
let status: OrderObjectStatus = paymentStatus;
if (order.order_status === "CancelledByTrainer")
status = "CancelledByTrainer"
else if (order.order_status === "CancelledByUser")
status = "CancelledByUser"
return {
id: order.id,
trainer: order.trainer,
startDate: dayjs(order.start_time).format('YYYY-MM-DDTHH:mm:ss[Z]'),
endDate: dayjs(order.end_time).format('YYYY-MM-DDTHH:mm:ss[Z]'),
status: status,
price: order.price,
created_at: order.created_at,
}
});
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
WHERE
((start_time >= $5 AND start_time < $6)
OR (end_time > $5 AND end_time <= $6))
AND trainer_id = $1
) 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 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: `http://localhost:5173/success`,
cancel_url: `http://localhost:5173/cancel`
});
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();
}
return res.status(200).send(orders);
} catch (error: DatabaseError | Error | any) {
console.error(error);
return res.sendStatus(500);

@ -0,0 +1,162 @@
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 } 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 { OrderStatus, PaymentIntentStatus } from "../interfaces/order";
dayjs.extend(isoWeek)
dayjs.extend(utc)
dayjs.extend(LocalizedFormat);
const router: Router = express.Router();
const orderSchema = Joi.object({
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
}
interface OrderLookup {
order_status: OrderStatus
start_time: Date
end_time: Date
trainer_id: number
timeslot_id: number
}
router.put("/order/:id", UserAuth, async (req: AuthedRequest, res: Response) => {
try {
const lookupResult = await client.query(`
SELECT
order_status,
start_time,
end_time,
trainer_id,
timeslot_id
FROM orders
LEFT JOIN payment_intents ON payment_intents.id = orders.payment_intent
LEFT JOIN reserved_timeslots ON reserved_timeslots.id = orders.timeslot_id
WHERE orders.id = $1
AND user_id = $2
AND order_status = 'Confirmed';
`, [
req.params.id,
req.user?.userId
]);
if (lookupResult.rows.length !== 1) {
return res.sendStatus(404);
}
const order: OrderLookup = lookupResult.rows[0];
const originalStartDate: dayjs.Dayjs = dayjs(order.start_time).utcOffset(0);
const originalEndDate: dayjs.Dayjs = dayjs(order.end_time).utcOffset(0);
/** The duration of the original timeslot in miliseconds */
const originalDuration: number = originalEndDate.diff(originalStartDate);
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();
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
WHERE
((start_time >= $5 AND start_time < $6)
OR (end_time > $5 AND end_time <= $6))
AND trainer_id = $1
) as time_already_reserved;
`, [
order.trainer_id,
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 duration = endDate.diff(startDate);
if (duration !== originalDuration)
return res.status(400).send([{
message: "timeslot is not same length as original",
path: [
"startDate"
],
type: "startDate.invalid"
}]);
await client.query(`
UPDATE reserved_timeslots SET
start_time = $1,
end_time = $2
WHERE id = $3;
`, [
startDate,
endDate,
order.timeslot_id
]);
return res.sendStatus(204);
} catch (error: DatabaseError | Error | any) {
console.error(error);
return res.sendStatus(500);
}
})
export default router;

@ -0,0 +1,178 @@
import express, { Router, Request, Response } from "express";
import Joi from "joi";
import dayjs from "dayjs";
import { client } from "../db";
import { DatabaseError } from "pg";
import { UserAuth } from "../middlewares/auth";
import { AuthedRequest } from "../interfaces/auth";
import Trainer from "../interfaces/trainer";
import { OrderObject, OrderObjectStatus, OrderStatus, PaymentIntentStatus } from "../interfaces/order";
import { ReservedTimeslots, Timeslot, WeeklyTimeslot } from "../interfaces/timeslot";
const router: Router = express.Router();
interface OrderLookup {
order_status: OrderStatus
start_time: Date
end_time: Date
trainer_id: number
timeslot_id: number
}
interface TimeProposal {
startDate: string
endDate: string
}
interface NewTimeslotPropsalFilter {
startDate: Date
endDate: Date
}
const newTimeslotPropsalFilterSchema = Joi.object({
startDate: Joi.date().default(Date.now()),
endDate: Joi.date().default(Date.now())
});
router.get("/order/:id/newtimeslots", UserAuth, async (req: AuthedRequest, res: Response) => {
try {
const lookupResult = await client.query(`
SELECT
order_status,
start_time,
end_time,
trainer_id,
timeslot_id
FROM orders
LEFT JOIN payment_intents ON payment_intents.id = orders.payment_intent
LEFT JOIN reserved_timeslots ON reserved_timeslots.id = orders.timeslot_id
WHERE orders.id = $1
AND user_id = $2
AND order_status = 'Confirmed';
`, [
req.params.id,
req.user?.userId
]);
if (lookupResult.rows.length !== 1) {
return res.sendStatus(404);
}
const order: OrderLookup = lookupResult.rows[0];
const dateValiadtion = Joi.date().min(Date.now()).validate(order.start_time)
if (dateValiadtion.error !== undefined) {
return res.status(400).send({ message: "Timeslot has already occurred" });
}
const filterValidation = newTimeslotPropsalFilterSchema.validate(req.query, { abortEarly: false, stripUnknown: true });
if (filterValidation.error !== undefined) {
return res.status(400).send(filterValidation.error.details);
}
const newTimeslotPropsalFilter: NewTimeslotPropsalFilter = filterValidation.value;
const originalStartDate: dayjs.Dayjs = dayjs(order.start_time).utcOffset(0);
const originalEndDate: dayjs.Dayjs = dayjs(order.end_time).utcOffset(0);
/** The duration of the original timeslot in miliseconds */
const originalDuration: number = originalStartDate.diff(originalEndDate);
const latestAllowedFilterEndDate: dayjs.Dayjs = originalStartDate.add(1, "month");
const nowDate: dayjs.Dayjs = dayjs().utcOffset(0).startOf("date");
let filterStartDate: dayjs.Dayjs = dayjs(newTimeslotPropsalFilter.startDate).utcOffset(0).startOf("date");
let filterEndDate: dayjs.Dayjs = dayjs(newTimeslotPropsalFilter.endDate).utcOffset(0).startOf("date");
if (filterStartDate.isBefore(nowDate)) {
filterStartDate = nowDate;
}
if (filterEndDate.isAfter(latestAllowedFilterEndDate)) {
filterEndDate = latestAllowedFilterEndDate;
}
let weekdays: number[] = [];
for (let day: dayjs.Dayjs = filterStartDate; !day.isAfter(filterEndDate); day = day.add(1, "day")) {
if (!weekdays.includes(day.isoWeekday()))
weekdays.push(day.isoWeekday());
}
const weeklyTimeslotLookup = await client.query(`
SELECT
id,
day_of_week,
start_time,
end_time
FROM weekly_timeslots
WHERE trainer_id = $1
AND day_of_week = ANY($2);
`, [
order.trainer_id,
weekdays
]);
const weeklyTimeslots: WeeklyTimeslot[] = weeklyTimeslotLookup.rows;
const reservedTimeslotLookup = await client.query(`
SELECT
start_time,
end_time
FROM public.reserved_timeslots
JOIN orders ON reserved_timeslots.id = orders.timeslot_id
WHERE
(start_time >= $1 AND end_time <= $2)
AND reserved_timeslots.trainer_id = $3
AND (
orders.order_status = 'Confirmed'
OR orders.order_status = 'Created'
);
`, [
filterStartDate,
filterEndDate.add(1, "day"),
order.trainer_id
]);
const reservedTimeslots: ReservedTimeslots[] = reservedTimeslotLookup.rows;
const timeProposals: TimeProposal[] = [];
for (let day: dayjs.Dayjs = filterStartDate; !day.isAfter(filterEndDate); day = day.add(1, "day")){
const weekDay = day.isoWeekday();
weeklyTimeslots: for (const weeklyTimeslot of weeklyTimeslots) {
if (weeklyTimeslot.day_of_week !== weekDay)
continue;
const startTime = dayjs(`${day.toISOString().split("T")[0]}T${weeklyTimeslot.start_time}`);
const endTime = dayjs(`${day.toISOString().split("T")[0]}T${weeklyTimeslot.end_time}`);
for (const reservedTimeslot of reservedTimeslots) {
const reservedTimeStart = dayjs(reservedTimeslot.start_time);
const reservedTimeEnd = dayjs(reservedTimeslot.end_time);
if ((!reservedTimeStart.isBefore(startTime) && reservedTimeStart.isBefore(endTime)) ||
(reservedTimeEnd.isAfter(startTime) && !reservedTimeEnd.isAfter(endTime))) {
continue weeklyTimeslots;
}
}
const proposedDuration: number = startTime.diff(endTime);
if (proposedDuration !== originalDuration)
continue;
timeProposals.push({
startDate: `${day.toISOString().split("T")[0]}T${weeklyTimeslot.start_time}Z`,
endDate: `${day.toISOString().split("T")[0]}T${weeklyTimeslot.end_time}Z`
});
}
}
return res.status(200).send(timeProposals);
} catch (error: DatabaseError | Error | any) {
console.error(error);
return res.sendStatus(500);
}
});
export default router;

@ -69,12 +69,12 @@ router.get("/timeslot", async (req: Request, res: Response) => {
const startDate = dayjs(timeslotFilters.startDate).utcOffset(0);
const endDate = dayjs(timeslotFilters.endDate).utcOffset(0);
let filterStartDate: dayjs.Dayjs = dayjs(timeslotFilters.startDate).utcOffset(0).startOf("date");
const filterEndDate: dayjs.Dayjs = dayjs(timeslotFilters.endDate).utcOffset(0).startOf("date");
const nowDate: dayjs.Dayjs = dayjs().utcOffset(0).startOf("date");
if (filterStartDate.isBefore(nowDate)) {
filterStartDate = nowDate;
}
@ -109,10 +109,7 @@ router.get("/timeslot", async (req: Request, res: Response) => {
)) FROM public.reserved_timeslots
JOIN orders ON reserved_timeslots.id = orders.timeslot_id
WHERE
(
(start_time >= $1 AND start_time < $2)
OR (end_time > $1 AND end_time <= $2)
)
(start_time >= $1 AND end_time <= $2)
AND weekly_timeslots.trainer_id = reserved_timeslots.trainer_id
AND (
orders.order_status = 'Confirmed'
@ -139,8 +136,8 @@ router.get("/timeslot", async (req: Request, res: Response) => {
ORDER BY
weekly_timeslots.trainer_id ASC;
`, [
startDate.toISOString(),
endDate.toISOString(),
filterStartDate.toISOString(),
filterEndDate.add(1, "day").toISOString(),
timeslotFilters.trainer !== undefined ? timeslotFilters.trainer : [],
timeslotFilters.trainer === undefined,
weekdays,
@ -194,10 +191,10 @@ router.get("/timeslot", async (req: Request, res: Response) => {
}
}
const hourlyPrice = trainer.trainer.hourly_price;
const hours = dayjs(endTime).diff(dayjs(startTime), "hours", true);
const price = Math.ceil(hours * hourlyPrice);
trainerWithAvailableTimeslots.timeslots.push({
startDate: `${day.toISOString().split("T")[0]}T${timeslot.start_time}Z`,
endDate: `${day.toISOString().split("T")[0]}T${timeslot.end_time}Z`,
@ -206,7 +203,7 @@ router.get("/timeslot", async (req: Request, res: Response) => {
}
}
trainerWithAvailableTimeslots.timeslots.sort((a,b)=>a.startDate.localeCompare(b.startDate));
trainerWithAvailableTimeslots.timeslots.sort((a, b) => a.startDate.localeCompare(b.startDate));
trainers.push(trainerWithAvailableTimeslots);
}

Loading…
Cancel
Save