From 81712b7b78647d3a334cabb4010e23fb72453c26 Mon Sep 17 00:00:00 2001 From: jason-duong4509 Date: Thu, 26 Mar 2026 16:02:59 -0600 Subject: [PATCH 1/3] SANC-94: Changed Bookings Schema and Fixed Bug - Made schema changes in the bookings table so that bookings.agency_id references the right column (organization.id not user.id) - Propagated the schema changes to affected files - Fixed a bug where agency users could not see bookings made by their agency --- drizzle/seed.ts | 27 ++++++++++++++++++++++++--- src/app/debug/bookings/page.tsx | 6 +++--- src/server/api/routers/bookings.ts | 18 ++++++++++++------ src/server/api/routers/trip.ts | 2 +- src/server/db/booking-schema.ts | 8 ++++---- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/drizzle/seed.ts b/drizzle/seed.ts index 8b82d9a..8b4284b 100644 --- a/drizzle/seed.ts +++ b/drizzle/seed.ts @@ -340,6 +340,13 @@ async function seedBookings() { const agencyUser = agencyUsers[i % agencyUsers.length]; if (!agencyUser) continue; + const userOrg = await db + .select() + .from(member) + .where(eq(member.userId, agencyUser.id)) + .limit(1) + .then((r) => r[0]); + if (!userOrg) continue; // User somehow does not belong to an org bookingData.push({ title: `${purposes[i % purposes.length]} - ${passengerNames[i % passengerNames.length]}`, @@ -350,7 +357,7 @@ async function seedBookings() { phoneNumber: `+1 (403) ${Math.floor(Math.random() * 900 + 100)}-${Math.floor(Math.random() * 9000 + 1000)}`, surveyCompleted: status === BookingStatus.COMPLETED ? Math.random() > 0.5 : false, status, - agencyId: agencyUser.id, + agencyId: userOrg.organizationId, startTime: startTime.toISOString(), endTime: endTime.toISOString(), driverId: driver.id, @@ -373,6 +380,13 @@ async function seedBookings() { const agencyUser = agencyUsers[(pastCount + i) % agencyUsers.length]; if (!agencyUser) continue; + const userOrg = await db + .select() + .from(member) + .where(eq(member.userId, agencyUser.id)) + .limit(1) + .then((r) => r[0]); + if (!userOrg) continue; // User somehow does not belong to an org bookingData.push({ title: `${purposes[(pastCount + i) % purposes.length]} - ${passengerNames[(pastCount + i) % passengerNames.length]}`, @@ -383,7 +397,7 @@ async function seedBookings() { phoneNumber: `+1 (403) ${Math.floor(Math.random() * 900 + 100)}-${Math.floor(Math.random() * 9000 + 1000)}`, surveyCompleted: false, status, - agencyId: agencyUser.id, + agencyId: userOrg.organizationId, startTime: startTime.toISOString(), endTime: endTime.toISOString(), driverId: driver.id, @@ -404,6 +418,13 @@ async function seedBookings() { const agencyUser = agencyUsers[(pastCount + todayCount + i) % agencyUsers.length]; if (!agencyUser) continue; + const userOrg = await db + .select() + .from(member) + .where(eq(member.userId, agencyUser.id)) + .limit(1) + .then((r) => r[0]); + if (!userOrg) continue; // User somehow does not belong to an org bookingData.push({ title: `${purposes[(pastCount + todayCount + i) % purposes.length]} - ${passengerNames[(pastCount + todayCount + i) % passengerNames.length]}`, @@ -415,7 +436,7 @@ async function seedBookings() { phoneNumber: `+1 (403) ${Math.floor(Math.random() * 900 + 100)}-${Math.floor(Math.random() * 9000 + 1000)}`, surveyCompleted: false, status: "incomplete" as BookingStatus, - agencyId: agencyUser.id, + agencyId: userOrg.organizationId, startTime: startTime.toISOString(), endTime: endTime.toISOString(), driverId: driver.id, diff --git a/src/app/debug/bookings/page.tsx b/src/app/debug/bookings/page.tsx index f38ded5..6227b6a 100644 --- a/src/app/debug/bookings/page.tsx +++ b/src/app/debug/bookings/page.tsx @@ -43,7 +43,7 @@ function formatTimeSlot(startTime: string, endTime: string): string { return `${start} – ${end}`; } -/** Example pre-filled booking for testing. agencyId must be a valid user.id; set dynamically from getCurrentUser. */ +/** Example pre-filled booking for testing. agencyId must be a valid organization.id; set dynamically from getCurrentUser. */ const EXAMPLE_BOOKING = { title: "Test Example", pickupAddress: "The Inn from the Cold, 110 11 Ave SE, Calgary, AB", @@ -150,7 +150,7 @@ export default function BookingDebugPage() { phoneNumber: EXAMPLE_BOOKING.phoneNumber ?? "", start: EXAMPLE_BOOKING.start, // will be kept in sync with picker (string) end: "", // will be kept in sync with picker (string), auto-calculated - agencyId: "", // set from getCurrentUser (must be valid user.id for FK) + agencyId: "", // set from getCurrentUser (must be valid organization.id for FK) purpose: EXAMPLE_BOOKING.purpose, driverId: "", status: BookingStatus.INCOMPLETE, @@ -183,7 +183,7 @@ export default function BookingDebugPage() { // Set agencyId from current user so it references a valid user (fixes FK constraint) useEffect(() => { if (currentUserQuery.data && !form.values.agencyId) { - form.setFieldValue("agencyId", currentUserQuery.data.id); + form.setFieldValue("agencyId", currentUserQuery.data.agencyId); } }, [currentUserQuery.data, form.setFieldValue, form.values.agencyId]); diff --git a/src/server/api/routers/bookings.ts b/src/server/api/routers/bookings.ts index d667d56..b5c8629 100644 --- a/src/server/api/routers/bookings.ts +++ b/src/server/api/routers/bookings.ts @@ -35,16 +35,19 @@ type DbContext = { db: typeof db }; /** * Throws if the session user is not admin and not the booking's agency. - * @param session - Session with user id and role + * @param session - Session with user id, role and affiliated agency * @param agencyId - Booking's agency id * @throws TRPCError FORBIDDEN when not allowed */ function assertCanAccessBooking( - session: { user: { id: string; role?: string | null } }, + session: { + user: { id: string; role?: string | null }; + session: { activeOrganizationId?: string | null | undefined }; + }, agencyId: string, ): void { const role = session.user.role ?? "user"; - const allowed = role === "admin" || agencyId === session.user.id; + const allowed = role === "admin" || agencyId === session.session.activeOrganizationId; if (!allowed) { throw new TRPCError({ code: "FORBIDDEN", @@ -245,6 +248,7 @@ export const bookingsRouter = createTRPCRouter({ return { id: ctx.session.user.id, role: ctx.session.user.role ?? "user", + agencyId: ctx.session.session.activeOrganizationId ?? "", }; }), @@ -386,8 +390,9 @@ export const bookingsRouter = createTRPCRouter({ const userId = ctx.session.user.id; const role = ctx.session.user.role ?? "user"; - // Only allow admins to specify agencyId; non-admins use their own ID - const agencyId = role === "admin" ? input.agencyId : userId; + // Only allow admins to specify agencyId; non-admins use their own agency ID + const agencyId = + role === "admin" ? input.agencyId : (ctx.session.session.activeOrganizationId ?? ""); const bookingData: BookingInsertType = { title: input.title, @@ -467,6 +472,7 @@ export const bookingsRouter = createTRPCRouter({ const userId = ctx.session.user.id; const role = ctx.session.user.role ?? "user"; const startDate = input?.startDate ?? "1970-01-01T00:00:00-07:00"; + const agencyId = ctx.session.session.activeOrganizationId ?? ""; let endDate = input?.endDate ?? ""; if (input === undefined || input.endDate === undefined) { @@ -516,7 +522,7 @@ export const bookingsRouter = createTRPCRouter({ and( or( eq(bookings.createdBy, userId), - eq(bookings.agencyId, userId), + eq(bookings.agencyId, agencyId), eq(bookings.driverId, userId), ), ...conditions, diff --git a/src/server/api/routers/trip.ts b/src/server/api/routers/trip.ts index b1410e2..7e2303e 100644 --- a/src/server/api/routers/trip.ts +++ b/src/server/api/routers/trip.ts @@ -26,7 +26,7 @@ export const tripRouter = createTRPCRouter({ destinationAddress: input.destinationAddress, passengerInfo: `${input.residentName}|${input.phoneNumber}|${input.additionalInfo || ""}`, phoneNumber: input.phoneNumber, - agencyId: ctx.session.user.id, + agencyId: ctx.session.session.activeOrganizationId ?? "", purpose: input.purpose, createdBy: ctx.session.user.id, startTime: input.startTime, diff --git a/src/server/db/booking-schema.ts b/src/server/db/booking-schema.ts index 722a378..4ee0953 100644 --- a/src/server/db/booking-schema.ts +++ b/src/server/db/booking-schema.ts @@ -1,7 +1,7 @@ import { type InferInsertModel, type InferSelectModel, relations } from "drizzle-orm"; import { boolean, index, pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core"; import { BOOKING_STATUSES, BookingStatus } from "@/types/types"; -import { user } from "./auth-schema"; +import { organization, user } from "./auth-schema"; export const bookings = pgTable( "bookings", @@ -18,7 +18,7 @@ export const bookings = pgTable( // the agency that created the booking agencyId: text("agency_id") .notNull() - .references(() => user.id, { onDelete: "cascade" }), + .references(() => organization.id, { onDelete: "cascade" }), startTime: timestamp("start_time", { mode: "string", withTimezone: true, @@ -51,9 +51,9 @@ export const bookings = pgTable( ); export const bookingsRelations = relations(bookings, ({ one }) => ({ - agency: one(user, { + agency: one(organization, { fields: [bookings.agencyId], - references: [user.id], + references: [organization.id], relationName: "agencyBookings", }), driver: one(user, { From da5db92074d96a58d40abcf4361b10921cd88ed1 Mon Sep 17 00:00:00 2001 From: jason-duong4509 Date: Wed, 1 Apr 2026 19:53:41 -0600 Subject: [PATCH 2/3] SANC-94: Minor Changes - Moved relation declaration from userRelations to organizationRelations since the bookings.agency_id FK moved from users to organizations - Added error handling for when a user does not belong to an organization --- src/server/api/routers/bookings.ts | 28 ++++++++++++++++++++++++---- src/server/api/routers/trip.ts | 9 ++++++++- src/server/db/auth-schema.ts | 2 +- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/server/api/routers/bookings.ts b/src/server/api/routers/bookings.ts index b988e6c..d3afcae 100644 --- a/src/server/api/routers/bookings.ts +++ b/src/server/api/routers/bookings.ts @@ -246,10 +246,16 @@ async function validateDriverForSlot( export const bookingsRouter = createTRPCRouter({ /** Returns the current user id and role for the debug form default agencyId. */ getCurrentUser: protectedProcedure.query(async ({ ctx }) => { + if (!ctx.session.session.activeOrganizationId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Not active organization ID set", + }); + } return { id: ctx.session.user.id, role: ctx.session.user.role ?? "user", - agencyId: ctx.session.session.activeOrganizationId ?? "", + agencyId: ctx.session.session.activeOrganizationId, }; }), @@ -401,9 +407,15 @@ export const bookingsRouter = createTRPCRouter({ const userId = ctx.session.user.id; const role = ctx.session.user.role ?? "user"; + if (!ctx.session.session.activeOrganizationId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Not active organization ID set", + }); + } + // Only allow admins to specify agencyId; non-admins use their own agency ID - const agencyId = - role === "admin" ? input.agencyId : (ctx.session.session.activeOrganizationId ?? ""); + const agencyId = role === "admin" ? input.agencyId : ctx.session.session.activeOrganizationId; const bookingData: BookingInsertType = { title: input.title, @@ -483,9 +495,17 @@ export const bookingsRouter = createTRPCRouter({ const userId = ctx.session.user.id; const role = ctx.session.user.role ?? "user"; const startDate = input?.startDate ?? "1970-01-01T00:00:00-07:00"; - const agencyId = ctx.session.session.activeOrganizationId ?? ""; let endDate = input?.endDate ?? ""; + if (!ctx.session.session.activeOrganizationId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Not active organization ID set", + }); + } + + const agencyId = ctx.session.session.activeOrganizationId; + if (input === undefined || input.endDate === undefined) { // No end date given; use explicit format so result matches isoTimeRegexFourDigitYears (-07:00) endDate = dayjs() diff --git a/src/server/api/routers/trip.ts b/src/server/api/routers/trip.ts index f378777..1e114cc 100644 --- a/src/server/api/routers/trip.ts +++ b/src/server/api/routers/trip.ts @@ -24,6 +24,13 @@ export const tripRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { + if (!ctx.session.session.activeOrganizationId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Not active organization ID set", + }); + } + const [inserted] = await ctx.db .insert(bookings) .values({ @@ -32,7 +39,7 @@ export const tripRouter = createTRPCRouter({ destinationAddress: input.destinationAddress, passengerInfo: `${input.residentName}|${input.phoneNumber}|${input.additionalInfo || ""}`, phoneNumber: input.phoneNumber, - agencyId: ctx.session.session.activeOrganizationId ?? "", + agencyId: ctx.session.session.activeOrganizationId, purpose: input.purpose, createdBy: ctx.session.user.id, startTime: input.startTime, diff --git a/src/server/db/auth-schema.ts b/src/server/db/auth-schema.ts index b5c85c3..16b4321 100644 --- a/src/server/db/auth-schema.ts +++ b/src/server/db/auth-schema.ts @@ -107,12 +107,12 @@ export const invitation = pgTable("invitation", { export const userRelations = relations(user, ({ many }) => ({ driverBookings: many(bookings, { relationName: "driverBookings" }), - agencyBookings: many(bookings, { relationName: "agencyBookings" }), memberships: many(member), })); export const organizationRelations = relations(organization, ({ many }) => ({ members: many(member), + agencyBookings: many(bookings, { relationName: "agencyBookings" }), })); export const memberRelations = relations(member, ({ one, many }) => ({ From b94e5bbd09315abceefc300e3e01fb0cbc4c597b Mon Sep 17 00:00:00 2001 From: jason-duong4509 Date: Mon, 6 Apr 2026 15:02:43 -0600 Subject: [PATCH 3/3] SANC-94: Changed some Wording to Improve Clarity - Some error messages were spelt wrong. They have been corrected - A comment was outdated due to changes made in this ticket. That has been corrected --- src/app/debug/bookings/page.tsx | 2 +- src/server/api/routers/bookings.ts | 6 +++--- src/server/api/routers/trip.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/debug/bookings/page.tsx b/src/app/debug/bookings/page.tsx index 6227b6a..fe3c367 100644 --- a/src/app/debug/bookings/page.tsx +++ b/src/app/debug/bookings/page.tsx @@ -180,7 +180,7 @@ export default function BookingDebugPage() { const listDriversQuery = api.bookings.listDrivers.useQuery(); const currentUserQuery = api.bookings.getCurrentUser.useQuery(); - // Set agencyId from current user so it references a valid user (fixes FK constraint) + // Set agencyId from current user so it references a valid organization (fixes FK constraint) useEffect(() => { if (currentUserQuery.data && !form.values.agencyId) { form.setFieldValue("agencyId", currentUserQuery.data.agencyId); diff --git a/src/server/api/routers/bookings.ts b/src/server/api/routers/bookings.ts index d3afcae..84bce1a 100644 --- a/src/server/api/routers/bookings.ts +++ b/src/server/api/routers/bookings.ts @@ -249,7 +249,7 @@ export const bookingsRouter = createTRPCRouter({ if (!ctx.session.session.activeOrganizationId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Not active organization ID set", + message: "No active organization ID set", }); } return { @@ -410,7 +410,7 @@ export const bookingsRouter = createTRPCRouter({ if (!ctx.session.session.activeOrganizationId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Not active organization ID set", + message: "No active organization ID set", }); } @@ -500,7 +500,7 @@ export const bookingsRouter = createTRPCRouter({ if (!ctx.session.session.activeOrganizationId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Not active organization ID set", + message: "No active organization ID set", }); } diff --git a/src/server/api/routers/trip.ts b/src/server/api/routers/trip.ts index 1e114cc..4c96547 100644 --- a/src/server/api/routers/trip.ts +++ b/src/server/api/routers/trip.ts @@ -27,7 +27,7 @@ export const tripRouter = createTRPCRouter({ if (!ctx.session.session.activeOrganizationId) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Not active organization ID set", + message: "No active organization ID set", }); }