* 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>
114 lines
3.6 KiB
TypeScript
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()
|
|
);
|
|
}
|
|
);
|
|
});
|
|
}
|