Files
calendar/packages/features/tasker/tasks/triggerFormSubmittedNoEvent/formSubmissionValidation.ts
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
96bec9f7b9 refactor: Move repositories from @calcom/lib to @calcom/features domain folders (#27570)
* refactor: move repositories from lib to features domain folders

- Move HolidayRepository to features/holidays/repositories
- Move PrismaTrackingRepository to features/bookings/repositories
- Move PrismaBookingPaymentRepository to features/bookings/repositories
- Move PrismaRoutingFormResponseRepository to features/routing-forms/repositories
- Move PrismaAssignmentReasonRepository to features/assignment-reason/repositories
- Move VerificationTokenRepository to features/auth/repositories
- Move WorkspacePlatformRepository to features/workspace-platform/repositories
- Move DTO files to their respective feature domains
- Merge lib DestinationCalendarRepository into features version
- Merge lib SelectedCalendarRepository into features version
- Update all import paths across the codebase

This follows the vertical slice architecture pattern by organizing
repositories by domain rather than by technical layer.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update VerificationTokenService import path to new location

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update test file imports to use new repository locations

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* mv

* fix structure

* fix

* refactor: merge unit tests for SelectedCalendarRepository into single file

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-09 08:41:04 -03:00

114 lines
3.6 KiB
TypeScript

import { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
import { RoutingFormResponseRepository } from "@calcom/features/routing-forms/repositories/RoutingFormResponseRepository";
import prisma from "@calcom/prisma";
import type { FORM_SUBMITTED_WEBHOOK_RESPONSES } from "@calcom/routing-forms/lib/formSubmissionUtils";
export interface ValidationOptions {
responseId: number;
formId: string;
responses: FORM_SUBMITTED_WEBHOOK_RESPONSES;
submittedAt?: Date;
}
export interface ValidationResult {
skip: boolean;
reason?: string;
}
/**
* Check if trigger should be skipped due to booking creation or duplicate submission
*/
export async function shouldTriggerFormSubmittedNoEvent(options: ValidationOptions) {
const { formId, responseId, responses, submittedAt } = options;
const bookingRepository = new BookingRepository(prisma);
// Check if a booking was created from this form response
const bookingFromResponse = await bookingRepository.findFirstBookingFromResponse({ responseId });
if (bookingFromResponse) return false;
// Check for duplicate form submissions
const hasDuplicate = await hasDuplicateSubmission({ formId, responseId, responses, submittedAt });
if (hasDuplicate) {
return false;
}
return true;
}
export function getSubmitterEmail(responses: FORM_SUBMITTED_WEBHOOK_RESPONSES): string | undefined {
const submitterEmail = Object.values(responses).find((response) => {
const value = typeof response === "object" && response && "value" in response ? response.value : response;
return typeof value === "string" && value.includes("@");
})?.value;
if (typeof submitterEmail !== "string") return undefined;
return submitterEmail;
}
/**
* Extracts the submitter's name from form responses.
* Looks for fields with identifiers "name", "firstName", or "lastName" in that order of preference.
*/
export function getSubmitterName(responses: FORM_SUBMITTED_WEBHOOK_RESPONSES) {
const nameField = responses?.name || responses?.firstName || responses?.lastName;
if (nameField && typeof nameField === "object") {
// Try the new response property first, then fall back to value
if (typeof nameField.response === "string") {
return nameField.response;
}
if (typeof nameField.value === "string") {
return nameField.value;
}
}
}
/**
* Check for duplicate form submissions within the last 60 minutes
*/
async function hasDuplicateSubmission({
formId,
responses,
responseId,
submittedAt,
}: {
formId: string;
responses: FORM_SUBMITTED_WEBHOOK_RESPONSES;
responseId: number;
submittedAt?: Date;
}): Promise<boolean> {
const submitterEmail = getSubmitterEmail(responses);
if (!submitterEmail) return false;
const date = submittedAt ?? new Date();
const formResponseRepository = new RoutingFormResponseRepository(prisma);
const sixtyMinutesAgo = new Date(date.getTime() - 60 * 60 * 1000);
const recentResponses = await formResponseRepository.findAllResponsesWithBooking({
formId,
responseId,
createdAfter: sixtyMinutesAgo,
createdBefore: new Date(),
});
// Check if there's a duplicate email in recent responses
return recentResponses.some((response) => {
if (!response.response || typeof response.response !== "object") return false;
return Object.values(response.response as Record<string, { value: string; label: string }>).some(
(field) => {
return (
typeof field === "object" &&
field &&
"value" in field &&
typeof field.value === "string" &&
field.value.toLowerCase() === submitterEmail.toLowerCase()
);
}
);
});
}