diff --git a/apps/web/modules/booking-audit/components/BookingHistory.tsx b/apps/web/modules/booking-audit/components/BookingHistory.tsx
index 4adef65646..2668f1af5c 100644
--- a/apps/web/modules/booking-audit/components/BookingHistory.tsx
+++ b/apps/web/modules/booking-audit/components/BookingHistory.tsx
@@ -139,7 +139,13 @@ function ActionTitle({ actionDisplayTitle }: { actionDisplayTitle: TranslationWi
values={actionDisplayTitle.params}
components={actionDisplayTitle.components.map((comp) =>
comp.type === "link" ? (
-
+
) : (
)
@@ -304,15 +310,15 @@ function BookingLogsTimeline({ logs }: BookingLogsTimelineProps) {
{/* Render displayFields if available, otherwise show type */}
{log.displayFields && log.displayFields.length > 0
? log.displayFields.map((field, idx) => (
-
- {t(field.labelKey)}
-
-
-
-
- ))
+
+ {t(field.labelKey)}
+
+
+
+
+ ))
: null}
{t("actor")}
diff --git a/apps/web/modules/bookings/components/BookingListContainer.tsx b/apps/web/modules/bookings/components/BookingListContainer.tsx
index edbe1b0e80..34ed99a4a3 100644
--- a/apps/web/modules/bookings/components/BookingListContainer.tsx
+++ b/apps/web/modules/bookings/components/BookingListContainer.tsx
@@ -13,13 +13,14 @@ import { ToggleGroup } from "@calcom/ui/components/form";
import { WipeMyCalActionButton } from "@calcom/web/components/apps/wipemycalother/wipeMyCalActionButton";
import { getCoreRowModel, getSortedRowModel, useReactTable } from "@tanstack/react-table";
import { useRouter } from "next/navigation";
-import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { useBookingFilters } from "~/bookings/hooks/useBookingFilters";
import { useBookingListColumns } from "~/bookings/hooks/useBookingListColumns";
import { useBookingListData } from "~/bookings/hooks/useBookingListData";
import { useBookingStatusTab } from "~/bookings/hooks/useBookingStatusTab";
import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues";
import { useListAutoSelector } from "~/bookings/hooks/useListAutoSelector";
+import { useSwitchToCorrectStatusTab } from "~/bookings/hooks/useSwitchToCorrectStatusTab";
import { DataTableFilters, DataTableSegment } from "~/data-table/components";
import {
BookingDetailsSheetStoreProvider,
@@ -231,17 +232,21 @@ function BookingListInner({
}
export function BookingListContainer(props: BookingListContainerProps) {
- const { limit, offset, setPageIndex, isValidatorPending } = useDataTable();
+ const { limit, offset, isValidatorPending } = useDataTable();
const { eventTypeIds, teamIds, userIds, dateRange, attendeeName, attendeeEmail, bookingUid } =
useBookingFilters();
+ const { resolvedTabStatus, isResolvingTabStatus, preSelectedBooking } = useSwitchToCorrectStatusTab({
+ defaultStatus: props.status,
+ });
+
// Build query input once - shared between query and prefetching
const queryInput = useMemo(
() => ({
limit,
offset,
filters: {
- statuses: [props.status],
+ statuses: [resolvedTabStatus],
eventTypeIds,
teamIds,
userIds,
@@ -257,7 +262,7 @@ export function BookingListContainer(props: BookingListContainerProps) {
[
limit,
offset,
- props.status,
+ resolvedTabStatus,
eventTypeIds,
teamIds,
userIds,
@@ -271,10 +276,19 @@ export function BookingListContainer(props: BookingListContainerProps) {
const query = trpc.viewer.bookings.get.useQuery(queryInput, {
staleTime: 5 * 60 * 1000, // 5 minutes - data is considered fresh
gcTime: 30 * 60 * 1000, // 30 minutes - cache retention time
- enabled: !isValidatorPending, // Wait for validator to be ready before fetching
+ // We wait for tab status to be resolved before fetching, so that we can fetch the correct bookings as per resolved tab status
+ enabled: !isValidatorPending && !isResolvingTabStatus, // Wait for validator to be ready before fetching
});
- const bookings = useMemo(() => query.data?.bookings ?? [], [query.data?.bookings]);
+ const bookings = useMemo(() => {
+ const queryBookings = query.data?.bookings ?? [];
+ if (!preSelectedBooking) return queryBookings;
+ if (queryBookings.some((b) => b.uid === preSelectedBooking.uid)) return queryBookings;
+ // It ensures that the drawer opens for a booking that isn't even in the bookings list
+ // Note that, bookings list doesn't use this so, it won't be visible in the list view but drawer will open for it
+ // We don't want to show this booking in the list view, as it might not match the filters/pagination applied
+ return [...queryBookings, preSelectedBooking];
+ }, [query.data?.bookings, preSelectedBooking]);
// Always call the hook and provide navigation capabilities
// The BookingDetailsSheet is only rendered when bookingsV3Enabled is true (see line 212)
@@ -290,6 +304,7 @@ export function BookingListContainer(props: BookingListContainerProps) {
{
+ it("returns 'cancelled' for CANCELLED bookings", () => {
+ expect(getTabForBooking(makeBooking({ status: "CANCELLED", endTime: future }))).toBe("cancelled");
+ });
+
+ it("returns 'cancelled' for REJECTED bookings", () => {
+ expect(getTabForBooking(makeBooking({ status: "REJECTED", endTime: future }))).toBe("cancelled");
+ });
+
+ it("returns 'cancelled' for CANCELLED bookings even if past", () => {
+ expect(getTabForBooking(makeBooking({ status: "CANCELLED", endTime: past }))).toBe("cancelled");
+ });
+
+ it("returns 'unconfirmed' for PENDING bookings in the future", () => {
+ expect(getTabForBooking(makeBooking({ status: "PENDING", endTime: future }))).toBe("unconfirmed");
+ });
+
+ it("returns 'past' for PENDING bookings that have ended", () => {
+ expect(getTabForBooking(makeBooking({ status: "PENDING", endTime: past }))).toBe("past");
+ });
+
+ it("returns 'past' for ACCEPTED bookings that have ended", () => {
+ expect(getTabForBooking(makeBooking({ status: "ACCEPTED", endTime: past }))).toBe("past");
+ });
+
+ it("returns 'recurring' for future ACCEPTED bookings with recurringEventId", () => {
+ expect(
+ getTabForBooking(makeBooking({ status: "ACCEPTED", endTime: future, recurringEventId: "rec-123" }))
+ ).toBe("recurring");
+ });
+
+ it("returns 'upcoming' for future ACCEPTED bookings without recurringEventId", () => {
+ expect(getTabForBooking(makeBooking({ status: "ACCEPTED", endTime: future }))).toBe("upcoming");
+ });
+
+ it("returns 'past' for recurring bookings that have ended", () => {
+ expect(
+ getTabForBooking(makeBooking({ status: "ACCEPTED", endTime: past, recurringEventId: "rec-123" }))
+ ).toBe("past");
+ });
+});
diff --git a/apps/web/modules/bookings/hooks/useSwitchToCorrectStatusTab.ts b/apps/web/modules/bookings/hooks/useSwitchToCorrectStatusTab.ts
new file mode 100644
index 0000000000..885e8c3b42
--- /dev/null
+++ b/apps/web/modules/bookings/hooks/useSwitchToCorrectStatusTab.ts
@@ -0,0 +1,103 @@
+import { trpc } from "@calcom/trpc/react";
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
+import { useEffect, useState, useTransition } from "react";
+import { validStatuses } from "../lib/validStatuses";
+import type { BookingListingStatus, BookingOutput } from "../types";
+
+interface BookingForTabResolution {
+ status: string;
+ endTime: Date | string;
+ recurringEventId: string | null;
+}
+
+interface UseSwitchToCorrectTabResult {
+ resolvedTabStatus: BookingListingStatus;
+ isResolvingTabStatus: boolean;
+ preSelectedBooking: BookingOutput | null;
+}
+
+export function getTabForBooking(booking: BookingForTabResolution): BookingListingStatus {
+ const isPast = new Date(booking.endTime) <= new Date();
+
+ if (booking.status === "CANCELLED" || booking.status === "REJECTED") {
+ return "cancelled";
+ }
+ if (booking.status === "PENDING" && !isPast) {
+ return "unconfirmed";
+ }
+ if (isPast) {
+ return "past";
+ }
+ if (booking.recurringEventId) {
+ return "recurring";
+ }
+ return "upcoming";
+}
+
+/**
+ * Ensures that the correct status tab is selected based on the pre-selected booking through query params
+ */
+export function useSwitchToCorrectStatusTab({
+ defaultStatus,
+}: {
+ defaultStatus: BookingListingStatus;
+}): UseSwitchToCorrectTabResult {
+ const {
+ preSelectedBookingUid,
+ preSelectedBooking,
+ preSelectedBookingFull,
+ isPending: isFetchingPreSelectedBooking,
+ } = usePreSelectedBooking();
+ const pathname = usePathname();
+ const router = useRouter();
+ const [resolvedTabStatus, setResolvedTab] = useState(defaultStatus);
+ const [isNavigatingToCorrectTab, startNavigationToCorrectTab] = useTransition();
+
+ useEffect(() => {
+ if (!preSelectedBooking) return;
+ const correctTab = getTabForBooking(preSelectedBooking);
+ const currentTab = pathname?.match(/\/bookings\/(\w+)/)?.[1];
+ const shouldNavigate = correctTab && currentTab && correctTab !== currentTab;
+ if (!shouldNavigate) return;
+ startNavigationToCorrectTab(() => {
+ const newPath = pathname.replace(`/bookings/${currentTab}`, `/bookings/${correctTab}`);
+ router.replace(`${newPath}${window.location.search}`);
+ });
+ setResolvedTab(correctTab);
+ }, [preSelectedBooking, pathname, router]);
+
+ const isResolvingTabStatus = preSelectedBookingUid
+ ? isFetchingPreSelectedBooking || isNavigatingToCorrectTab
+ : false;
+
+ return { resolvedTabStatus, isResolvingTabStatus, preSelectedBooking: preSelectedBookingFull };
+}
+
+export function usePreSelectedBooking(): {
+ preSelectedBookingUid: string | undefined;
+ preSelectedBooking: BookingForTabResolution | null;
+ preSelectedBookingFull: BookingOutput | null;
+ isPending: boolean;
+}{
+ const searchParams = useSearchParams();
+ const preSelectedBookingUid = searchParams?.get("uid") ?? undefined;
+
+ const { data: preSelectedBookingData, isPending } = trpc.viewer.bookings.get.useQuery(
+ {
+ limit: 1,
+ offset: 0,
+ filters: {
+ bookingUid: preSelectedBookingUid,
+ statuses: [...validStatuses],
+ },
+ },
+ {
+ enabled: !!preSelectedBookingUid,
+ staleTime: 5 * 60 * 1000,
+ }
+ );
+
+ const preSelectedBookingFull = preSelectedBookingData?.bookings?.[0] ?? null;
+ const preSelectedBooking: BookingForTabResolution | null = preSelectedBookingFull;
+ return { preSelectedBookingUid, preSelectedBooking, preSelectedBookingFull, isPending };
+}
diff --git a/packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts b/packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts
index c6e8961bea..ea5ba725fc 100644
--- a/packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts
+++ b/packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts
@@ -1,14 +1,13 @@
import { z } from "zod";
-
import { NumberChangeSchema, StringChangeSchema } from "../common/changeSchemas";
import type { DataRequirements } from "../service/EnrichmentDataStore";
import { AuditActionServiceHelper } from "./AuditActionServiceHelper";
import type {
+ BaseStoredAuditData,
+ GetDisplayJsonParams,
+ GetDisplayTitleParams,
IAuditActionService,
TranslationWithParams,
- GetDisplayTitleParams,
- GetDisplayJsonParams,
- BaseStoredAuditData,
} from "./IAuditActionService";
/**
@@ -91,7 +90,7 @@ export class RescheduledAuditActionService implements IAuditActionService {
newDate,
},
components: rescheduledToUid
- ? [{ type: "link", href: `/booking/${rescheduledToUid}/logs` }]
+ ? [{ type: "link", href: `/bookings?uid=${rescheduledToUid}&activeSegment=history` }]
: undefined,
};
}
@@ -122,7 +121,7 @@ export class RescheduledAuditActionService implements IAuditActionService {
oldDate,
newDate,
},
- components: [{ type: "link", href: `/booking/${fromRescheduleUid}/logs` }],
+ components: [{ type: "link", href: `/bookings?uid=${fromRescheduleUid}&activeSegment=history` }],
};
}
diff --git a/packages/features/booking-audit/lib/actions/SeatRescheduledAuditActionService.ts b/packages/features/booking-audit/lib/actions/SeatRescheduledAuditActionService.ts
index 55958c0d71..b1700ec078 100644
--- a/packages/features/booking-audit/lib/actions/SeatRescheduledAuditActionService.ts
+++ b/packages/features/booking-audit/lib/actions/SeatRescheduledAuditActionService.ts
@@ -89,9 +89,9 @@ export class SeatRescheduledAuditActionService implements IAuditActionService {
oldDate,
newDate,
},
- components: rescheduledToBookingUid
- ? [{ type: "link", href: `/booking/${rescheduledToBookingUid}/logs` }]
- : undefined,
+ components: rescheduledToBookingUid
+ ? [{ type: "link", href: `/bookings?uid=${rescheduledToBookingUid}&activeSegment=history` }]
+ : undefined,
};
}
diff --git a/packages/features/booking-audit/lib/actions/__tests__/RescheduledAuditActionService.test.ts b/packages/features/booking-audit/lib/actions/__tests__/RescheduledAuditActionService.test.ts
index 122420373f..0aef95dd7b 100644
--- a/packages/features/booking-audit/lib/actions/__tests__/RescheduledAuditActionService.test.ts
+++ b/packages/features/booking-audit/lib/actions/__tests__/RescheduledAuditActionService.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it, beforeEach } from "vitest";
-import { verifyDataRequirementsContract } from "./contractVerification";
+import { verifyDataRequirementsContract, createTrackingDbStore, createEmptyAccessedData } from "./contractVerification";
import { RescheduledAuditActionService } from "../RescheduledAuditActionService";
describe("RescheduledAuditActionService - getDataRequirements contract", () => {
@@ -26,3 +26,89 @@ describe("RescheduledAuditActionService - getDataRequirements contract", () => {
expect(accessedData.userUuids.size).toBe(0);
});
});
+
+describe("RescheduledAuditActionService - getDisplayTitle", () => {
+ let service: RescheduledAuditActionService;
+
+ beforeEach(() => {
+ service = new RescheduledAuditActionService();
+ });
+
+ it("should generate deep link to booking drawer with history tab", async () => {
+ const rescheduledToUid = "new-booking-uid-123";
+ const storedData = {
+ version: 1,
+ fields: {
+ startTime: { old: 1700000000000, new: 1700100000000 },
+ endTime: { old: 1700003600000, new: 1700103600000 },
+ rescheduledToUid: { old: null, new: rescheduledToUid },
+ },
+ };
+
+ const dbStore = createTrackingDbStore(createEmptyAccessedData());
+ const result = await service.getDisplayTitle({
+ storedData,
+ userTimeZone: "UTC",
+ dbStore,
+ });
+
+ expect(result.components).toBeDefined();
+ expect(result.components).toHaveLength(1);
+ expect(result.components![0].href).toBe(
+ `/bookings?uid=${rescheduledToUid}&activeSegment=history`
+ );
+ });
+
+ it("should not include components when rescheduledToUid is null", async () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ startTime: { old: 1700000000000, new: 1700100000000 },
+ endTime: { old: 1700003600000, new: 1700103600000 },
+ rescheduledToUid: { old: null, new: null },
+ },
+ };
+
+ const dbStore = createTrackingDbStore(createEmptyAccessedData());
+ const result = await service.getDisplayTitle({
+ storedData,
+ userTimeZone: "UTC",
+ dbStore,
+ });
+
+ expect(result.components).toBeUndefined();
+ });
+});
+
+describe("RescheduledAuditActionService - getDisplayTitleForRescheduledFromLog", () => {
+ let service: RescheduledAuditActionService;
+
+ beforeEach(() => {
+ service = new RescheduledAuditActionService();
+ });
+
+ it("should generate deep link to original booking's history tab", () => {
+ const fromRescheduleUid = "original-booking-uid-456";
+ const storedData = {
+ version: 1,
+ fields: {
+ startTime: { old: 1700000000000, new: 1700100000000 },
+ endTime: { old: 1700003600000, new: 1700103600000 },
+ rescheduledToUid: { old: null, new: "some-uid" },
+ },
+ };
+
+ const result = service.getDisplayTitleForRescheduledFromLog({
+ fromRescheduleUid,
+ userTimeZone: "UTC",
+ storedData,
+ });
+
+
+ expect(result.components).toBeDefined();
+ expect(result.components).toHaveLength(1);
+ expect(result.components![0].href).toBe(
+ `/bookings?uid=${fromRescheduleUid}&activeSegment=history`
+ );
+ });
+});
diff --git a/packages/features/booking-audit/lib/actions/__tests__/SeatRescheduledAuditActionService.test.ts b/packages/features/booking-audit/lib/actions/__tests__/SeatRescheduledAuditActionService.test.ts
index 40a3994b01..c04dfa9586 100644
--- a/packages/features/booking-audit/lib/actions/__tests__/SeatRescheduledAuditActionService.test.ts
+++ b/packages/features/booking-audit/lib/actions/__tests__/SeatRescheduledAuditActionService.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it, beforeEach } from "vitest";
-import { verifyDataRequirementsContract } from "./contractVerification";
+import { verifyDataRequirementsContract, createTrackingDbStore, createEmptyAccessedData } from "./contractVerification";
import { SeatRescheduledAuditActionService } from "../SeatRescheduledAuditActionService";
describe("SeatRescheduledAuditActionService - getDataRequirements contract", () => {
@@ -27,3 +27,60 @@ describe("SeatRescheduledAuditActionService - getDataRequirements contract", ()
expect(accessedData.userUuids.size).toBe(0);
});
});
+
+describe("SeatRescheduledAuditActionService - getDisplayTitle", () => {
+ let service: SeatRescheduledAuditActionService;
+
+ beforeEach(() => {
+ service = new SeatRescheduledAuditActionService();
+ });
+
+ it("should generate deep link to booking drawer with history tab", async () => {
+ const rescheduledToBookingUid = "new-booking-uid-789";
+ const storedData = {
+ version: 1,
+ fields: {
+ seatReferenceUid: "seat-456",
+ attendeeEmail: "attendee@example.com",
+ startTime: { old: 1700000000000, new: 1700100000000 },
+ endTime: { old: 1700003600000, new: 1700103600000 },
+ rescheduledToBookingUid: { old: null, new: rescheduledToBookingUid },
+ },
+ };
+
+ const dbStore = createTrackingDbStore(createEmptyAccessedData());
+ const result = await service.getDisplayTitle({
+ storedData,
+ userTimeZone: "UTC",
+ dbStore,
+ });
+
+ expect(result.components).toBeDefined();
+ expect(result.components).toHaveLength(1);
+ expect(result.components![0].href).toBe(
+ `/bookings?uid=${rescheduledToBookingUid}&activeSegment=history`
+ );
+ });
+
+ it("should not include components when rescheduledToBookingUid is null", async () => {
+ const storedData = {
+ version: 1,
+ fields: {
+ seatReferenceUid: "seat-456",
+ attendeeEmail: "attendee@example.com",
+ startTime: { old: 1700000000000, new: 1700100000000 },
+ endTime: { old: 1700003600000, new: 1700103600000 },
+ rescheduledToBookingUid: { old: null, new: null },
+ },
+ };
+
+ const dbStore = createTrackingDbStore(createEmptyAccessedData());
+ const result = await service.getDisplayTitle({
+ storedData,
+ userTimeZone: "UTC",
+ dbStore,
+ });
+
+ expect(result.components).toBeUndefined();
+ });
+});
diff --git a/scripts/seed-booking-audit.ts b/scripts/seed-booking-audit.ts
index 4d329fc1b5..6962fb6bc8 100644
--- a/scripts/seed-booking-audit.ts
+++ b/scripts/seed-booking-audit.ts
@@ -61,13 +61,19 @@ function hoursFromNow(hours: number): number {
async function seedAuditLogsForBooking({
bookingUid,
userUuid,
+ userId,
attendeeId,
attendeeEmail,
+ eventTypeId,
+ eventTypeLength,
}: {
bookingUid: string;
userUuid: string;
+ userId: number;
attendeeId: number | undefined;
attendeeEmail: string;
+ eventTypeId: number;
+ eventTypeLength: number;
}): Promise {
const existingLogs = await prisma.bookingAudit.findFirst({
where: { bookingUid },
@@ -89,7 +95,30 @@ async function seedAuditLogsForBooking({
attendeeActor = await createAttendeeActor(attendeeId);
}
+ // Create the "rescheduled-to" booking so rescheduledToUid in audit logs points to a real booking
const rescheduledToUid = uuidv4();
+ const rescheduledStartTime = new Date(Date.now() + 2 * 24 * 60 * 60 * 1000); // 2 days from now (matches RESCHEDULED log's "new" times)
+ const rescheduledEndTime = new Date(rescheduledStartTime.getTime() + eventTypeLength * 60 * 1000);
+
+ await prisma.booking.create({
+ data: {
+ uid: rescheduledToUid,
+ title: "Audit Log Test Booking - Rescheduled",
+ startTime: rescheduledStartTime,
+ endTime: rescheduledEndTime,
+ status: BookingStatus.ACCEPTED,
+ userId,
+ eventTypeId,
+ fromReschedule: bookingUid,
+ attendees: {
+ create: {
+ email: attendeeEmail,
+ name: "James Wilson",
+ timeZone: "UTC",
+ },
+ },
+ },
+ });
const now = Date.now();
let timestampOffset = 0;
@@ -136,41 +165,6 @@ async function seedAuditLogsForBooking({
},
});
- auditLogs.push({
- bookingUid,
- actorId: (attendeeActor ?? guestActor).id,
- action: "RESCHEDULE_REQUESTED",
- type: "RECORD_UPDATED",
- timestamp: nextTimestamp(),
- source: "WEBAPP",
- operationId: uuidv4(),
- data: {
- version: 1,
- fields: {
- rescheduleReason: "Conflict with another meeting",
- rescheduledRequestedBy: attendeeEmail,
- },
- },
- });
-
- auditLogs.push({
- bookingUid,
- actorId: userActor.id,
- action: "RESCHEDULED",
- type: "RECORD_UPDATED",
- timestamp: nextTimestamp(),
- source: "WEBAPP",
- operationId: uuidv4(),
- data: {
- version: 1,
- fields: {
- startTime: { old: hoursFromNow(24), new: hoursFromNow(48) },
- endTime: { old: hoursFromNow(24.5), new: hoursFromNow(48.5) },
- rescheduledToUid: { old: null, new: rescheduledToUid },
- },
- },
- });
-
auditLogs.push({
bookingUid,
actorId: userActor.id,
@@ -299,24 +293,6 @@ async function seedAuditLogsForBooking({
context: { impersonatedBy: userUuid },
});
- auditLogs.push({
- bookingUid,
- actorId: appActor.id,
- action: "CANCELLED",
- type: "RECORD_UPDATED",
- timestamp: nextTimestamp(),
- source: "WEBHOOK",
- operationId: uuidv4(),
- data: {
- version: 1,
- fields: {
- cancellationReason: "Payment failed - automatic cancellation",
- cancelledBy: "stripe@app.internal",
- status: { old: BookingStatus.ACCEPTED, new: BookingStatus.CANCELLED },
- },
- },
- });
-
auditLogs.push({
bookingUid,
actorId: userActor.id,
@@ -376,6 +352,59 @@ async function seedAuditLogsForBooking({
},
});
+ auditLogs.push({
+ bookingUid,
+ actorId: appActor.id,
+ action: "CANCELLED",
+ type: "RECORD_UPDATED",
+ timestamp: nextTimestamp(),
+ source: "WEBHOOK",
+ operationId: uuidv4(),
+ data: {
+ version: 1,
+ fields: {
+ cancellationReason: "Payment failed - automatic cancellation",
+ cancelledBy: "stripe@app.internal",
+ status: { old: BookingStatus.ACCEPTED, new: BookingStatus.CANCELLED },
+ },
+ },
+ });
+
+ auditLogs.push({
+ bookingUid,
+ actorId: (attendeeActor ?? guestActor).id,
+ action: "RESCHEDULE_REQUESTED",
+ type: "RECORD_UPDATED",
+ timestamp: nextTimestamp(),
+ source: "WEBAPP",
+ operationId: uuidv4(),
+ data: {
+ version: 1,
+ fields: {
+ rescheduleReason: "Conflict with another meeting",
+ rescheduledRequestedBy: attendeeEmail,
+ },
+ },
+ });
+
+ auditLogs.push({
+ bookingUid,
+ actorId: userActor.id,
+ action: "RESCHEDULED",
+ type: "RECORD_UPDATED",
+ timestamp: nextTimestamp(),
+ source: "WEBAPP",
+ operationId: uuidv4(),
+ data: {
+ version: 1,
+ fields: {
+ startTime: { old: hoursFromNow(24), new: hoursFromNow(48) },
+ endTime: { old: hoursFromNow(24.5), new: hoursFromNow(48.5) },
+ rescheduledToUid: { old: null, new: rescheduledToUid },
+ },
+ },
+ });
+
for (const logData of auditLogs) {
await prisma.bookingAudit.create({ data: logData });
}
@@ -423,19 +452,7 @@ export default async function seedBookingAuditLogs() {
});
console.log(" ā
Enabled bookings-v3 globally");
- // Enable booking-audit at team level for owner1-acme's organization
if (organizationId) {
- await prisma.feature.upsert({
- where: { slug: "booking-audit" },
- create: {
- slug: "booking-audit",
- enabled: false,
- description: "Enable booking audit trails - Track all booking actions and changes for organizations",
- type: "OPERATIONAL",
- },
- update: {},
- });
-
await prisma.teamFeatures.upsert({
where: { teamId_featureId: { teamId: organizationId, featureId: "booking-audit" } },
create: {
@@ -446,7 +463,20 @@ export default async function seedBookingAuditLogs() {
},
update: { enabled: true },
});
+
console.log(` ā
Enabled booking-audit for organization ${organizationId}`);
+
+ await prisma.teamFeatures.upsert({
+ where: { teamId_featureId: { teamId: organizationId, featureId: "bookings-v3" } },
+ create: {
+ teamId: organizationId,
+ featureId: "bookings-v3",
+ enabled: true,
+ assignedBy: "seed-script",
+ },
+ update: { enabled: true },
+ });
+ console.log(` ā
Enabled bookings-v3 for organization ${organizationId}`);
}
// Find an event type for this user to create a booking
@@ -497,19 +527,22 @@ export default async function seedBookingAuditLogs() {
const count = await seedAuditLogsForBooking({
bookingUid: booking.uid,
userUuid: user.uuid,
+ userId: user.id,
attendeeId: booking.attendees[0]?.id,
attendeeEmail: booking.attendees[0]?.email ?? "james.wilson@techstart.dev",
+ eventTypeId: eventType.id,
+ eventTypeLength: eventType.length,
});
console.log(` ā
Created ${count} audit log entries`);
- console.log(` View logs at: /booking/${booking.uid}/logs`);
+ console.log(` View logs at: /bookings/upcoming?uid=${booking.uid}&activeSegment=history`);
console.log(`\nš Summary:`);
console.log(` Booking UID: ${booking.uid}`);
console.log(` Total audit log entries created: ${count}`);
- console.log(" Actions covered: CREATED, ACCEPTED, RESCHEDULE_REQUESTED, RESCHEDULED,");
- console.log(" LOCATION_CHANGED, ATTENDEE_ADDED, ATTENDEE_REMOVED, REASSIGNMENT (x2),");
- console.log(" NO_SHOW_UPDATED (x2), CANCELLED, REJECTED, SEAT_BOOKED, SEAT_RESCHEDULED");
+ console.log(" Actions covered: CREATED, ACCEPTED, LOCATION_CHANGED, ATTENDEE_ADDED,");
+ console.log(" ATTENDEE_REMOVED, REASSIGNMENT (x2), NO_SHOW_UPDATED (x2), REJECTED,");
+ console.log(" SEAT_BOOKED, SEAT_RESCHEDULED, CANCELLED, RESCHEDULE_REQUESTED, RESCHEDULED");
console.log(" Actor types: USER, ATTENDEE, GUEST, SYSTEM, APP");
console.log(" Sources: WEBAPP, API_V1, API_V2, WEBHOOK, MAGIC_LINK, SYSTEM");
}