From 11bba98ae4fc83029ea0080336c3563717d25483 Mon Sep 17 00:00:00 2001 From: Filip Borum Poulsen Date: Tue, 25 Apr 2023 10:58:31 +0200 Subject: [PATCH] Added user email notifications --- server/example.env | 4 ++ server/package-lock.json | 22 ++++++- server/package.json | 2 + server/src/environment.ts | 4 ++ server/src/interfaces/order.ts | 12 ++++ server/src/interfaces/user.ts | 2 +- server/src/mail/index.ts | 13 ++++ server/src/mail/orderConfirmation.ts | 21 ++++++ server/src/mail/signUp.ts | 15 +++++ server/src/mail/userCancelledOrder.ts | 21 ++++++ server/src/mail/userMovedOrder.ts | 21 ++++++ server/src/migrations/populate.ts | 22 +++---- server/src/routes/createOrder.ts | 54 +++++++-------- server/src/routes/order.ts | 51 ++++++++++---- server/src/routes/orderNewTime.ts | 66 ++++++++++++++----- server/src/routes/register.ts | 8 ++- server/src/utils/dates.ts | 24 +++++++ .../src/webhooks/checkoutSessionCompleted.ts | 52 +++++++++++++++ 18 files changed, 340 insertions(+), 74 deletions(-) create mode 100644 server/src/mail/index.ts create mode 100644 server/src/mail/orderConfirmation.ts create mode 100644 server/src/mail/signUp.ts create mode 100644 server/src/mail/userCancelledOrder.ts create mode 100644 server/src/mail/userMovedOrder.ts create mode 100644 server/src/utils/dates.ts diff --git a/server/example.env b/server/example.env index 0f96dd5..55127a0 100644 --- a/server/example.env +++ b/server/example.env @@ -9,3 +9,7 @@ PRIVATE_KEY_LOCATION=/data/cert/private.pem STRIPE_PUBLIC_KEY=pk_test_... STRIPE_SECRET_KEY=sk_test_... BASE_URL=https://merit.bpfilip.dk +SMTP_HOSTNAME=email-smtp.eu-central-1.amazonaws.com +SMTP_USERNAME=user123 +SMTP_PASSWORD=password12345 +SMTP_ADDRESS=user@example.com \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 3ba714f..4c84a2f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -16,6 +16,7 @@ "express": "^4.18.2", "joi": "^17.9.1", "jsonwebtoken": "^9.0.0", + "nodemailer": "^6.9.1", "pg": "^8.10.0", "stripe": "^12.1.1" }, @@ -25,6 +26,7 @@ "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.1", "@types/node": "^18.15.11", + "@types/nodemailer": "^6.4.7", "@types/pg": "^8.6.6", "concurrently": "^8.0.1", "nodemon": "^2.0.22", @@ -249,8 +251,16 @@ "node_modules/@types/node": { "version": "18.15.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz", - "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==", - "dev": true + "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==" + }, + "node_modules/@types/nodemailer": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.7.tgz", + "integrity": "sha512-f5qCBGAn/f0qtRcd4SEn88c8Fp3Swct1731X4ryPKqS61/A3LmmzN8zaEz7hneJvpjFbUUgY7lru/B/7ODTazg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/pg": { "version": "8.6.6", @@ -1469,6 +1479,14 @@ } } }, + "node_modules/nodemailer": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.1.tgz", + "integrity": "sha512-qHw7dOiU5UKNnQpXktdgQ1d3OFgRAekuvbJLcdG5dnEo/GtcTHRYM7+UfJARdOFU9WUQO8OiIamgWPmiSFHYAA==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", diff --git a/server/package.json b/server/package.json index ac150b8..4b5ed8c 100644 --- a/server/package.json +++ b/server/package.json @@ -21,6 +21,7 @@ "express": "^4.18.2", "joi": "^17.9.1", "jsonwebtoken": "^9.0.0", + "nodemailer": "^6.9.1", "pg": "^8.10.0", "stripe": "^12.1.1" }, @@ -30,6 +31,7 @@ "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.1", "@types/node": "^18.15.11", + "@types/nodemailer": "^6.4.7", "@types/pg": "^8.6.6", "concurrently": "^8.0.1", "nodemon": "^2.0.22", diff --git a/server/src/environment.ts b/server/src/environment.ts index 5623fea..2eaee7f 100644 --- a/server/src/environment.ts +++ b/server/src/environment.ts @@ -43,3 +43,7 @@ export const stripeWebhookSecret: string = process.env.STRIPE_WEBHOOK_SECRET; export const baseURL: string = process.env.BASE_URL; +export const smtpHostname: string | undefined = process.env.SMTP_HOSTNAME; +export const smtpUsername: string | undefined = process.env.SMTP_USERNAME; +export const smtpPassword: string | undefined = process.env.SMTP_PASSWORD; +export const smtpAddress: string | undefined = process.env.SMTP_ADDRESS; \ No newline at end of file diff --git a/server/src/interfaces/order.ts b/server/src/interfaces/order.ts index 8e337f1..8a58f86 100644 --- a/server/src/interfaces/order.ts +++ b/server/src/interfaces/order.ts @@ -1,4 +1,6 @@ +import { Dayjs } from "dayjs"; import Trainer from "./trainer"; +import User from "./user"; export type OrderStatus = "Created" | "Confirmed" | "Failed" | "CancelledByTrainer" | "CancelledByUser"; export type PaymentIntentStatus = "Created" | "Successful" | "Failed"; @@ -13,4 +15,14 @@ export interface OrderObject { created_at: Date } +export interface Order { + id: number + trainer: Trainer + startDate: Date|Dayjs + endDate: Date|Dayjs + status: OrderObjectStatus + price: number + created_at: Date +} + export type OrderObjectStatus = "Successful" | "Processing" | "CancelledByTrainer" | "CancelledByUser"; \ No newline at end of file diff --git a/server/src/interfaces/user.ts b/server/src/interfaces/user.ts index 205d567..a5de26d 100644 --- a/server/src/interfaces/user.ts +++ b/server/src/interfaces/user.ts @@ -3,6 +3,6 @@ interface User { id: number first_name: string last_name: string - password_hash: string + password_hash?: string } export default User; \ No newline at end of file diff --git a/server/src/mail/index.ts b/server/src/mail/index.ts new file mode 100644 index 0000000..e761b02 --- /dev/null +++ b/server/src/mail/index.ts @@ -0,0 +1,13 @@ +import nodemailer from "nodemailer"; +import { smtpHostname, smtpPassword, smtpUsername } from "../environment"; + +export const transporter = nodemailer.createTransport({ + host: smtpHostname, + port: 587, + secure: false, + requireTLS: true, + auth: { + user: smtpUsername, + pass: smtpPassword, + } +}); \ No newline at end of file diff --git a/server/src/mail/orderConfirmation.ts b/server/src/mail/orderConfirmation.ts new file mode 100644 index 0000000..e3c66a4 --- /dev/null +++ b/server/src/mail/orderConfirmation.ts @@ -0,0 +1,21 @@ +import { transporter } from "."; +import { smtpAddress } from "../environment"; +import { Order, OrderObject } from "../interfaces/order"; +import User from "../interfaces/user"; +import { formatDate, formatTime } from "../utils/dates"; + +export async function sendOrderConfirmationEmail(user: User, order: Order) { + await transporter.sendMail({ + from: `Fitness World <${smtpAddress}>`, + to: user.email, + subject: "Ordre bekræftelse - Fitness World", + text: + `Hej, ${user.first_name} ${user.last_name}. + +Din tid ved ${order.trainer.first_name} ${order.trainer.last_name} i ${order.trainer.center_name} er blevet reserveret. + +Vi glæder os til at se dig ${formatDate(order.startDate)} ${formatTime(order.startDate)} - ${formatTime(order.endDate)} + +Venlig hilsen Fitness World.` + }) +} \ No newline at end of file diff --git a/server/src/mail/signUp.ts b/server/src/mail/signUp.ts new file mode 100644 index 0000000..685071f --- /dev/null +++ b/server/src/mail/signUp.ts @@ -0,0 +1,15 @@ +import { transporter } from "."; +import { smtpAddress } from "../environment"; +import User from "../interfaces/user"; + +export async function sendSignupEmail(user: User) { + await transporter.sendMail({ + from: `Fitness World <${smtpAddress}>`, + to: user.email, + subject: "Welcome to Fitness World", + text: + `Hej, ${user.first_name} ${user.last_name}. + +Velkommen til Fitness World.` + }) +} \ No newline at end of file diff --git a/server/src/mail/userCancelledOrder.ts b/server/src/mail/userCancelledOrder.ts new file mode 100644 index 0000000..ac6160b --- /dev/null +++ b/server/src/mail/userCancelledOrder.ts @@ -0,0 +1,21 @@ +import { transporter } from "."; +import { smtpAddress } from "../environment"; +import { Order } from "../interfaces/order"; +import User from "../interfaces/user"; +import { formatDate, formatTime } from "../utils/dates"; + +export async function sendUserCancelledOrderEmail(user: User, order: Order) { + await transporter.sendMail({ + from: `Fitness World <${smtpAddress}>`, + to: user.email, + subject: "Ordre afbestilt - Fitness World", + text: + `Hej, ${user.first_name} ${user.last_name}. + +Du har afbestilt din tid ved ${order.trainer.first_name} ${order.trainer.last_name} i ${order.trainer.center_name} ${formatDate(order.startDate)} ${formatTime(order.startDate)} - ${formatTime(order.endDate)} + +Vi håber at se dig en anden dag. + +Venlig hilsen Fitness World.` + }) +} \ No newline at end of file diff --git a/server/src/mail/userMovedOrder.ts b/server/src/mail/userMovedOrder.ts new file mode 100644 index 0000000..5d59492 --- /dev/null +++ b/server/src/mail/userMovedOrder.ts @@ -0,0 +1,21 @@ +import { transporter } from "."; +import { smtpAddress } from "../environment"; +import { Order, OrderObject } from "../interfaces/order"; +import User from "../interfaces/user"; +import { formatDate, formatTime } from "../utils/dates"; + +export async function sendUserMovedOrderEmail(user: User, order: Order) { + await transporter.sendMail({ + from: `Fitness World <${smtpAddress}>`, + to: user.email, + subject: "Ordre flyttet - Fitness World", + text: + `Hej, ${user.first_name} ${user.last_name}. + +Din tid ved ${order.trainer.first_name} ${order.trainer.last_name} i ${order.trainer.center_name} er blevet flyttet. + +Vi glæder os til at se dig ${formatDate(order.startDate)} ${formatTime(order.startDate)} - ${formatTime(order.endDate)} + +Venlig hilsen Fitness World.` + }) +} \ No newline at end of file diff --git a/server/src/migrations/populate.ts b/server/src/migrations/populate.ts index 3989082..a9e982c 100644 --- a/server/src/migrations/populate.ts +++ b/server/src/migrations/populate.ts @@ -8,13 +8,13 @@ async function main() { const users = await client.query(` INSERT INTO users (first_name, last_name, email, password_hash, email_verified, is_admin) VALUES -('Filip', 'B P', 'fbp@gmail.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, true), -('User1', 'Lastname', 'u1@test.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), -('User2', 'Lastname', 'u2@test.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), -('User3', 'Lastname', 'u3@test.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), -('Trainer1', 'Lastname', 't1@test.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), -('Trainer2', 'Lastname', 't2@test.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), -('Admin1', 'Lastname', 'a1@test.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, true) +('Filip', 'B P', 'bpfilip@gmail.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, true), +('User1', 'Lastname', 'bpfilip+u1@gmail.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), +('User2', 'Lastname', 'bpfilip+u2@gmail.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), +('User3', 'Lastname', 'bpfilip+u3@gmail.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), +('Trainer1', 'Lastname', 'bpfilip+t1@gmail.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), +('Trainer2', 'Lastname', 'bpfilip+t2@gmail.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, false), +('Admin1', 'Lastname', 'bpfilip+a1@gmail.com', '$2b$10$zxqwqZXo3DrLVTFx.hkQQ.uKeqhHMnok.G/4.Ivq1g647RaqYtgKC', true, true) RETURNING id, email; `); @@ -31,9 +31,9 @@ INSERT INTO trainers (user_id, center_id, hourly_price) VALUES ($3, $4, 20000) RETURNING id, user_id; `, [ - users.rows.find(user => user.email === "t1@test.com").id, + users.rows.find(user => user.email === "bpfilip+t1@gmail.com").id, centers.rows[0].id, - users.rows.find(user => user.email === "t2@test.com").id, + users.rows.find(user => user.email === "bpfilip+t2@gmail.com").id, centers.rows[1].id, ]); @@ -81,8 +81,8 @@ INSERT INTO orders (timeslot_id, user_id, order_status, price, checkout_session) ($7, $1, 'Failed', 20000, '') RETURNING id; `, [ - users.rows.find(user => user.email === "u1@test.com").id, - users.rows.find(user => user.email === "u2@test.com").id, + users.rows.find(user => user.email === "bpfilip+u1@gmail.com").id, + users.rows.find(user => user.email === "bpfilip+u2@gmail.com").id, reserved_timeslots.rows[0].id, reserved_timeslots.rows[1].id, reserved_timeslots.rows[2].id, diff --git a/server/src/routes/createOrder.ts b/server/src/routes/createOrder.ts index b774e1d..cf39d50 100644 --- a/server/src/routes/createOrder.ts +++ b/server/src/routes/createOrder.ts @@ -1,10 +1,6 @@ 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"; @@ -15,10 +11,10 @@ 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); +import { sendOrderConfirmationEmail } from "../mail/orderConfirmation"; +import User from "../interfaces/user"; +import { Order } from "../interfaces/order"; +import { formatDate, formatTime } from "../utils/dates"; const router: Router = express.Router(); @@ -39,21 +35,6 @@ interface TimeslotValidQueryResult { 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(); @@ -137,13 +118,18 @@ router.post("/order", UserAuth, async (req: AuthedRequest, res: Response) => { type: "startDate.invalid" }]); - const emailQuery = await client.query(` - SELECT email FROM users WHERE id = $1; + const userQuery = await client.query(` + SELECT + id, + email, + first_name, + last_name + FROM users WHERE id = $1; `, [ req.user?.userId ]); - const email: string = emailQuery.rows[0].email; + const user: User = userQuery.rows[0]; const priceQuery = await client.query(` SELECT hourly_price FROM trainers WHERE id = $1; @@ -177,7 +163,7 @@ router.post("/order", UserAuth, async (req: AuthedRequest, res: Response) => { mode: 'payment', success_url: `${baseURL}/user/orders`, cancel_url: `${baseURL}`, - customer_email: email + customer_email: user.email }); const insertQuery = await client.query(` @@ -190,7 +176,7 @@ router.post("/order", UserAuth, async (req: AuthedRequest, res: Response) => { INSERT INTO orders (timeslot_id, user_id, price, checkout_session) select id, $4, $5, $6 FROM inserted_reserved_timeslot - RETURNING id; + RETURNING id, created_at; `, [ orderBody.trainer, startDate, @@ -202,10 +188,20 @@ router.post("/order", UserAuth, async (req: AuthedRequest, res: Response) => { const insertedData = insertQuery.rows[0]; + const order: Order = { + id: insertedData.id, + startDate: orderBody.startDate, + endDate: orderBody.endDate, + status: "Processing", + price, + trainer, + created_at: insertedData.created_at + }; + await client.query("COMMIT"); return res.status(200).send({ - id: insertedData.id, + id: order.id, trainerId: orderBody.trainer, startDate: orderBody.startDate, endDate: orderBody.endDate, diff --git a/server/src/routes/order.ts b/server/src/routes/order.ts index f5fec28..55497f6 100644 --- a/server/src/routes/order.ts +++ b/server/src/routes/order.ts @@ -7,7 +7,9 @@ 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 { Order, OrderObject, OrderObjectStatus, OrderStatus, PaymentIntentStatus } from "../interfaces/order"; +import { sendUserCancelledOrderEmail } from "../mail/userCancelledOrder"; +import User from "../interfaces/user"; const router: Router = express.Router(); @@ -20,15 +22,36 @@ interface CancelOrderLookup { 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 + SELECT + orders.id, + order_status, + price, + created_at, + payment_intents.status as payment_intent_status, + start_time as "startDate", + end_time as "endDate", + json_build_object( + 'id',trainers.id, + 'first_name',trainer_user.first_name, + 'last_name',trainer_user.last_name, + 'center_id',trainers.center_id, + 'center_name',centers.name + ) as trainer, + json_build_object( + 'id', order_user.id, + 'first_name', order_user.first_name, + 'last_name', order_user.last_name, + 'email', order_user.email + ) as user + 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 AS trainer_user on trainer_user.id = trainers.user_id + LEFT JOIN users AS order_user on order_user.id = orders.user_id + WHERE orders.id = $1 + AND orders.user_id = $2 AND order_status = 'Confirmed'; `, [ req.params.id, @@ -39,9 +62,11 @@ router.post("/order/:id/cancel", UserAuth, async (req: AuthedRequest, res: Respo return res.sendStatus(404); } - const order: CancelOrderLookup = lookupResult.rows[0]; + const user: User = lookupResult.rows[0].user; + lookupResult.rows[0].user = undefined; + const order: Order = lookupResult.rows[0]; - const dateValidation = Joi.date().min(Date.now()).validate(order.start_time) + const dateValidation = Joi.date().min(Date.now()).validate(order.startDate) if (dateValidation.error !== undefined) { return res.status(400).send({ message: "Timeslot has already occurred" }); } @@ -55,6 +80,8 @@ router.post("/order/:id/cancel", UserAuth, async (req: AuthedRequest, res: Respo req.params.id ]); + sendUserCancelledOrderEmail(user, order); + return res.sendStatus(204); } catch (error: DatabaseError | Error | any) { console.error(error); diff --git a/server/src/routes/orderNewTime.ts b/server/src/routes/orderNewTime.ts index 0b340ad..b434bbf 100644 --- a/server/src/routes/orderNewTime.ts +++ b/server/src/routes/orderNewTime.ts @@ -14,7 +14,10 @@ import { AuthedRequest } from "../interfaces/auth"; import Stripe from 'stripe'; import { stripe } from "../stripe"; import Trainer from "../interfaces/trainer"; +import { Order } from "../interfaces/order"; import { OrderStatus, PaymentIntentStatus } from "../interfaces/order"; +import { sendUserMovedOrderEmail } from "../mail/userMovedOrder"; +import User from "../interfaces/user"; dayjs.extend(isoWeek) dayjs.extend(utc) @@ -49,17 +52,37 @@ interface OrderLookup { 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 + SELECT + orders.id, + order_status, + price, + created_at, + payment_intents.status as payment_intent_status, + start_time as "startDate", + end_time as "endDate", + timeslot_id, + json_build_object( + 'id',trainers.id, + 'first_name',trainer_user.first_name, + 'last_name',trainer_user.last_name, + 'center_id',trainers.center_id, + 'center_name',centers.name + ) as trainer, + json_build_object( + 'id', order_user.id, + 'first_name', order_user.first_name, + 'last_name', order_user.last_name, + 'email', order_user.email + ) as user + 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 AS trainer_user on trainer_user.id = trainers.user_id + LEFT JOIN users AS order_user on order_user.id = orders.user_id + WHERE orders.id = $1 + AND orders.user_id = $2 AND order_status = 'Confirmed'; `, [ req.params.id, @@ -70,10 +93,14 @@ router.put("/order/:id", UserAuth, async (req: AuthedRequest, res: Response) => return res.sendStatus(404); } - const order: OrderLookup = lookupResult.rows[0]; + const user: User = lookupResult.rows[0].user; + lookupResult.rows[0].user = undefined; + const timeslot_id: number = lookupResult.rows[0].timeslot_id; + lookupResult.rows[0].timeslot_id = undefined; + const order: Order = lookupResult.rows[0]; - const originalStartDate: dayjs.Dayjs = dayjs(order.start_time).utcOffset(0); - const originalEndDate: dayjs.Dayjs = dayjs(order.end_time).utcOffset(0); + const originalStartDate: dayjs.Dayjs = dayjs(order.startDate).utcOffset(0); + const originalEndDate: dayjs.Dayjs = dayjs(order.endDate).utcOffset(0); /** The duration of the original timeslot in miliseconds */ const originalDuration: number = originalEndDate.diff(originalStartDate); @@ -83,7 +110,7 @@ router.put("/order/:id", UserAuth, async (req: AuthedRequest, res: Response) => return res.status(400).send(validation.error.details); } - const dateValidation = Joi.date().min(Date.now()).validate(order.start_time) + const dateValidation = Joi.date().min(Date.now()).validate(order.startDate) if (dateValidation.error !== undefined) { return res.status(400).send({ message: "Timeslot has already occurred" }); } @@ -121,7 +148,7 @@ router.put("/order/:id", UserAuth, async (req: AuthedRequest, res: Response) => ) ) as time_already_reserved; `, [ - order.trainer_id, + order.trainer.id, weekday, startTime, endTime, @@ -159,9 +186,14 @@ router.put("/order/:id", UserAuth, async (req: AuthedRequest, res: Response) => `, [ startDate, endDate, - order.timeslot_id + timeslot_id ]); + order.startDate = startDate; + order.endDate = endDate; + + sendUserMovedOrderEmail(user, order); + return res.sendStatus(204); } catch (error: DatabaseError | Error | any) { console.error(error); diff --git a/server/src/routes/register.ts b/server/src/routes/register.ts index 7114b56..e073a42 100644 --- a/server/src/routes/register.ts +++ b/server/src/routes/register.ts @@ -8,6 +8,8 @@ import jsonwebtoken, { JsonWebTokenError } from "jsonwebtoken"; import { DatabaseError } from "pg"; import { UserTokenData } from "../interfaces/auth"; import { private_key } from "../environment" +import User from "../interfaces/user"; +import { sendSignupEmail } from "../mail/signUp"; const router: Router = express.Router(); @@ -32,7 +34,7 @@ router.post("/register", async (req: Request, res: Response) => { const insertResult = await client.query(` INSERT INTO users (first_name, last_name, email, password_hash) VALUES ($1, $2, $3, $4) -RETURNING id; +RETURNING id, first_name, last_name, email; `, [ userData.firstname, userData.lastname, @@ -40,7 +42,7 @@ RETURNING id; password_hash ]); - const user = insertResult.rows[0]; + const user: User = insertResult.rows[0]; const jwtData: UserTokenData = { tokenType: "User", @@ -51,6 +53,8 @@ RETURNING id; res.cookie("auth-token", jwt, { httpOnly: true, maxAge: 60 * 60 * 4 }); + sendSignupEmail(user); + return res.status(200).send({ ...userData, password: undefined }); } catch (error: DatabaseError | Error | any) { if (error.constraint == "users_email_key") { diff --git a/server/src/utils/dates.ts b/server/src/utils/dates.ts new file mode 100644 index 0000000..d444756 --- /dev/null +++ b/server/src/utils/dates.ts @@ -0,0 +1,24 @@ +import dayjs, { Dayjs } from "dayjs" +import isoWeek from "dayjs/plugin/isoWeek" +import utc from "dayjs/plugin/utc" +import LocalizedFormat from "dayjs/plugin/localizedFormat" +import localeDA from "dayjs/locale/da"; + +dayjs.extend(isoWeek) +dayjs.extend(utc) +dayjs.extend(LocalizedFormat); + +export function formatDate(date: Date | Dayjs): string { + let output: string = dayjs(date).locale(localeDA).format("dddd D. MMM YYYY"); + let outputStringArray = output.split(""); + outputStringArray[0] = outputStringArray[0].toLocaleUpperCase(); + output = outputStringArray.join(""); + + return output; +} + +export function formatTime(date: Date | Dayjs): string { + let output: string = dayjs(date).locale(localeDA).format("HH:mm"); + + return output; +} \ No newline at end of file diff --git a/server/src/webhooks/checkoutSessionCompleted.ts b/server/src/webhooks/checkoutSessionCompleted.ts index 15e44c4..5656d4e 100644 --- a/server/src/webhooks/checkoutSessionCompleted.ts +++ b/server/src/webhooks/checkoutSessionCompleted.ts @@ -1,6 +1,9 @@ import { client } from "../db"; import Stripe from 'stripe'; +import { sendOrderConfirmationEmail } from "../mail/orderConfirmation"; +import User from "../interfaces/user"; +import { Order } from "../interfaces/order"; const PaymentIntentStatusMap = { "paid": "Successful", @@ -38,6 +41,7 @@ export default async function checkoutSessionCompleted(event: Stripe.Event) { paymentIntentId, session.id ]); + await client.query(` UPDATE orders SET order_status = 'Confirmed' @@ -47,4 +51,52 @@ export default async function checkoutSessionCompleted(event: Stripe.Event) { `, [ session.id ]); + + const orderLookup = await client.query(` + SELECT + orders.id, + order_status, + price, + 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 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 + checkout_session = $1; + `, [ + session.id + ]); + + const order: Order = orderLookup.rows[0]; + + order.startDate = orderLookup.rows[0].start_time; + order.endDate = orderLookup.rows[0].end_time; + + const userLookup = await client.query(` + SELECT + users.email, + users.id, + users.first_name, + users.last_name + FROM orders + LEFT JOIN users on users.id = orders.user_id + WHERE + checkout_session = $1; + `, [ + session.id + ]); + + const user: User = userLookup.rows[0]; + + sendOrderConfirmationEmail(user, order); } \ No newline at end of file