diff --git a/apps/api/v1/test/lib/utils/isLockedOrBlocked.test.ts b/apps/api/v1/test/lib/utils/isLockedOrBlocked.test.ts index 9a79a7663bff6b..b67f45ca19cc6e 100644 --- a/apps/api/v1/test/lib/utils/isLockedOrBlocked.test.ts +++ b/apps/api/v1/test/lib/utils/isLockedOrBlocked.test.ts @@ -1,9 +1,8 @@ import prismock from "../../../../../../tests/libs/__mocks__/prisma"; +import type { NextApiRequest } from "next"; import { describe, expect, it, beforeEach } from "vitest"; -import { WatchlistSeverity } from "@calcom/prisma/enums"; - import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked"; describe("isLockedOrBlocked", () => { @@ -18,7 +17,6 @@ describe("isLockedOrBlocked", () => { { type: "DOMAIN", value: "blocked.com", - severity: WatchlistSeverity.CRITICAL, createdById: 1, }, ], @@ -26,13 +24,13 @@ describe("isLockedOrBlocked", () => { }); it("should return false if no user in request", async () => { - const req = { userId: null, user: null } as any; + const req = { userId: null, user: null } as unknown as NextApiRequest; const result = await isLockedOrBlocked(req); expect(result).toBe(false); }); it("should return false if user has no email", async () => { - const req = { userId: 123, user: { email: null } } as any; + const req = { userId: 123, user: { email: null } } as unknown as NextApiRequest; const result = await isLockedOrBlocked(req); expect(result).toBe(false); }); @@ -44,7 +42,7 @@ describe("isLockedOrBlocked", () => { locked: true, email: "test@example.com", }, - } as any; + } as unknown as NextApiRequest; const result = await isLockedOrBlocked(req); expect(result).toBe(true); @@ -57,7 +55,7 @@ describe("isLockedOrBlocked", () => { locked: false, email: "test@blocked.com", }, - } as any; + } as unknown as NextApiRequest; const result = await isLockedOrBlocked(req); expect(result).toBe(true); @@ -70,7 +68,7 @@ describe("isLockedOrBlocked", () => { locked: false, email: "test@example.com", }, - } as any; + } as unknown as NextApiRequest; const result = await isLockedOrBlocked(req); expect(result).toBe(false); @@ -83,7 +81,7 @@ describe("isLockedOrBlocked", () => { locked: false, email: "test@BLOCKED.COM", }, - } as any; + } as unknown as NextApiRequest; const result = await isLockedOrBlocked(req); expect(result).toBe(true); diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index 23877852fe4339..d52394affb84fe 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -59,7 +59,7 @@ import { createWatchlistEntry } from "@calcom/features/watchlist/lib/testUtils"; import { WEBSITE_URL, WEBAPP_URL } from "@calcom/lib/constants"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { resetTestEmails } from "@calcom/lib/testEmails"; -import { CreationSource, WatchlistSeverity, WatchlistType } from "@calcom/prisma/enums"; +import { CreationSource, WatchlistType } from "@calcom/prisma/enums"; import { BookingStatus, SchedulingType } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; @@ -199,7 +199,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -263,6 +263,7 @@ describe("handleNewBooking", () => { 3. Should fallback to creating the booking in the first connected Calendar when neither event nor organizer has a destination calendar - This doesn't practically happen because organizer is always required to have a schedule set 3. Should trigger BOOKING_CREATED webhook `, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -363,7 +364,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -423,6 +424,7 @@ describe("handleNewBooking", () => { 3. Should fallback to create a booking in the Organizer Calendar if event doesn't have destination calendar 3. Should trigger BOOKING_CREATED webhook `, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -525,7 +527,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -578,6 +580,7 @@ describe("handleNewBooking", () => { test( `an error in creating a calendar event should not stop the booking creation - Current behaviour is wrong as the booking is created but no-one is notified of it`, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -666,7 +669,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -699,6 +702,7 @@ describe("handleNewBooking", () => { test( "If destination calendar has no credential ID due to some reason, it should create the event in first connected calendar instead", + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -811,7 +815,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -865,6 +869,7 @@ describe("handleNewBooking", () => { test( "If destination calendar is there for Google Calendar but there are no Google Calendar credentials but there is an Apple Calendar credential connected, it should create the event in Apple Calendar", + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -970,7 +975,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -1022,6 +1027,7 @@ describe("handleNewBooking", () => { describe("Event's first location should be used when location is unspecied", () => { test( `should create a successful booking with right location app when event has location option as video client`, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -1115,6 +1121,7 @@ describe("handleNewBooking", () => { `should create a successful booking with right location when event's location is not a conferencing app `, //test with inPerson event type + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -1198,6 +1205,7 @@ describe("handleNewBooking", () => { ); test( `should create a successful booking with organizer default conferencing app when event's location is not set`, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -1292,6 +1300,7 @@ describe("handleNewBooking", () => { describe("Video Meeting Creation", () => { test( `should create a successful booking with Zoom if used`, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const subscriberUrl = "http://my-webhook.example.com"; @@ -1380,6 +1389,7 @@ describe("handleNewBooking", () => { test( `Booking should still be created using calvideo, if error occurs with zoom`, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const subscriberUrl = "http://my-webhook.example.com"; @@ -1534,6 +1544,7 @@ describe("handleNewBooking", () => { describe("UTM tracking tests", () => { test( "should create a booking with UTM tracking", + // eslint-disable-next-line @typescript-eslint/no-unused-vars async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -1609,7 +1620,6 @@ describe("handleNewBooking", () => { bookingData: mockedBookingData, }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion expectBookingTrackingToBeInDatabase(tracking, createdBooking.uid!); }, timeout @@ -1619,6 +1629,7 @@ describe("handleNewBooking", () => { describe("Creation source tests", () => { test( `should create a booking with creation source as WEBAPP`, + // eslint-disable-next-line @typescript-eslint/no-unused-vars async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -1700,7 +1711,7 @@ describe("handleNewBooking", () => { () => { test( `should fail a booking if there is already a Cal.com booking overlapping the time`, - async ({}) => { + async () => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -1929,7 +1940,7 @@ describe("handleNewBooking", () => { }); expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -2098,7 +2109,7 @@ describe("handleNewBooking", () => { }); expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -2119,6 +2130,7 @@ describe("handleNewBooking", () => { 3. Should trigger BOOKING_REQUESTED webhook 4. Should trigger BOOKING_REQUESTED workflow `, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const subscriberUrl = "http://my-webhook.example.com"; @@ -2220,7 +2232,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.PENDING, @@ -2256,6 +2268,7 @@ describe("handleNewBooking", () => { 3. Should trigger BOOKING_REQUESTED webhook 4. Should trigger BOOKING_REQUESTED workflow `, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const subscriberUrl = "http://my-webhook.example.com"; @@ -2345,7 +2358,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.PENDING, @@ -2382,6 +2395,7 @@ describe("handleNewBooking", () => { 2. Should send emails to the booker as well as organizer 3. Should trigger BOOKING_CREATED webhook `, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -2479,7 +2493,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -2518,6 +2532,7 @@ describe("handleNewBooking", () => { 3. Should trigger BOOKING_REQUESTED webhook 4. Should trigger BOOKING_REQUESTED workflows `, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const subscriberUrl = "http://my-webhook.example.com"; @@ -2621,7 +2636,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.PENDING, @@ -2648,7 +2663,7 @@ describe("handleNewBooking", () => { // FIXME: We shouldn't throw error here, the behaviour should be fixed. test( `if booking with Cal Video(Daily Video) fails, booking creation fails with uncaught error`, - async ({}) => { + async () => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ email: "booker@example.org", @@ -2716,6 +2731,7 @@ describe("handleNewBooking", () => { 2. Should send emails to the booker as well as organizer 3. Should trigger BOOKING_CREATED webhook `, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -2801,7 +2817,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -2842,6 +2858,7 @@ describe("handleNewBooking", () => { 6. Workflow should not trigger before payment is made 7. Workflow triggers once payment is successful `, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -2963,7 +2980,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", location: BookingLocations.CalVideo, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.PENDING, @@ -2984,7 +3001,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + paymentId: createdBooking.paymentId!, }); @@ -2993,7 +3010,7 @@ describe("handleNewBooking", () => { expect(webhookResponse?.statusCode).toBe(200); await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -3024,6 +3041,7 @@ describe("handleNewBooking", () => { 6. Should trigger BOOKING_REQUESTED workflow 7. Booking should still stay in pending state `, + async ({ emails }) => { const bookingInitiatedEmail = "booking_initiated@workflow.com"; const handleNewBooking = getNewBookingHandler(); @@ -3145,7 +3163,7 @@ describe("handleNewBooking", () => { ); await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.PENDING, @@ -3165,7 +3183,7 @@ describe("handleNewBooking", () => { organizer, location: BookingLocations.CalVideo, subscriberUrl: "http://my-webhook.example.com", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + paymentId: createdBooking.paymentId!, }); expectWorkflowToBeTriggered({ emailsToReceive: [bookingInitiatedEmail], emails }); @@ -3178,7 +3196,7 @@ describe("handleNewBooking", () => { expect(webhookResponse?.statusCode).toBe(200); await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.PENDING, @@ -3203,6 +3221,7 @@ describe("handleNewBooking", () => { ); test( `cannot book same slot multiple times `, + async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -3298,7 +3317,7 @@ describe("handleNewBooking", () => { await expectBookingToBeInDatabase({ description: "", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, @@ -3351,6 +3370,7 @@ describe("handleNewBooking", () => { test( `Payment retry scenario - should return existing payment UID and prevent duplicate bookings when retrying canceled payment`, + // eslint-disable-next-line @typescript-eslint/no-unused-vars async ({ emails }) => { const handleNewBooking = getNewBookingHandler(); const booker = getBooker({ @@ -3400,7 +3420,7 @@ describe("handleNewBooking", () => { mockSuccessfulVideoMeetingCreation({ metadataLookupKey: "dailyvideo", }); - const { paymentUid } = mockPaymentApp({ + const { paymentUid: _paymentUid } = mockPaymentApp({ metadataLookupKey: "stripe", appStoreLookupKey: "stripepayment", }); @@ -3693,7 +3713,6 @@ describe("handleNewBooking", () => { await createWatchlistEntry({ type: WatchlistType.USERNAME, - severity: WatchlistSeverity.CRITICAL, value: organizer.username, }); @@ -3755,7 +3774,6 @@ describe("handleNewBooking", () => { await createWatchlistEntry({ type: WatchlistType.DOMAIN, - severity: WatchlistSeverity.CRITICAL, value: "spammer.com", }); @@ -3817,7 +3835,6 @@ describe("handleNewBooking", () => { await createWatchlistEntry({ type: WatchlistType.EMAIL, - severity: WatchlistSeverity.CRITICAL, value: "spam@spammer.com", }); diff --git a/packages/features/watchlist/lib/testUtils.ts b/packages/features/watchlist/lib/testUtils.ts index 84a92916b053e6..c0f93e3b5cb52f 100644 --- a/packages/features/watchlist/lib/testUtils.ts +++ b/packages/features/watchlist/lib/testUtils.ts @@ -1,9 +1,8 @@ import prismock from "../../../../tests/libs/__mocks__/prisma"; -import type { WatchlistSeverity, WatchlistType } from "@calcom/prisma/enums"; +import type { WatchlistType } from "@calcom/prisma/enums"; interface WatchlistInput { - severity: WatchlistSeverity; type: WatchlistType; value: string; } diff --git a/packages/features/watchlist/watchlist.repository.ts b/packages/features/watchlist/watchlist.repository.ts index 0891a39ef78dc2..b986bf9b9a34a6 100644 --- a/packages/features/watchlist/watchlist.repository.ts +++ b/packages/features/watchlist/watchlist.repository.ts @@ -1,7 +1,7 @@ import { captureException } from "@sentry/nextjs"; import db from "@calcom/prisma"; -import { WatchlistType, WatchlistSeverity } from "@calcom/prisma/enums"; +import { WatchlistType } from "@calcom/prisma/enums"; import type { IWatchlistRepository } from "./watchlist.repository.interface"; @@ -11,7 +11,6 @@ export class WatchlistRepository implements IWatchlistRepository { try { const emailInWatchlist = await db.watchlist.findFirst({ where: { - severity: WatchlistSeverity.CRITICAL, OR: [ { type: WatchlistType.EMAIL, value: email }, { type: WatchlistType.DOMAIN, value: domain }, @@ -53,7 +52,6 @@ export class WatchlistRepository implements IWatchlistRepository { try { const blockedRecords = await db.watchlist.findMany({ where: { - severity: WatchlistSeverity.CRITICAL, OR: [ ...(usernames.length > 0 ? [ diff --git a/packages/prisma/migrations/20251003103832_upsert_watchlist_audit/migration.sql b/packages/prisma/migrations/20251003103832_upsert_watchlist_audit/migration.sql new file mode 100644 index 00000000000000..035e6d76f79e15 --- /dev/null +++ b/packages/prisma/migrations/20251003103832_upsert_watchlist_audit/migration.sql @@ -0,0 +1,113 @@ +-- CreateEnum +CREATE TYPE "WatchlistSource" AS ENUM ('MANUAL', 'FREE_DOMAIN_POLICY'); + +-- AlterEnum +ALTER TYPE "WatchlistAction" ADD VALUE 'ALERT'; + + +-- 1) Add a temporary UUID column `uid` and backfill it without extensions +ALTER TABLE "Watchlist" + ADD COLUMN IF NOT EXISTS "uid" UUID; + +-- Backfill existing rows with UUIDs using gen_random_uuid() +UPDATE "Watchlist" +SET "uid" = COALESCE("uid", gen_random_uuid()); + +-- Enforce NOT NULL now that all rows have a value +ALTER TABLE "Watchlist" + ALTER COLUMN "uid" SET NOT NULL; + +-- 2) Adjust uniques/indexes that reference old id? (none in your snippet) +-- If you had FKs or PK references elsewhere, you'd update them here. + +-- 3) Switch primary key from old `id` to new `uid`: +-- Drop old PK (name might vary in your DB; adjust if needed) +DO $$ BEGIN + ALTER TABLE "Watchlist" DROP CONSTRAINT "Watchlist_pkey"; +EXCEPTION + WHEN undefined_object THEN NULL; +END $$; + +-- 4) Drop the old `id` column and rename `uid`→`id` +ALTER TABLE "Watchlist" DROP COLUMN "id"; +ALTER TABLE "Watchlist" RENAME COLUMN "uid" TO "id"; + +-- 5) Add the new PK on `id` (UUID) +ALTER TABLE "Watchlist" ADD CONSTRAINT "Watchlist_pkey" PRIMARY KEY ("id"); + +-- DropForeignKey - Do this before modifying Watchlist structure +ALTER TABLE "Watchlist" DROP CONSTRAINT "Watchlist_createdById_fkey"; + +-- DropForeignKey +ALTER TABLE "Watchlist" DROP CONSTRAINT "Watchlist_updatedById_fkey"; + +-- Modify Watchlist table structure FIRST +-- AlterTable +ALTER TABLE "Watchlist" DROP COLUMN "createdAt", +DROP COLUMN "createdById", +DROP COLUMN "severity", +DROP COLUMN "updatedAt", +DROP COLUMN "updatedById", +ADD COLUMN "isGlobal" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "lastUpdatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "source" "WatchlistSource" NOT NULL DEFAULT 'MANUAL'; + +-- Create WatchlistAudit table AFTER Watchlist has UUID id +-- CreateTable +CREATE TABLE "WatchlistAudit" ( + "id" UUID NOT NULL, + "type" "WatchlistType" NOT NULL, + "value" TEXT NOT NULL, + "description" TEXT, + "action" "WatchlistAction" NOT NULL DEFAULT 'REPORT', + "changedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "changedByUserId" INTEGER, + "watchlistId" UUID NOT NULL, + + CONSTRAINT "WatchlistAudit_pkey" PRIMARY KEY ("id") +); + +-- Drop the BlockedBookingLog table +-- DropTable +DROP TABLE "BlockedBookingLog"; + +-- Drop unused enum +-- DropEnum +DROP TYPE "WatchlistSeverity"; + +-- Create WatchlistEventAudit table +-- CreateTable +CREATE TABLE "WatchlistEventAudit" ( + "id" UUID NOT NULL, + "watchlistId" UUID NOT NULL, + "eventTypeId" INTEGER NOT NULL, + "actionTaken" "WatchlistAction" NOT NULL, + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "WatchlistEventAudit_pkey" PRIMARY KEY ("id") +); + +-- Create indexes for performance +-- CreateIndex +CREATE UNIQUE INDEX IF NOT EXISTS "WatchlistAudit_id_key" ON "WatchlistAudit"("id"); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "WatchlistAudit_watchlistId_changedAt_idx" ON "WatchlistAudit"("watchlistId", "changedAt"); + +-- CreateIndex - Main composite index matching schema +CREATE INDEX IF NOT EXISTS "Watchlist_type_value_organizationId_action_idx" ON "Watchlist"("type", "value", "organizationId", "action"); + +-- CreateIndex - For organization-specific and global queries +CREATE INDEX IF NOT EXISTS "Watchlist_organizationId_isGlobal_idx" ON "Watchlist"("organizationId", "isGlobal"); + +-- CreateIndex - For source-based queries +CREATE INDEX IF NOT EXISTS "Watchlist_source_idx" ON "Watchlist"("source"); + +-- CreateIndex - Add unique constraint matching schema +CREATE UNIQUE INDEX IF NOT EXISTS "Watchlist_type_value_organizationId_key" ON "Watchlist"("type", "value", "organizationId"); + +-- CreateIndex - Performance index for WatchlistEventAudit queries +CREATE INDEX IF NOT EXISTS "WatchlistEventAudit_watchlistId_timestamp_idx" ON "WatchlistEventAudit"("watchlistId", "timestamp"); + +-- CreateIndex - For eventType-based queries +CREATE INDEX IF NOT EXISTS "WatchlistEventAudit_eventTypeId_timestamp_idx" ON "WatchlistEventAudit"("eventTypeId", "timestamp"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 0362019f6e9ddf..c3c254de42edb7 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -451,8 +451,6 @@ model User { updatedAttributeToUsers AttributeToUser[] @relation("updatedBy") createdTranslations EventTypeTranslation[] @relation("CreatedEventTypeTranslations") updatedTranslations EventTypeTranslation[] @relation("UpdatedEventTypeTranslations") - createdWatchlists Watchlist[] @relation("CreatedWatchlists") - updatedWatchlists Watchlist[] @relation("UpdatedWatchlists") BookingInternalNote BookingInternalNote[] creationSource CreationSource? createdOrganizationOnboardings OrganizationOnboarding[] @relation("CreatedOrganizationOnboardings") @@ -2269,54 +2267,54 @@ enum WatchlistType { USERNAME } -enum WatchlistSeverity { - LOW - MEDIUM - HIGH - CRITICAL -} - enum WatchlistAction { REPORT BLOCK + ALERT } -model Watchlist { - id String @id @unique @default(cuid()) - type WatchlistType - - value String - description String? +enum WatchlistSource { + MANUAL + FREE_DOMAIN_POLICY +} +model Watchlist { + id String @id @default(uuid()) @db.Uuid + type WatchlistType + value String + description String? + isGlobal Boolean @default(false) organizationId Int? - - action WatchlistAction @default(REPORT) - severity WatchlistSeverity @default(LOW) - createdAt DateTime @default(now()) - createdBy User? @relation("CreatedWatchlists", onDelete: SetNull, fields: [createdById], references: [id]) - createdById Int? - updatedAt DateTime @updatedAt - updatedBy User? @relation("UpdatedWatchlists", onDelete: SetNull, fields: [updatedById], references: [id]) - updatedById Int? + action WatchlistAction @default(REPORT) + source WatchlistSource @default(MANUAL) + lastUpdatedAt DateTime @default(now()) @@unique([type, value, organizationId]) @@index([type, value, organizationId, action]) } -model BlockedBookingLog { - id String @id @default(uuid()) - email String - eventTypeId Int? +model WatchlistAudit { + id String @id @default(uuid(7)) @db.Uuid + type WatchlistType + value String + description String? - organizationId Int? + action WatchlistAction @default(REPORT) - bookingData Json? - createdAt DateTime @default(now()) - watchlistId String? + changedAt DateTime @default(now()) + changedByUserId Int? - @@index([organizationId, createdAt]) - @@index([email]) - @@index([watchlistId]) + watchlistId String @db.Uuid + + @@index([watchlistId, changedAt]) +} + +model WatchlistEventAudit { + id String @id @default(uuid(7)) @db.Uuid + watchlistId String @db.Uuid + eventTypeId Int + actionTaken WatchlistAction + timestamp DateTime @default(now()) } enum BillingPeriod {