import process from "node:process"; import { prisma } from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; import { v4 as uuidv4 } from "uuid"; async function createUserActor(userUuid: string) { return prisma.auditActor.upsert({ where: { userUuid }, create: { type: "USER", userUuid }, update: {}, select: { id: true }, }); } async function createAttendeeActor(attendeeId: number) { return prisma.auditActor.upsert({ where: { attendeeId }, create: { type: "ATTENDEE", attendeeId }, update: {}, select: { id: true }, }); } async function createSystemActor() { const systemEmail = "system@system.internal"; return prisma.auditActor.upsert({ where: { email: systemEmail }, create: { type: "SYSTEM", email: systemEmail, name: "System" }, update: {}, select: { id: true }, }); } async function createGuestActor(email: string, name: string) { const existing = await prisma.auditActor.findUnique({ where: { email }, select: { id: true }, }); if (existing) return existing; return prisma.auditActor.create({ data: { type: "GUEST", email, name }, select: { id: true }, }); } async function createAppActor(appSlug: string, appName: string) { const email = `${appSlug}@app.internal`; return prisma.auditActor.upsert({ where: { email }, create: { type: "APP", email, name: appName }, update: {}, select: { id: true }, }); } function hoursFromNow(hours: number): number { return Date.now() + hours * 60 * 60 * 1000; } 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 }, select: { id: true }, }); if (existingLogs) { console.log(` ā­ļø Audit logs already exist for ${bookingUid}, skipping.`); return 0; } const userActor = await createUserActor(userUuid); const systemActor = await createSystemActor(); const guestActor = await createGuestActor("priya.sharma@acmecorp.io", "Priya Sharma"); const appActor = await createAppActor("stripe", "Stripe"); let attendeeActor: { id: string } | null = null; if (attendeeId) { 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; function nextTimestamp(): Date { timestampOffset += 1; return new Date(now - (20 - timestampOffset) * 60 * 1000); } const auditLogs: Parameters[0]["data"][] = []; auditLogs.push({ bookingUid, actorId: userActor.id, action: "CREATED", type: "RECORD_CREATED", timestamp: nextTimestamp(), source: "WEBAPP", operationId: uuidv4(), data: { version: 1, fields: { startTime: hoursFromNow(24), endTime: hoursFromNow(24.5), status: BookingStatus.PENDING, hostUserUuid: userUuid, seatReferenceUid: null, }, }, }); auditLogs.push({ bookingUid, actorId: userActor.id, action: "ACCEPTED", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "WEBAPP", operationId: uuidv4(), data: { version: 1, fields: { status: { old: BookingStatus.PENDING, new: BookingStatus.ACCEPTED }, }, }, }); auditLogs.push({ bookingUid, actorId: userActor.id, action: "LOCATION_CHANGED", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "WEBAPP", operationId: uuidv4(), data: { version: 1, fields: { location: { old: "integrations:google:meet", new: "integrations:zoom" }, }, }, }); auditLogs.push({ bookingUid, actorId: guestActor.id, action: "ATTENDEE_ADDED", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "MAGIC_LINK", operationId: uuidv4(), data: { version: 1, fields: { added: ["rachel.nguyen@designhub.co"], }, }, }); auditLogs.push({ bookingUid, actorId: userActor.id, action: "ATTENDEE_REMOVED", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "WEBAPP", operationId: uuidv4(), data: { version: 1, fields: { attendees: { old: [attendeeEmail, "rachel.nguyen@designhub.co"], new: [attendeeEmail], }, }, }, }); auditLogs.push({ bookingUid, actorId: systemActor.id, action: "REASSIGNMENT", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "SYSTEM", operationId: uuidv4(), data: { version: 1, fields: { organizerUuid: { old: null, new: userUuid }, reassignmentReason: "Round-robin auto-reassignment", reassignmentType: "roundRobin", }, }, }); auditLogs.push({ bookingUid, actorId: userActor.id, action: "REASSIGNMENT", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "WEBAPP", operationId: uuidv4(), data: { version: 1, fields: { organizerUuid: { old: userUuid, new: userUuid }, reassignmentReason: "Manual reassignment by admin", reassignmentType: "manual", }, }, }); auditLogs.push({ bookingUid, actorId: userActor.id, action: "NO_SHOW_UPDATED", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "WEBAPP", operationId: uuidv4(), data: { version: 1, fields: { attendeesNoShow: [ { attendeeEmail, noShow: { old: false, new: true }, }, ], }, }, }); auditLogs.push({ bookingUid, actorId: userActor.id, action: "NO_SHOW_UPDATED", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "WEBAPP", operationId: uuidv4(), data: { version: 1, fields: { host: { userUuid, noShow: { old: null, new: true }, }, }, }, context: { impersonatedBy: userUuid }, }); auditLogs.push({ bookingUid, actorId: userActor.id, action: "REJECTED", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "API_V2", operationId: uuidv4(), data: { version: 1, fields: { rejectionReason: "Time slot no longer available", status: { old: BookingStatus.PENDING, new: BookingStatus.REJECTED }, }, }, }); const seatRefUid = uuidv4(); auditLogs.push({ bookingUid, actorId: guestActor.id, action: "SEAT_BOOKED", type: "RECORD_CREATED", timestamp: nextTimestamp(), source: "WEBAPP", operationId: uuidv4(), data: { version: 1, fields: { seatReferenceUid: seatRefUid, attendeeEmail: "marcus.johnson@consulting.io", attendeeName: "Marcus Johnson", startTime: hoursFromNow(48), endTime: hoursFromNow(48.5), }, }, }); auditLogs.push({ bookingUid, actorId: (attendeeActor ?? guestActor).id, action: "SEAT_RESCHEDULED", type: "RECORD_UPDATED", timestamp: nextTimestamp(), source: "API_V1", operationId: uuidv4(), data: { version: 1, fields: { seatReferenceUid: seatRefUid, attendeeEmail: "marcus.johnson@consulting.io", startTime: { old: hoursFromNow(48), new: hoursFromNow(72) }, endTime: { old: hoursFromNow(48.5), new: hoursFromNow(72.5) }, rescheduledToBookingUid: { old: null, new: rescheduledToUid }, }, }, }); 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 }); } return auditLogs.length; } export default async function seedBookingAuditLogs() { console.log("šŸ” Seeding booking audit logs..."); // Audit logs is an org-only feature, so we only seed for owner1-acme const user = await prisma.user.findFirst({ where: { username: "owner1-acme" }, select: { id: true, uuid: true, username: true, email: true, profiles: { select: { organizationId: true } }, }, }); if (!user) { console.log("āŒ User owner1-acme not found. Run the main seed first."); return; } if (!user.profiles.length) { console.log("āŒ User owner1-acme has no organization profile. Run the main seed first."); return; } const organizationId = user.profiles[0].organizationId; // Enable bookings-v3 globally await prisma.feature.upsert({ where: { slug: "bookings-v3" }, create: { slug: "bookings-v3", enabled: true, description: "Enable bookings redesign v3 for all users", type: "EXPERIMENT", }, update: { enabled: true }, }); console.log(" āœ… Enabled bookings-v3 globally"); if (organizationId) { await prisma.teamFeatures.upsert({ where: { teamId_featureId: { teamId: organizationId, featureId: "booking-audit" } }, create: { teamId: organizationId, featureId: "booking-audit", enabled: true, assignedBy: "seed-script", }, 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 const eventType = await prisma.eventType.findFirst({ where: { userId: user.id }, select: { id: true, title: true, length: true }, }); if (!eventType) { console.log("āŒ No event type found for owner1-acme. Run the main seed first."); return; } // Create a new upcoming booking specifically for audit logs const bookingUid = uuidv4(); const startTime = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days from now const endTime = new Date(startTime.getTime() + eventType.length * 60 * 1000); console.log(`šŸ“‹ Creating new booking for audit logs...`); const booking = await prisma.booking.create({ data: { uid: bookingUid, title: `Audit Log Test Booking - ${eventType.title}`, startTime, endTime, status: BookingStatus.ACCEPTED, userId: user.id, eventTypeId: eventType.id, attendees: { create: { email: "james.wilson@techstart.dev", name: "James Wilson", timeZone: "UTC", }, }, }, select: { uid: true, attendees: { select: { id: true, email: true }, take: 1 }, }, }); console.log(` āœ… Created booking: ${booking.uid}`); console.log(`šŸ“‹ Seeding audit logs for ${user.username} — booking ${booking.uid}`); 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: /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, 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"); } seedBookingAuditLogs() .then(() => { console.log("\nšŸŽ‰ Booking audit seed complete!"); process.exit(0); }) .catch((err) => { console.error("āŒ Seed failed:", err); process.exit(1); });