From 589c04a53087bfb28bd2610bb54ed08e62a184cf Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:26:03 +0530 Subject: [PATCH 1/4] fix: allow CTA elements to proceed when marked required (#1415) (#7293) Co-authored-by: Cursor --- packages/surveys/src/lib/validation/evaluator.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/surveys/src/lib/validation/evaluator.ts b/packages/surveys/src/lib/validation/evaluator.ts index b777d4474b8e..dfb6b1485612 100644 --- a/packages/surveys/src/lib/validation/evaluator.ts +++ b/packages/surveys/src/lib/validation/evaluator.ts @@ -137,6 +137,11 @@ const checkRequiredField = ( return null; } + // CTA elements never block progression (informational only) + if (element.type === TSurveyElementTypeEnum.CTA) { + return null; + } + if (element.type === TSurveyElementTypeEnum.Ranking) { return validateRequiredRanking(value, t); } From 225217330bd9cfaf8d129306cb136318e0c313dd Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:47:58 +0400 Subject: [PATCH 2/4] fix: adds dataType filter in bc code (#7294) --- .../ee/contacts/segments/lib/filter/prisma-query.test.ts | 7 ++++--- .../ee/contacts/segments/lib/filter/prisma-query.ts | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/web/modules/ee/contacts/segments/lib/filter/prisma-query.test.ts b/apps/web/modules/ee/contacts/segments/lib/filter/prisma-query.test.ts index 9a0a96368d61..5cfe941f81a6 100644 --- a/apps/web/modules/ee/contacts/segments/lib/filter/prisma-query.test.ts +++ b/apps/web/modules/ee/contacts/segments/lib/filter/prisma-query.test.ts @@ -1276,6 +1276,7 @@ describe("segmentFilterToPrismaQuery", () => { attributeKey: { key: "age", environmentId: mockEnvironmentId, + dataType: "number", }, valueNumber: null, }, @@ -1362,7 +1363,7 @@ describe("segmentFilterToPrismaQuery", () => { { attributes: { some: { - attributeKey: { key: "purchaseDate" }, + attributeKey: { key: "purchaseDate", dataType: "date" }, OR: [ { valueDate: { lt: new Date(targetDate) } }, { valueDate: null, value: { lt: new Date(targetDate).toISOString() } }, @@ -1406,7 +1407,7 @@ describe("segmentFilterToPrismaQuery", () => { { attributes: { some: { - attributeKey: { key: "signupDate" }, + attributeKey: { key: "signupDate", dataType: "date" }, OR: [ { valueDate: { gt: new Date(targetDate) } }, { valueDate: null, value: { gt: new Date(targetDate).toISOString() } }, @@ -1451,7 +1452,7 @@ describe("segmentFilterToPrismaQuery", () => { { attributes: { some: { - attributeKey: { key: "lastActivityDate" }, + attributeKey: { key: "lastActivityDate", dataType: "date" }, OR: [ { valueDate: { gte: new Date(startDate), lte: new Date(endDate) } }, { diff --git a/apps/web/modules/ee/contacts/segments/lib/filter/prisma-query.ts b/apps/web/modules/ee/contacts/segments/lib/filter/prisma-query.ts index 230e3d4fc32b..30eb01e6b150 100644 --- a/apps/web/modules/ee/contacts/segments/lib/filter/prisma-query.ts +++ b/apps/web/modules/ee/contacts/segments/lib/filter/prisma-query.ts @@ -107,7 +107,7 @@ const buildDateAttributeFilterWhereClause = (filter: TSegmentAttributeFilter): P return { attributes: { some: { - attributeKey: { key: contactAttributeKey }, + attributeKey: { key: contactAttributeKey, dataType: "date" }, OR: [{ valueDate: dateCondition }, { valueDate: null, value: stringDateCondition }], }, }, @@ -165,6 +165,7 @@ const buildNumberAttributeFilterWhereClause = async ( attributeKey: { key: contactAttributeKey, environmentId, + dataType: "number", }, valueNumber: null, }, @@ -183,6 +184,7 @@ const buildNumberAttributeFilterWhereClause = async ( JOIN "ContactAttributeKey" cak ON ca."attributeKeyId" = cak.id WHERE cak.key = $1 AND cak."environmentId" = $4 + AND cak."dataType" = 'number' AND ca."valueNumber" IS NULL AND ca.value ~ $3 AND ca.value::double precision ${sqlOp} $2 From 7c8a7606b784e7b37a841df6bbcd44b8ce072d12 Mon Sep 17 00:00:00 2001 From: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:16:18 +0400 Subject: [PATCH 3/4] fix: fixes the no segment in draft surveys bug (#7290) --- .../ee/contacts/segments/lib/segments.test.ts | 68 +++++++++++++++++++ .../ee/contacts/segments/lib/segments.ts | 56 ++++++++++----- 2 files changed, 106 insertions(+), 18 deletions(-) diff --git a/apps/web/modules/ee/contacts/segments/lib/segments.test.ts b/apps/web/modules/ee/contacts/segments/lib/segments.test.ts index acf5c6b57cbb..8ed0216b0c9e 100644 --- a/apps/web/modules/ee/contacts/segments/lib/segments.test.ts +++ b/apps/web/modules/ee/contacts/segments/lib/segments.test.ts @@ -37,6 +37,7 @@ vi.mock("@formbricks/database", () => ({ create: vi.fn(), delete: vi.fn(), update: vi.fn(), + upsert: vi.fn(), findFirst: vi.fn(), }, survey: { @@ -206,6 +207,73 @@ describe("Segment Service Tests", () => { vi.mocked(prisma.segment.create).mockRejectedValue(new Error("DB error")); await expect(createSegment(mockSegmentCreateInput)).rejects.toThrow(Error); }); + + test("should upsert a private segment without surveyId", async () => { + const privateInput: TSegmentCreateInput = { + ...mockSegmentCreateInput, + isPrivate: true, + }; + const privateSegmentPrisma = { ...mockSegmentPrisma, isPrivate: true }; + vi.mocked(prisma.segment.upsert).mockResolvedValue(privateSegmentPrisma); + const segment = await createSegment(privateInput); + expect(segment).toEqual({ ...mockSegment, isPrivate: true }); + expect(prisma.segment.upsert).toHaveBeenCalledWith({ + where: { + environmentId_title: { + environmentId, + title: privateInput.title, + }, + }, + create: { + environmentId, + title: privateInput.title, + description: undefined, + isPrivate: true, + filters: [], + }, + update: { + description: undefined, + filters: [], + }, + select: selectSegment, + }); + expect(prisma.segment.create).not.toHaveBeenCalled(); + }); + + test("should upsert a private segment with surveyId", async () => { + const privateInputWithSurvey: TSegmentCreateInput = { + ...mockSegmentCreateInput, + isPrivate: true, + surveyId, + }; + const privateSegmentPrisma = { ...mockSegmentPrisma, isPrivate: true }; + vi.mocked(prisma.segment.upsert).mockResolvedValue(privateSegmentPrisma); + const segment = await createSegment(privateInputWithSurvey); + expect(segment).toEqual({ ...mockSegment, isPrivate: true }); + expect(prisma.segment.upsert).toHaveBeenCalledWith({ + where: { + environmentId_title: { + environmentId, + title: privateInputWithSurvey.title, + }, + }, + create: { + environmentId, + title: privateInputWithSurvey.title, + description: undefined, + isPrivate: true, + filters: [], + surveys: { connect: { id: surveyId } }, + }, + update: { + description: undefined, + filters: [], + surveys: { connect: { id: surveyId } }, + }, + select: selectSegment, + }); + expect(prisma.segment.create).not.toHaveBeenCalled(); + }); }); describe("cloneSegment", () => { diff --git a/apps/web/modules/ee/contacts/segments/lib/segments.ts b/apps/web/modules/ee/contacts/segments/lib/segments.ts index 30a8f9d9d1e9..c770eb0916b3 100644 --- a/apps/web/modules/ee/contacts/segments/lib/segments.ts +++ b/apps/web/modules/ee/contacts/segments/lib/segments.ts @@ -136,28 +136,48 @@ export const createSegment = async (segmentCreateInput: TSegmentCreateInput): Pr const { description, environmentId, filters, isPrivate, surveyId, title } = segmentCreateInput; - let data: Prisma.SegmentCreateArgs["data"] = { - environmentId, - title, - description, - isPrivate, - filters, - }; + const surveyConnect = surveyId ? { surveys: { connect: { id: surveyId } } } : {}; - if (surveyId) { - data = { - ...data, - surveys: { - connect: { - id: surveyId, + try { + // Private segments use upsert because auto-save may have already created a + // default (empty-filter) segment via connectOrCreate before the user publishes. + // Without upsert the second create hits the (environmentId, title) unique constraint. + if (isPrivate) { + const segment = await prisma.segment.upsert({ + where: { + environmentId_title: { + environmentId, + title, + }, }, - }, - }; - } + create: { + environmentId, + title, + description, + isPrivate, + filters, + ...surveyConnect, + }, + update: { + description, + filters, + ...surveyConnect, + }, + select: selectSegment, + }); + + return transformPrismaSegment(segment); + } - try { const segment = await prisma.segment.create({ - data, + data: { + environmentId, + title, + description, + isPrivate, + filters, + ...surveyConnect, + }, select: selectSegment, }); From f4ac9a8292fdc290e4e41d8ec3c80efecc544232 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:26:42 +0530 Subject: [PATCH 4/4] fix: always validate only responseData fields in client/management APIs (#7292) (#7296) Co-authored-by: Cursor --- .../responses/[responseId]/route.ts | 3 - .../client/[environmentId]/responses/route.ts | 1 - .../responses/[responseId]/route.ts | 1 - .../app/api/v1/management/responses/route.ts | 1 - .../client/[environmentId]/responses/route.ts | 1 - apps/web/modules/api/lib/validation.test.ts | 61 ++++++++++++++----- apps/web/modules/api/lib/validation.ts | 14 ++--- .../responses/[responseId]/route.ts | 1 - .../api/v2/management/responses/route.ts | 1 - 9 files changed, 52 insertions(+), 32 deletions(-) diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts index 122af961e34f..aee7b98faa76 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/[responseId]/route.ts @@ -44,13 +44,10 @@ const validateResponse = ( ...responseUpdateInput.data, }; - const isFinished = responseUpdateInput.finished ?? false; - const validationErrors = validateResponseData( survey.blocks, mergedData, responseUpdateInput.language ?? response.language ?? "en", - isFinished, survey.questions ); diff --git a/apps/web/app/api/v1/client/[environmentId]/responses/route.ts b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts index af4aaff8e6dc..0178320b07b3 100644 --- a/apps/web/app/api/v1/client/[environmentId]/responses/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/responses/route.ts @@ -41,7 +41,6 @@ const validateResponse = (responseInputData: TResponseInput, survey: TSurvey) => survey.blocks, responseInputData.data, responseInputData.language ?? "en", - responseInputData.finished, survey.questions ); diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts index 189fdfee639c..737d861a0f99 100644 --- a/apps/web/app/api/v1/management/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -146,7 +146,6 @@ export const PUT = withV1ApiWrapper({ result.survey.blocks, responseUpdate.data, responseUpdate.language ?? "en", - responseUpdate.finished, result.survey.questions ); diff --git a/apps/web/app/api/v1/management/responses/route.ts b/apps/web/app/api/v1/management/responses/route.ts index 8d9219754335..3375a804b20f 100644 --- a/apps/web/app/api/v1/management/responses/route.ts +++ b/apps/web/app/api/v1/management/responses/route.ts @@ -155,7 +155,6 @@ export const POST = withV1ApiWrapper({ surveyResult.survey.blocks, responseInput.data, responseInput.language ?? "en", - responseInput.finished, surveyResult.survey.questions ); diff --git a/apps/web/app/api/v2/client/[environmentId]/responses/route.ts b/apps/web/app/api/v2/client/[environmentId]/responses/route.ts index d4427739e07b..84f9c079ee68 100644 --- a/apps/web/app/api/v2/client/[environmentId]/responses/route.ts +++ b/apps/web/app/api/v2/client/[environmentId]/responses/route.ts @@ -112,7 +112,6 @@ export const POST = async (request: Request, context: Context): Promise { mockGetElementsFromBlocks.mockReturnValue(mockElements); mockValidateBlockResponses.mockReturnValue({}); - validateResponseData([], mockResponseData, "en", true, mockQuestions); + validateResponseData([], mockResponseData, "en", mockQuestions); expect(mockTransformQuestionsToBlocks).toHaveBeenCalledWith(mockQuestions, []); expect(mockGetElementsFromBlocks).toHaveBeenCalledWith(transformedBlocks); @@ -105,15 +105,15 @@ describe("validateResponseData", () => { mockGetElementsFromBlocks.mockReturnValue(mockElements); mockValidateBlockResponses.mockReturnValue({}); - validateResponseData(mockBlocks, mockResponseData, "en", true, mockQuestions); + validateResponseData(mockBlocks, mockResponseData, "en", mockQuestions); expect(mockTransformQuestionsToBlocks).not.toHaveBeenCalled(); }); test("should return null when both blocks and questions are empty", () => { - expect(validateResponseData([], mockResponseData, "en", true, [])).toBeNull(); - expect(validateResponseData(null, mockResponseData, "en", true, [])).toBeNull(); - expect(validateResponseData(undefined, mockResponseData, "en", true, null)).toBeNull(); + expect(validateResponseData([], mockResponseData, "en", [])).toBeNull(); + expect(validateResponseData(null, mockResponseData, "en", [])).toBeNull(); + expect(validateResponseData(undefined, mockResponseData, "en", null)).toBeNull(); }); test("should use default language code", () => { @@ -125,25 +125,58 @@ describe("validateResponseData", () => { expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, mockResponseData, "en"); }); - test("should validate only present fields when finished is false", () => { + test("should validate only fields present in responseData", () => { const partialResponseData: TResponseData = { element1: "test" }; - const partialElements = [mockElements[0]]; + const elementsToValidate = [mockElements[0]]; mockGetElementsFromBlocks.mockReturnValue(mockElements); mockValidateBlockResponses.mockReturnValue({}); - validateResponseData(mockBlocks, partialResponseData, "en", false); + validateResponseData(mockBlocks, partialResponseData, "en"); - expect(mockValidateBlockResponses).toHaveBeenCalledWith(partialElements, partialResponseData, "en"); + expect(mockValidateBlockResponses).toHaveBeenCalledWith(elementsToValidate, partialResponseData, "en"); }); - test("should validate all fields when finished is true", () => { - const partialResponseData: TResponseData = { element1: "test" }; - mockGetElementsFromBlocks.mockReturnValue(mockElements); + test("should never validate elements not in responseData", () => { + const blocksWithTwoElements: TSurveyBlock[] = [ + ...mockBlocks, + { + id: "block2", + name: "Block 2", + elements: [ + { + id: "element2", + type: TSurveyElementTypeEnum.OpenText, + headline: { default: "Q2" }, + required: true, + inputType: "text", + charLimit: { enabled: false }, + }, + ], + }, + ]; + const allElements = [ + ...mockElements, + { + id: "element2", + type: TSurveyElementTypeEnum.OpenText, + headline: { default: "Q2" }, + required: true, + inputType: "text", + charLimit: { enabled: false }, + }, + ]; + const responseDataWithOnlyElement1: TResponseData = { element1: "test" }; + mockGetElementsFromBlocks.mockReturnValue(allElements); mockValidateBlockResponses.mockReturnValue({}); - validateResponseData(mockBlocks, partialResponseData, "en", true); + validateResponseData(blocksWithTwoElements, responseDataWithOnlyElement1, "en"); - expect(mockValidateBlockResponses).toHaveBeenCalledWith(mockElements, partialResponseData, "en"); + // Only element1 should be validated, not element2 (even though it's required) + expect(mockValidateBlockResponses).toHaveBeenCalledWith( + [allElements[0]], + responseDataWithOnlyElement1, + "en" + ); }); }); diff --git a/apps/web/modules/api/lib/validation.ts b/apps/web/modules/api/lib/validation.ts index 67861c6bbdb6..37f31cdf7f31 100644 --- a/apps/web/modules/api/lib/validation.ts +++ b/apps/web/modules/api/lib/validation.ts @@ -9,13 +9,13 @@ import { getElementsFromBlocks } from "@/lib/survey/utils"; import { ApiErrorDetails } from "@/modules/api/v2/types/api-error"; /** - * Validates response data against survey validation rules - * Handles partial responses (in-progress) by only validating present fields when finished is false + * Validates response data against survey validation rules. + * Only validates elements that have data in responseData - never validates + * all survey elements regardless of completion status. * * @param blocks - Survey blocks containing elements with validation rules (preferred) * @param responseData - Response data to validate (keyed by element ID) * @param languageCode - Language code for error messages (defaults to "en") - * @param finished - Whether the response is finished (defaults to true for management APIs) * @param questions - Survey questions (legacy format, used as fallback if blocks are empty) * @returns Validation error map keyed by element ID, or null if validation passes */ @@ -23,7 +23,6 @@ export const validateResponseData = ( blocks: TSurveyBlock[] | undefined | null, responseData: TResponseData, languageCode: string = "en", - finished: boolean = true, questions?: TSurveyQuestion[] | undefined | null ): TValidationErrorMap | null => { // Use blocks if available, otherwise transform questions to blocks @@ -42,11 +41,8 @@ export const validateResponseData = ( // Extract elements from blocks const allElements = getElementsFromBlocks(blocksToUse); - // If response is not finished, only validate elements that are present in the response data - // This prevents "required" errors for fields the user hasn't reached yet - const elementsToValidate = finished - ? allElements - : allElements.filter((element) => Object.keys(responseData).includes(element.id)); + // Always validate only elements that are present in responseData + const elementsToValidate = allElements.filter((element) => Object.keys(responseData).includes(element.id)); // Validate selected elements const errorMap = validateBlockResponses(elementsToValidate, responseData, languageCode); diff --git a/apps/web/modules/api/v2/management/responses/[responseId]/route.ts b/apps/web/modules/api/v2/management/responses/[responseId]/route.ts index c8bd252fd529..30f3f5815611 100644 --- a/apps/web/modules/api/v2/management/responses/[responseId]/route.ts +++ b/apps/web/modules/api/v2/management/responses/[responseId]/route.ts @@ -198,7 +198,6 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str questionsResponse.data.blocks, body.data, body.language ?? "en", - body.finished, questionsResponse.data.questions ); diff --git a/apps/web/modules/api/v2/management/responses/route.ts b/apps/web/modules/api/v2/management/responses/route.ts index 438e0dff554b..1966271ccf2e 100644 --- a/apps/web/modules/api/v2/management/responses/route.ts +++ b/apps/web/modules/api/v2/management/responses/route.ts @@ -134,7 +134,6 @@ export const POST = async (request: Request) => surveyQuestions.data.blocks, body.data, body.language ?? "en", - body.finished, surveyQuestions.data.questions );