From 464656cbe2a90b49ec49a8a387492c28a75e1304 Mon Sep 17 00:00:00 2001 From: Jeffery <61447509+jeffplays2005@users.noreply.github.com> Date: Tue, 30 Jul 2024 11:32:08 +1200 Subject: [PATCH] 608 Backend send confirmation on manual booking (#718) * Change /bookings/create-booking to take a single userid Changed tests and updated interfaces * Move create-bookings from BookingController to AdminController Moved corresponding tests too. Next objective is to update frontend * Change client to use /admin/bookings/create Changed client end to use the endpoint specified instead of /bookings/create-bookings * Edit AdminController to use an Array of dates & users Revert change to `UIdssByDateRangeResponse` interface * Add mail service for manual bookings Also fixed some weird conflicts * Add jest mock for nodemailer This fixes the error while running `yarn test` --- .../WrappedAdminBookingCreationPopUp.tsx | 2 +- client/src/models/__generated__/schema.d.ts | 74 +++---- client/src/services/Admin/AdminService.ts | 10 +- server/src/middleware/__generated__/routes.ts | 100 +++++----- .../src/middleware/__generated__/swagger.json | 187 +++++++++--------- server/src/middleware/routes.setup.ts | 8 + .../middleware/tests/AdminController.test.ts | 132 +++++++++++++ .../tests/BookingController.test.ts | 137 ------------- .../controllers/AdminController.ts | 111 +++++++++++ .../controllers/BookingController.ts | 89 +-------- .../request-models/UserRequests.ts | 2 +- 11 files changed, 439 insertions(+), 413 deletions(-) diff --git a/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingCreationPopUp.tsx b/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingCreationPopUp.tsx index a4e6e134c..7e8bd9911 100644 --- a/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingCreationPopUp.tsx +++ b/client/src/components/composite/Admin/AdminBookingView/WrappedAdminBookingCreationPopUp.tsx @@ -43,7 +43,7 @@ const WrappedAdminBookingCreationPopUp = ({ isPending={isPending} isLoading={hasNextPage} bookingCreationHandler={async (startDate, endDate, uid) => { - await handleAddUserToBooking({ startDate, endDate, userIds: [uid] }) + await handleAddUserToBooking({ startDate, endDate, userId: uid }) }} /> diff --git a/client/src/models/__generated__/schema.d.ts b/client/src/models/__generated__/schema.d.ts index 796d500e8..7d67516cd 100644 --- a/client/src/models/__generated__/schema.d.ts +++ b/client/src/models/__generated__/schema.d.ts @@ -48,10 +48,6 @@ export interface paths { */ post: operations["GetBookingPayment"]; }; - "/bookings/create-bookings": { - /** @description An admin method to create bookings for a list of users within a date range. */ - post: operations["CreateBookings"]; - }; "/bookings": { /** @description Fetches all bookings for a user based on their UID. */ get: operations["GetAllBookings"]; @@ -75,6 +71,10 @@ export interface paths { /** @description Decreases availability count to 0 for all booking slots in a date range. */ post: operations["MakeDateUnavailable"]; }; + "/admin/bookings/create": { + /** @description An admin method to create bookings for a list of users within a date range. */ + post: operations["CreateBookings"]; + }; "/admin/bookings/delete": { /** @description Delete a users booking by booking ID. */ post: operations["RemoveBooking"]; @@ -263,22 +263,6 @@ export interface components { /** @description Firestore timestamp, should represent a UTC date that is set to exactly midnight */ endDate?: components["schemas"]["FirebaseFirestore.Timestamp"]; }; - /** @description Represents the response structure for fetching user ids by date range. */ - UIdssByDateRangeResponse: { - data?: { - users: string[]; - date: components["schemas"]["FirebaseFirestore.Timestamp"]; - }[]; - error?: string; - }; - CreateBookingsRequestModel: { - /** @description Firestore timestamp, should represent a UTC date that is set to exactly midnight */ - startDate: components["schemas"]["FirebaseFirestore.Timestamp"]; - /** @description Firestore timestamp, should represent a UTC date that is set to exactly midnight */ - endDate: components["schemas"]["FirebaseFirestore.Timestamp"]; - /** @description List of users to add to the bookings between date range */ - userIds: string[]; - }; AllUserBookingSlotsResponse: { error?: string; message?: string; @@ -376,6 +360,22 @@ export interface components { }; /** @description Construct a type with the properties of T except for those in type K. */ "Omit_MakeDatesAvailableRequestBody.slots_": components["schemas"]["Pick_MakeDatesAvailableRequestBody.Exclude_keyofMakeDatesAvailableRequestBody.slots__"]; + /** @description Represents the response structure for fetching user ids by date range. */ + UIdssByDateRangeResponse: { + data?: { + users: string[]; + date: components["schemas"]["FirebaseFirestore.Timestamp"]; + }[]; + error?: string; + }; + CreateBookingsRequestModel: { + /** @description Firestore timestamp, should represent a UTC date that is set to exactly midnight */ + startDate: components["schemas"]["FirebaseFirestore.Timestamp"]; + /** @description Firestore timestamp, should represent a UTC date that is set to exactly midnight */ + endDate: components["schemas"]["FirebaseFirestore.Timestamp"]; + /** @description List of users to add to the bookings between date range */ + userId: string; + }; BookingDeleteResponse: { error?: string; message?: string; @@ -679,23 +679,6 @@ export interface operations { }; }; }; - /** @description An admin method to create bookings for a list of users within a date range. */ - CreateBookings: { - /** @description - The date range and list of user ids to create bookings for. */ - requestBody: { - content: { - "application/json": components["schemas"]["CreateBookingsRequestModel"]; - }; - }; - responses: { - /** @description Bookings successfully created */ - 200: { - content: { - "application/json": components["schemas"]["UIdssByDateRangeResponse"]; - }; - }; - }; - }; /** @description Fetches all bookings for a user based on their UID. */ GetAllBookings: { responses: { @@ -778,6 +761,23 @@ export interface operations { }; }; }; + /** @description An admin method to create bookings for a list of users within a date range. */ + CreateBookings: { + /** @description - The date range and list of user ids to create bookings for. */ + requestBody: { + content: { + "application/json": components["schemas"]["CreateBookingsRequestModel"]; + }; + }; + responses: { + /** @description Bookings successfully created */ + 200: { + content: { + "application/json": components["schemas"]["UIdssByDateRangeResponse"]; + }; + }; + }; + }; /** @description Delete a users booking by booking ID. */ RemoveBooking: { /** @description - The booking ID to delete. */ diff --git a/client/src/services/Admin/AdminService.ts b/client/src/services/Admin/AdminService.ts index 8b40941aa..24bb3935b 100644 --- a/client/src/services/Admin/AdminService.ts +++ b/client/src/services/Admin/AdminService.ts @@ -143,26 +143,26 @@ const AdminService = { addUsersToBookingForDateRange: async function ({ startDate, endDate, - userIds + userId }: { startDate: Timestamp endDate: Timestamp - userIds: string[] + userId: string }) { const { response, data } = await fetchClient.POST( - "/bookings/create-bookings", + "/admin/bookings/create", { body: { startDate, endDate, - userIds + userId } } ) if (!response.ok) { throw new Error( - `Failed to add the users ${userIds.join(",")} to the date range ${startDate.toString()} to ${endDate.toString()} ` + `Failed to add the user, ${userId} to the date range ${startDate.toString()} to ${endDate.toString()} ` ) } diff --git a/server/src/middleware/__generated__/routes.ts b/server/src/middleware/__generated__/routes.ts index ed1da3678..758cfe4db 100644 --- a/server/src/middleware/__generated__/routes.ts +++ b/server/src/middleware/__generated__/routes.ts @@ -163,25 +163,6 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "UIdssByDateRangeResponse": { - "dataType": "refObject", - "properties": { - "data": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"users":{"dataType":"array","array":{"dataType":"string"},"required":true},"date":{"ref":"FirebaseFirestore.Timestamp","required":true}}}}, - "error": {"dataType":"string"}, - }, - "additionalProperties": false, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "CreateBookingsRequestModel": { - "dataType": "refObject", - "properties": { - "startDate": {"ref":"FirebaseFirestore.Timestamp","required":true}, - "endDate": {"ref":"FirebaseFirestore.Timestamp","required":true}, - "userIds": {"dataType":"array","array":{"dataType":"string"},"required":true}, - }, - "additionalProperties": false, - }, - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "AllUserBookingSlotsResponse": { "dataType": "refObject", "properties": { @@ -297,6 +278,25 @@ const models: TsoaRoute.Models = { "type": {"ref":"Pick_MakeDatesAvailableRequestBody.Exclude_keyofMakeDatesAvailableRequestBody.slots__","validators":{}}, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UIdssByDateRangeResponse": { + "dataType": "refObject", + "properties": { + "data": {"dataType":"array","array":{"dataType":"nestedObjectLiteral","nestedProperties":{"users":{"dataType":"array","array":{"dataType":"string"},"required":true},"date":{"ref":"FirebaseFirestore.Timestamp","required":true}}}}, + "error": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "CreateBookingsRequestModel": { + "dataType": "refObject", + "properties": { + "startDate": {"ref":"FirebaseFirestore.Timestamp","required":true}, + "endDate": {"ref":"FirebaseFirestore.Timestamp","required":true}, + "userId": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "BookingDeleteResponse": { "dataType": "refObject", "properties": { @@ -721,37 +721,6 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - app.post('/bookings/create-bookings', - authenticateMiddleware([{"jwt":["admin"]}]), - ...(fetchMiddlewares(BookingController)), - ...(fetchMiddlewares(BookingController.prototype.createBookings)), - - function BookingController_createBookings(request: ExRequest, response: ExResponse, next: any) { - const args: Record = { - requestBody: {"in":"body","name":"requestBody","required":true,"ref":"CreateBookingsRequestModel"}, - }; - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - let validatedArgs: any[] = []; - try { - validatedArgs = templateService.getValidatedArgs({ args, request, response }); - - const controller = new BookingController(); - - templateService.apiHandler({ - methodName: 'createBookings', - controller, - response, - next, - validatedArgs, - successStatus: 200, - }); - } catch (err) { - return next(err); - } - }); - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/bookings', authenticateMiddleware([{"jwt":["member"]}]), ...(fetchMiddlewares(BookingController)), @@ -907,6 +876,37 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.post('/admin/bookings/create', + authenticateMiddleware([{"jwt":["admin"]}]), + ...(fetchMiddlewares(AdminController)), + ...(fetchMiddlewares(AdminController.prototype.createBookings)), + + function AdminController_createBookings(request: ExRequest, response: ExResponse, next: any) { + const args: Record = { + requestBody: {"in":"body","name":"requestBody","required":true,"ref":"CreateBookingsRequestModel"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args, request, response }); + + const controller = new AdminController(); + + templateService.apiHandler({ + methodName: 'createBookings', + controller, + response, + next, + validatedArgs, + successStatus: 200, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.post('/admin/bookings/delete', authenticateMiddleware([{"jwt":["admin"]}]), ...(fetchMiddlewares(AdminController)), diff --git a/server/src/middleware/__generated__/swagger.json b/server/src/middleware/__generated__/swagger.json index cf4498544..54664ae0e 100644 --- a/server/src/middleware/__generated__/swagger.json +++ b/server/src/middleware/__generated__/swagger.json @@ -344,63 +344,6 @@ "type": "object", "additionalProperties": false }, - "UIdssByDateRangeResponse": { - "description": "Represents the response structure for fetching user ids by date range.", - "properties": { - "data": { - "items": { - "properties": { - "users": { - "items": { - "type": "string" - }, - "type": "array" - }, - "date": { - "$ref": "#/components/schemas/FirebaseFirestore.Timestamp" - } - }, - "required": [ - "users", - "date" - ], - "type": "object" - }, - "type": "array" - }, - "error": { - "type": "string" - } - }, - "type": "object", - "additionalProperties": false - }, - "CreateBookingsRequestModel": { - "properties": { - "startDate": { - "$ref": "#/components/schemas/FirebaseFirestore.Timestamp", - "description": "Firestore timestamp, should represent a UTC date that is set to exactly midnight" - }, - "endDate": { - "$ref": "#/components/schemas/FirebaseFirestore.Timestamp", - "description": "Firestore timestamp, should represent a UTC date that is set to exactly midnight" - }, - "userIds": { - "items": { - "type": "string" - }, - "type": "array", - "description": "List of users to add to the bookings between date range" - } - }, - "required": [ - "startDate", - "endDate", - "userIds" - ], - "type": "object", - "additionalProperties": false - }, "AllUserBookingSlotsResponse": { "properties": { "error": { @@ -700,6 +643,60 @@ "$ref": "#/components/schemas/Pick_MakeDatesAvailableRequestBody.Exclude_keyofMakeDatesAvailableRequestBody.slots__", "description": "Construct a type with the properties of T except for those in type K." }, + "UIdssByDateRangeResponse": { + "description": "Represents the response structure for fetching user ids by date range.", + "properties": { + "data": { + "items": { + "properties": { + "users": { + "items": { + "type": "string" + }, + "type": "array" + }, + "date": { + "$ref": "#/components/schemas/FirebaseFirestore.Timestamp" + } + }, + "required": [ + "users", + "date" + ], + "type": "object" + }, + "type": "array" + }, + "error": { + "type": "string" + } + }, + "type": "object", + "additionalProperties": false + }, + "CreateBookingsRequestModel": { + "properties": { + "startDate": { + "$ref": "#/components/schemas/FirebaseFirestore.Timestamp", + "description": "Firestore timestamp, should represent a UTC date that is set to exactly midnight" + }, + "endDate": { + "$ref": "#/components/schemas/FirebaseFirestore.Timestamp", + "description": "Firestore timestamp, should represent a UTC date that is set to exactly midnight" + }, + "userId": { + "type": "string", + "description": "List of users to add to the bookings between date range" + } + }, + "required": [ + "startDate", + "endDate", + "userId" + ], + "type": "object", + "additionalProperties": false + }, "BookingDeleteResponse": { "properties": { "error": { @@ -1421,44 +1418,6 @@ } } }, - "/bookings/create-bookings": { - "post": { - "operationId": "CreateBookings", - "responses": { - "200": { - "description": "Bookings successfully created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UIdssByDateRangeResponse" - } - } - } - } - }, - "description": "An admin method to create bookings for a list of users within a date range.", - "security": [ - { - "jwt": [ - "admin" - ] - } - ], - "parameters": [], - "requestBody": { - "description": "- The date range and list of user ids to create bookings for.", - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateBookingsRequestModel", - "description": "- The date range and list of user ids to create bookings for." - } - } - } - } - } - }, "/bookings": { "get": { "operationId": "GetAllBookings", @@ -1637,6 +1596,44 @@ } } }, + "/admin/bookings/create": { + "post": { + "operationId": "CreateBookings", + "responses": { + "200": { + "description": "Bookings successfully created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UIdssByDateRangeResponse" + } + } + } + } + }, + "description": "An admin method to create bookings for a list of users within a date range.", + "security": [ + { + "jwt": [ + "admin" + ] + } + ], + "parameters": [], + "requestBody": { + "description": "- The date range and list of user ids to create bookings for.", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBookingsRequestModel", + "description": "- The date range and list of user ids to create bookings for." + } + } + } + } + } + }, "/admin/bookings/delete": { "post": { "operationId": "RemoveBooking", diff --git a/server/src/middleware/routes.setup.ts b/server/src/middleware/routes.setup.ts index 4ca46302c..74c33eda6 100644 --- a/server/src/middleware/routes.setup.ts +++ b/server/src/middleware/routes.setup.ts @@ -19,6 +19,14 @@ import { cleanAuth, cleanFirestore } from "test-config/TestUtils" /** * This needs to be updated as we add more stripe functions... */ +jest.mock("nodemailer", () => ({ + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: () => { + return { messageId: "test" } + } + })) +})) + jest.mock("stripe", () => { return { __esModule: true, diff --git a/server/src/middleware/tests/AdminController.test.ts b/server/src/middleware/tests/AdminController.test.ts index d857fd5cb..0af3dc379 100644 --- a/server/src/middleware/tests/AdminController.test.ts +++ b/server/src/middleware/tests/AdminController.test.ts @@ -475,6 +475,138 @@ describe("AdminController endpoint tests", () => { }) }) + describe("/admin/bookings/create", () => { + it("should create bookings a user within the date range", async () => { + const bookingSlotService = new BookingSlotService() + + const startDate = dateToFirestoreTimeStamp(new Date("01/01/2022")) + const endDate = dateToFirestoreTimeStamp(new Date("12/31/2023")) + + await bookingSlotService.createBookingSlot({ + date: dateToFirestoreTimeStamp(new Date("02/01/2023")), + max_bookings: 10 + }) + + await bookingSlotService.createBookingSlot({ + date: dateToFirestoreTimeStamp(new Date("03/01/2023")), + max_bookings: 10 + }) + + // Important test case, don't return dates with no bookings + await bookingSlotService.createBookingSlot({ + date: dateToFirestoreTimeStamp(new Date("01/01/2023")), + max_bookings: 10 + }) + + const res = await request + .post("/admin/bookings/create") + .set("Authorization", `Bearer ${adminToken}`) + .send({ + startDate, + endDate, + userId: MEMBER_USER_UID + }) + + expect(res.status).toEqual(200) + expect(res.body.data).toHaveLength(3) + expect.arrayContaining([ + expect.objectContaining({ + users: expect.arrayContaining([ + expect.objectContaining({ uid: MEMBER_USER_UID }) + ]) + }) + ]) + }) + + it("should return unauthorized error for non-admin users", async () => { + const startDate = dateToFirestoreTimeStamp(new Date("01/01/2023")) + const endDate = dateToFirestoreTimeStamp(new Date("12/31/2023")) + + const res = await request + .post("/admin/bookings/create") + .set("Authorization", `Bearer ${memberToken}`) + .send({ + startDate, + endDate, + userId: undefined + }) + + expect(res.status).toEqual(401) + }) + + it("Shouldn't duplicate members in the same slot", async () => { + const bookingSlotService = new BookingSlotService() + const bookingDataService = new BookingDataService() + + const startDate = dateToFirestoreTimeStamp(new Date("01/01/2022")) + const endDate = dateToFirestoreTimeStamp(new Date("12/31/2023")) + + const slot1 = await bookingSlotService.createBookingSlot({ + date: dateToFirestoreTimeStamp(new Date("02/01/2023")), + max_bookings: 10 + }) + + await bookingSlotService.createBookingSlot({ + date: dateToFirestoreTimeStamp(new Date("01/01/2024")), + max_bookings: 10 + }) + + await bookingDataService.createBooking({ + user_id: MEMBER_USER_UID, + booking_slot_id: slot1.id, + stripe_payment_id: "" + }) + + let res = await request + .post("/admin/bookings/create") + .set("Authorization", `Bearer ${adminToken}`) + .send({ + startDate, + endDate, + userId: MEMBER_USER_UID + }) + + expect(res.status).toEqual(200) + expect(res.body.data).toHaveLength(1) + expect.arrayContaining([ + expect.objectContaining({ + users: expect.arrayContaining([ + expect.objectContaining({ uid: MEMBER_USER_UID }) + ]) + }) + ]) + + expect( + ( + await bookingSlotService.getBookingSlotsBetweenDateRange( + startDate, + endDate + ) + ).length + ).toEqual(1) + + const newEndDate = dateToFirestoreTimeStamp(new Date("01/01/2024")) + + res = await request + .post("/admin/bookings/create") + .set("Authorization", `Bearer ${adminToken}`) + .send({ + startDate, + newEndDate, + userId: MEMBER_USER_UID + }) + + expect( + ( + await bookingSlotService.getBookingSlotsBetweenDateRange( + startDate, + newEndDate + ) + ).length + ).toEqual(2) + }) + }) + describe("/admin/bookings/delete", () => { it("should error on deleting invalid booking id", async () => { const res = await request diff --git a/server/src/middleware/tests/BookingController.test.ts b/server/src/middleware/tests/BookingController.test.ts index c82cfbf19..b20daaa3b 100644 --- a/server/src/middleware/tests/BookingController.test.ts +++ b/server/src/middleware/tests/BookingController.test.ts @@ -325,141 +325,4 @@ describe("BookingController endpoint tests", () => { expect(res.status).toEqual(401) }) }) - - describe("/bookings/create-bookings", () => { - it("should create bookings for userIds within the date range", async () => { - const bookingSlotService = new BookingSlotService() - - const startDate = dateToFirestoreTimeStamp(new Date("01/01/2022")) - const endDate = dateToFirestoreTimeStamp(new Date("12/31/2023")) - - await bookingSlotService.createBookingSlot({ - date: dateToFirestoreTimeStamp(new Date("02/01/2023")), - max_bookings: 10 - }) - - await bookingSlotService.createBookingSlot({ - date: dateToFirestoreTimeStamp(new Date("03/01/2023")), - max_bookings: 10 - }) - - // Important test case, don't return dates with no bookings - await bookingSlotService.createBookingSlot({ - date: dateToFirestoreTimeStamp(new Date("01/01/2023")), - max_bookings: 10 - }) - - const res = await request - .post("/bookings/create-bookings") - .set("Authorization", `Bearer ${adminToken}`) - .send({ - startDate, - endDate, - userIds: [GUEST_USER_UID, MEMBER_USER_UID] - }) - - expect(res.status).toEqual(200) - expect(res.body.data).toHaveLength(3) - expect.arrayContaining([ - expect.objectContaining({ - users: expect.arrayContaining([ - expect.objectContaining({ uid: MEMBER_USER_UID }) - ]) - }), - expect.objectContaining({ - users: expect.arrayContaining([ - expect.objectContaining({ uid: GUEST_USER_UID }) - ]) - }) - ]) - }) - - it("should return unauthorized error for non-admin users", async () => { - const startDate = dateToFirestoreTimeStamp(new Date("01/01/2023")) - const endDate = dateToFirestoreTimeStamp(new Date("12/31/2023")) - - const res = await request - .post("/bookings/create-bookings") - .set("Authorization", `Bearer ${memberToken}`) - .send({ - startDate, - endDate, - userIds: [] - }) - - expect(res.status).toEqual(401) - }) - - it("Shouldn't duplicate members in the same slot", async () => { - const bookingSlotService = new BookingSlotService() - const bookingDataService = new BookingDataService() - - const startDate = dateToFirestoreTimeStamp(new Date("01/01/2022")) - const endDate = dateToFirestoreTimeStamp(new Date("12/31/2023")) - - const slot1 = await bookingSlotService.createBookingSlot({ - date: dateToFirestoreTimeStamp(new Date("02/01/2023")), - max_bookings: 10 - }) - - await bookingSlotService.createBookingSlot({ - date: dateToFirestoreTimeStamp(new Date("01/01/2024")), - max_bookings: 10 - }) - - await bookingDataService.createBooking({ - user_id: MEMBER_USER_UID, - booking_slot_id: slot1.id, - stripe_payment_id: "" - }) - - let res = await request - .post("/bookings/create-bookings") - .set("Authorization", `Bearer ${adminToken}`) - .send({ - startDate, - endDate, - userIds: [MEMBER_USER_UID] - }) - - expect(res.status).toEqual(200) - expect(res.body.data).toHaveLength(1) - expect.arrayContaining([ - expect.objectContaining({ - users: expect.arrayContaining([ - expect.objectContaining({ uid: MEMBER_USER_UID }) - ]) - }) - ]) - - expect( - ( - await bookingSlotService.getBookingSlotsBetweenDateRange( - startDate, - endDate - ) - ).length - ).toEqual(1) - - const newEndDate = dateToFirestoreTimeStamp(new Date("01/01/2024")) - - res = await request - .post("/bookings/create-bookings") - .set("Authorization", `Bearer ${adminToken}`) - .send({ - startDate, - newEndDate, - userIds: [MEMBER_USER_UID] - }) - - expect( - ( - await bookingSlotService.getBookingSlotsBetweenDateRange( - startDate, - newEndDate - ) - ).length - ).toEqual(2) - }) - }) }) diff --git a/server/src/service-layer/controllers/AdminController.ts b/server/src/service-layer/controllers/AdminController.ts index db395ea83..032a7e345 100644 --- a/server/src/service-layer/controllers/AdminController.ts +++ b/server/src/service-layer/controllers/AdminController.ts @@ -4,6 +4,7 @@ import { EMPTY_BOOKING_SLOTS } from "business-layer/utils/BookingConstants" import { + UTCDateToDdMmYyyy, firestoreTimestampToDate, timestampsInRange } from "data-layer/adapters/DateUtils" @@ -17,12 +18,14 @@ import { MakeDatesAvailableRequestBody } from "service-layer/request-models/AdminRequests" import { + CreateBookingsRequestModel, CreateUserRequestBody, DemoteUserRequestBody, EditUsersRequestBody, PromoteUserRequestBody } from "service-layer/request-models/UserRequests" import { + UIdssByDateRangeResponse, BookingDeleteResponse, BookingSlotUpdateResponse } from "service-layer/response-models/BookingResponse" @@ -47,6 +50,8 @@ import * as console from "console" import StripeService from "../../business-layer/services/StripeService" import { UserAccountTypes } from "../../business-layer/utils/AuthServiceClaims" import { UserRecord } from "firebase-admin/auth" +import { Timestamp } from "firebase-admin/firestore" +import MailService from "business-layer/services/MailService" @Route("admin") @Security("jwt", ["admin"]) @@ -164,6 +169,112 @@ export class AdminController extends Controller { } } + /** + * An admin method to create bookings for a list of users within a date range. + * @param requestBody - The date range and list of user ids to create bookings for. + * @returns A list of users and timestamps that were successfully added to the booking slots. + */ + @SuccessResponse("200", "Bookings successfully created") + @Post("/bookings/create") + public async createBookings( + @Body() requestBody: CreateBookingsRequestModel + ): Promise { + try { + const { startDate, endDate, userId } = requestBody + + const responseData: Array<{ + date: Timestamp + users: string[] + }> = [] + + /** Creating instances of the required services */ + const bookingSlotService = new BookingSlotService() + const bookingDataService = new BookingDataService() + + // Query to get all booking slots within date range + const bookingSlots = + await bookingSlotService.getBookingSlotsBetweenDateRange( + startDate, + endDate + ) + + /** Iterating through each booking slot */ + const bookingPromises = bookingSlots.map(async (slot) => { + /** For every slotid add a booking for that id only if user doesn't already have a booking */ + const existingBooking = + await bookingDataService.getBookingsByUserId(userId) + if ( + !existingBooking.some( + (booking) => booking.booking_slot_id === slot.id + ) + ) { + await bookingDataService.createBooking({ + user_id: userId, + booking_slot_id: slot.id, + stripe_payment_id: "manual_entry" + }) + } + responseData.push({ + date: slot.date, + users: [userId] + }) + }) + + await Promise.all(bookingPromises) + + this.setStatus(200) + /** + * Send confirmation using MailService so that admins do not need to manually + * followup on manual bookings. + */ + const mailService = new MailService() + + try { + const { first_name, last_name } = + await new UserDataService().getUserData(userId) + const [userAuthData] = await new AuthService().bulkRetrieveUsersByUids([ + { uid: userId } + ]) + /** + * Used for formatted display to user + */ + const BOOKING_START_DATE = UTCDateToDdMmYyyy( + new Date(firestoreTimestampToDate(startDate)) + ) + const BOOKING_END_DATE = UTCDateToDdMmYyyy( + new Date(firestoreTimestampToDate(endDate)) + ) + mailService.sendBookingConfirmationEmail( + userAuthData.email, + `${first_name} ${last_name}`, + BOOKING_START_DATE, + BOOKING_END_DATE + ) + } catch (e) { + console.error( + `Was unable to send a confirmation email for manual booking` + ) + return { + data: responseData.filter((data) => !!data), + error: `Was unable to send a confirmation email for manual booking` + } + } + + /** + * Returning the response data + * + * The filter is required to not include data that is null + * because of the early return in the map + */ + return { data: responseData.filter((data) => !!data) } + } catch (e) { + console.error("Error in getBookingsByDateRange:", e) + this.setStatus(500) + + return { error: "Something went wrong" } + } + } + /** * Delete a users booking by booking ID. * @param requestBody - The booking ID to delete. diff --git a/server/src/service-layer/controllers/BookingController.ts b/server/src/service-layer/controllers/BookingController.ts index 449daa808..a0e5117d7 100644 --- a/server/src/service-layer/controllers/BookingController.ts +++ b/server/src/service-layer/controllers/BookingController.ts @@ -1,17 +1,13 @@ import { AvailableDatesRequestModel, - BookingsByDateRangeRequestModel, - CreateBookingsRequestModel + BookingsByDateRangeRequestModel } from "service-layer/request-models/UserRequests" import { AvailableDatesResponse } from "service-layer/response-models/PaymentResponse" import { Timestamp } from "firebase-admin/firestore" import BookingDataService from "data-layer/services/BookingDataService" import BookingSlotService from "data-layer/services/BookingSlotsService" -import { - AllUserBookingSlotsResponse, - UIdssByDateRangeResponse -} from "service-layer/response-models/BookingResponse" +import { AllUserBookingSlotsResponse } from "service-layer/response-models/BookingResponse" import { AllUserBookingsRequestBody } from "service-layer/request-models/BookingRequests" import { Controller, @@ -42,87 +38,6 @@ import BookingUtils from "business-layer/utils/BookingUtils" @Route("bookings") export class BookingController extends Controller { - /** - * An admin method to create bookings for a list of users within a date range. - * @param requestBody - The date range and list of user ids to create bookings for. - * @returns A list of users and timestamps that were successfully added to the booking slots. - */ - @SuccessResponse("200", "Bookings successfully created") - @Security("jwt", ["admin"]) - @Post("create-bookings") - public async createBookings( - @Body() requestBody: CreateBookingsRequestModel - ): Promise { - try { - const { startDate, endDate } = requestBody - - /** Creating instances of the required services */ - const bookingSlotService = new BookingSlotService() - const bookingDataService = new BookingDataService() - - // Query to get all booking slots within date range - const bookingSlots = - await bookingSlotService.getBookingSlotsBetweenDateRange( - startDate, - endDate - ) - - /** The response data array */ - const responseData: Array<{ - date: Timestamp - users: string[] - }> = [] - - /** Iterating through each booking slot */ - const bookingPromises = bookingSlots.map(async (slot) => { - let userIds = [...requestBody.userIds] - /** For every slotid add a booking for that id only if user doesn't already have a booking */ - const userIdsPromises = userIds.map(async (userId) => { - const existingBookingsForUser = - await bookingDataService.getBookingsByUserId(userId) - if ( - existingBookingsForUser.some( - (booking) => booking.booking_slot_id === slot.id - ) - ) { - userIds = userIds.filter((id) => id !== userId) // Remove user from list if they already have a booking - } else { - await bookingDataService.createBooking({ - user_id: userId, - booking_slot_id: slot.id, - stripe_payment_id: "manual_entry" - }) - } - }) - - /** List of usersIds successfully added */ - responseData.push({ - date: slot.date, - users: userIds - }) - - await Promise.all(userIdsPromises) - }) - - await Promise.all(bookingPromises) - - this.setStatus(200) - - /** - * Returning the response data - * - * The filter is required to not include data that is null - * because of the early return in the map - */ - return { data: responseData.filter((data) => !!data) } - } catch (e) { - console.error("Error in getBookingsByDateRange:", e) - this.setStatus(500) - - return { error: "Something went wrong" } - } - } - /** * Fetches all bookings for a user based on their UID. * @param request - The request object that includes the UserRecord. diff --git a/server/src/service-layer/request-models/UserRequests.ts b/server/src/service-layer/request-models/UserRequests.ts index 8fe2e1e13..8c3c9d100 100644 --- a/server/src/service-layer/request-models/UserRequests.ts +++ b/server/src/service-layer/request-models/UserRequests.ts @@ -82,5 +82,5 @@ export interface CreateBookingsRequestModel /** * List of users to add to the bookings between date range */ - userIds: string[] + userId: string }