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"); }