From 98b6d63164d5c2f1e96f2bae209a4390aeea7a66 Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Wed, 11 Feb 2026 15:47:14 +0100 Subject: [PATCH] refactor: apply biome formatting to packages/features (#27844) * refactor: apply biome formatting to packages/features (batch 1 - small subdirs) Format small subdirectories in packages/features: di, flags, holidays, oauth, settings, users, assignment-reason, selectedCalendar, hashedLink, host, form, form-builder, availability, data-table, pbac, schedules, troubleshooter, eventtypes, calendar-subscription, and root-level files. Also includes straggler apps/web BookEventForm.tsx. Co-Authored-By: Claude Opus 4.6 * refactor: apply biome formatting to packages/features (batch 2 - medium subdirs) Format medium subdirectories in packages/features: auth, credentials, calendars, routing-forms, routing-trace, attributes, watchlist, calAIPhone, tasker, and webhooks. Co-Authored-By: Claude Opus 4.6 * refactor: apply biome formatting to packages/features (batch 3 - bookings + insights) Format bookings and insights subdirectories in packages/features. Co-Authored-By: Claude Opus 4.6 * refactor: apply biome formatting to packages/features (batch 4 - ee) Format packages/features/ee subdirectory covering billing, workflows, organizations, teams, managed-event-types, round-robin, dsync, integration-attribute-sync, and payments. Co-Authored-By: Claude Opus 4.6 * refactor: apply biome formatting to packages/features (batch 5 - booking-audit part 1) Format booking-audit di, actions, common, dto, repository, and types subdirectories in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 * refactor: apply biome formatting to packages/features (batch 6 - booking-audit part 2) Format booking-audit service subdirectory in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../BookEventForm/BookEventForm.tsx | 75 +- packages/features/CalendarEventBuilder.ts | 86 +- .../AssignmentReasonRepository.ts | 1 - .../di/AttributeService.container.ts | 5 +- .../attributes/di/AttributeService.module.ts | 6 +- .../attributes/lib/getAttributes.test.ts | 60 +- .../features/attributes/lib/getAttributes.ts | 124 +-- .../repositories/PrismaAttributeRepository.ts | 8 +- .../attributes/services/AttributeService.ts | 7 +- .../services/attributeService.test.ts | 11 +- .../auth/__mocks__/getServerSession.mocks.ts | 4 +- .../features/auth/lib/getLocaleFromRequest.ts | 1 - .../auth/lib/getServerSession.test.ts | 15 +- .../auth/lib/next-auth-options.test.ts | 8 +- .../features/auth/lib/next-auth-options.ts | 54 +- .../features/auth/lib/samlAccountLinking.ts | 9 +- .../auth/lib/sendVerificationRequest.ts | 2 +- ...ateOrUpdateMemberships.integration-test.ts | 11 +- .../signup/utils/createOrUpdateMemberships.ts | 4 +- .../getAggregatedAvailability.ts | 19 +- .../di/ActorRepository.module.ts | 18 +- .../di/AuditActorRepository.container.ts | 4 +- .../di/AuditActorRepository.module.ts | 19 +- .../BookingAuditProducerService.container.ts | 5 +- .../di/BookingAuditTaskConsumer.container.ts | 5 +- .../di/BookingAuditTaskConsumer.module.ts | 2 +- ...ookingAuditTaskerProducerService.module.ts | 2 +- .../di/BookingAuditViewerService.container.ts | 1 - .../di/BookingAuditViewerService.module.ts | 5 +- packages/features/booking-audit/di/tokens.ts | 24 +- .../lib/actions/AcceptedAuditActionService.ts | 115 +-- .../AttendeeRemovedAuditActionService.ts | 119 +-- .../lib/actions/AuditActionServiceHelper.ts | 7 +- .../actions/CancelledAuditActionService.ts | 127 +-- .../lib/actions/CreatedAuditActionService.ts | 163 ++-- .../NoShowUpdatedAuditActionService.ts | 5 +- .../actions/RescheduledAuditActionService.ts | 284 +++--- .../NoShowUpdatedAuditActionService.test.ts | 8 +- .../ReassignmentAuditActionService.test.ts | 16 +- .../actions/__tests__/contractVerification.ts | 20 +- .../booking-audit/lib/common/changeSchemas.ts | 21 +- .../features/booking-audit/lib/dto/types.ts | 49 +- .../booking-audit/lib/getAppNameFromSlug.ts | 18 +- .../features/booking-audit/lib/makeActor.ts | 86 +- .../lib/repository/IAuditActorRepository.ts | 14 +- .../lib/repository/IBookingAuditRepository.ts | 121 +-- .../repository/PrismaAuditActorRepository.ts | 219 +++-- .../PrismaBookingAuditRepository.ts | 193 ++-- .../lib/service/ActorStrategies.ts | 4 +- .../lib/service/BookingAuditAccessService.ts | 151 ++-- .../BookingAuditActionServiceRegistry.ts | 137 +-- .../BookingAuditTaskerProducerService.ts | 849 +++++++++--------- .../lib/service/BookingAuditViewerService.ts | 7 +- .../service/BookingHistoryViewerService.ts | 171 ++-- .../BookingAuditAccessService.test.ts | 200 +++-- .../BookingAuditViewerService.test.ts | 8 +- .../__tests__/EnrichmentDataStore.test.ts | 97 +- .../cancelled-action.integration-test.ts | 1 - .../booking-audit/lib/types/actionSource.ts | 19 +- .../lib/types/bookingAuditTask.ts | 72 +- .../bookings/Booker/hooks/useBookerLayout.ts | 2 +- .../bookings/Booker/hooks/useBookerTime.ts | 2 +- .../bookings/Booker/hooks/useBookingForm.ts | 2 +- .../Booker/hooks/useInitialFormValues.test.ts | 2 +- .../Booker/hooks/useInitialFormValues.ts | 2 +- .../bookings/Booker/hooks/useLocalSet.tsx | 2 +- packages/features/bookings/Booker/store.ts | 67 +- .../features/bookings/Booker/utils/dates.tsx | 10 +- .../utils/getPrefetchMonthCount.test.ts | 8 +- .../BookingEventHandlerService.container.ts | 1 - .../features/bookings/lib/EventManager.ts | 6 +- .../bookings/lib/buildEventUrlFromBooking.ts | 2 +- packages/features/bookings/lib/get-booking.ts | 58 +- .../getAllCredentials.test.ts | 8 +- .../getAllCredentials.ts | 8 +- .../lib/getAssignmentReasonCategory.test.ts | 4 +- .../features/bookings/lib/getBookingFields.ts | 4 +- .../lib/getBookingResponsesSchema.test.ts | 2 +- .../bookings/lib/getBookingResponsesSchema.ts | 105 +-- .../bookings/lib/getHostsAndGuests.ts | 10 +- .../features/bookings/lib/getLuckyUser.ts | 36 +- .../lib/handleNewBooking/createBooking.ts | 13 +- .../handleNewBooking/getAuditActionSource.ts | 34 +- .../getCustomInputsResponses.ts | 28 +- .../getLocationValuesForDb.ts | 2 +- .../lib/handleNewBooking/handleAppsStatus.ts | 23 +- .../test/buildEventForTeamEventType.test.ts | 2 +- .../test/fresh-booking.test.ts | 17 +- .../test/team-bookings/round-robin.test.ts | 267 +++--- .../validateBookingTimeIsNotOutOfBounds.ts | 2 +- .../features/bookings/lib/handlePayment.ts | 8 +- .../lib/handleSeats/create/createNewSeat.ts | 61 +- .../lib/lastAttendeeDeleteBooking.ts | 1 - .../attendeeRescheduleSeatedBooking.ts | 7 +- .../owner/combineTwoSeatedBookings.ts | 1 - .../owner/moveSeatedBookingToNewTimeSlot.ts | 20 +- .../filterHostsBySameRoundRobinHost.ts | 2 +- ...QualifiedHostsWithDelegationCredentials.ts | 10 +- .../BookingEventHandlerService.ts | 132 ++- .../lib/payment/handleNoShowFee.test.ts | 16 +- ...ermineReschedulePreventionRedirect.test.ts | 36 +- .../service/InstantBookingCreateService.ts | 23 +- .../lib/service/RegularBookingService.ts | 7 +- .../BookingRepository.integration-test.ts | 6 +- .../repositories/IAttendeeRepository.ts | 7 +- .../repositories/IBookingRepository.ts | 8 +- .../services/BookingAccessService.test.ts | 8 +- .../bookings/services/BookingAccessService.ts | 4 +- .../calAIPhone/AIPhoneServiceRegistry.test.ts | 2 +- .../interfaces/AIPhoneService.interface.ts | 20 +- ...etellAIPhoneServiceProviderFactory.test.ts | 56 +- .../retellAI/RetellAIService.test.ts | 12 +- .../retellAI/RetellSDKClient.test.ts | 6 +- .../calAIPhone/providers/retellAI/errors.ts | 6 +- .../retellAI/services/AgentService.ts | 4 +- .../retellAI/services/BillingService.ts | 26 +- .../retellAI/services/__tests__/test-utils.ts | 10 +- .../repositories/PrismaAgentRepository.ts | 31 +- .../PrismaPhoneNumberRepository.ts | 74 +- .../features/calAIPhone/retellAIService.ts | 10 +- .../GoogleCalendarSubscription.adapter.ts | 12 +- .../GoogleCalendarSubscriptionAdapter.test.ts | 2 +- .../lib/telemetry/CalendarTelemetryWrapper.ts | 15 +- .../calendars/components/DatePicker.tsx | 173 ++-- .../DestinationCalendarSelector.tsx | 1 - .../features/calendars/lib/CalendarManager.ts | 3 +- .../lib/tasker/CalendarsSyncTasker.ts | 4 +- .../calendars/weeklyview/utils/overlap.ts | 22 +- .../credentials/deleteCredential.test.ts | 5 +- .../credentials/handleDeleteCredential.ts | 19 +- .../repositories/CredentialRepository.ts | 90 +- .../services/CredentialAccessService.test.ts | 24 +- .../data-table/hooks/useDebouncedWidth.ts | 1 - .../data-table/hooks/useFilterValue.ts | 2 +- packages/features/data-table/lib/types.ts | 35 +- .../data-table/repositories/filterSegment.ts | 4 +- .../di/containers/BookingAccessService.ts | 2 - .../BookingAuditViewerService.container.ts | 3 +- .../di/containers/FeaturesRepository.ts | 5 +- .../di/containers/ManagedEventReassignment.ts | 5 +- .../features/di/modules/AssignmentReason.ts | 1 - .../di/modules/BookingAccessService.ts | 2 - packages/features/di/modules/Credential.ts | 1 - packages/features/di/modules/Holiday.ts | 1 - .../di/modules/ManagedEventReassignment.ts | 1 - .../di/shared/services/tasker.service.ts | 2 +- .../features/ee/api-keys/lib/autoLock.test.ts | 4 +- .../ee/api-keys/services/ApiKeyService.ts | 30 +- .../services/ActiveUserBillingService.ts | 63 +- .../ee/billing/api/webhook/__handler.test.ts | 2 +- .../webhook/_invoice.payment_failed.test.ts | 10 +- .../api/webhook/_invoice.payment_failed.ts | 5 +- .../billing/api/webhook/hwm-webhook-utils.ts | 12 +- .../ee/billing/credit-service.test.ts | 28 +- .../highWaterMark/HighWaterMarkRepository.ts | 19 +- .../MonthlyProrationTeamRepository.ts | 4 +- packages/features/ee/billing/seed.ts | 7 +- .../service/dueInvoice/seed-proration-test.ts | 5 +- .../HighWaterMarkStripe.integration-test.ts | 93 +- .../service/highWaterMark/seed-hwm-test.ts | 43 +- .../proration/MonthlyProrationService.ts | 10 +- .../proration/ProrationEmailService.ts | 87 +- ...onthlyProrationService.integration-test.ts | 68 +- .../tasker/ProrationEmailSyncTasker.ts | 7 +- .../trigger/scheduleMonthlyProration.ts | 24 +- .../SeatBillingStrategyFactory.ts | 25 +- .../SeatBillingStrategyFactory.test.ts | 10 +- .../service/teams/ITeamBillingService.ts | 5 +- .../ee/dsync/lib/assignValueToUser.ts | 2 +- .../ee/dsync/lib/handleUserEvents.test.ts | 8 +- .../di/AttributeSyncRuleService.container.ts | 4 +- .../IIntegrationAttributeSyncRepository.ts | 30 +- .../schemas/zod.test.ts | 6 +- .../integration-attribute-sync/schemas/zod.ts | 6 +- .../AttributeSyncFieldMappingService.ts | 51 +- .../services/AttributeSyncRuleService.ts | 60 +- .../IntegrationAttributeSyncService.test.ts | 89 +- .../IntegrationAttributeSyncService.ts | 98 +- .../lib/handleChildrenEventTypes.ts | 13 +- .../managed-event-types/reassignment/index.ts | 1 - ...ventManualReassignment.integration-test.ts | 3 +- .../managedEventManualReassignment.ts | 2 +- ...nagedEventReassignment.integration-test.ts | 9 +- .../reassignment/managedEventReassignment.ts | 1 - .../ManagedEventAssignmentReasonRecorder.ts | 2 +- .../ManagedEventManualReassignmentService.ts | 30 +- .../ManagedEventReassignmentService.ts | 3 +- .../reassignment/services/container.ts | 3 +- .../reassignment/utils/buildNewBookingPlan.ts | 1 - .../utils/findTargetChildEventType.ts | 12 +- .../utils/validateManagedEventReassignment.ts | 10 +- .../__mocks__/organizationMock.ts | 16 +- .../OrganizationMembershipService.module.ts | 19 +- .../di/OrganizationRepository.module.ts | 19 +- .../ee/organizations/di/tasker/tokens.ts | 4 +- .../features/ee/organizations/di/tokens.ts | 8 +- .../lib/server/orgCreationUtils.ts | 5 +- .../service/OrganizationMembershipService.ts | 47 +- .../__tests__/BaseOnboardingService.test.ts | 18 +- .../features/ee/organizations/lib/utils.ts | 4 +- packages/features/ee/payments/api/webhook.ts | 69 +- .../handleRescheduleEventManager.ts | 4 +- .../roundRobinManualReassignment.ts | 4 +- .../ee/teams/lib/getTeamMemberEmailFromCrm.ts | 8 +- packages/features/ee/teams/lib/payments.ts | 43 +- .../ee/teams/repositories/TeamRepository.ts | 112 +-- .../services/teamService.alternative.test.ts | 6 +- .../services/teamService.integration-test.ts | 4 +- packages/features/ee/tsconfig.json | 10 +- .../workflows/api/scheduleEmailReminders.ts | 2 +- .../ee/workflows/hooks/useVoicePreview.ts | 3 +- .../features/ee/workflows/lib/getOptions.ts | 11 +- .../lib/reminders/reminderScheduler.ts | 5 +- .../lib/reminders/templates/customTemplate.ts | 6 +- .../lib/service/EmailWorkflowService.ts | 15 +- .../lib/service/WorkflowService.test.ts | 8 +- .../lib/service/workflowOptOutService.ts | 5 +- .../compareReminderBodyToTemplate.test.ts | 2 - .../workflows/lib/test/twilioWebhook.test.ts | 10 +- .../features/ee/workflows/lib/urlScanner.ts | 15 +- .../ee/workflows/lib/variableTranslations.ts | 4 +- .../repositories/WorkflowRepository.test.ts | 1 - .../BulkEditDefaultForEventsModal.tsx | 4 +- .../components/CheckedTeamSelect.tsx | 5 +- .../components/tabs/limits/EventLimitsTab.tsx | 42 +- .../recurring/RecurringEventController.tsx | 4 +- .../lib/getEventTypeById.integration-test.ts | 21 +- .../features/eventtypes/lib/getPublicEvent.ts | 10 +- .../flags/features.repository.mock.ts | 13 +- .../features/flags/features.repository.ts | 2 +- .../form-builder/utils/variantsConfig.ts | 41 +- packages/features/form/components/Select.tsx | 7 +- packages/features/handleMarkNoShow.test.ts | 6 +- packages/features/handleMarkNoShow.ts | 7 +- .../lib/service/HashedLinkService.ts | 8 +- .../repositories/HolidayRepository.test.ts | 1 - .../repositories/HolidayRepository.ts | 1 - .../host/repositories/HostRepository.test.ts | 4 +- .../features/insights/lib/bookingUtils.ts | 5 +- .../insights/server/routing-events.ts | 56 +- .../services/InsightsBookingBaseService.ts | 201 +++-- .../services/InsightsRoutingBaseService.ts | 173 ++-- .../__tests__/csvDataTransformer.test.ts | 79 +- .../insights/services/csvDataTransformer.ts | 16 +- .../repositories/OAuthClientRepository.ts | 1 - .../features/oauth/services/OAuthService.ts | 4 +- .../domain/errors/role-management.error.ts | 5 +- .../__tests__/event-type-permissions.test.ts | 16 +- .../__tests__/resource-permissions.test.ts | 8 +- .../features/pbac/services/role.service.ts | 4 +- .../pbac/utils/permissionTraversal.ts | 5 +- packages/features/routing-forms/di/tokens.ts | 4 +- .../lib/findFieldValueByIdentifier.ts | 1 - .../findTeamMembersMatchingAttributeLogic.ts | 7 +- .../routing-forms/lib/getFieldIdentifier.ts | 1 - .../lib/getFieldResponseByIdentifier.ts | 20 +- .../lib/parseRoutingFormResponse.ts | 7 +- packages/features/routing-forms/lib/types.ts | 1 - packages/features/routing-forms/lib/zod.ts | 77 +- .../routing-trace/components/DomainIcon.tsx | 18 +- .../RoutingFormTracePresenter.test.ts | 8 +- .../presenters/RoutingFormTracePresenter.ts | 4 +- .../presenters/RoutingTracePresenter.test.ts | 4 +- .../SalesforceRoutingTracePresenter.test.ts | 28 +- ...PendingRoutingTraceRepository.interface.ts | 14 +- .../PrismaPendingRoutingTraceRepository.ts | 21 +- .../services/RoutingTraceService.test.ts | 5 +- .../services/RoutingTraceService.ts | 88 +- .../components/ScheduleComponent.tsx | 46 +- .../schedules/components/ScheduleListItem.tsx | 75 +- .../SelectedCalendarRepository.ts | 5 +- .../settings/appDir/SettingsHeader.tsx | 2 +- .../appDir/SettingsHeaderWithBackButton.tsx | 4 +- packages/features/tasker/repository.ts | 2 +- .../features/tasker/tasks/bookingAudit.ts | 58 +- .../crm/__tests__/createCRMEvent.test.ts | 5 +- .../tasker/tasks/executeAIPhoneCall.ts | 4 +- .../features/tasker/tasks/scanWorkflowBody.ts | 6 +- .../tasker/tasks/sendWorkflowEmails.test.ts | 16 +- .../tasker/tasks/sendWorkflowEmails.ts | 16 +- .../tasks/test/scanWorkflowBody.test.ts | 11 +- .../tasks/test/scanWorkflowUrls.test.ts | 21 +- .../triggerFormSubmittedNoEventWebhook.ts | 5 +- .../triggerNoShow/triggerGuestNoShow.test.ts | 12 +- .../CalendarToggleContainerComponent.tsx | 35 +- .../components/EventTypeSelectComponent.tsx | 24 +- packages/features/users/lib/getRoutedUsers.ts | 4 +- .../users/repositories/UserRepository.test.ts | 1 - .../watchlist/lib/errors/WatchlistErrors.ts | 9 +- .../lib/repository/WatchlistRepository.ts | 5 +- .../OrganizationWatchlistOperationsService.ts | 5 +- .../operations/check-user-blocking.test.ts | 18 +- .../filter-blocked-hosts.controller.test.ts | 34 +- .../filter-blocked-users.controller.test.ts | 54 +- .../features/webhooks/lib/WebhookService.ts | 5 +- .../base/BaseDelegationPayloadBuilder.ts | 1 - .../base/BaseFormPayloadBuilder.test.ts | 4 +- .../versioned/PayloadBuilderFactory.test.ts | 56 +- .../lib/factory/versioned/registry.test.ts | 26 +- .../v2021-10-20/BookingPayloadBuilder.test.ts | 1 - .../v2021-10-20/DelegationPayloadBuilder.ts | 1 - packages/features/webhooks/lib/getWebhooks.ts | 1 - .../handleWebhookScheduledTriggers.test.ts | 2 +- .../lib/handleWebhookScheduledTriggers.ts | 2 +- .../mappers/WebhookOutputMapper.ts | 1 - .../WebhookNotificationHandler.test.ts | 11 +- .../lib/service/WebhookNotificationHandler.ts | 5 +- .../webhooks/lib/service/WebhookNotifier.ts | 5 +- .../lib/service/WebhookService.test.ts | 23 +- .../lib/tasker/trigger/deliver-webhook.ts | 50 +- 310 files changed, 4309 insertions(+), 4585 deletions(-) diff --git a/apps/web/modules/bookings/components/BookEventForm/BookEventForm.tsx b/apps/web/modules/bookings/components/BookEventForm/BookEventForm.tsx index 69619b28c7..7db2fa5dc2 100644 --- a/apps/web/modules/bookings/components/BookEventForm/BookEventForm.tsx +++ b/apps/web/modules/bookings/components/BookEventForm/BookEventForm.tsx @@ -8,11 +8,7 @@ import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBooke import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider"; import type { BookerEvent } from "@calcom/features/bookings/types"; import ServerTrans from "@calcom/lib/components/ServerTrans"; -import { - APP_NAME, - WEBSITE_PRIVACY_POLICY_URL, - WEBSITE_TERMS_URL, -} from "@calcom/lib/constants"; +import { APP_NAME, WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { TimeFormat } from "@calcom/lib/timeFormat"; @@ -24,10 +20,7 @@ import { Form } from "@calcom/ui/components/form"; import { formatEventFromTime } from "@calcom/features/bookings/Booker/utils/dates"; import { useBookerTime } from "@calcom/features/bookings/Booker/hooks/useBookerTime"; import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/hooks/useBookingForm"; -import type { - IUseBookingErrors, - IUseBookingLoadingStates, -} from "../../hooks/useBookings"; +import type { IUseBookingErrors, IUseBookingLoadingStates } from "../../hooks/useBookings"; import { BookingFields } from "./BookingFields"; import { FormSkeleton } from "./Skeleton"; @@ -75,10 +68,7 @@ export const BookEventForm = ({ eventQuery: { isError: boolean; isPending: boolean; - data?: Pick< - BookerEvent, - "price" | "currency" | "metadata" | "bookingFields" | "locations" - > | null; + data?: Pick | null; }; }) => { const eventType = eventQuery.data; @@ -86,9 +76,7 @@ export const BookEventForm = ({ const bookingData = useBookerStoreContext((state) => state.bookingData); const rescheduleUid = useBookerStoreContext((state) => state.rescheduleUid); const username = useBookerStoreContext((state) => state.username); - const isInstantMeeting = useBookerStoreContext( - (state) => state.isInstantMeeting - ); + const isInstantMeeting = useBookerStoreContext((state) => state.isInstantMeeting); const isPlatformBookerEmbed = useIsPlatformBookerEmbed(); const { timeFormat, timezone } = useBookerTime(); @@ -98,11 +86,7 @@ export const BookEventForm = ({ const isPaidEvent = useMemo(() => { if (!eventType?.price) return false; const paymentAppData = getPaymentAppData(eventType); - return ( - eventType?.price > 0 && - !Number.isNaN(paymentAppData.price) && - paymentAppData.price > 0 - ); + return eventType?.price > 0 && !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0; }, [eventType]); const paymentCurrency = useMemo(() => { @@ -110,8 +94,7 @@ export const BookEventForm = ({ return getPaymentAppData(eventType)?.currency || "USD"; }, [eventType]); - if (eventQuery.isError) - return ; + if (eventQuery.isError) return ; if (eventQuery.isPending || !eventQuery.data) return ; if (!timeslot) return ( @@ -144,8 +127,7 @@ export const BookEventForm = ({ }} form={bookingForm} handleSubmit={onSubmit} - noValidate - > + noValidate> -1)} fields={eventType.bookingFields} @@ -187,8 +169,7 @@ export const BookEventForm = ({ key="please-select-a-new-time-button" type="button" className="underline" - onClick={onCancel} - > + onClick={onCancel}> Please select a new time , ]} @@ -209,16 +190,14 @@ export const BookEventForm = ({ className="text-emphasis hover:underline" key="terms" href={`${WEBSITE_TERMS_URL}`} - target="_blank" - > + target="_blank"> Terms , + target="_blank"> Privacy Policy. , ]} @@ -233,8 +212,7 @@ export const BookEventForm = ({ className="text-emphasis hover:underline" key="terms" href={`${WEBSITE_TERMS_URL}`} - target="_blank" - > + target="_blank"> {t("terms")} {" "} {t("and")}{" "} @@ -242,8 +220,7 @@ export const BookEventForm = ({ className="text-emphasis hover:underline" key="privacy" href={`${WEBSITE_PRIVACY_POLICY_URL}`} - target="_blank" - > + target="_blank"> {t("privacy_policy")} . @@ -251,11 +228,7 @@ export const BookEventForm = ({ )}
{isInstantMeeting ? ( - ) : ( @@ -266,8 +239,7 @@ export const BookEventForm = ({ type="button" onClick={onCancel} data-testid="back" - className={classNames?.backButton} - > + className={classNames?.backButton}> {t("back")} )} @@ -276,9 +248,7 @@ export const BookEventForm = ({ type="submit" color="primary" disabled={ - (!!shouldRenderCaptcha && !watchedCfToken) || - isTimeslotUnavailable || - confirmButtonDisabled + (!!shouldRenderCaptcha && !watchedCfToken) || isTimeslotUnavailable || confirmButtonDisabled } loading={ loadingStates.creatingBooking || @@ -287,11 +257,8 @@ export const BookEventForm = ({ } className={classNames?.confirmButton} data-testid={ - rescheduleUid && bookingData - ? "confirm-reschedule-button" - : "confirm-book-button" - } - > + rescheduleUid && bookingData ? "confirm-reschedule-button" : "confirm-book-button" + }> {rescheduleUid && bookingData ? t("reschedule") : renderConfirmNotVerifyEmailButtonCond @@ -352,9 +319,7 @@ const getError = ({ } const messageKey = - error.message === ErrorCode.BookerLimitExceeded - ? "booker_upcoming_limit_reached" - : error.message; + error.message === ErrorCode.BookerLimitExceeded ? "booker_upcoming_limit_reached" : error.message; return error?.message ? ( <> @@ -362,9 +327,7 @@ const getError = ({ {error.data?.traceId && (
{t("trace_reference_id")}: - - {error.data.traceId} - + {error.data.traceId}
)} diff --git a/packages/features/CalendarEventBuilder.ts b/packages/features/CalendarEventBuilder.ts index cdf8269926..41aba2b776 100644 --- a/packages/features/CalendarEventBuilder.ts +++ b/packages/features/CalendarEventBuilder.ts @@ -86,22 +86,22 @@ export class CalendarEventBuilder { if (!eventType) throw new Error(`Booking ${uid} is missing eventType — it may have been deleted.`); const builder = new CalendarEventBuilder(); - const { - description, - attendees, - references, - title, - startTime, - endTime, - location, - responses, - customInputs, - iCalUID, - iCalSequence, - oneTimePassword, - seatsReferences, - assignmentReason, - } = booking; + const { + description, + attendees, + references, + title, + startTime, + endTime, + location, + responses, + customInputs, + iCalUID, + iCalSequence, + oneTimePassword, + seatsReferences, + assignmentReason, + } = booking; const { conferenceCredentialId, @@ -187,18 +187,18 @@ export class CalendarEventBuilder { platformCancelUrl, platformBookingUrl, }) - .withRecurring(recurring) - .withUid(uid) - .withOneTimePassword(oneTimePassword) - .withOrganization(organizationId) - .withAssignmentReason( - assignmentReason?.[0]?.reasonEnum - ? { - category: getAssignmentReasonCategory(assignmentReason[0].reasonEnum), - details: assignmentReason[0].reasonString ?? null, - } - : null - ); + .withRecurring(recurring) + .withUid(uid) + .withOneTimePassword(oneTimePassword) + .withOrganization(organizationId) + .withAssignmentReason( + assignmentReason?.[0]?.reasonEnum + ? { + category: getAssignmentReasonCategory(assignmentReason[0].reasonEnum), + details: assignmentReason[0].reasonString ?? null, + } + : null + ); // Seats if (seatsReferences?.length && bookingResponses) { @@ -533,23 +533,23 @@ export class CalendarEventBuilder { return this; } - withHashedLink(hashedLink?: string | null) { - this.event = { - ...this.event, - hashedLink, - }; - return this; - } + withHashedLink(hashedLink?: string | null) { + this.event = { + ...this.event, + hashedLink, + }; + return this; + } - withAssignmentReason(assignmentReason?: { category: string; details?: string | null } | null) { - this.event = { - ...this.event, - assignmentReason, - }; - return this; - } + withAssignmentReason(assignmentReason?: { category: string; details?: string | null } | null) { + this.event = { + ...this.event, + assignmentReason, + }; + return this; + } - build(): CalendarEvent | null { + build(): CalendarEvent | null { // Validate required fields if ( !this.event.startTime || diff --git a/packages/features/assignment-reason/repositories/AssignmentReasonRepository.ts b/packages/features/assignment-reason/repositories/AssignmentReasonRepository.ts index b6b6c4cd4a..9674740566 100644 --- a/packages/features/assignment-reason/repositories/AssignmentReasonRepository.ts +++ b/packages/features/assignment-reason/repositories/AssignmentReasonRepository.ts @@ -64,4 +64,3 @@ export class AssignmentReasonRepository { }); } } - diff --git a/packages/features/attributes/di/AttributeService.container.ts b/packages/features/attributes/di/AttributeService.container.ts index 09bee93f56..32ba316001 100644 --- a/packages/features/attributes/di/AttributeService.container.ts +++ b/packages/features/attributes/di/AttributeService.container.ts @@ -1,9 +1,6 @@ import { createContainer } from "@calcom/features/di/di"; -import { - type AttributeService, - moduleLoader as attributeServiceModule, -} from "./AttributeService.module"; +import { type AttributeService, moduleLoader as attributeServiceModule } from "./AttributeService.module"; const attributeServiceContainer = createContainer(); diff --git a/packages/features/attributes/di/AttributeService.module.ts b/packages/features/attributes/di/AttributeService.module.ts index 81a38cf182..43ed9e5fd1 100644 --- a/packages/features/attributes/di/AttributeService.module.ts +++ b/packages/features/attributes/di/AttributeService.module.ts @@ -1,8 +1,4 @@ -import { - bindModuleToClassOnToken, - createModule, - type ModuleLoader, -} from "@calcom/features/di/di"; +import { bindModuleToClassOnToken, createModule, type ModuleLoader } from "@calcom/features/di/di"; import { AttributeService } from "../services/AttributeService"; import { moduleLoader as attributeToUserRepositoryModuleLoader } from "./AttributeToUserRepository.module"; diff --git a/packages/features/attributes/lib/getAttributes.test.ts b/packages/features/attributes/lib/getAttributes.test.ts index 4e91b6fe2a..2cfe6d5322 100644 --- a/packages/features/attributes/lib/getAttributes.test.ts +++ b/packages/features/attributes/lib/getAttributes.test.ts @@ -868,18 +868,22 @@ describe("getAttributes", () => { attributeOptionId: "level-senior", }); - const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } = await getAttributesAssignmentData({ - teamId: team.id, - orgId, - attributeIds: ["attr1", "attr2"], - }); + const { attributesOfTheOrg, attributesAssignedToTeamMembersWithOptions } = + await getAttributesAssignmentData({ + teamId: team.id, + orgId, + attributeIds: ["attr1", "attr2"], + }); expect(attributesOfTheOrg).toHaveLength(2); expect(attributesOfTheOrg.map((a) => a.id).sort()).toEqual(["attr1", "attr2"]); expect(attributesAssignedToTeamMembersWithOptions).toHaveLength(1); expect(attributesAssignedToTeamMembersWithOptions[0].userId).toBe(user.id); - expect(Object.keys(attributesAssignedToTeamMembersWithOptions[0].attributes)).toEqual(["attr1", "attr2"]); + expect(Object.keys(attributesAssignedToTeamMembersWithOptions[0].attributes)).toEqual([ + "attr1", + "attr2", + ]); expect(attributesAssignedToTeamMembersWithOptions[0].attributes.attr1).toBeDefined(); expect(attributesAssignedToTeamMembersWithOptions[0].attributes.attr2).toBeDefined(); expect(attributesAssignedToTeamMembersWithOptions[0].attributes.attr3).toBeUndefined(); @@ -898,7 +902,9 @@ describe("getAttributes", () => { name: "Department", slug: "department", type: AttributeType.SINGLE_SELECT, - options: [{ id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }], + options: [ + { id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }, + ], }); await createMockAttribute({ @@ -942,7 +948,9 @@ describe("getAttributes", () => { name: "Department", slug: "department", type: AttributeType.SINGLE_SELECT, - options: [{ id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }], + options: [ + { id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }, + ], }); await createMockAttributeAssignment({ @@ -973,7 +981,9 @@ describe("getAttributes", () => { name: "Department", slug: "department", type: AttributeType.SINGLE_SELECT, - options: [{ id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }], + options: [ + { id: "dept-eng", value: "Engineering", slug: "engineering", isGroup: false, contains: [] }, + ], }); await createMockAttributeAssignment({ @@ -1006,10 +1016,22 @@ describe("getAttributes", () => { data: { name: "User 2", email: "user2@test.com" }, }); const orgMembership2 = await prismock.membership.create({ - data: { role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, teamId: orgId, userId: user2.id }, + data: { + role: MembershipRole.MEMBER, + disableImpersonation: false, + accepted: true, + teamId: orgId, + userId: user2.id, + }, }); await prismock.membership.create({ - data: { role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, teamId: team.id, userId: user2.id }, + data: { + role: MembershipRole.MEMBER, + disableImpersonation: false, + accepted: true, + teamId: team.id, + userId: user2.id, + }, }); // User 3 has no attribute assignments at all @@ -1017,10 +1039,22 @@ describe("getAttributes", () => { data: { name: "User 3", email: "user3@test.com" }, }); await prismock.membership.create({ - data: { role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, teamId: orgId, userId: user3.id }, + data: { + role: MembershipRole.MEMBER, + disableImpersonation: false, + accepted: true, + teamId: orgId, + userId: user3.id, + }, }); await prismock.membership.create({ - data: { role: MembershipRole.MEMBER, disableImpersonation: false, accepted: true, teamId: team.id, userId: user3.id }, + data: { + role: MembershipRole.MEMBER, + disableImpersonation: false, + accepted: true, + teamId: team.id, + userId: user3.id, + }, }); await createMockAttribute({ diff --git a/packages/features/attributes/lib/getAttributes.ts b/packages/features/attributes/lib/getAttributes.ts index b393022c78..366b769142 100644 --- a/packages/features/attributes/lib/getAttributes.ts +++ b/packages/features/attributes/lib/getAttributes.ts @@ -1,9 +1,6 @@ // TODO: Queries in this file are not optimized. Need to optimize them. import type { Attribute } from "@calcom/app-store/routing-forms/types/types"; -import type { - AttributeId, - AttributeOptionValueWithType, -} from "@calcom/app-store/routing-forms/types/types"; +import type { AttributeId, AttributeOptionValueWithType } from "@calcom/app-store/routing-forms/types/types"; import { PrismaAttributeRepository } from "@calcom/features/attributes/repositories/PrismaAttributeRepository"; import { PrismaAttributeToUserRepository } from "@calcom/features/attributes/repositories/PrismaAttributeToUserRepository"; import logger from "@calcom/lib/logger"; @@ -88,13 +85,7 @@ type FullAttribute = { }[]; }; -async function _findMembershipsForBothOrgAndTeam({ - orgId, - teamId, -}: { - orgId: number; - teamId: number; -}) { +async function _findMembershipsForBothOrgAndTeam({ orgId, teamId }: { orgId: number; teamId: number }) { const memberships = await prisma.membership.findMany({ where: { teamId: { @@ -186,10 +177,7 @@ function _prepareAssignmentData({ newAttributeOptionValue, ]; } else if (currentAttributeOptionValue) { - attributes[attribute.id].attributeOption = [ - currentAttributeOptionValue, - newAttributeOptionValue, - ]; + attributes[attribute.id].attributeOption = [currentAttributeOptionValue, newAttributeOptionValue]; } else { // Set the first value attributes[attribute.id] = { @@ -231,9 +219,7 @@ function _prepareAssignmentData({ console.error( `Enriching "contains" for attribute ${ attribute.name - }: Option with id ${optionId} not found. Looked up in ${JSON.stringify( - allOptions - )}` + }: Option with id ${optionId} not found. Looked up in ${JSON.stringify(allOptions)}` ); return null; } @@ -243,9 +229,7 @@ function _prepareAssignmentData({ slug: option.slug, }; }) - .filter( - (option): option is NonNullable => option !== null - ); + .filter((option): option is NonNullable => option !== null); } } @@ -255,10 +239,7 @@ function _prepareAssignmentData({ */ function _buildAttributeLookupMaps(attributesOfTheOrg: FullAttribute[]) { const optionIdToAttribute = new Map(); - const optionIdToOption = new Map< - AttributeOptionId, - FullAttribute["options"][number] - >(); + const optionIdToOption = new Map(); const attributeIdToOptions = new Map(); for (const attribute of attributesOfTheOrg) { @@ -294,31 +275,19 @@ function _getAttributeOptionFromId({ return lookupMaps.optionIdToOption.get(attributeOptionId); } -async function _getOrgMembershipToUserIdForTeam({ - orgId, - teamId, -}: { - orgId: number; - teamId: number; -}) { - const { orgMemberships, teamMemberships } = - await _findMembershipsForBothOrgAndTeam({ - orgId, - teamId, - }); +async function _getOrgMembershipToUserIdForTeam({ orgId, teamId }: { orgId: number; teamId: number }) { + const { orgMemberships, teamMemberships } = await _findMembershipsForBothOrgAndTeam({ + orgId, + teamId, + }); // Using map for performance lookup as it matters in the below loop working with 1000s of records - const orgMembershipsByUserId = new Map( - orgMemberships.map((m) => [m.userId, m]) - ); + const orgMembershipsByUserId = new Map(orgMemberships.map((m) => [m.userId, m])); /** * Holds the records of orgMembershipId to userId for the sub-team's members only. */ - const orgMembershipToUserIdForTeamMembers = new Map< - OrgMembershipId, - UserId - >(); + const orgMembershipToUserIdForTeamMembers = new Map(); /** * For an organization with 3000 users and 10 teams, with every team having around 300 members, the total memberships we get for a team are 3000+300 = 3300 @@ -332,10 +301,7 @@ async function _getOrgMembershipToUserIdForTeam({ // ); return; } - orgMembershipToUserIdForTeamMembers.set( - orgMembership.id, - orgMembership.userId - ); + orgMembershipToUserIdForTeamMembers.set(orgMembership.id, orgMembership.userId); }); return orgMembershipToUserIdForTeamMembers; @@ -354,23 +320,19 @@ async function _queryAllData({ const attributeRepo = new PrismaAttributeRepository(prisma); const attributeToUserRepo = new PrismaAttributeToUserRepository(prisma); - const [orgMembershipToUserIdForTeamMembers, attributesOfTheOrg] = - await Promise.all([ - _getOrgMembershipToUserIdForTeam({ orgId, teamId }), - attributeRepo.findManyByOrgId({ orgId, attributeIds }), - ]); + const [orgMembershipToUserIdForTeamMembers, attributesOfTheOrg] = await Promise.all([ + _getOrgMembershipToUserIdForTeam({ orgId, teamId }), + attributeRepo.findManyByOrgId({ orgId, attributeIds }), + ]); - const orgMembershipIds = Array.from( - orgMembershipToUserIdForTeamMembers.keys() - ); + const orgMembershipIds = Array.from(orgMembershipToUserIdForTeamMembers.keys()); // Get the attributes assigned to the members of the team // If attributeIds is provided, only fetch assignments for those specific attributes - const attributesToUsersForTeam = - await attributeToUserRepo.findManyByOrgMembershipIds({ - orgMembershipIds, - attributeIds, - }); + const attributesToUsersForTeam = await attributeToUserRepo.findManyByOrgMembershipIds({ + orgMembershipIds, + attributeIds, + }); return { attributesOfTheOrg, @@ -379,13 +341,7 @@ async function _queryAllData({ }; } -async function getAttributesAssignedToMembersOfTeam({ - teamId, - userId, -}: { - teamId: number; - userId?: number; -}) { +async function getAttributesAssignedToMembersOfTeam({ teamId, userId }: { teamId: number; userId?: number }) { const log = logger.getSubLogger({ prefix: ["getAttributeToUserWithMembershipAndAttributes"], }); @@ -455,9 +411,7 @@ function _buildAssignmentsForTeam({ const orgMembershipId = attributeToUser.memberId; const userId = orgMembershipToUserIdForTeamMembers.get(orgMembershipId); if (!userId) { - console.error( - `No org membership found for membership id ${orgMembershipId}` - ); + console.error(`No org membership found for membership id ${orgMembershipId}`); return null; } const attribute = _getAttributeFromAttributeOption({ @@ -484,10 +438,7 @@ function _buildAssignmentsForTeam({ attributeOption, }; }) - .filter( - (assignment): assignment is NonNullable => - assignment !== null - ); + .filter((assignment): assignment is NonNullable => assignment !== null); } export async function getAttributesAssignmentData({ @@ -501,15 +452,12 @@ export async function getAttributesAssignmentData({ * This significantly improves performance when only a few attributes are needed. */ attributeIds?: string[]; }) { - const { - attributesOfTheOrg, - attributesToUsersForTeam, - orgMembershipToUserIdForTeamMembers, - } = await _queryAllData({ - orgId, - teamId, - attributeIds, - }); + const { attributesOfTheOrg, attributesToUsersForTeam, orgMembershipToUserIdForTeamMembers } = + await _queryAllData({ + orgId, + teamId, + attributeIds, + }); const lookupMaps = _buildAttributeLookupMaps(attributesOfTheOrg); @@ -550,12 +498,6 @@ export async function getAttributesForTeam({ teamId }: { teamId: number }) { return attributes satisfies Attribute[]; } -export async function getUsersAttributes({ - userId, - teamId, -}: { - userId: number; - teamId: number; -}) { +export async function getUsersAttributes({ userId, teamId }: { userId: number; teamId: number }) { return await getAttributesAssignedToMembersOfTeam({ teamId, userId }); } diff --git a/packages/features/attributes/repositories/PrismaAttributeRepository.ts b/packages/features/attributes/repositories/PrismaAttributeRepository.ts index 4e14c8b593..36fdefa2f9 100644 --- a/packages/features/attributes/repositories/PrismaAttributeRepository.ts +++ b/packages/features/attributes/repositories/PrismaAttributeRepository.ts @@ -103,13 +103,7 @@ export class PrismaAttributeRepository { }); } - findManyByIdsAndOrgIdWithOptions({ - attributeIds, - orgId, - }: { - attributeIds: string[]; - orgId: number; - }) { + findManyByIdsAndOrgIdWithOptions({ attributeIds, orgId }: { attributeIds: string[]; orgId: number }) { return this.prismaClient.attribute.findMany({ where: { teamId: orgId, diff --git a/packages/features/attributes/services/AttributeService.ts b/packages/features/attributes/services/AttributeService.ts index 5f06c26dd1..611edb4b29 100644 --- a/packages/features/attributes/services/AttributeService.ts +++ b/packages/features/attributes/services/AttributeService.ts @@ -30,10 +30,11 @@ export class AttributeService { userId: number; orgId: number; }): Promise> { - const attributeOptionsAssignedToUser = - await this.deps.attributeToUserRepository.findManyIncludeAttribute({ + const attributeOptionsAssignedToUser = await this.deps.attributeToUserRepository.findManyIncludeAttribute( + { member: { userId, teamId: orgId }, - }); + } + ); const userAttributes: Record = {}; diff --git a/packages/features/attributes/services/attributeService.test.ts b/packages/features/attributes/services/attributeService.test.ts index a0fb5fa740..1dbf3252dd 100644 --- a/packages/features/attributes/services/attributeService.test.ts +++ b/packages/features/attributes/services/attributeService.test.ts @@ -17,16 +17,13 @@ describe("AttributeService", () => { }; service = new AttributeService({ - attributeToUserRepository: - mockAttributeToUserRepository as unknown as PrismaAttributeToUserRepository, + attributeToUserRepository: mockAttributeToUserRepository as unknown as PrismaAttributeToUserRepository, }); }); describe("getUsersAttributesByOrgMembershipId", () => { it("should return empty object when user has no attributes", async () => { - mockAttributeToUserRepository.findManyIncludeAttribute.mockResolvedValue( - [] - ); + mockAttributeToUserRepository.findManyIncludeAttribute.mockResolvedValue([]); const result = await service.getUsersAttributesByOrgMembershipId({ userId: 1, @@ -34,9 +31,7 @@ describe("AttributeService", () => { }); expect(result).toEqual({}); - expect( - mockAttributeToUserRepository.findManyIncludeAttribute - ).toHaveBeenCalledWith({ + expect(mockAttributeToUserRepository.findManyIncludeAttribute).toHaveBeenCalledWith({ member: { userId: 1, teamId: 100 }, }); }); diff --git a/packages/features/auth/__mocks__/getServerSession.mocks.ts b/packages/features/auth/__mocks__/getServerSession.mocks.ts index 754cbeb88f..667b2eed3a 100644 --- a/packages/features/auth/__mocks__/getServerSession.mocks.ts +++ b/packages/features/auth/__mocks__/getServerSession.mocks.ts @@ -77,7 +77,9 @@ function createDeploymentRepositoryMock(): { } function createUserRepositoryMock(): { - UserRepository: new (_prisma: PrismaClient) => { + UserRepository: new ( + _prisma: PrismaClient + ) => { enrichUserWithTheProfile: (params: { user: User }) => Promise; }; } { diff --git a/packages/features/auth/lib/getLocaleFromRequest.ts b/packages/features/auth/lib/getLocaleFromRequest.ts index 29ae8a0df3..957ddb29ce 100644 --- a/packages/features/auth/lib/getLocaleFromRequest.ts +++ b/packages/features/auth/lib/getLocaleFromRequest.ts @@ -1,4 +1,3 @@ - import parser from "accept-language-parser"; import type { GetServerSidePropsContext, NextApiRequest } from "next"; diff --git a/packages/features/auth/lib/getServerSession.test.ts b/packages/features/auth/lib/getServerSession.test.ts index 8bc261a45b..74645e2ad8 100644 --- a/packages/features/auth/lib/getServerSession.test.ts +++ b/packages/features/auth/lib/getServerSession.test.ts @@ -64,17 +64,14 @@ describe("getServerSession", () => { }); describe("User ID Validation", () => { - it.each(["", "invalid", "0", "-1"])( - "returns null when token.sub is invalid (%s)", - async (sub) => { - setupGetTokenMock(createMockToken({ sub })); + it.each(["", "invalid", "0", "-1"])("returns null when token.sub is invalid (%s)", async (sub) => { + setupGetTokenMock(createMockToken({ sub })); - const result = await getServerSession({ req: createMockRequest() }); + const result = await getServerSession({ req: createMockRequest() }); - expect(result).toBeNull(); - expect(prismaMock.user.findUnique).not.toHaveBeenCalled(); - } - ); + expect(result).toBeNull(); + expect(prismaMock.user.findUnique).not.toHaveBeenCalled(); + }); }); describe("User Lookup", () => { diff --git a/packages/features/auth/lib/next-auth-options.test.ts b/packages/features/auth/lib/next-auth-options.test.ts index 17fd6c86c3..38706d4145 100644 --- a/packages/features/auth/lib/next-auth-options.test.ts +++ b/packages/features/auth/lib/next-auth-options.test.ts @@ -23,9 +23,11 @@ const mockFindByEmailAndIncludeProfilesAndPassword = vi.fn(); vi.mock("@calcom/features/users/repositories/UserRepository", () => { return { - UserRepository: vi.fn().mockImplementation(function() { return { - findByEmailAndIncludeProfilesAndPassword: mockFindByEmailAndIncludeProfilesAndPassword, - }; }), + UserRepository: vi.fn().mockImplementation(function () { + return { + findByEmailAndIncludeProfilesAndPassword: mockFindByEmailAndIncludeProfilesAndPassword, + }; + }), }; }); diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 731a3ca04a..cd830fc7dc 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -640,14 +640,14 @@ export const getOptions = ({ org: profileOrg && !profileOrg.isPlatform ? { - id: profileOrg.id, - name: profileOrg.name, - slug: profileOrg.slug ?? profileOrg.requestedSlug ?? "", - logoUrl: profileOrg.logoUrl, - fullDomain: getOrgFullOrigin(profileOrg.slug ?? profileOrg.requestedSlug ?? ""), - domainSuffix: subdomainSuffix(), - role: orgRole as MembershipRole, // It can't be undefined if we have a profileOrg - } + id: profileOrg.id, + name: profileOrg.name, + slug: profileOrg.slug ?? profileOrg.requestedSlug ?? "", + logoUrl: profileOrg.logoUrl, + fullDomain: getOrgFullOrigin(profileOrg.slug ?? profileOrg.requestedSlug ?? ""), + domainSuffix: subdomainSuffix(), + role: orgRole as MembershipRole, // It can't be undefined if we have a profileOrg + } : null, } as JWT; }; @@ -881,7 +881,10 @@ export const getOptions = ({ } if (!user.name) { - log.warn("callbacks:signIn - user name is missing", { emailDomain: user.email.split("@")[1], provider: account?.provider }); + log.warn("callbacks:signIn - user name is missing", { + emailDomain: user.email.split("@")[1], + provider: account?.provider, + }); return false; } if (account?.provider) { @@ -1024,7 +1027,11 @@ export const getOptions = ({ // Verify SAML IdP is authoritative before auto-merge if (idP === IdentityProvider.SAML) { const samlTenant = getSamlTenant(); - const validation = await validateSamlAccountConversion(samlTenant, user.email, "SelfHosted→SAML"); + const validation = await validateSamlAccountConversion( + samlTenant, + user.email, + "SelfHosted→SAML" + ); if (!validation.allowed) { return validation.errorUrl; } @@ -1109,12 +1116,8 @@ export const getOptions = ({ } else { return true; } - } else if ( - existingUserWithEmail.identityProvider === IdentityProvider.CAL - ) { - log.error( - `Userid ${user.id} already exists with CAL identity provider` - ); + } else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) { + log.error(`Userid ${user.id} already exists with CAL identity provider`); return `/auth/error?error=wrong-provider&provider=${existingUserWithEmail.identityProvider}`; } else if ( existingUserWithEmail.identityProvider === IdentityProvider.GOOGLE && @@ -1143,17 +1146,14 @@ export const getOptions = ({ return true; } } - log.error( - `Userid ${user.id} trying to login with the wrong provider`, - { - userId: user.id, - account: { - providerAccountId: account?.providerAccountId, - type: account?.type, - provider: account?.provider, - }, - } - ); + log.error(`Userid ${user.id} trying to login with the wrong provider`, { + userId: user.id, + account: { + providerAccountId: account?.providerAccountId, + type: account?.type, + provider: account?.provider, + }, + }); return `/auth/error?error=wrong-provider&provider=${existingUserWithEmail.identityProvider}`; } diff --git a/packages/features/auth/lib/samlAccountLinking.ts b/packages/features/auth/lib/samlAccountLinking.ts index 0e2a339e8a..de618e5337 100644 --- a/packages/features/auth/lib/samlAccountLinking.ts +++ b/packages/features/auth/lib/samlAccountLinking.ts @@ -65,9 +65,7 @@ export class SamlAccountLinkingService { } } -export type AccountConversionValidationResult = - | { allowed: true } - | { allowed: false; errorUrl: string }; +export type AccountConversionValidationResult = { allowed: true } | { allowed: false; errorUrl: string }; export async function validateSamlAccountConversion( samlTenant: string | undefined, @@ -76,7 +74,10 @@ export async function validateSamlAccountConversion( ): Promise { if (!samlTenant) { // Deny by default - if tenant is missing, we cannot verify IdP authority - log.error("SAML conversion blocked - missing tenant", { emailDomain: email.split("@")[1], conversionContext }); + log.error("SAML conversion blocked - missing tenant", { + emailDomain: email.split("@")[1], + conversionContext, + }); return { allowed: false, errorUrl: SAML_NOT_AUTHORITATIVE_ERROR_URL }; } diff --git a/packages/features/auth/lib/sendVerificationRequest.ts b/packages/features/auth/lib/sendVerificationRequest.ts index 9b7638d510..15bce5a2a9 100644 --- a/packages/features/auth/lib/sendVerificationRequest.ts +++ b/packages/features/auth/lib/sendVerificationRequest.ts @@ -3,7 +3,7 @@ import Handlebars from "handlebars"; import type { SendVerificationRequestParams } from "next-auth/providers/email"; import type { TransportOptions } from "nodemailer"; import nodemailer from "nodemailer"; -import path from "node:path" +import path from "node:path"; import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants"; import { serverConfig } from "@calcom/lib/serverConfig"; diff --git a/packages/features/auth/signup/utils/createOrUpdateMemberships.integration-test.ts b/packages/features/auth/signup/utils/createOrUpdateMemberships.integration-test.ts index 54b0dcc85b..9c8e9fcb12 100644 --- a/packages/features/auth/signup/utils/createOrUpdateMemberships.integration-test.ts +++ b/packages/features/auth/signup/utils/createOrUpdateMemberships.integration-test.ts @@ -60,11 +60,7 @@ async function createTestOrganization(data: { return { ...team, organizationSettings }; } -async function createTestSubteam(data: { - name: string; - slug: string; - parentId: number; -}): Promise { +async function createTestSubteam(data: { name: string; slug: string; parentId: number }): Promise { const uniqueId = generateUniqueId(); const uniqueSlug = `${data.slug}-${uniqueId}`; @@ -95,10 +91,7 @@ describe("createOrUpdateMemberships Integration Tests", () => { // Clean up in reverse dependency order await prisma.profile.deleteMany({ where: { - OR: [ - { userId: { in: userIds } }, - { organizationId: { in: teamIds } }, - ], + OR: [{ userId: { in: userIds } }, { organizationId: { in: teamIds } }], }, }); await prisma.membership.deleteMany({ diff --git a/packages/features/auth/signup/utils/createOrUpdateMemberships.ts b/packages/features/auth/signup/utils/createOrUpdateMemberships.ts index 493a1339fb..928ba1f88f 100644 --- a/packages/features/auth/signup/utils/createOrUpdateMemberships.ts +++ b/packages/features/auth/signup/utils/createOrUpdateMemberships.ts @@ -23,10 +23,10 @@ export const createOrUpdateMemberships = async ({ }) => { return await prisma.$transaction(async (tx) => { // Determine the organization context - either the team itself (if it's an org) or its parent - const organizationId = team.isOrganization ? team.id : team.parent?.id ?? null; + const organizationId = team.isOrganization ? team.id : (team.parent?.id ?? null); const orgSettings = team.isOrganization ? team.organizationSettings - : team.parent?.organizationSettings ?? null; + : (team.parent?.organizationSettings ?? null); // Create profile if user is joining an organization context (either directly or via sub-team) if (organizationId) { diff --git a/packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.ts b/packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.ts index 5e6f164550..0dc5dfbb6e 100644 --- a/packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.ts +++ b/packages/features/availability/lib/getAggregatedAvailability/getAggregatedAvailability.ts @@ -46,14 +46,17 @@ export const getAggregatedAvailability = ( const roundRobinHosts = userAvailability.filter(({ user }) => user?.isFixed !== true); if (roundRobinHosts.length) { // Group round robin hosts by their groupId - const hostsByGroup = roundRobinHosts.reduce((groups, host) => { - const groupId = host.user?.groupId || DEFAULT_GROUP_ID; - if (!groups[groupId]) { - groups[groupId] = []; - } - groups[groupId].push(host); - return groups; - }, {} as Record); + const hostsByGroup = roundRobinHosts.reduce( + (groups, host) => { + const groupId = host.user?.groupId || DEFAULT_GROUP_ID; + if (!groups[groupId]) { + groups[groupId] = []; + } + groups[groupId].push(host); + return groups; + }, + {} as Record + ); // at least one host from each group needs to be available Object.values(hostsByGroup).forEach((groupHosts) => { diff --git a/packages/features/booking-audit/di/ActorRepository.module.ts b/packages/features/booking-audit/di/ActorRepository.module.ts index 02cf670980..ec9cf86716 100644 --- a/packages/features/booking-audit/di/ActorRepository.module.ts +++ b/packages/features/booking-audit/di/ActorRepository.module.ts @@ -8,16 +8,16 @@ export const actorRepositoryModule = createModule(); const token = BOOKING_AUDIT_DI_TOKENS.AUDIT_ACTOR_REPOSITORY; const moduleToken = BOOKING_AUDIT_DI_TOKENS.AUDIT_ACTOR_REPOSITORY_MODULE; const loadModule = bindModuleToClassOnToken({ - module: actorRepositoryModule, - moduleToken, - token, - classs: PrismaAuditActorRepository, - depsMap: { - prismaClient: prismaModuleLoader, - }, + module: actorRepositoryModule, + moduleToken, + token, + classs: PrismaAuditActorRepository, + depsMap: { + prismaClient: prismaModuleLoader, + }, }); export const moduleLoader = { - token, - loadModule, + token, + loadModule, }; diff --git a/packages/features/booking-audit/di/AuditActorRepository.container.ts b/packages/features/booking-audit/di/AuditActorRepository.container.ts index 1b32065dbe..8cca39b2f4 100644 --- a/packages/features/booking-audit/di/AuditActorRepository.container.ts +++ b/packages/features/booking-audit/di/AuditActorRepository.container.ts @@ -1,9 +1,7 @@ import { createContainer } from "@calcom/features/di/di"; import type { IAuditActorRepository } from "@calcom/features/booking-audit/lib/repository/IAuditActorRepository"; -import { - moduleLoader as auditActorRepositoryModule, -} from "./AuditActorRepository.module"; +import { moduleLoader as auditActorRepositoryModule } from "./AuditActorRepository.module"; const container = createContainer(); diff --git a/packages/features/booking-audit/di/AuditActorRepository.module.ts b/packages/features/booking-audit/di/AuditActorRepository.module.ts index 0cfb3b1ae1..85dca2e6a2 100644 --- a/packages/features/booking-audit/di/AuditActorRepository.module.ts +++ b/packages/features/booking-audit/di/AuditActorRepository.module.ts @@ -8,17 +8,16 @@ export const auditActorRepositoryModule = createModule(); const token = BOOKING_AUDIT_DI_TOKENS.AUDIT_ACTOR_REPOSITORY; const moduleToken = BOOKING_AUDIT_DI_TOKENS.AUDIT_ACTOR_REPOSITORY_MODULE; const loadModule = bindModuleToClassOnToken({ - module: auditActorRepositoryModule, - moduleToken, - token, - classs: PrismaAuditActorRepository, - depsMap: { - prismaClient: prismaModuleLoader, - }, + module: auditActorRepositoryModule, + moduleToken, + token, + classs: PrismaAuditActorRepository, + depsMap: { + prismaClient: prismaModuleLoader, + }, }); export const moduleLoader = { - token, - loadModule, + token, + loadModule, }; - diff --git a/packages/features/booking-audit/di/BookingAuditProducerService.container.ts b/packages/features/booking-audit/di/BookingAuditProducerService.container.ts index 5bd4660e1a..4e26c49a1d 100644 --- a/packages/features/booking-audit/di/BookingAuditProducerService.container.ts +++ b/packages/features/booking-audit/di/BookingAuditProducerService.container.ts @@ -1,9 +1,7 @@ import { createContainer } from "@calcom/features/di/di"; import type { BookingAuditProducerService } from "@calcom/features/booking-audit/lib/service/BookingAuditProducerService.interface"; -import { - moduleLoader as bookingAuditTaskerProducerServiceModule, -} from "./BookingAuditTaskerProducerService.module"; +import { moduleLoader as bookingAuditTaskerProducerServiceModule } from "./BookingAuditTaskerProducerService.module"; const container = createContainer(); @@ -12,4 +10,3 @@ export function getBookingAuditProducerService() { return container.get(bookingAuditTaskerProducerServiceModule.token); } - diff --git a/packages/features/booking-audit/di/BookingAuditTaskConsumer.container.ts b/packages/features/booking-audit/di/BookingAuditTaskConsumer.container.ts index 162f2e1f10..b9b0c5459b 100644 --- a/packages/features/booking-audit/di/BookingAuditTaskConsumer.container.ts +++ b/packages/features/booking-audit/di/BookingAuditTaskConsumer.container.ts @@ -1,9 +1,7 @@ import { createContainer } from "@calcom/features/di/di"; import type { BookingAuditTaskConsumer } from "@calcom/features/booking-audit/lib/service/BookingAuditTaskConsumer"; -import { - moduleLoader as bookingAuditTaskConsumerModule, -} from "./BookingAuditTaskConsumer.module"; +import { moduleLoader as bookingAuditTaskConsumerModule } from "./BookingAuditTaskConsumer.module"; const container = createContainer(); @@ -12,4 +10,3 @@ export function getBookingAuditTaskConsumer() { return container.get(bookingAuditTaskConsumerModule.token); } - diff --git a/packages/features/booking-audit/di/BookingAuditTaskConsumer.module.ts b/packages/features/booking-audit/di/BookingAuditTaskConsumer.module.ts index 01db014301..d29c6721f4 100644 --- a/packages/features/booking-audit/di/BookingAuditTaskConsumer.module.ts +++ b/packages/features/booking-audit/di/BookingAuditTaskConsumer.module.ts @@ -28,5 +28,5 @@ const loadModule = bindModuleToClassOnToken({ export const moduleLoader = { token, - loadModule + loadModule, }; diff --git a/packages/features/booking-audit/di/BookingAuditTaskerProducerService.module.ts b/packages/features/booking-audit/di/BookingAuditTaskerProducerService.module.ts index 9466a6d8b8..7c42279fe8 100644 --- a/packages/features/booking-audit/di/BookingAuditTaskerProducerService.module.ts +++ b/packages/features/booking-audit/di/BookingAuditTaskerProducerService.module.ts @@ -24,5 +24,5 @@ const loadModule = bindModuleToClassOnToken({ export const moduleLoader = { token, - loadModule + loadModule, }; diff --git a/packages/features/booking-audit/di/BookingAuditViewerService.container.ts b/packages/features/booking-audit/di/BookingAuditViewerService.container.ts index add38c9a55..8601a81fd4 100644 --- a/packages/features/booking-audit/di/BookingAuditViewerService.container.ts +++ b/packages/features/booking-audit/di/BookingAuditViewerService.container.ts @@ -12,4 +12,3 @@ export function getBookingAuditViewerService() { return container.get(bookingAuditViewerServiceModule.token); } - diff --git a/packages/features/booking-audit/di/BookingAuditViewerService.module.ts b/packages/features/booking-audit/di/BookingAuditViewerService.module.ts index 629297b08a..e902d0b9fb 100644 --- a/packages/features/booking-audit/di/BookingAuditViewerService.module.ts +++ b/packages/features/booking-audit/di/BookingAuditViewerService.module.ts @@ -14,7 +14,7 @@ export const bookingAuditViewerServiceModule = createModule(); const token = BOOKING_AUDIT_DI_TOKENS.BOOKING_AUDIT_VIEWER_SERVICE; const moduleToken = BOOKING_AUDIT_DI_TOKENS.BOOKING_AUDIT_VIEWER_SERVICE_MODULE; -export { BookingAuditViewerService } +export { BookingAuditViewerService }; const loadModule = bindModuleToClassOnToken({ module: bookingAuditViewerServiceModule, @@ -34,6 +34,5 @@ const loadModule = bindModuleToClassOnToken({ export const moduleLoader = { token, - loadModule + loadModule, }; - diff --git a/packages/features/booking-audit/di/tokens.ts b/packages/features/booking-audit/di/tokens.ts index 50b03f750a..f376e83276 100644 --- a/packages/features/booking-audit/di/tokens.ts +++ b/packages/features/booking-audit/di/tokens.ts @@ -1,14 +1,14 @@ export const BOOKING_AUDIT_DI_TOKENS = { - BOOKING_AUDIT_VIEWER_SERVICE: Symbol("BookingAuditViewerService"), - BOOKING_AUDIT_VIEWER_SERVICE_MODULE: Symbol("BookingAuditViewerServiceModule"), - BOOKING_AUDIT_PRODUCER_SERVICE: Symbol("BookingAuditProducerService"), - BOOKING_AUDIT_PRODUCER_SERVICE_MODULE: Symbol("BookingAuditProducerServiceModule"), - BOOKING_AUDIT_TASK_CONSUMER: Symbol("BookingAuditTaskConsumer"), - BOOKING_AUDIT_TASK_CONSUMER_MODULE: Symbol("BookingAuditTaskConsumerModule"), - BOOKING_AUDIT_REPOSITORY: Symbol("BookingAuditRepository"), - BOOKING_AUDIT_REPOSITORY_MODULE: Symbol("BookingAuditRepositoryModule"), - AUDIT_ACTOR_REPOSITORY: Symbol("AuditActorRepository"), - AUDIT_ACTOR_REPOSITORY_MODULE: Symbol("AuditActorRepositoryModule"), - BOOKING_HISTORY_VIEWER_SERVICE: Symbol("BookingHistoryViewerService"), - BOOKING_HISTORY_VIEWER_SERVICE_MODULE: Symbol("BookingHistoryViewerServiceModule"), + BOOKING_AUDIT_VIEWER_SERVICE: Symbol("BookingAuditViewerService"), + BOOKING_AUDIT_VIEWER_SERVICE_MODULE: Symbol("BookingAuditViewerServiceModule"), + BOOKING_AUDIT_PRODUCER_SERVICE: Symbol("BookingAuditProducerService"), + BOOKING_AUDIT_PRODUCER_SERVICE_MODULE: Symbol("BookingAuditProducerServiceModule"), + BOOKING_AUDIT_TASK_CONSUMER: Symbol("BookingAuditTaskConsumer"), + BOOKING_AUDIT_TASK_CONSUMER_MODULE: Symbol("BookingAuditTaskConsumerModule"), + BOOKING_AUDIT_REPOSITORY: Symbol("BookingAuditRepository"), + BOOKING_AUDIT_REPOSITORY_MODULE: Symbol("BookingAuditRepositoryModule"), + AUDIT_ACTOR_REPOSITORY: Symbol("AuditActorRepository"), + AUDIT_ACTOR_REPOSITORY_MODULE: Symbol("AuditActorRepositoryModule"), + BOOKING_HISTORY_VIEWER_SERVICE: Symbol("BookingHistoryViewerService"), + BOOKING_HISTORY_VIEWER_SERVICE_MODULE: Symbol("BookingHistoryViewerServiceModule"), }; diff --git a/packages/features/booking-audit/lib/actions/AcceptedAuditActionService.ts b/packages/features/booking-audit/lib/actions/AcceptedAuditActionService.ts index 6b66679738..28056b5c0d 100644 --- a/packages/features/booking-audit/lib/actions/AcceptedAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/AcceptedAuditActionService.ts @@ -4,7 +4,12 @@ import { z } from "zod"; import { BookingStatusChangeSchema } from "../common/changeSchemas"; import type { DataRequirements } from "../service/EnrichmentDataStore"; import { AuditActionServiceHelper } from "./AuditActionServiceHelper"; -import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, GetDisplayJsonParams } from "./IAuditActionService"; +import type { + IAuditActionService, + TranslationWithParams, + GetDisplayTitleParams, + GetDisplayJsonParams, +} from "./IAuditActionService"; /** * Accepted Audit Action Service @@ -13,75 +18,73 @@ import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, // Module-level because it is passed to IAuditActionService type outside the class scope const fieldsSchemaV1 = z.object({ - status: BookingStatusChangeSchema, + status: BookingStatusChangeSchema, }); export class AcceptedAuditActionService implements IAuditActionService { - readonly VERSION = 1; - public static readonly TYPE = "ACCEPTED" as const; - private static dataSchemaV1 = z.object({ - version: z.literal(1), - fields: fieldsSchemaV1, + readonly VERSION = 1; + public static readonly TYPE = "ACCEPTED" as const; + private static dataSchemaV1 = z.object({ + version: z.literal(1), + fields: fieldsSchemaV1, + }); + private static fieldsSchemaV1 = fieldsSchemaV1; + public static readonly latestFieldsSchema = fieldsSchemaV1; + // Union of all versions + public static readonly storedDataSchema = AcceptedAuditActionService.dataSchemaV1; + // Union of all versions + public static readonly storedFieldsSchema = AcceptedAuditActionService.fieldsSchemaV1; + private helper: AuditActionServiceHelper< + typeof AcceptedAuditActionService.latestFieldsSchema, + typeof AcceptedAuditActionService.storedDataSchema + >; + + constructor() { + this.helper = new AuditActionServiceHelper({ + latestVersion: this.VERSION, + latestFieldsSchema: AcceptedAuditActionService.latestFieldsSchema, + storedDataSchema: AcceptedAuditActionService.storedDataSchema, }); - private static fieldsSchemaV1 = fieldsSchemaV1; - public static readonly latestFieldsSchema = fieldsSchemaV1; - // Union of all versions - public static readonly storedDataSchema = AcceptedAuditActionService.dataSchemaV1; - // Union of all versions - public static readonly storedFieldsSchema = AcceptedAuditActionService.fieldsSchemaV1; - private helper: AuditActionServiceHelper< - typeof AcceptedAuditActionService.latestFieldsSchema, - typeof AcceptedAuditActionService.storedDataSchema - >; + } - constructor() { - this.helper = new AuditActionServiceHelper({ - latestVersion: this.VERSION, - latestFieldsSchema: AcceptedAuditActionService.latestFieldsSchema, - storedDataSchema: AcceptedAuditActionService.storedDataSchema, - }); - } + getVersionedData(fields: unknown) { + return this.helper.getVersionedData(fields); + } - getVersionedData(fields: unknown) { - return this.helper.getVersionedData(fields); - } + parseStored(data: unknown) { + return this.helper.parseStored(data); + } - parseStored(data: unknown) { - return this.helper.parseStored(data); - } + getVersion(data: unknown): number { + return this.helper.getVersion(data); + } - getVersion(data: unknown): number { - return this.helper.getVersion(data); - } + migrateToLatest(data: unknown) { + // V1-only: validate and return as-is (no migration needed) + const validated = fieldsSchemaV1.parse(data); + return { isMigrated: false, latestData: validated }; + } - migrateToLatest(data: unknown) { - // V1-only: validate and return as-is (no migration needed) - const validated = fieldsSchemaV1.parse(data); - return { isMigrated: false, latestData: validated }; - } + getDataRequirements(): DataRequirements { + return { userUuids: [] }; + } - getDataRequirements(): DataRequirements { - return { userUuids: [] }; - } + async getDisplayTitle(_: GetDisplayTitleParams): Promise { + return { key: "booking_audit_action.accepted" }; + } - async getDisplayTitle(_: GetDisplayTitleParams): Promise { - return { key: "booking_audit_action.accepted" }; - } - - getDisplayJson({ - storedData, - }: GetDisplayJsonParams): AcceptedAuditDisplayData { - const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); - return { - previousStatus: fields.status.old ?? null, - newStatus: fields.status.new ?? null, - }; - } + getDisplayJson({ storedData }: GetDisplayJsonParams): AcceptedAuditDisplayData { + const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); + return { + previousStatus: fields.status.old ?? null, + newStatus: fields.status.new ?? null, + }; + } } export type AcceptedAuditData = z.infer; export type AcceptedAuditDisplayData = { - previousStatus: BookingStatus | null; - newStatus: BookingStatus | null; + previousStatus: BookingStatus | null; + newStatus: BookingStatus | null; }; diff --git a/packages/features/booking-audit/lib/actions/AttendeeRemovedAuditActionService.ts b/packages/features/booking-audit/lib/actions/AttendeeRemovedAuditActionService.ts index 82b23c1e96..75909fbbc7 100644 --- a/packages/features/booking-audit/lib/actions/AttendeeRemovedAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/AttendeeRemovedAuditActionService.ts @@ -3,7 +3,12 @@ import { z } from "zod"; import { StringArrayChangeSchema } from "../common/changeSchemas"; import type { DataRequirements } from "../service/EnrichmentDataStore"; import { AuditActionServiceHelper } from "./AuditActionServiceHelper"; -import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, GetDisplayJsonParams } from "./IAuditActionService"; +import type { + IAuditActionService, + TranslationWithParams, + GetDisplayTitleParams, + GetDisplayJsonParams, +} from "./IAuditActionService"; /** * Attendee Removed Audit Action Service @@ -12,77 +17,75 @@ import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, // Module-level because it is passed to IAuditActionService type outside the class scope const fieldsSchemaV1 = z.object({ - attendees: StringArrayChangeSchema, + attendees: StringArrayChangeSchema, }); export class AttendeeRemovedAuditActionService implements IAuditActionService { - readonly VERSION = 1; - public static readonly TYPE = "ATTENDEE_REMOVED" as const; - private static dataSchemaV1 = z.object({ - version: z.literal(1), - fields: fieldsSchemaV1, + readonly VERSION = 1; + public static readonly TYPE = "ATTENDEE_REMOVED" as const; + private static dataSchemaV1 = z.object({ + version: z.literal(1), + fields: fieldsSchemaV1, + }); + private static fieldsSchemaV1 = fieldsSchemaV1; + public static readonly latestFieldsSchema = fieldsSchemaV1; + // Union of all versions + public static readonly storedDataSchema = AttendeeRemovedAuditActionService.dataSchemaV1; + // Union of all versions + public static readonly storedFieldsSchema = AttendeeRemovedAuditActionService.fieldsSchemaV1; + private helper: AuditActionServiceHelper< + typeof AttendeeRemovedAuditActionService.latestFieldsSchema, + typeof AttendeeRemovedAuditActionService.storedDataSchema + >; + + constructor() { + this.helper = new AuditActionServiceHelper({ + latestVersion: this.VERSION, + latestFieldsSchema: AttendeeRemovedAuditActionService.latestFieldsSchema, + storedDataSchema: AttendeeRemovedAuditActionService.storedDataSchema, }); - private static fieldsSchemaV1 = fieldsSchemaV1; - public static readonly latestFieldsSchema = fieldsSchemaV1; - // Union of all versions - public static readonly storedDataSchema = AttendeeRemovedAuditActionService.dataSchemaV1; - // Union of all versions - public static readonly storedFieldsSchema = AttendeeRemovedAuditActionService.fieldsSchemaV1; - private helper: AuditActionServiceHelper< - typeof AttendeeRemovedAuditActionService.latestFieldsSchema, - typeof AttendeeRemovedAuditActionService.storedDataSchema - >; + } - constructor() { - this.helper = new AuditActionServiceHelper({ - latestVersion: this.VERSION, - latestFieldsSchema: AttendeeRemovedAuditActionService.latestFieldsSchema, - storedDataSchema: AttendeeRemovedAuditActionService.storedDataSchema, - }); - } + getVersionedData(fields: unknown) { + return this.helper.getVersionedData(fields); + } - getVersionedData(fields: unknown) { - return this.helper.getVersionedData(fields); - } + parseStored(data: unknown) { + return this.helper.parseStored(data); + } - parseStored(data: unknown) { - return this.helper.parseStored(data); - } + getVersion(data: unknown): number { + return this.helper.getVersion(data); + } - getVersion(data: unknown): number { - return this.helper.getVersion(data); - } + migrateToLatest(data: unknown) { + // V1-only: validate and return as-is (no migration needed) + const validated = fieldsSchemaV1.parse(data); + return { isMigrated: false, latestData: validated }; + } - migrateToLatest(data: unknown) { - // V1-only: validate and return as-is (no migration needed) - const validated = fieldsSchemaV1.parse(data); - return { isMigrated: false, latestData: validated }; - } + getDataRequirements(): DataRequirements { + return { userUuids: [] }; + } - getDataRequirements(): DataRequirements { - return { userUuids: [] }; - } + async getDisplayTitle(_: GetDisplayTitleParams): Promise { + return { key: "booking_audit_action.attendee_removed" }; + } - async getDisplayTitle(_: GetDisplayTitleParams): Promise { - return { key: "booking_audit_action.attendee_removed" }; - } - - getDisplayJson({ - storedData, - }: GetDisplayJsonParams): AttendeeRemovedAuditDisplayData { - const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); - const remainingAttendeesSet = new Set(fields.attendees.new ?? []); - const removedAttendees = (fields.attendees.old ?? []).filter( - (email) => !remainingAttendeesSet.has(email) - ); - return { - removedAttendees, - }; - } + getDisplayJson({ storedData }: GetDisplayJsonParams): AttendeeRemovedAuditDisplayData { + const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); + const remainingAttendeesSet = new Set(fields.attendees.new ?? []); + const removedAttendees = (fields.attendees.old ?? []).filter( + (email) => !remainingAttendeesSet.has(email) + ); + return { + removedAttendees, + }; + } } export type AttendeeRemovedAuditData = z.infer; export type AttendeeRemovedAuditDisplayData = { - removedAttendees: string[]; + removedAttendees: string[]; }; diff --git a/packages/features/booking-audit/lib/actions/AuditActionServiceHelper.ts b/packages/features/booking-audit/lib/actions/AuditActionServiceHelper.ts index e5a0a63ecb..3177c6a3ca 100644 --- a/packages/features/booking-audit/lib/actions/AuditActionServiceHelper.ts +++ b/packages/features/booking-audit/lib/actions/AuditActionServiceHelper.ts @@ -3,14 +3,14 @@ import { formatInTimeZone } from "date-fns-tz"; /** * Audit Action Service Helper - * + * * Provides reusable utility methods for audit action services via composition. - * + * * We use composition instead of inheritance for Action services so that services can evolve to v2, v3 independently without polluting a shared base class */ export class AuditActionServiceHelper< TLatestFieldsSchema extends z.ZodTypeAny, - TStoredDataSchema extends z.ZodTypeAny + TStoredDataSchema extends z.ZodTypeAny, > { private readonly latestFieldsSchema: TLatestFieldsSchema; private readonly latestVersion: number; @@ -83,4 +83,3 @@ export class AuditActionServiceHelper< return formatInTimeZone(new Date(date), timeZone, "yyyy-MM-dd HH:mm:ss"); } } - diff --git a/packages/features/booking-audit/lib/actions/CancelledAuditActionService.ts b/packages/features/booking-audit/lib/actions/CancelledAuditActionService.ts index 2c9a1d6be9..672f0da197 100644 --- a/packages/features/booking-audit/lib/actions/CancelledAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/CancelledAuditActionService.ts @@ -3,7 +3,12 @@ import { z } from "zod"; import { BookingStatusChangeSchema } from "../common/changeSchemas"; import type { DataRequirements } from "../service/EnrichmentDataStore"; import { AuditActionServiceHelper } from "./AuditActionServiceHelper"; -import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, GetDisplayJsonParams } from "./IAuditActionService"; +import type { + IAuditActionService, + TranslationWithParams, + GetDisplayTitleParams, + GetDisplayJsonParams, +} from "./IAuditActionService"; /** * Cancelled Audit Action Service @@ -12,81 +17,79 @@ import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, // Module-level because it is passed to IAuditActionService type outside the class scope const fieldsSchemaV1 = z.object({ - cancellationReason: z.string().nullable(), - cancelledBy: z.string().nullable(), - status: BookingStatusChangeSchema, + cancellationReason: z.string().nullable(), + cancelledBy: z.string().nullable(), + status: BookingStatusChangeSchema, }); export class CancelledAuditActionService implements IAuditActionService { - readonly VERSION = 1; - public static readonly TYPE = "CANCELLED" as const; - private static dataSchemaV1 = z.object({ - version: z.literal(1), - fields: fieldsSchemaV1, + readonly VERSION = 1; + public static readonly TYPE = "CANCELLED" as const; + private static dataSchemaV1 = z.object({ + version: z.literal(1), + fields: fieldsSchemaV1, + }); + private static fieldsSchemaV1 = fieldsSchemaV1; + public static readonly latestFieldsSchema = fieldsSchemaV1; + // Union of all versions + public static readonly storedDataSchema = CancelledAuditActionService.dataSchemaV1; + // Union of all versions + public static readonly storedFieldsSchema = CancelledAuditActionService.fieldsSchemaV1; + private helper: AuditActionServiceHelper< + typeof CancelledAuditActionService.latestFieldsSchema, + typeof CancelledAuditActionService.storedDataSchema + >; + + constructor() { + this.helper = new AuditActionServiceHelper({ + latestVersion: this.VERSION, + latestFieldsSchema: CancelledAuditActionService.latestFieldsSchema, + storedDataSchema: CancelledAuditActionService.storedDataSchema, }); - private static fieldsSchemaV1 = fieldsSchemaV1; - public static readonly latestFieldsSchema = fieldsSchemaV1; - // Union of all versions - public static readonly storedDataSchema = CancelledAuditActionService.dataSchemaV1; - // Union of all versions - public static readonly storedFieldsSchema = CancelledAuditActionService.fieldsSchemaV1; - private helper: AuditActionServiceHelper< - typeof CancelledAuditActionService.latestFieldsSchema, - typeof CancelledAuditActionService.storedDataSchema - >; + } - constructor() { - this.helper = new AuditActionServiceHelper({ - latestVersion: this.VERSION, - latestFieldsSchema: CancelledAuditActionService.latestFieldsSchema, - storedDataSchema: CancelledAuditActionService.storedDataSchema, - }); - } + getVersionedData(fields: unknown) { + return this.helper.getVersionedData(fields); + } - getVersionedData(fields: unknown) { - return this.helper.getVersionedData(fields); - } + parseStored(data: unknown) { + return this.helper.parseStored(data); + } - parseStored(data: unknown) { - return this.helper.parseStored(data); - } + getVersion(data: unknown): number { + return this.helper.getVersion(data); + } - getVersion(data: unknown): number { - return this.helper.getVersion(data); - } + migrateToLatest(data: unknown) { + // V1-only: validate and return as-is (no migration needed) + const validated = fieldsSchemaV1.parse(data); + return { isMigrated: false, latestData: validated }; + } - migrateToLatest(data: unknown) { - // V1-only: validate and return as-is (no migration needed) - const validated = fieldsSchemaV1.parse(data); - return { isMigrated: false, latestData: validated }; - } + getDataRequirements(): DataRequirements { + return { userUuids: [] }; + } - getDataRequirements(): DataRequirements { - return { userUuids: [] }; - } + async getDisplayTitle(_: GetDisplayTitleParams): Promise { + return { key: "booking_audit_action.cancelled" }; + } - async getDisplayTitle(_: GetDisplayTitleParams): Promise { - return { key: "booking_audit_action.cancelled" }; - } - - getDisplayJson({ - storedData, - }: GetDisplayJsonParams): CancelledAuditDisplayData { - const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); - return { - cancellationReason: fields.cancellationReason ?? null, - cancelledBy: fields.cancelledBy ?? null, - previousStatus: fields.status.old ?? null, - newStatus: fields.status.new ?? null, - }; - } + getDisplayJson({ storedData }: GetDisplayJsonParams): CancelledAuditDisplayData { + const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); + return { + cancellationReason: fields.cancellationReason ?? null, + cancelledBy: fields.cancelledBy ?? null, + previousStatus: fields.status.old ?? null, + newStatus: fields.status.new ?? null, + }; + } } export type CancelledAuditData = z.infer; export type CancelledAuditDisplayData = { - cancellationReason: string | null; - cancelledBy: string | null; - previousStatus: string | null; - newStatus: string | null; + cancellationReason: string | null; + cancelledBy: string | null; + previousStatus: string | null; + newStatus: string | null; }; diff --git a/packages/features/booking-audit/lib/actions/CreatedAuditActionService.ts b/packages/features/booking-audit/lib/actions/CreatedAuditActionService.ts index fff749b7a8..83106534f2 100644 --- a/packages/features/booking-audit/lib/actions/CreatedAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/CreatedAuditActionService.ts @@ -2,105 +2,110 @@ import { z } from "zod"; import { BookingStatus } from "@calcom/prisma/enums"; import { AuditActionServiceHelper } from "./AuditActionServiceHelper"; -import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, GetDisplayJsonParams, BaseStoredAuditData } from "./IAuditActionService"; +import type { + IAuditActionService, + TranslationWithParams, + GetDisplayTitleParams, + GetDisplayJsonParams, + BaseStoredAuditData, +} from "./IAuditActionService"; import type { DataRequirements } from "../service/EnrichmentDataStore"; /** * Created Audit Action Service - * + * * Note: CREATED action captures initial state, so it doesn't use { old, new } pattern */ // Module-level because it is passed to IAuditActionService type outside the class scope const fieldsSchemaV1 = z.object({ - startTime: z.number(), - endTime: z.number(), - status: z.nativeEnum(BookingStatus), - hostUserUuid: z.string().nullable(), - // Allowing it to be optional because most of the time(non-seated booking) it won't be there - seatReferenceUid: z.string().nullish(), + startTime: z.number(), + endTime: z.number(), + status: z.nativeEnum(BookingStatus), + hostUserUuid: z.string().nullable(), + // Allowing it to be optional because most of the time(non-seated booking) it won't be there + seatReferenceUid: z.string().nullish(), }); export class CreatedAuditActionService implements IAuditActionService { - readonly VERSION = 1; - public static readonly TYPE = "CREATED" as const; - private static dataSchemaV1 = z.object({ - version: z.literal(1), - fields: fieldsSchemaV1, + readonly VERSION = 1; + public static readonly TYPE = "CREATED" as const; + private static dataSchemaV1 = z.object({ + version: z.literal(1), + fields: fieldsSchemaV1, + }); + private static fieldsSchemaV1 = fieldsSchemaV1; + public static readonly latestFieldsSchema = fieldsSchemaV1; + // Union of all versions + public static readonly storedDataSchema = CreatedAuditActionService.dataSchemaV1; + // Union of all versions + public static readonly storedFieldsSchema = CreatedAuditActionService.fieldsSchemaV1; + private helper: AuditActionServiceHelper< + typeof CreatedAuditActionService.latestFieldsSchema, + typeof CreatedAuditActionService.storedDataSchema + >; + + constructor() { + this.helper = new AuditActionServiceHelper({ + latestVersion: this.VERSION, + latestFieldsSchema: CreatedAuditActionService.latestFieldsSchema, + storedDataSchema: CreatedAuditActionService.storedDataSchema, }); - private static fieldsSchemaV1 = fieldsSchemaV1; - public static readonly latestFieldsSchema = fieldsSchemaV1; - // Union of all versions - public static readonly storedDataSchema = CreatedAuditActionService.dataSchemaV1; - // Union of all versions - public static readonly storedFieldsSchema = CreatedAuditActionService.fieldsSchemaV1; - private helper: AuditActionServiceHelper; + } - constructor() { - this.helper = new AuditActionServiceHelper({ - latestVersion: this.VERSION, - latestFieldsSchema: CreatedAuditActionService.latestFieldsSchema, - storedDataSchema: CreatedAuditActionService.storedDataSchema, - }); + getVersionedData(fields: unknown) { + return this.helper.getVersionedData(fields); + } + + parseStored(data: unknown) { + return this.helper.parseStored(data); + } + + getVersion(data: unknown): number { + return this.helper.getVersion(data); + } + + migrateToLatest(data: unknown) { + // V1-only: validate and return as-is (no migration needed) + const validated = fieldsSchemaV1.parse(data); + return { isMigrated: false, latestData: validated }; + } + + getDataRequirements(storedData: BaseStoredAuditData): DataRequirements { + const { fields } = this.parseStored(storedData); + return { + userUuids: fields.hostUserUuid ? [fields.hostUserUuid] : [], + }; + } + + async getDisplayTitle({ storedData, dbStore }: GetDisplayTitleParams): Promise { + const { fields } = this.parseStored(storedData); + const hostUser = fields.hostUserUuid ? dbStore.getUserByUuid(fields.hostUserUuid) : null; + const hostName = hostUser?.name || "Unknown"; + if (fields.seatReferenceUid) { + return { key: "booking_audit_action.created_with_seat", params: { host: hostName } }; } + return { key: "booking_audit_action.created", params: { host: hostName } }; + } - getVersionedData(fields: unknown) { - return this.helper.getVersionedData(fields); - } + getDisplayJson({ storedData, userTimeZone }: GetDisplayJsonParams): CreatedAuditDisplayData { + const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); + const timeZone = userTimeZone; - parseStored(data: unknown) { - return this.helper.parseStored(data); - } - - getVersion(data: unknown): number { - return this.helper.getVersion(data); - } - - migrateToLatest(data: unknown) { - // V1-only: validate and return as-is (no migration needed) - const validated = fieldsSchemaV1.parse(data); - return { isMigrated: false, latestData: validated }; - } - - getDataRequirements(storedData: BaseStoredAuditData): DataRequirements { - const { fields } = this.parseStored(storedData); - return { - userUuids: fields.hostUserUuid ? [fields.hostUserUuid] : [], - }; - } - - async getDisplayTitle({ storedData, dbStore }: GetDisplayTitleParams): Promise { - const { fields } = this.parseStored(storedData); - const hostUser = fields.hostUserUuid ? dbStore.getUserByUuid(fields.hostUserUuid) : null; - const hostName = hostUser?.name || "Unknown"; - if (fields.seatReferenceUid) { - return { key: "booking_audit_action.created_with_seat", params: { host: hostName } }; - } - return { key: "booking_audit_action.created", params: { host: hostName } }; - } - - getDisplayJson({ - storedData, - userTimeZone, - }: GetDisplayJsonParams): CreatedAuditDisplayData { - const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); - const timeZone = userTimeZone; - - return { - startTime: AuditActionServiceHelper.formatDateTimeInTimeZone(fields.startTime, timeZone), - endTime: AuditActionServiceHelper.formatDateTimeInTimeZone(fields.endTime, timeZone), - status: fields.status, - ...(fields.seatReferenceUid ? { seatReferenceUid: fields.seatReferenceUid } : {}), - }; - } + return { + startTime: AuditActionServiceHelper.formatDateTimeInTimeZone(fields.startTime, timeZone), + endTime: AuditActionServiceHelper.formatDateTimeInTimeZone(fields.endTime, timeZone), + status: fields.status, + ...(fields.seatReferenceUid ? { seatReferenceUid: fields.seatReferenceUid } : {}), + }; + } } export type CreatedAuditData = z.infer; export type CreatedAuditDisplayData = { - startTime: string; - endTime: string; - status: BookingStatus; - seatReferenceUid?: string; + startTime: string; + endTime: string; + status: BookingStatus; + seatReferenceUid?: string; }; - diff --git a/packages/features/booking-audit/lib/actions/NoShowUpdatedAuditActionService.ts b/packages/features/booking-audit/lib/actions/NoShowUpdatedAuditActionService.ts index 91f5f9a3d7..7cec3f7e78 100644 --- a/packages/features/booking-audit/lib/actions/NoShowUpdatedAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/NoShowUpdatedAuditActionService.ts @@ -95,7 +95,10 @@ export class NoShowUpdatedAuditActionService implements IAuditActionService { } async getDisplayTitle({ storedData }: GetDisplayTitleParams): Promise { - const { fields: parsedFields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); + const { fields: parsedFields } = this.parseStored({ + version: storedData.version, + fields: storedData.fields, + }); if (this.isHostSet(parsedFields) && this.isAttendeesNoShowSet(parsedFields)) { return { key: "booking_audit_action.no_show_updated" }; } diff --git a/packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts b/packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts index f5ea5cd69c..c6e8961bea 100644 --- a/packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts +++ b/packages/features/booking-audit/lib/actions/RescheduledAuditActionService.ts @@ -3,7 +3,13 @@ import { z } from "zod"; import { NumberChangeSchema, StringChangeSchema } from "../common/changeSchemas"; import type { DataRequirements } from "../service/EnrichmentDataStore"; import { AuditActionServiceHelper } from "./AuditActionServiceHelper"; -import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, GetDisplayJsonParams, BaseStoredAuditData } from "./IAuditActionService"; +import type { + IAuditActionService, + TranslationWithParams, + GetDisplayTitleParams, + GetDisplayJsonParams, + BaseStoredAuditData, +} from "./IAuditActionService"; /** * Rescheduled Audit Action Service @@ -12,166 +18,164 @@ import type { IAuditActionService, TranslationWithParams, GetDisplayTitleParams, // Module-level because it is passed to IAuditActionService type outside the class scope const fieldsSchemaV1 = z.object({ - startTime: NumberChangeSchema, - endTime: NumberChangeSchema, - rescheduledToUid: StringChangeSchema, + startTime: NumberChangeSchema, + endTime: NumberChangeSchema, + rescheduledToUid: StringChangeSchema, }); export class RescheduledAuditActionService implements IAuditActionService { - readonly VERSION = 1; - public static readonly TYPE = "RESCHEDULED" as const; - private static dataSchemaV1 = z.object({ - version: z.literal(1), - fields: fieldsSchemaV1, + readonly VERSION = 1; + public static readonly TYPE = "RESCHEDULED" as const; + private static dataSchemaV1 = z.object({ + version: z.literal(1), + fields: fieldsSchemaV1, + }); + private static fieldsSchemaV1 = fieldsSchemaV1; + public static readonly latestFieldsSchema = fieldsSchemaV1; + // Union of all versions + public static readonly storedDataSchema = RescheduledAuditActionService.dataSchemaV1; + // Union of all versions + public static readonly storedFieldsSchema = RescheduledAuditActionService.fieldsSchemaV1; + private helper: AuditActionServiceHelper< + typeof RescheduledAuditActionService.latestFieldsSchema, + typeof RescheduledAuditActionService.storedDataSchema + >; + + constructor() { + this.helper = new AuditActionServiceHelper({ + latestVersion: this.VERSION, + latestFieldsSchema: RescheduledAuditActionService.latestFieldsSchema, + storedDataSchema: RescheduledAuditActionService.storedDataSchema, }); - private static fieldsSchemaV1 = fieldsSchemaV1; - public static readonly latestFieldsSchema = fieldsSchemaV1; - // Union of all versions - public static readonly storedDataSchema = RescheduledAuditActionService.dataSchemaV1; - // Union of all versions - public static readonly storedFieldsSchema = RescheduledAuditActionService.fieldsSchemaV1; - private helper: AuditActionServiceHelper< - typeof RescheduledAuditActionService.latestFieldsSchema, - typeof RescheduledAuditActionService.storedDataSchema - >; + } - constructor() { - this.helper = new AuditActionServiceHelper({ - latestVersion: this.VERSION, - latestFieldsSchema: RescheduledAuditActionService.latestFieldsSchema, - storedDataSchema: RescheduledAuditActionService.storedDataSchema, - }); - } + getVersionedData(fields: unknown) { + return this.helper.getVersionedData(fields); + } - getVersionedData(fields: unknown) { - return this.helper.getVersionedData(fields); - } + parseStored(data: unknown) { + return this.helper.parseStored(data); + } - parseStored(data: unknown) { - return this.helper.parseStored(data); - } + getVersion(data: unknown): number { + return this.helper.getVersion(data); + } - getVersion(data: unknown): number { - return this.helper.getVersion(data); - } + migrateToLatest(data: unknown) { + // V1-only: validate and return as-is (no migration needed) + const validated = fieldsSchemaV1.parse(data); + return { isMigrated: false, latestData: validated }; + } - migrateToLatest(data: unknown) { - // V1-only: validate and return as-is (no migration needed) - const validated = fieldsSchemaV1.parse(data); - return { isMigrated: false, latestData: validated }; - } + getDataRequirements(): DataRequirements { + return { userUuids: [] }; + } - getDataRequirements(): DataRequirements { - return { userUuids: [] }; - } + async getDisplayTitle({ storedData, userTimeZone }: GetDisplayTitleParams): Promise { + const { fields } = this.parseStored(storedData); + const rescheduledToUid = fields.rescheduledToUid.new; + const timeZone = userTimeZone; - async getDisplayTitle({ - storedData, - userTimeZone, - }: GetDisplayTitleParams): Promise { - const { fields } = this.parseStored(storedData); - const rescheduledToUid = fields.rescheduledToUid.new; - const timeZone = userTimeZone; + // Format dates in user timezone + const oldDate = fields.startTime.old + ? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.old, timeZone) + : ""; + const newDate = fields.startTime.new + ? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.new, timeZone) + : ""; - // Format dates in user timezone - const oldDate = fields.startTime.old - ? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.old, timeZone) - : ""; - const newDate = fields.startTime.new - ? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.new, timeZone) - : ""; + return { + key: "booking_audit_action.rescheduled", + params: { + oldDate, + newDate, + }, + components: rescheduledToUid + ? [{ type: "link", href: `/booking/${rescheduledToUid}/logs` }] + : undefined, + }; + } - return { - key: "booking_audit_action.rescheduled", - params: { - oldDate, - newDate, - }, - components: rescheduledToUid ? [{ type: "link", href: `/booking/${rescheduledToUid}/logs` }] : undefined, - }; - } + getDisplayTitleForRescheduledFromLog({ + fromRescheduleUid, + userTimeZone, + storedData, + }: { + fromRescheduleUid: string; + userTimeZone: string; + storedData: BaseStoredAuditData; + }): TranslationWithParams { + const timeZone = userTimeZone; + const { fields } = this.parseStored(storedData); - getDisplayTitleForRescheduledFromLog({ - fromRescheduleUid, - userTimeZone, - storedData, - }: { - fromRescheduleUid: string; - userTimeZone: string; - storedData: BaseStoredAuditData; - }): TranslationWithParams { - const timeZone = userTimeZone; - const { fields } = this.parseStored(storedData); + // Format dates in user timezone + const oldDate = fields.startTime.old + ? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.old, timeZone) + : ""; + const newDate = fields.startTime.new + ? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.new, timeZone) + : ""; - // Format dates in user timezone - const oldDate = fields.startTime.old - ? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.old, timeZone) - : ""; - const newDate = fields.startTime.new - ? AuditActionServiceHelper.formatDateInTimeZone(fields.startTime.new, timeZone) - : ""; + return { + key: "booking_audit_action.rescheduled_from", + params: { + oldDate, + newDate, + }, + components: [{ type: "link", href: `/booking/${fromRescheduleUid}/logs` }], + }; + } - return { - key: "booking_audit_action.rescheduled_from", - params: { - oldDate, - newDate, - }, - components: [{ type: "link", href: `/booking/${fromRescheduleUid}/logs` }], - }; - } + getDisplayJson({ storedData, userTimeZone }: GetDisplayJsonParams): RescheduledAuditDisplayData { + const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); + const timeZone = userTimeZone; - getDisplayJson({ - storedData, - userTimeZone, - }: GetDisplayJsonParams): RescheduledAuditDisplayData { - const { fields } = this.parseStored({ version: storedData.version, fields: storedData.fields }); - const timeZone = userTimeZone; + return { + previousStartTime: fields.startTime.old + ? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.startTime.old, timeZone) + : null, + newStartTime: fields.startTime.new + ? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.startTime.new, timeZone) + : null, + previousEndTime: fields.endTime.old + ? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.endTime.old, timeZone) + : null, + newEndTime: fields.endTime.new + ? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.endTime.new, timeZone) + : null, + rescheduledToUid: fields.rescheduledToUid.new ?? null, + }; + } - return { - previousStartTime: fields.startTime.old - ? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.startTime.old, timeZone) - : null, - newStartTime: fields.startTime.new - ? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.startTime.new, timeZone) - : null, - previousEndTime: fields.endTime.old - ? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.endTime.old, timeZone) - : null, - newEndTime: fields.endTime.new - ? AuditActionServiceHelper.formatDateTimeInTimeZone(fields.endTime.new, timeZone) - : null, - rescheduledToUid: fields.rescheduledToUid.new ?? null, - }; - } - - /** - * Finds the rescheduled log that created a specific booking - * by matching the rescheduledToUid field with the target booking UID - * @param rescheduledLogs - Array of rescheduled audit logs to search through - * @param rescheduledToBookingUid - The UID of the booking that was created from the reschedule - * @returns The matching log or null if not found - */ - getMatchingLog({ - rescheduledLogs, - rescheduledToBookingUid, - }: { - rescheduledLogs: T[]; - rescheduledToBookingUid: string; - }): T | null { - return rescheduledLogs.find((log) => { - const parsedData = this.parseStored(log.data); - return parsedData.fields.rescheduledToUid.new === rescheduledToBookingUid; - }) ?? null; - } + /** + * Finds the rescheduled log that created a specific booking + * by matching the rescheduledToUid field with the target booking UID + * @param rescheduledLogs - Array of rescheduled audit logs to search through + * @param rescheduledToBookingUid - The UID of the booking that was created from the reschedule + * @returns The matching log or null if not found + */ + getMatchingLog({ + rescheduledLogs, + rescheduledToBookingUid, + }: { + rescheduledLogs: T[]; + rescheduledToBookingUid: string; + }): T | null { + return ( + rescheduledLogs.find((log) => { + const parsedData = this.parseStored(log.data); + return parsedData.fields.rescheduledToUid.new === rescheduledToBookingUid; + }) ?? null + ); + } } export type RescheduledAuditData = z.infer; export type RescheduledAuditDisplayData = { - previousStartTime: string | null; - newStartTime: string | null; - previousEndTime: string | null; - newEndTime: string | null; - rescheduledToUid: string | null; + previousStartTime: string | null; + newStartTime: string | null; + previousEndTime: string | null; + newEndTime: string | null; + rescheduledToUid: string | null; }; diff --git a/packages/features/booking-audit/lib/actions/__tests__/NoShowUpdatedAuditActionService.test.ts b/packages/features/booking-audit/lib/actions/__tests__/NoShowUpdatedAuditActionService.test.ts index 6c1f0010be..748b7b3c5a 100644 --- a/packages/features/booking-audit/lib/actions/__tests__/NoShowUpdatedAuditActionService.test.ts +++ b/packages/features/booking-audit/lib/actions/__tests__/NoShowUpdatedAuditActionService.test.ts @@ -21,9 +21,9 @@ describe("NoShowUpdatedAuditActionService - getDataRequirements contract", () => }, }; - const { errors, accessedData} = await verifyDataRequirementsContract(service, storedData); + const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData); expect(errors).toEqual([]); - expect(accessedData.userUuids.size).toBe(1) + expect(accessedData.userUuids.size).toBe(1); }); it("should declare empty userUuids when only attendees are set", async () => { @@ -34,9 +34,9 @@ describe("NoShowUpdatedAuditActionService - getDataRequirements contract", () => }, }; - const { errors, accessedData} = await verifyDataRequirementsContract(service, storedData); + const { errors, accessedData } = await verifyDataRequirementsContract(service, storedData); expect(errors).toEqual([]); - expect(accessedData.userUuids.size).toBe(0) + expect(accessedData.userUuids.size).toBe(0); }); it("should declare exactly the userUuids accessed when both host and attendees are set", async () => { diff --git a/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts b/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts index 907c5fbfbd..13655bcaef 100644 --- a/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts +++ b/packages/features/booking-audit/lib/actions/__tests__/ReassignmentAuditActionService.test.ts @@ -338,7 +338,13 @@ describe("ReassignmentAuditActionService", () => { const dbStore = createMockEnrichmentDataStore( { users: [ - { id: 1, uuid: "organizer-old", name: "Previous Host", email: "old@example.com", avatarUrl: null }, + { + id: 1, + uuid: "organizer-old", + name: "Previous Host", + email: "old@example.com", + avatarUrl: null, + }, { id: 2, uuid: "organizer-new", name: "New Host", email: "new@example.com", avatarUrl: null }, ], }, @@ -372,7 +378,13 @@ describe("ReassignmentAuditActionService", () => { const dbStore = createMockEnrichmentDataStore( { users: [ - { id: 1, uuid: "organizer-old", name: "Previous Host", email: "old@example.com", avatarUrl: null }, + { + id: 1, + uuid: "organizer-old", + name: "Previous Host", + email: "old@example.com", + avatarUrl: null, + }, { id: 2, uuid: "organizer-new", name: "New Host", email: "new@example.com", avatarUrl: null }, ], }, diff --git a/packages/features/booking-audit/lib/actions/__tests__/contractVerification.ts b/packages/features/booking-audit/lib/actions/__tests__/contractVerification.ts index 4bc5e17dbc..41d99b76e7 100644 --- a/packages/features/booking-audit/lib/actions/__tests__/contractVerification.ts +++ b/packages/features/booking-audit/lib/actions/__tests__/contractVerification.ts @@ -1,4 +1,10 @@ -import type { EnrichmentDataStore, DataRequirements, StoredUser, StoredAttendee, StoredCredential } from "../../service/EnrichmentDataStore"; +import type { + EnrichmentDataStore, + DataRequirements, + StoredUser, + StoredAttendee, + StoredCredential, +} from "../../service/EnrichmentDataStore"; import type { IAuditActionService, BaseStoredAuditData } from "../IAuditActionService"; type AccessedData = { @@ -146,7 +152,9 @@ export async function verifyDataRequirementsContract( for (const uuid of accessedData.userUuids) { if (!declaredRequirements.userUuids?.includes(uuid)) { - errors.push(`Under-declaration: getUserByUuid("${uuid}") was called but not declared in getDataRequirements`); + errors.push( + `Under-declaration: getUserByUuid("${uuid}") was called but not declared in getDataRequirements` + ); } } @@ -158,7 +166,9 @@ export async function verifyDataRequirementsContract( for (const id of accessedData.attendeeIds) { if (!declaredRequirements.attendeeIds?.includes(id)) { - errors.push(`Under-declaration: getAttendeeById(${id}) was called but not declared in getDataRequirements`); + errors.push( + `Under-declaration: getAttendeeById(${id}) was called but not declared in getDataRequirements` + ); } } @@ -170,7 +180,9 @@ export async function verifyDataRequirementsContract( for (const id of accessedData.credentialIds) { if (!declaredRequirements.credentialIds?.includes(id)) { - errors.push(`Under-declaration: getCredentialById(${id}) was called but not declared in getDataRequirements`); + errors.push( + `Under-declaration: getCredentialById(${id}) was called but not declared in getDataRequirements` + ); } } diff --git a/packages/features/booking-audit/lib/common/changeSchemas.ts b/packages/features/booking-audit/lib/common/changeSchemas.ts index 55dece6107..ec1ac3024e 100644 --- a/packages/features/booking-audit/lib/common/changeSchemas.ts +++ b/packages/features/booking-audit/lib/common/changeSchemas.ts @@ -7,27 +7,26 @@ import { BookingStatus } from "@calcom/prisma/enums"; */ export const StringChangeSchema = z.object({ - old: z.string().nullable(), - new: z.string().nullable(), + old: z.string().nullable(), + new: z.string().nullable(), }); export const BooleanChangeSchema = z.object({ - old: z.boolean().nullable(), - new: z.boolean(), + old: z.boolean().nullable(), + new: z.boolean(), }); export const StringArrayChangeSchema = z.object({ - old: z.array(z.string()).nullable(), - new: z.array(z.string()), + old: z.array(z.string()).nullable(), + new: z.array(z.string()), }); export const NumberChangeSchema = z.object({ - old: z.number().nullable(), - new: z.number(), + old: z.number().nullable(), + new: z.number(), }); export const BookingStatusChangeSchema = z.object({ - old: z.nativeEnum(BookingStatus).nullable(), - new: z.nativeEnum(BookingStatus), + old: z.nativeEnum(BookingStatus).nullable(), + new: z.nativeEnum(BookingStatus), }); - diff --git a/packages/features/booking-audit/lib/dto/types.ts b/packages/features/booking-audit/lib/dto/types.ts index 794e57edc5..58d3bd71c9 100644 --- a/packages/features/booking-audit/lib/dto/types.ts +++ b/packages/features/booking-audit/lib/dto/types.ts @@ -1,50 +1,50 @@ import { z } from "zod"; const UserActorSchema = z.object({ - identifiedBy: z.literal("user"), - userUuid: z.string(), + identifiedBy: z.literal("user"), + userUuid: z.string(), }); const AttendeeActorSchema = z.object({ - identifiedBy: z.literal("attendee"), - attendeeId: z.number(), + identifiedBy: z.literal("attendee"), + attendeeId: z.number(), }); const ActorByIdSchema = z.object({ - identifiedBy: z.literal("id"), - id: z.string(), + identifiedBy: z.literal("id"), + id: z.string(), }); const GuestActorSchema = z.object({ - identifiedBy: z.literal("guest"), - email: z.string(), - name: z.string().nullable(), + identifiedBy: z.literal("guest"), + email: z.string(), + name: z.string().nullable(), }); const AppActorByCredentialIdSchema = z.object({ - identifiedBy: z.literal("app"), - credentialId: z.number(), + identifiedBy: z.literal("app"), + credentialId: z.number(), }); const AppActorBySlugSchema = z.object({ - identifiedBy: z.literal("appSlug"), - appSlug: z.string(), - name: z.string(), + identifiedBy: z.literal("appSlug"), + appSlug: z.string(), + name: z.string(), }); export const ActorSchema = z.discriminatedUnion("identifiedBy", [ - ActorByIdSchema, - UserActorSchema, - AttendeeActorSchema, - GuestActorSchema, - AppActorByCredentialIdSchema, - AppActorBySlugSchema, + ActorByIdSchema, + UserActorSchema, + AttendeeActorSchema, + GuestActorSchema, + AppActorByCredentialIdSchema, + AppActorBySlugSchema, ]); export const PiiFreeActorSchema = z.discriminatedUnion("identifiedBy", [ - ActorByIdSchema, - UserActorSchema, - AttendeeActorSchema, + ActorByIdSchema, + UserActorSchema, + AttendeeActorSchema, ]); export type Actor = z.infer; @@ -63,8 +63,7 @@ export type AppActorBySlug = z.infer; * This is separate from action-specific data because impersonation is orthogonal to the action type */ export const BookingAuditContextSchema = z.object({ - impersonatedBy: z.string().optional(), + impersonatedBy: z.string().optional(), }); export type BookingAuditContext = z.infer; - diff --git a/packages/features/booking-audit/lib/getAppNameFromSlug.ts b/packages/features/booking-audit/lib/getAppNameFromSlug.ts index d12efc9a61..db0d385cff 100644 --- a/packages/features/booking-audit/lib/getAppNameFromSlug.ts +++ b/packages/features/booking-audit/lib/getAppNameFromSlug.ts @@ -3,16 +3,16 @@ * Falls back to slug itself if not in map */ const APP_SLUG_TO_NAME: Record = { - stripe: "Stripe", - paypal: "Paypal", - alby: "Alby", - hitpay: "HitPay", - btcpayserver: "BTCPayServer", + stripe: "Stripe", + paypal: "Paypal", + alby: "Alby", + hitpay: "HitPay", + btcpayserver: "BTCPayServer", }; export function getAppNameFromSlug({ appSlug }: { appSlug: string | null }): string { - if (!appSlug) { - return "Unknown App"; - } - return APP_SLUG_TO_NAME[appSlug] ?? appSlug; + if (!appSlug) { + return "Unknown App"; + } + return APP_SLUG_TO_NAME[appSlug] ?? appSlug; } diff --git a/packages/features/booking-audit/lib/makeActor.ts b/packages/features/booking-audit/lib/makeActor.ts index 2c7e53305f..3d114cfeed 100644 --- a/packages/features/booking-audit/lib/makeActor.ts +++ b/packages/features/booking-audit/lib/makeActor.ts @@ -1,4 +1,11 @@ -import type { UserActor, GuestActor, AttendeeActor, ActorById, AppActorByCredentialId, AppActorBySlug } from "./dto/types"; +import type { + UserActor, + GuestActor, + AttendeeActor, + ActorById, + AppActorByCredentialId, + AppActorBySlug, +} from "./dto/types"; import { v4 as uuidv4 } from "uuid"; const SYSTEM_ACTOR_ID = "00000000-0000-0000-0000-000000000000"; @@ -7,18 +14,18 @@ const SYSTEM_ACTOR_ID = "00000000-0000-0000-0000-000000000000"; * Creates an Actor representing a User by UUID */ export function makeUserActor(userUuid: string): UserActor { - return { - identifiedBy: "user", - userUuid, - }; + return { + identifiedBy: "user", + userUuid, + }; } -export function makeGuestActor({ email, name }: { email: string, name: string | null }): GuestActor { - return { - identifiedBy: "guest", - email, - name: name ?? null, - }; +export function makeGuestActor({ email, name }: { email: string; name: string | null }): GuestActor { + return { + identifiedBy: "guest", + email, + name: name ?? null, + }; } /** @@ -26,31 +33,30 @@ export function makeGuestActor({ email, name }: { email: string, name: string | * System actors must be referenced by ID (requires migration) */ export function makeSystemActor(): ActorById { - return { - identifiedBy: "id", - id: SYSTEM_ACTOR_ID, - }; + return { + identifiedBy: "id", + id: SYSTEM_ACTOR_ID, + }; } - /** * Creates an Actor by existing actor ID */ export function makeActorById(id: string): ActorById { - return { - identifiedBy: "id", - id, - }; + return { + identifiedBy: "id", + id, + }; } /** * Creates an Actor representing an Attendee by attendee ID */ export function makeAttendeeActor(attendeeId: number): AttendeeActor { - return { - identifiedBy: "attendee", - attendeeId, - }; + return { + identifiedBy: "attendee", + attendeeId, + }; } /** @@ -59,10 +65,10 @@ export function makeAttendeeActor(attendeeId: number): AttendeeActor { * App name and slug are derived from the credential at display time */ export function makeAppActor(params: { credentialId: number }): AppActorByCredentialId { - return { - identifiedBy: "app", - credentialId: params.credentialId, - }; + return { + identifiedBy: "app", + credentialId: params.credentialId, + }; } /** @@ -71,20 +77,26 @@ export function makeAppActor(params: { credentialId: number }): AppActorByCreden * App actors use @app.internal email convention */ export function makeAppActorUsingSlug(params: { appSlug: string; name: string }): AppActorBySlug { - return { - identifiedBy: "appSlug", - appSlug: params.appSlug, - name: params.name, - }; + return { + identifiedBy: "appSlug", + appSlug: params.appSlug, + name: params.name, + }; } /** * identifier should be unique for that actor */ -export function buildActorEmail({ identifier, actorType }: { identifier: string, actorType: "system" | "guest" | "app" }): string { - return `${identifier}@${actorType}.internal`; +export function buildActorEmail({ + identifier, + actorType, +}: { + identifier: string; + actorType: "system" | "guest" | "app"; +}): string { + return `${identifier}@${actorType}.internal`; } export function getUniqueIdentifier({ prefix }: { prefix: string }): string { - return `${prefix}-${uuidv4()}`; -} \ No newline at end of file + return `${prefix}-${uuidv4()}`; +} diff --git a/packages/features/booking-audit/lib/repository/IAuditActorRepository.ts b/packages/features/booking-audit/lib/repository/IAuditActorRepository.ts index 76f40b0b2c..8f081b88e7 100644 --- a/packages/features/booking-audit/lib/repository/IAuditActorRepository.ts +++ b/packages/features/booking-audit/lib/repository/IAuditActorRepository.ts @@ -10,15 +10,17 @@ type AuditActor = { phone: string | null; name: string | null; createdAt: Date; -} +}; export interface IAuditActorRepository { findByUserUuid(userUuid: string): Promise; createIfNotExistsUserActor(params: { userUuid: string }): Promise; createIfNotExistsAttendeeActor(params: { attendeeId: number }): Promise; - createIfNotExistsGuestActor(params: { email: string | null; name: string | null; phone: string | null }): Promise; - createIfNotExistsAppActor(params: - | { credentialId: number } - | { email: string; name: string } + createIfNotExistsGuestActor(params: { + email: string | null; + name: string | null; + phone: string | null; + }): Promise; + createIfNotExistsAppActor( + params: { credentialId: number } | { email: string; name: string } ): Promise; } - diff --git a/packages/features/booking-audit/lib/repository/IBookingAuditRepository.ts b/packages/features/booking-audit/lib/repository/IBookingAuditRepository.ts index 38767bce36..a0cc734054 100644 --- a/packages/features/booking-audit/lib/repository/IBookingAuditRepository.ts +++ b/packages/features/booking-audit/lib/repository/IBookingAuditRepository.ts @@ -3,74 +3,89 @@ import type { AuditActorType } from "./IAuditActorRepository"; import type { ActionSource } from "../types/actionSource"; import type { BookingAuditContext } from "../dto/types"; -export type BookingAuditType = "RECORD_CREATED" | "RECORD_UPDATED" | "RECORD_DELETED" +export type BookingAuditType = "RECORD_CREATED" | "RECORD_UPDATED" | "RECORD_DELETED"; /** * Booking audit actions track changes to bookings throughout their lifecycle. - * + * * Note: PENDING and AWAITING_HOST represent initial booking states, not transitions. * They are reserved in the enum for potential future use but should not appear in audit logs. * Use the CREATED action to capture initial booking status instead. */ -export type BookingAuditAction = "CREATED" | "CANCELLED" | "ACCEPTED" | "REJECTED" | "PENDING" | "AWAITING_HOST" | "RESCHEDULED" | "ATTENDEE_ADDED" | "ATTENDEE_REMOVED" | "REASSIGNMENT" | "LOCATION_CHANGED" | "NO_SHOW_UPDATED" | "RESCHEDULE_REQUESTED" | "SEAT_BOOKED" | "SEAT_RESCHEDULED" +export type BookingAuditAction = + | "CREATED" + | "CANCELLED" + | "ACCEPTED" + | "REJECTED" + | "PENDING" + | "AWAITING_HOST" + | "RESCHEDULED" + | "ATTENDEE_ADDED" + | "ATTENDEE_REMOVED" + | "REASSIGNMENT" + | "LOCATION_CHANGED" + | "NO_SHOW_UPDATED" + | "RESCHEDULE_REQUESTED" + | "SEAT_BOOKED" + | "SEAT_RESCHEDULED"; export type BookingAuditCreateInput = { - bookingUid: string; - actorId: string; - action: BookingAuditAction; - data: JsonValue; - type: BookingAuditType; - timestamp: Date; - source: ActionSource; - operationId: string; - context?: BookingAuditContext; -} + bookingUid: string; + actorId: string; + action: BookingAuditAction; + data: JsonValue; + type: BookingAuditType; + timestamp: Date; + source: ActionSource; + operationId: string; + context?: BookingAuditContext; +}; type BookingAudit = { - id: string; - bookingUid: string; - actorId: string; - action: BookingAuditAction; - type: BookingAuditType; - timestamp: Date; - createdAt: Date; - updatedAt: Date; - data: JsonValue; - source: ActionSource; - operationId: string; -} + id: string; + bookingUid: string; + actorId: string; + action: BookingAuditAction; + type: BookingAuditType; + timestamp: Date; + createdAt: Date; + updatedAt: Date; + data: JsonValue; + source: ActionSource; + operationId: string; +}; export type BookingAuditWithActor = BookingAudit & { - context: BookingAuditContext | null; - actor: { - id: string; - type: AuditActorType; - userUuid: string | null; - attendeeId: number | null; - credentialId: number | null; - name: string | null; - createdAt: Date; - }; -} + context: BookingAuditContext | null; + actor: { + id: string; + type: AuditActorType; + userUuid: string | null; + attendeeId: number | null; + credentialId: number | null; + name: string | null; + createdAt: Date; + }; +}; export interface IBookingAuditRepository { - /** - * Creates a new booking audit record - */ - create(bookingAudit: BookingAuditCreateInput): Promise; + /** + * Creates a new booking audit record + */ + create(bookingAudit: BookingAuditCreateInput): Promise; - createMany(bookingAudits: BookingAuditCreateInput[]): Promise<{ count: number }>; + createMany(bookingAudits: BookingAuditCreateInput[]): Promise<{ count: number }>; - /** - * Retrieves all audit logs for a specific booking - * @param bookingUid - The unique identifier of the booking - * @returns Array of audit logs with actor information, ordered by timestamp DESC - */ - findAllForBooking(bookingUid: string): Promise; + /** + * Retrieves all audit logs for a specific booking + * @param bookingUid - The unique identifier of the booking + * @returns Array of audit logs with actor information, ordered by timestamp DESC + */ + findAllForBooking(bookingUid: string): Promise; - /** - * Retrieves all RESCHEDULED audit logs for a specific booking - * @param bookingUid - The unique identifier of the booking - * @returns Array of RESCHEDULED audit logs with actor information, ordered by timestamp DESC - */ - findRescheduledLogsOfBooking(bookingUid: string): Promise; + /** + * Retrieves all RESCHEDULED audit logs for a specific booking + * @param bookingUid - The unique identifier of the booking + * @returns Array of RESCHEDULED audit logs with actor information, ordered by timestamp DESC + */ + findRescheduledLogsOfBooking(bookingUid: string): Promise; } diff --git a/packages/features/booking-audit/lib/repository/PrismaAuditActorRepository.ts b/packages/features/booking-audit/lib/repository/PrismaAuditActorRepository.ts index f3b7fd3062..be4e7fc5ba 100644 --- a/packages/features/booking-audit/lib/repository/PrismaAuditActorRepository.ts +++ b/packages/features/booking-audit/lib/repository/PrismaAuditActorRepository.ts @@ -2,128 +2,127 @@ import type { PrismaClient } from "@calcom/prisma/client"; import type { IAuditActorRepository } from "./IAuditActorRepository"; type Dependencies = { - prismaClient: PrismaClient; -} + prismaClient: PrismaClient; +}; export class PrismaAuditActorRepository implements IAuditActorRepository { - constructor(private readonly deps: Dependencies) { } - async findByUserUuid(userUuid: string) { - return this.deps.prismaClient.auditActor.findUnique({ - where: { userUuid }, - }); + constructor(private readonly deps: Dependencies) {} + async findByUserUuid(userUuid: string) { + return this.deps.prismaClient.auditActor.findUnique({ + where: { userUuid }, + }); + } + + async createIfNotExistsUserActor(params: { userUuid: string }) { + return this.deps.prismaClient.auditActor.upsert({ + where: { userUuid: params.userUuid }, + create: { + type: "USER", + userUuid: params.userUuid, + }, + update: {}, + }); + } + + async createIfNotExistsGuestActor(params: { + email: string | null; + name: string | null; + phone: string | null; + }) { + const { email, name, phone } = params; + const normalizedEmail = email && email.trim() !== "" ? email : null; + const normalizedName = name && name.trim() !== "" ? name : null; + const normalizedPhone = phone && phone.trim() !== "" ? phone : null; + + // If all fields are null, we can't use upsert (no unique constraint), so just create a new record + if (!normalizedEmail && !normalizedPhone) { + return this.deps.prismaClient.auditActor.create({ + data: { + type: "GUEST", + email: null, + name: normalizedName, + phone: null, + }, + }); } - async createIfNotExistsUserActor(params: { userUuid: string }) { - return this.deps.prismaClient.auditActor.upsert({ - where: { userUuid: params.userUuid }, - create: { - type: "USER", - userUuid: params.userUuid, - }, - update: {}, + // First try to find by email if email exists + if (normalizedEmail) { + const existingByEmail = await this.deps.prismaClient.auditActor.findUnique({ + where: { email: normalizedEmail }, + }); + + if (existingByEmail) { + // Update existing record found by email + return this.deps.prismaClient.auditActor.update({ + where: { email: normalizedEmail }, + data: { + name: normalizedName ?? undefined, + phone: normalizedPhone ?? undefined, + }, }); + } } - async createIfNotExistsGuestActor(params: { email: string | null; name: string | null; phone: string | null }) { - const { email, name, phone } = params; - const normalizedEmail = email && email.trim() !== "" ? email : null; - const normalizedName = name && name.trim() !== "" ? name : null; - const normalizedPhone = phone && phone.trim() !== "" ? phone : null; + // If not found by email and phone exists, try to find by phone + if (normalizedPhone) { + const existingByPhone = await this.deps.prismaClient.auditActor.findUnique({ + where: { phone: normalizedPhone }, + }); - // If all fields are null, we can't use upsert (no unique constraint), so just create a new record - if (!normalizedEmail && !normalizedPhone) { - return this.deps.prismaClient.auditActor.create({ - data: { - type: "GUEST", - email: null, - name: normalizedName, - phone: null, - }, - }); - } - - // First try to find by email if email exists - if (normalizedEmail) { - const existingByEmail = await this.deps.prismaClient.auditActor.findUnique({ - where: { email: normalizedEmail }, - }); - - if (existingByEmail) { - // Update existing record found by email - return this.deps.prismaClient.auditActor.update({ - where: { email: normalizedEmail }, - data: { - name: normalizedName ?? undefined, - phone: normalizedPhone ?? undefined, - }, - }); - } - } - - // If not found by email and phone exists, try to find by phone - if (normalizedPhone) { - const existingByPhone = await this.deps.prismaClient.auditActor.findUnique({ - where: { phone: normalizedPhone }, - }); - - if (existingByPhone) { - // Update existing record found by phone - return this.deps.prismaClient.auditActor.update({ - where: { phone: normalizedPhone }, - data: { - email: normalizedEmail ?? undefined, - name: normalizedName ?? undefined, - }, - }); - } - } - - // Not found by either email or phone, create new record - return this.deps.prismaClient.auditActor.create({ - data: { - type: "GUEST", - email: normalizedEmail, - name: normalizedName, - phone: normalizedPhone, - }, + if (existingByPhone) { + // Update existing record found by phone + return this.deps.prismaClient.auditActor.update({ + where: { phone: normalizedPhone }, + data: { + email: normalizedEmail ?? undefined, + name: normalizedName ?? undefined, + }, }); + } } - async createIfNotExistsAttendeeActor(params: { attendeeId: number }) { - return this.deps.prismaClient.auditActor.upsert({ - where: { attendeeId: params.attendeeId }, - create: { - type: "ATTENDEE", - attendeeId: params.attendeeId, - }, - update: {}, - }); - } + // Not found by either email or phone, create new record + return this.deps.prismaClient.auditActor.create({ + data: { + type: "GUEST", + email: normalizedEmail, + name: normalizedName, + phone: normalizedPhone, + }, + }); + } - async createIfNotExistsAppActor(params: - | { credentialId: number } - | { email: string; name: string } - ) { - if ('credentialId' in params) { - return this.deps.prismaClient.auditActor.upsert({ - where: { credentialId: params.credentialId }, - create: { - type: "APP", - credentialId: params.credentialId, - }, - update: {}, - }); - } + async createIfNotExistsAttendeeActor(params: { attendeeId: number }) { + return this.deps.prismaClient.auditActor.upsert({ + where: { attendeeId: params.attendeeId }, + create: { + type: "ATTENDEE", + attendeeId: params.attendeeId, + }, + update: {}, + }); + } - return this.deps.prismaClient.auditActor.upsert({ - where: { email: params.email }, - create: { - type: "APP", - email: params.email, - name: params.name, - }, - update: {}, - }); + async createIfNotExistsAppActor(params: { credentialId: number } | { email: string; name: string }) { + if ("credentialId" in params) { + return this.deps.prismaClient.auditActor.upsert({ + where: { credentialId: params.credentialId }, + create: { + type: "APP", + credentialId: params.credentialId, + }, + update: {}, + }); } + return this.deps.prismaClient.auditActor.upsert({ + where: { email: params.email }, + create: { + type: "APP", + email: params.email, + name: params.name, + }, + update: {}, + }); + } } - diff --git a/packages/features/booking-audit/lib/repository/PrismaBookingAuditRepository.ts b/packages/features/booking-audit/lib/repository/PrismaBookingAuditRepository.ts index f037c756af..48bd14891e 100644 --- a/packages/features/booking-audit/lib/repository/PrismaBookingAuditRepository.ts +++ b/packages/features/booking-audit/lib/repository/PrismaBookingAuditRepository.ts @@ -1,122 +1,125 @@ import type { PrismaClient } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; -import type { IBookingAuditRepository, BookingAuditCreateInput, BookingAuditWithActor } from "./IBookingAuditRepository"; +import type { + IBookingAuditRepository, + BookingAuditCreateInput, + BookingAuditWithActor, +} from "./IBookingAuditRepository"; import { BookingAuditContextSchema } from "../dto/types"; type Dependencies = { - prismaClient: PrismaClient; -} + prismaClient: PrismaClient; +}; /** * Safe actor fields to expose in audit logs * Excludes PII fields like email and phone that aren't needed for display */ const safeActorSelect = { - id: true, - type: true, - userUuid: true, - attendeeId: true, - credentialId: true, - name: true, - createdAt: true, + id: true, + type: true, + userUuid: true, + attendeeId: true, + credentialId: true, + name: true, + createdAt: true, } as const; const safeBookingAuditSelect = { - id: true, - bookingUid: true, - actorId: true, - action: true, - type: true, - timestamp: true, - source: true, - operationId: true, - data: true, - context: true, - createdAt: true, - updatedAt: true, + id: true, + bookingUid: true, + actorId: true, + action: true, + type: true, + timestamp: true, + source: true, + operationId: true, + data: true, + context: true, + createdAt: true, + updatedAt: true, } as const; export class PrismaBookingAuditRepository implements IBookingAuditRepository { - constructor(private readonly deps: Dependencies) { } + constructor(private readonly deps: Dependencies) {} - private parsed(auditLog: T) { - return { - ...auditLog, - context: auditLog.context ? BookingAuditContextSchema.parse(auditLog.context) : null, - }; - } + private parsed(auditLog: T) { + return { + ...auditLog, + context: auditLog.context ? BookingAuditContextSchema.parse(auditLog.context) : null, + }; + } - async create(bookingAudit: BookingAuditCreateInput) { - const created = await this.deps.prismaClient.bookingAudit.create({ - data: { - bookingUid: bookingAudit.bookingUid, - actorId: bookingAudit.actorId, - action: bookingAudit.action, - type: bookingAudit.type, - timestamp: bookingAudit.timestamp, - source: bookingAudit.source, - operationId: bookingAudit.operationId, - data: bookingAudit.data === null ? undefined : bookingAudit.data, - context: bookingAudit.context ?? undefined, - }, - }); + async create(bookingAudit: BookingAuditCreateInput) { + const created = await this.deps.prismaClient.bookingAudit.create({ + data: { + bookingUid: bookingAudit.bookingUid, + actorId: bookingAudit.actorId, + action: bookingAudit.action, + type: bookingAudit.type, + timestamp: bookingAudit.timestamp, + source: bookingAudit.source, + operationId: bookingAudit.operationId, + data: bookingAudit.data === null ? undefined : bookingAudit.data, + context: bookingAudit.context ?? undefined, + }, + }); - return this.parsed(created); - } + return this.parsed(created); + } - async createMany(bookingAudits: BookingAuditCreateInput[]) { - const result = await this.deps.prismaClient.bookingAudit.createMany({ - data: bookingAudits.map((bookingAudit) => ({ - bookingUid: bookingAudit.bookingUid, - actorId: bookingAudit.actorId, - action: bookingAudit.action, - type: bookingAudit.type, - timestamp: bookingAudit.timestamp, - source: bookingAudit.source, - operationId: bookingAudit.operationId, - data: bookingAudit.data === null ? undefined : bookingAudit.data, - context: bookingAudit.context === undefined ? undefined : bookingAudit.context, - })), - }); - return { count: result.count }; - } + async createMany(bookingAudits: BookingAuditCreateInput[]) { + const result = await this.deps.prismaClient.bookingAudit.createMany({ + data: bookingAudits.map((bookingAudit) => ({ + bookingUid: bookingAudit.bookingUid, + actorId: bookingAudit.actorId, + action: bookingAudit.action, + type: bookingAudit.type, + timestamp: bookingAudit.timestamp, + source: bookingAudit.source, + operationId: bookingAudit.operationId, + data: bookingAudit.data === null ? undefined : bookingAudit.data, + context: bookingAudit.context === undefined ? undefined : bookingAudit.context, + })), + }); + return { count: result.count }; + } - async findAllForBooking(bookingUid: string): Promise { - const results = await this.deps.prismaClient.bookingAudit.findMany({ - where: { - bookingUid, - }, - select: { - ...safeBookingAuditSelect, - actor: { - select: safeActorSelect, - }, - }, - orderBy: { - timestamp: "desc", - }, - }); + async findAllForBooking(bookingUid: string): Promise { + const results = await this.deps.prismaClient.bookingAudit.findMany({ + where: { + bookingUid, + }, + select: { + ...safeBookingAuditSelect, + actor: { + select: safeActorSelect, + }, + }, + orderBy: { + timestamp: "desc", + }, + }); - return results.map(this.parsed); - } + return results.map(this.parsed); + } - async findRescheduledLogsOfBooking(bookingUid: string): Promise { - const results = await this.deps.prismaClient.bookingAudit.findMany({ - where: { - bookingUid, - action: "RESCHEDULED", - }, - select: { - ...safeBookingAuditSelect, - actor: { - select: safeActorSelect - }, - }, - orderBy: { timestamp: "desc" }, - }); + async findRescheduledLogsOfBooking(bookingUid: string): Promise { + const results = await this.deps.prismaClient.bookingAudit.findMany({ + where: { + bookingUid, + action: "RESCHEDULED", + }, + select: { + ...safeBookingAuditSelect, + actor: { + select: safeActorSelect, + }, + }, + orderBy: { timestamp: "desc" }, + }); - return results.map(this.parsed); - } + return results.map(this.parsed); + } } - diff --git a/packages/features/booking-audit/lib/service/ActorStrategies.ts b/packages/features/booking-audit/lib/service/ActorStrategies.ts index c2b7a00092..08fa6c20dc 100644 --- a/packages/features/booking-audit/lib/service/ActorStrategies.ts +++ b/packages/features/booking-audit/lib/service/ActorStrategies.ts @@ -64,7 +64,9 @@ export const ACTOR_STRATEGIES: Record = { enrich: (actor, dbStore) => { const credential = actor.credentialId ? dbStore.getCredentialById(actor.credentialId) : null; return { - displayName: credential ? getAppNameFromSlug({ appSlug: credential.appId }) : (actor.name ?? "Deleted App"), + displayName: credential + ? getAppNameFromSlug({ appSlug: credential.appId }) + : (actor.name ?? "Deleted App"), displayEmail: null, displayAvatar: null, }; diff --git a/packages/features/booking-audit/lib/service/BookingAuditAccessService.ts b/packages/features/booking-audit/lib/service/BookingAuditAccessService.ts index c5c05af498..e8bc54e3d7 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditAccessService.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditAccessService.ts @@ -4,23 +4,23 @@ import { PermissionCheckService } from "@calcom/features/pbac/services/permissio import { MembershipRole } from "@calcom/prisma/enums"; export enum BookingAuditErrorCode { - ORGANIZATION_ID_REQUIRED = "ORGANIZATION_ID_REQUIRED", - BOOKING_NOT_FOUND_OR_PERMISSION_DENIED = "BOOKING_NOT_FOUND_OR_PERMISSION_DENIED", - BOOKING_HAS_NO_OWNER = "BOOKING_HAS_NO_OWNER", - OWNER_NOT_IN_ORGANIZATION = "OWNER_NOT_IN_ORGANIZATION", - PERMISSION_DENIED = "PERMISSION_DENIED", + ORGANIZATION_ID_REQUIRED = "ORGANIZATION_ID_REQUIRED", + BOOKING_NOT_FOUND_OR_PERMISSION_DENIED = "BOOKING_NOT_FOUND_OR_PERMISSION_DENIED", + BOOKING_HAS_NO_OWNER = "BOOKING_HAS_NO_OWNER", + OWNER_NOT_IN_ORGANIZATION = "OWNER_NOT_IN_ORGANIZATION", + PERMISSION_DENIED = "PERMISSION_DENIED", } export class BookingAuditPermissionError extends Error { - constructor(public readonly code: BookingAuditErrorCode) { - super(code); - this.name = "BookingAuditPermissionError"; - } + constructor(public readonly code: BookingAuditErrorCode) { + super(code); + this.name = "BookingAuditPermissionError"; + } } interface BookingAuditAccessServiceDeps { - bookingRepository: BookingRepository; - membershipRepository: MembershipRepository; + bookingRepository: BookingRepository; + membershipRepository: MembershipRepository; } /** @@ -29,67 +29,78 @@ interface BookingAuditAccessServiceDeps { * Regular users (including booking organizers and hosts) cannot view audit logs. */ export class BookingAuditAccessService { - private readonly bookingRepository: BookingRepository; - private readonly membershipRepository: MembershipRepository; - private readonly permissionCheckService: PermissionCheckService; + private readonly bookingRepository: BookingRepository; + private readonly membershipRepository: MembershipRepository; + private readonly permissionCheckService: PermissionCheckService; - constructor(deps: BookingAuditAccessServiceDeps) { - this.bookingRepository = deps.bookingRepository; - this.membershipRepository = deps.membershipRepository; - this.permissionCheckService = new PermissionCheckService(); + constructor(deps: BookingAuditAccessServiceDeps) { + this.bookingRepository = deps.bookingRepository; + this.membershipRepository = deps.membershipRepository; + this.permissionCheckService = new PermissionCheckService(); + } + + /** + * Check if user has permission to view audit logs for a booking + * Throws BookingAuditPermissionError if access is denied + */ + async assertPermissions({ + bookingUid, + userId, + organizationId, + }: { + bookingUid: string; + userId: number; + organizationId: number | null; + }): Promise { + if (!organizationId) { + throw new BookingAuditPermissionError(BookingAuditErrorCode.ORGANIZATION_ID_REQUIRED); } - /** - * Check if user has permission to view audit logs for a booking - * Throws BookingAuditPermissionError if access is denied - */ - async assertPermissions({ bookingUid, userId, organizationId }: { bookingUid: string, userId: number, organizationId: number | null }): Promise { - if (!organizationId) { - throw new BookingAuditPermissionError(BookingAuditErrorCode.ORGANIZATION_ID_REQUIRED); - } - - const booking = await this.bookingRepository.findByUidIncludeEventType({ bookingUid }); - if (!booking) { - throw new BookingAuditPermissionError(BookingAuditErrorCode.BOOKING_NOT_FOUND_OR_PERMISSION_DENIED); - } - - const bookingEventType = booking.eventType; - const bookingEventTypeTeamId = bookingEventType?.teamId ?? bookingEventType?.parent?.teamId; - - if (bookingEventTypeTeamId) { - const hasAccess = await this.permissionCheckService.checkPermission({ - userId, - teamId: bookingEventTypeTeamId, - permission: "booking.readTeamAuditLogs", - fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], - }); - if (hasAccess) { - return; - } - } - - const bookingOwnerId = booking.userId; - - if (!bookingOwnerId) { - throw new BookingAuditPermissionError(BookingAuditErrorCode.BOOKING_HAS_NO_OWNER); - } - - const isBookingOwnerMemberOfOrganization = await this.membershipRepository.hasMembership({ userId: bookingOwnerId, teamId: organizationId }); - - if (!isBookingOwnerMemberOfOrganization) { - throw new BookingAuditPermissionError(BookingAuditErrorCode.OWNER_NOT_IN_ORGANIZATION); - } - - const hasAccess = await this.permissionCheckService.checkPermission({ - userId, - teamId: organizationId, - permission: "booking.readOrgAuditLogs", - fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], - }); - - if (hasAccess) { - return; - } - throw new BookingAuditPermissionError(BookingAuditErrorCode.PERMISSION_DENIED); + const booking = await this.bookingRepository.findByUidIncludeEventType({ bookingUid }); + if (!booking) { + throw new BookingAuditPermissionError(BookingAuditErrorCode.BOOKING_NOT_FOUND_OR_PERMISSION_DENIED); } + + const bookingEventType = booking.eventType; + const bookingEventTypeTeamId = bookingEventType?.teamId ?? bookingEventType?.parent?.teamId; + + if (bookingEventTypeTeamId) { + const hasAccess = await this.permissionCheckService.checkPermission({ + userId, + teamId: bookingEventTypeTeamId, + permission: "booking.readTeamAuditLogs", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + if (hasAccess) { + return; + } + } + + const bookingOwnerId = booking.userId; + + if (!bookingOwnerId) { + throw new BookingAuditPermissionError(BookingAuditErrorCode.BOOKING_HAS_NO_OWNER); + } + + const isBookingOwnerMemberOfOrganization = await this.membershipRepository.hasMembership({ + userId: bookingOwnerId, + teamId: organizationId, + }); + + if (!isBookingOwnerMemberOfOrganization) { + throw new BookingAuditPermissionError(BookingAuditErrorCode.OWNER_NOT_IN_ORGANIZATION); + } + + const hasAccess = await this.permissionCheckService.checkPermission({ + userId, + teamId: organizationId, + permission: "booking.readOrgAuditLogs", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (hasAccess) { + return; + } + throw new BookingAuditPermissionError(BookingAuditErrorCode.PERMISSION_DENIED); + } } diff --git a/packages/features/booking-audit/lib/service/BookingAuditActionServiceRegistry.ts b/packages/features/booking-audit/lib/service/BookingAuditActionServiceRegistry.ts index 0048438f52..6a8e0ba0b1 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditActionServiceRegistry.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditActionServiceRegistry.ts @@ -3,78 +3,105 @@ import type { BookingAuditAction } from "../repository/IBookingAuditRepository"; import { CreatedAuditActionService, type CreatedAuditData } from "../actions/CreatedAuditActionService"; import { CancelledAuditActionService, type CancelledAuditData } from "../actions/CancelledAuditActionService"; -import { RescheduledAuditActionService, type RescheduledAuditData } from "../actions/RescheduledAuditActionService"; +import { + RescheduledAuditActionService, + type RescheduledAuditData, +} from "../actions/RescheduledAuditActionService"; import { AcceptedAuditActionService, type AcceptedAuditData } from "../actions/AcceptedAuditActionService"; -import { RescheduleRequestedAuditActionService, type RescheduleRequestedAuditData } from "../actions/RescheduleRequestedAuditActionService"; -import { AttendeeAddedAuditActionService, type AttendeeAddedAuditData } from "../actions/AttendeeAddedAuditActionService"; -import { NoShowUpdatedAuditActionService, type NoShowUpdatedAuditData } from "../actions/NoShowUpdatedAuditActionService"; +import { + RescheduleRequestedAuditActionService, + type RescheduleRequestedAuditData, +} from "../actions/RescheduleRequestedAuditActionService"; +import { + AttendeeAddedAuditActionService, + type AttendeeAddedAuditData, +} from "../actions/AttendeeAddedAuditActionService"; +import { + NoShowUpdatedAuditActionService, + type NoShowUpdatedAuditData, +} from "../actions/NoShowUpdatedAuditActionService"; import { RejectedAuditActionService, type RejectedAuditData } from "../actions/RejectedAuditActionService"; -import { AttendeeRemovedAuditActionService, type AttendeeRemovedAuditData } from "../actions/AttendeeRemovedAuditActionService"; -import { ReassignmentAuditActionService, type ReassignmentAuditData } from "../actions/ReassignmentAuditActionService"; -import { LocationChangedAuditActionService, type LocationChangedAuditData } from "../actions/LocationChangedAuditActionService"; -import { SeatBookedAuditActionService, type SeatBookedAuditData } from "../actions/SeatBookedAuditActionService"; -import { SeatRescheduledAuditActionService, type SeatRescheduledAuditData } from "../actions/SeatRescheduledAuditActionService"; +import { + AttendeeRemovedAuditActionService, + type AttendeeRemovedAuditData, +} from "../actions/AttendeeRemovedAuditActionService"; +import { + ReassignmentAuditActionService, + type ReassignmentAuditData, +} from "../actions/ReassignmentAuditActionService"; +import { + LocationChangedAuditActionService, + type LocationChangedAuditData, +} from "../actions/LocationChangedAuditActionService"; +import { + SeatBookedAuditActionService, + type SeatBookedAuditData, +} from "../actions/SeatBookedAuditActionService"; +import { + SeatRescheduledAuditActionService, + type SeatRescheduledAuditData, +} from "../actions/SeatRescheduledAuditActionService"; /** * Union type for all audit action data types * Used for type-safe handling of action-specific data */ export type AuditActionData = - | CreatedAuditData - | CancelledAuditData - | RescheduledAuditData - | AcceptedAuditData - | RescheduleRequestedAuditData - | AttendeeAddedAuditData - | NoShowUpdatedAuditData - | RejectedAuditData - | AttendeeRemovedAuditData - | ReassignmentAuditData - | LocationChangedAuditData - | SeatBookedAuditData - | SeatRescheduledAuditData; + | CreatedAuditData + | CancelledAuditData + | RescheduledAuditData + | AcceptedAuditData + | RescheduleRequestedAuditData + | AttendeeAddedAuditData + | NoShowUpdatedAuditData + | RejectedAuditData + | AttendeeRemovedAuditData + | ReassignmentAuditData + | LocationChangedAuditData + | SeatBookedAuditData + | SeatRescheduledAuditData; /** * BookingAuditActionServiceRegistry - * + * * Centralized registry for all booking audit action services. * Provides a single source of truth for action service mapping and eliminates * code duplication between consumer and viewer services. */ export class BookingAuditActionServiceRegistry { - private readonly actionServices: Map; + private readonly actionServices: Map; - constructor() { - const services: Array<[BookingAuditAction, IAuditActionService]> = [ - ["CREATED", new CreatedAuditActionService()], - ["CANCELLED", new CancelledAuditActionService()], - ["RESCHEDULED", new RescheduledAuditActionService()], - ["ACCEPTED", new AcceptedAuditActionService()], - ["RESCHEDULE_REQUESTED", new RescheduleRequestedAuditActionService()], - ["ATTENDEE_ADDED", new AttendeeAddedAuditActionService()], - ["NO_SHOW_UPDATED", new NoShowUpdatedAuditActionService()], - ["REJECTED", new RejectedAuditActionService()], - ["ATTENDEE_REMOVED", new AttendeeRemovedAuditActionService()], - ["REASSIGNMENT", new ReassignmentAuditActionService()], - ["LOCATION_CHANGED", new LocationChangedAuditActionService()], - ["SEAT_BOOKED", new SeatBookedAuditActionService()], - ["SEAT_RESCHEDULED", new SeatRescheduledAuditActionService()], - ]; - this.actionServices = new Map(services); - } + constructor() { + const services: Array<[BookingAuditAction, IAuditActionService]> = [ + ["CREATED", new CreatedAuditActionService()], + ["CANCELLED", new CancelledAuditActionService()], + ["RESCHEDULED", new RescheduledAuditActionService()], + ["ACCEPTED", new AcceptedAuditActionService()], + ["RESCHEDULE_REQUESTED", new RescheduleRequestedAuditActionService()], + ["ATTENDEE_ADDED", new AttendeeAddedAuditActionService()], + ["NO_SHOW_UPDATED", new NoShowUpdatedAuditActionService()], + ["REJECTED", new RejectedAuditActionService()], + ["ATTENDEE_REMOVED", new AttendeeRemovedAuditActionService()], + ["REASSIGNMENT", new ReassignmentAuditActionService()], + ["LOCATION_CHANGED", new LocationChangedAuditActionService()], + ["SEAT_BOOKED", new SeatBookedAuditActionService()], + ["SEAT_RESCHEDULED", new SeatRescheduledAuditActionService()], + ]; + this.actionServices = new Map(services); + } - /** - * Get Action Service - Returns the appropriate action service for the given action type - * - * @param action - The booking audit action type - * @returns The corresponding action service instance with proper typing - * @throws Error if no service is found for the action - */ - getActionService(action: BookingAuditAction): IAuditActionService { - const service = this.actionServices.get(action); - if (!service) { - throw new Error(`No action service found for: ${action}`); - } - return service; + /** + * Get Action Service - Returns the appropriate action service for the given action type + * + * @param action - The booking audit action type + * @returns The corresponding action service instance with proper typing + * @throws Error if no service is found for the action + */ + getActionService(action: BookingAuditAction): IAuditActionService { + const service = this.actionServices.get(action); + if (!service) { + throw new Error(`No action service found for: ${action}`); } + return service; + } } diff --git a/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts b/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts index 669c362dac..54d29b5bb8 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditTaskerProducerService.ts @@ -25,460 +25,459 @@ import { SeatRescheduledAuditActionService } from "../actions/SeatRescheduledAud import type { BookingAuditProducerService } from "./BookingAuditProducerService.interface"; interface BookingAuditTaskerProducerServiceDeps { - tasker: Tasker; - log: ISimpleLogger; - auditActorRepository: IAuditActorRepository; + tasker: Tasker; + log: ISimpleLogger; + auditActorRepository: IAuditActorRepository; } /** * BookingAuditTaskerProducerService - Tasker-based implementation of BookingAuditProducerService - * + * * Producer that uses Tasker for local/background job processing. * Task processing is handled by BookingAuditTaskConsumer. - * + * * For future migration to trigger.dev, create BookingAuditTriggerProducerService * that implements the same BookingAuditProducerService interface. */ export class BookingAuditTaskerProducerService implements BookingAuditProducerService { - private readonly tasker: Tasker; - private readonly log: BookingAuditTaskerProducerServiceDeps["log"]; - private readonly auditActorRepository: IAuditActorRepository; + private readonly tasker: Tasker; + private readonly log: BookingAuditTaskerProducerServiceDeps["log"]; + private readonly auditActorRepository: IAuditActorRepository; - constructor(private readonly deps: BookingAuditTaskerProducerServiceDeps) { - this.tasker = deps.tasker; - this.log = deps.log; - this.auditActorRepository = deps.auditActorRepository; + constructor(private readonly deps: BookingAuditTaskerProducerServiceDeps) { + this.tasker = deps.tasker; + this.log = deps.log; + this.auditActorRepository = deps.auditActorRepository; + } + + private async getPIIFreeBookingAuditActor(params: { actor: Actor }): Promise { + const { actor } = params; + + if (actor.identifiedBy === "user" || actor.identifiedBy === "attendee" || actor.identifiedBy === "id") { + return actor; } - private async getPIIFreeBookingAuditActor(params: { - actor: Actor; - }): Promise { - const { actor } = params; - - if (actor.identifiedBy === "user" || actor.identifiedBy === "attendee" || actor.identifiedBy === "id") { - return actor; - } - - if (actor.identifiedBy === "app") { - const piiFreeActor = await this.auditActorRepository.createIfNotExistsAppActor({ - credentialId: actor.credentialId, - }); - return makeActorById(piiFreeActor.id); - } - - if (actor.identifiedBy === "appSlug") { - const email = buildActorEmail({ identifier: actor.appSlug, actorType: "app" }); - const piiFreeActor = await this.auditActorRepository.createIfNotExistsAppActor({ - email, - name: actor.name, - }); - return makeActorById(piiFreeActor.id); - } - - // Must be guest actor at this point - const piiFreeActor = await this.auditActorRepository.createIfNotExistsGuestActor({ - email: actor.email, - name: actor.name ?? null, - phone: null, - }); - return makeActorById(piiFreeActor.id); + if (actor.identifiedBy === "app") { + const piiFreeActor = await this.auditActorRepository.createIfNotExistsAppActor({ + credentialId: actor.credentialId, + }); + return makeActorById(piiFreeActor.id); } - /** - * Internal helper to queue audit task to Tasker - * @param params.action - Must be a valid BookingAuditAction value (TYPE from action services are string-typed) - * @param params.operationId - Optional operation ID for correlating bulk operations. If null, will be auto-generated. - * @param params.isBookingAuditEnabled - Flag indicating if booking audit is enabled for the organization. - * When false, skips queueing. - */ - private async queueTask(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - action: string; - source: ActionSource; - operationId?: string | null; - data: unknown; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - // Skip queueing for non-organization bookings - if (params.organizationId === null) { - return; - } - // Skip queueing if booking audit is disabled for this organization - if (!params.isBookingAuditEnabled) { - this.log.debug( - `Skipping ${params.action} audit: booking-audit feature is disabled for organization`, - { organizationId: params.organizationId, bookingUid: params.bookingUid, action: params.action } - ); - return; - } - try { - const piiFreeActor = await this.getPIIFreeBookingAuditActor({ - actor: params.actor, - }); - - const operationId = params.operationId ?? uuidv4(); - - await this.tasker.create("bookingAudit", { - isBulk: false, - bookingUid: params.bookingUid, - actor: piiFreeActor, - organizationId: params.organizationId, - timestamp: Date.now(), - action: params.action as BookingAuditAction, - source: params.source, - operationId, - data: params.data, - context: params.context, - }); - } catch (error) { - this.log.error(`Error while queueing ${params.action} audit`, safeStringify(error)); - } + if (actor.identifiedBy === "appSlug") { + const email = buildActorEmail({ identifier: actor.appSlug, actorType: "app" }); + const piiFreeActor = await this.auditActorRepository.createIfNotExistsAppActor({ + email, + name: actor.name, + }); + return makeActorById(piiFreeActor.id); } - async queueCreatedAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: CreatedAuditActionService.TYPE, - }); + // Must be guest actor at this point + const piiFreeActor = await this.auditActorRepository.createIfNotExistsGuestActor({ + email: actor.email, + name: actor.name ?? null, + phone: null, + }); + return makeActorById(piiFreeActor.id); + } + + /** + * Internal helper to queue audit task to Tasker + * @param params.action - Must be a valid BookingAuditAction value (TYPE from action services are string-typed) + * @param params.operationId - Optional operation ID for correlating bulk operations. If null, will be auto-generated. + * @param params.isBookingAuditEnabled - Flag indicating if booking audit is enabled for the organization. + * When false, skips queueing. + */ + private async queueTask(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + action: string; + source: ActionSource; + operationId?: string | null; + data: unknown; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + // Skip queueing for non-organization bookings + if (params.organizationId === null) { + return; } - - async queueRescheduledAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: RescheduledAuditActionService.TYPE, - }); + // Skip queueing if booking audit is disabled for this organization + if (!params.isBookingAuditEnabled) { + this.log.debug(`Skipping ${params.action} audit: booking-audit feature is disabled for organization`, { + organizationId: params.organizationId, + bookingUid: params.bookingUid, + action: params.action, + }); + return; } + try { + const piiFreeActor = await this.getPIIFreeBookingAuditActor({ + actor: params.actor, + }); - async queueAcceptedAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: AcceptedAuditActionService.TYPE, - }); + const operationId = params.operationId ?? uuidv4(); + + await this.tasker.create("bookingAudit", { + isBulk: false, + bookingUid: params.bookingUid, + actor: piiFreeActor, + organizationId: params.organizationId, + timestamp: Date.now(), + action: params.action as BookingAuditAction, + source: params.source, + operationId, + data: params.data, + context: params.context, + }); + } catch (error) { + this.log.error(`Error while queueing ${params.action} audit`, safeStringify(error)); } + } - async queueCancelledAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: CancelledAuditActionService.TYPE, - }); + async queueCreatedAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: CreatedAuditActionService.TYPE, + }); + } + + async queueRescheduledAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: RescheduledAuditActionService.TYPE, + }); + } + + async queueAcceptedAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: AcceptedAuditActionService.TYPE, + }); + } + + async queueCancelledAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: CancelledAuditActionService.TYPE, + }); + } + + async queueRescheduleRequestedAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: RescheduleRequestedAuditActionService.TYPE, + }); + } + + async queueAttendeeAddedAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: AttendeeAddedAuditActionService.TYPE, + }); + } + + async queueNoShowUpdatedAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: NoShowUpdatedAuditActionService.TYPE, + }); + } + + async queueRejectedAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: RejectedAuditActionService.TYPE, + }); + } + + async queueAttendeeRemovedAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: AttendeeRemovedAuditActionService.TYPE, + }); + } + + async queueReassignmentAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: ReassignmentAuditActionService.TYPE, + }); + } + + async queueLocationChangedAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: LocationChangedAuditActionService.TYPE, + }); + } + + async queueSeatBookedAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: SeatBookedAuditActionService.TYPE, + }); + } + + async queueSeatRescheduledAudit(params: { + bookingUid: string; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + data: z.infer; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueTask({ + ...params, + action: SeatRescheduledAuditActionService.TYPE, + }); + } + + private async queueBulkTask(params: { + bookings: Array<{ + bookingUid: string; + data: unknown; + }>; + actor: Actor; + organizationId: number | null; + action: string; + source: ActionSource; + operationId?: string | null; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + // Skip queueing for non-organization bookings + if (params.organizationId === null) { + return; } - - async queueRescheduleRequestedAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: RescheduleRequestedAuditActionService.TYPE, - }); + // Skip queueing if booking audit is disabled for this organization + if (!params.isBookingAuditEnabled) { + this.log.debug( + `Skipping bulk ${params.action} audit: booking-audit feature is disabled for organization`, + { organizationId: params.organizationId, bookingCount: params.bookings.length, action: params.action } + ); + return; } + try { + const piiFreeActor = await this.getPIIFreeBookingAuditActor({ + actor: params.actor, + }); - async queueAttendeeAddedAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: AttendeeAddedAuditActionService.TYPE, - }); + const operationId = params.operationId ?? uuidv4(); + + await this.tasker.create("bookingAudit", { + isBulk: true, + bookings: params.bookings, + actor: piiFreeActor, + organizationId: params.organizationId, + timestamp: Date.now(), + action: params.action as BookingAuditAction, + source: params.source, + operationId, + context: params.context, + }); + } catch (error) { + this.log.error(`Error while queueing bulk ${params.action} audit`, safeStringify(error)); } + } - async queueNoShowUpdatedAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: NoShowUpdatedAuditActionService.TYPE, - }); - } + async queueBulkAcceptedAudit(params: { + bookings: Array<{ + bookingUid: string; + data: z.infer; + }>; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueBulkTask({ + ...params, + action: AcceptedAuditActionService.TYPE, + }); + } - async queueRejectedAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: RejectedAuditActionService.TYPE, - }); - } + async queueBulkCancelledAudit(params: { + bookings: Array<{ + bookingUid: string; + data: z.infer; + }>; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueBulkTask({ + ...params, + action: CancelledAuditActionService.TYPE, + }); + } - async queueAttendeeRemovedAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: AttendeeRemovedAuditActionService.TYPE, - }); - } + async queueBulkCreatedAudit(params: { + bookings: Array<{ + bookingUid: string; + data: z.infer; + }>; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueBulkTask({ + ...params, + action: CreatedAuditActionService.TYPE, + }); + } - async queueReassignmentAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: ReassignmentAuditActionService.TYPE, - }); - } + async queueBulkRescheduledAudit(params: { + bookings: Array<{ + bookingUid: string; + data: z.infer; + }>; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueBulkTask({ + ...params, + action: RescheduledAuditActionService.TYPE, + }); + } - async queueLocationChangedAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: LocationChangedAuditActionService.TYPE, - }); - } - - async queueSeatBookedAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: SeatBookedAuditActionService.TYPE, - }); - } - - async queueSeatRescheduledAudit(params: { - bookingUid: string; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - data: z.infer; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueTask({ - ...params, - action: SeatRescheduledAuditActionService.TYPE, - }); - } - - private async queueBulkTask(params: { - bookings: Array<{ - bookingUid: string; - data: unknown; - }>; - actor: Actor; - organizationId: number | null; - action: string; - source: ActionSource; - operationId?: string | null; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - // Skip queueing for non-organization bookings - if (params.organizationId === null) { - return; - } - // Skip queueing if booking audit is disabled for this organization - if (!params.isBookingAuditEnabled) { - this.log.debug( - `Skipping bulk ${params.action} audit: booking-audit feature is disabled for organization`, - { organizationId: params.organizationId, bookingCount: params.bookings.length, action: params.action } - ); - return; - } - try { - const piiFreeActor = await this.getPIIFreeBookingAuditActor({ - actor: params.actor, - }); - - const operationId = params.operationId ?? uuidv4(); - - await this.tasker.create("bookingAudit", { - isBulk: true, - bookings: params.bookings, - actor: piiFreeActor, - organizationId: params.organizationId, - timestamp: Date.now(), - action: params.action as BookingAuditAction, - source: params.source, - operationId, - context: params.context, - }); - } catch (error) { - this.log.error(`Error while queueing bulk ${params.action} audit`, safeStringify(error)); - } - } - - async queueBulkAcceptedAudit(params: { - bookings: Array<{ - bookingUid: string; - data: z.infer; - }>; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueBulkTask({ - ...params, - action: AcceptedAuditActionService.TYPE, - }); - } - - async queueBulkCancelledAudit(params: { - bookings: Array<{ - bookingUid: string; - data: z.infer; - }>; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueBulkTask({ - ...params, - action: CancelledAuditActionService.TYPE, - }); - } - - async queueBulkCreatedAudit(params: { - bookings: Array<{ - bookingUid: string; - data: z.infer; - }>; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueBulkTask({ - ...params, - action: CreatedAuditActionService.TYPE, - }); - } - - async queueBulkRescheduledAudit(params: { - bookings: Array<{ - bookingUid: string; - data: z.infer; - }>; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueBulkTask({ - ...params, - action: RescheduledAuditActionService.TYPE, - }); - } - - async queueBulkRejectedAudit(params: { - bookings: Array<{ - bookingUid: string; - data: z.infer; - }>; - actor: Actor; - organizationId: number | null; - source: ActionSource; - operationId?: string | null; - context?: BookingAuditContext; - isBookingAuditEnabled: boolean; - }): Promise { - await this.queueBulkTask({ - ...params, - action: RejectedAuditActionService.TYPE, - }); - } + async queueBulkRejectedAudit(params: { + bookings: Array<{ + bookingUid: string; + data: z.infer; + }>; + actor: Actor; + organizationId: number | null; + source: ActionSource; + operationId?: string | null; + context?: BookingAuditContext; + isBookingAuditEnabled: boolean; + }): Promise { + await this.queueBulkTask({ + ...params, + action: RejectedAuditActionService.TYPE, + }); + } } diff --git a/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts b/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts index 4d5a5c888d..ffcfcf6ef1 100644 --- a/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts +++ b/packages/features/booking-audit/lib/service/BookingAuditViewerService.ts @@ -339,12 +339,11 @@ export class BookingAuditViewerService { }; } - private mergeDataRequirements(...requirements: DataRequirements[]): DataRequirements { const userUuids = new Set(); const attendeeIds = new Set(); const credentialIds = new Set(); - + for (const requirement of requirements) { for (const uuid of requirement.userUuids || []) userUuids.add(uuid); for (const id of requirement.attendeeIds || []) attendeeIds.add(id); @@ -368,7 +367,7 @@ export class BookingAuditViewerService { for (const log of auditLogs) { actorRequirements.push(getActorDataRequirements(log.actor)); - const context = log.context + const context = log.context; if (context?.impersonatedBy) { contextRequirements.push({ userUuids: [context.impersonatedBy] }); } @@ -377,7 +376,7 @@ export class BookingAuditViewerService { const actionService = this.actionServiceRegistry.getActionService(log.action); const parsedData = actionService.parseStored(log.data); serviceRequirements.push(actionService.getDataRequirements(parsedData)); - } catch(error) { + } catch (error) { this.log.error( `Failed to get data requirements for action ${log.action}: ${error instanceof Error ? error.message : String(error)}` ); diff --git a/packages/features/booking-audit/lib/service/BookingHistoryViewerService.ts b/packages/features/booking-audit/lib/service/BookingHistoryViewerService.ts index 1c64100770..239a9cc7a9 100644 --- a/packages/features/booking-audit/lib/service/BookingHistoryViewerService.ts +++ b/packages/features/booking-audit/lib/service/BookingHistoryViewerService.ts @@ -4,102 +4,111 @@ import type { BookingAuditViewerService, DisplayBookingAuditLog } from "./Bookin import { getFieldResponseByIdentifier } from "@calcom/features/routing-forms/lib/getFieldResponseByIdentifier"; type GetHistoryForBookingParams = { - bookingUid: string; - userId: number; - userEmail: string; - userTimeZone: string; - organizationId: number | null; + bookingUid: string; + userId: number; + userEmail: string; + userTimeZone: string; + organizationId: number | null; }; type BookingHistoryLog = DisplayBookingAuditLog; interface BookingHistoryViewerServiceDeps { - bookingAuditViewerService: BookingAuditViewerService; - routingFormResponseRepository: RoutingFormResponseRepositoryInterface; + bookingAuditViewerService: BookingAuditViewerService; + routingFormResponseRepository: RoutingFormResponseRepositoryInterface; } export class BookingHistoryViewerService { - private readonly bookingAuditViewerService: BookingAuditViewerService; - private readonly routingFormResponseRepository: RoutingFormResponseRepositoryInterface; + private readonly bookingAuditViewerService: BookingAuditViewerService; + private readonly routingFormResponseRepository: RoutingFormResponseRepositoryInterface; - constructor(private readonly deps: BookingHistoryViewerServiceDeps) { - this.bookingAuditViewerService = deps.bookingAuditViewerService; - this.routingFormResponseRepository = deps.routingFormResponseRepository; + constructor(private readonly deps: BookingHistoryViewerServiceDeps) { + this.bookingAuditViewerService = deps.bookingAuditViewerService; + this.routingFormResponseRepository = deps.routingFormResponseRepository; + } + + private sortLogsReverseChronologically(historyLogs: BookingHistoryLog[]): BookingHistoryLog[] { + return historyLogs.sort((a, b) => { + const timestampA = new Date(a.timestamp).getTime(); + const timestampB = new Date(b.timestamp).getTime(); + return timestampB - timestampA; + }); + } + + private async getFormAuditLogsForBooking(bookingUid: string): Promise { + // TODO: Form doesn't have its Audit Logs yet, so we replicate them using the Form Response directly for now. + const formResponse = await this.routingFormResponseRepository.findByBookingUidIncludeForm(bookingUid); + if (!formResponse) { + return []; } + return [this.createFormSubmissionEntry({ formResponse, bookingUid })]; + } - private sortLogsReverseChronologically(historyLogs: BookingHistoryLog[]): BookingHistoryLog[] { - return historyLogs.sort((a, b) => { - const timestampA = new Date(a.timestamp).getTime(); - const timestampB = new Date(b.timestamp).getTime(); - return timestampB - timestampA; - }); - } + async getHistoryForBooking( + params: GetHistoryForBookingParams + ): Promise<{ bookingUid: string; auditLogs: BookingHistoryLog[] }> { + const { bookingUid } = params; - private async getFormAuditLogsForBooking(bookingUid: string): Promise { - // TODO: Form doesn't have its Audit Logs yet, so we replicate them using the Form Response directly for now. - const formResponse = await this.routingFormResponseRepository.findByBookingUidIncludeForm(bookingUid); - if (!formResponse) { - return []; - } - return [this.createFormSubmissionEntry({ formResponse, bookingUid })]; - } + const { auditLogs: bookingAuditLogs } = + await this.bookingAuditViewerService.getAuditLogsForBooking(params); - async getHistoryForBooking( - params: GetHistoryForBookingParams - ): Promise<{ bookingUid: string; auditLogs: BookingHistoryLog[] }> { - const { bookingUid } = params; + const historyEntries: BookingHistoryLog[] = [ + ...bookingAuditLogs, + ...(await this.getFormAuditLogsForBooking(bookingUid)), + ]; - const { auditLogs: bookingAuditLogs } = await this.bookingAuditViewerService.getAuditLogsForBooking(params); + const sortedLogs = this.sortLogsReverseChronologically(historyEntries); - const historyEntries: BookingHistoryLog[] = [...bookingAuditLogs, ...await this.getFormAuditLogsForBooking(bookingUid)]; + return { + bookingUid, + auditLogs: sortedLogs, + }; + } - const sortedLogs = this.sortLogsReverseChronologically(historyEntries); + private createFormSubmissionEntry({ + formResponse, + bookingUid, + }: { + formResponse: NonNullable< + Awaited> + >; + bookingUid: string; + }): BookingHistoryLog { + const timestamp = formResponse.createdAt.toISOString(); - return { - bookingUid, - auditLogs: sortedLogs, - }; - } - - private createFormSubmissionEntry({ - formResponse, - bookingUid, - }: { - formResponse: NonNullable< - Awaited> - >; - bookingUid: string; - }): BookingHistoryLog { - const timestamp = formResponse.createdAt.toISOString(); - - const emailFieldResult = getFieldResponseByIdentifier({ responsePayload: formResponse.response, formFields: formResponse.form.fields, identifier: "email" }); - const emailFieldValueFromResponse = emailFieldResult.success ? emailFieldResult.data : null; - // A valid string can be the email otherwise we assume it is not an email - const submitterEmail = typeof emailFieldValueFromResponse === "string" ? emailFieldValueFromResponse : null; - const uniqueId = `form-submission-${formResponse.id}`; - return { - id: uniqueId, - bookingUid, - type: "RECORD_CREATED", - action: "CREATED", - timestamp, - createdAt: timestamp, - source: "WEBAPP", - operationId: uniqueId, - displayJson: null, - actionDisplayTitle: { key: "form_submitted" }, - displayFields: null, - actor: { - id: `form-submission-actor-${formResponse.id}`, - type: "GUEST", - userUuid: null, - attendeeId: null, - name: null, - createdAt: formResponse.createdAt, - displayName: submitterEmail ? `${submitterEmail}` : "Guest", - displayEmail: submitterEmail || null, - displayAvatar: null, - }, - }; - } + const emailFieldResult = getFieldResponseByIdentifier({ + responsePayload: formResponse.response, + formFields: formResponse.form.fields, + identifier: "email", + }); + const emailFieldValueFromResponse = emailFieldResult.success ? emailFieldResult.data : null; + // A valid string can be the email otherwise we assume it is not an email + const submitterEmail = + typeof emailFieldValueFromResponse === "string" ? emailFieldValueFromResponse : null; + const uniqueId = `form-submission-${formResponse.id}`; + return { + id: uniqueId, + bookingUid, + type: "RECORD_CREATED", + action: "CREATED", + timestamp, + createdAt: timestamp, + source: "WEBAPP", + operationId: uniqueId, + displayJson: null, + actionDisplayTitle: { key: "form_submitted" }, + displayFields: null, + actor: { + id: `form-submission-actor-${formResponse.id}`, + type: "GUEST", + userUuid: null, + attendeeId: null, + name: null, + createdAt: formResponse.createdAt, + displayName: submitterEmail ? `${submitterEmail}` : "Guest", + displayEmail: submitterEmail || null, + displayAvatar: null, + }, + }; + } } diff --git a/packages/features/booking-audit/lib/service/__tests__/BookingAuditAccessService.test.ts b/packages/features/booking-audit/lib/service/__tests__/BookingAuditAccessService.test.ts index d3c61a502c..1c3c465f08 100644 --- a/packages/features/booking-audit/lib/service/__tests__/BookingAuditAccessService.test.ts +++ b/packages/features/booking-audit/lib/service/__tests__/BookingAuditAccessService.test.ts @@ -5,26 +5,33 @@ import { BookingRepository } from "@calcom/features/bookings/repositories/Bookin import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository"; import { MembershipRole } from "@calcom/prisma/enums"; -import { BookingAuditAccessService, BookingAuditErrorCode, BookingAuditPermissionError } from "../BookingAuditAccessService"; +import { + BookingAuditAccessService, + BookingAuditErrorCode, + BookingAuditPermissionError, +} from "../BookingAuditAccessService"; vi.mock("@calcom/features/pbac/services/permission-check.service"); vi.mock("@calcom/features/bookings/repositories/BookingRepository"); vi.mock("@calcom/features/membership/repositories/MembershipRepository"); const DB = { - bookings: {} as Record, + bookings: {} as Record< + string, + { + uid: string; + userId: number | null; + eventType: { + teamId: number | null; + parent: { teamId: number } | null; + hosts: never[]; + users: never[]; + }; + user: { id: number; email: string }; + attendees: never[]; + } + >, memberships: {} as Record, -} +}; const createMockTeamBooking = (overrides: { bookingUid: string; @@ -36,20 +43,20 @@ const createMockTeamBooking = (overrides: { uid: overrides.bookingUid, userId: overrides?.userId ?? 456, eventType: { - teamId: (overrides && "teamId" in overrides ? overrides.teamId : overrides?.teamId ?? 100) ?? null, + teamId: (overrides && "teamId" in overrides ? overrides.teamId : (overrides?.teamId ?? 100)) ?? null, parent: (overrides?.parentTeamId ? { teamId: overrides.parentTeamId } : undefined) ?? null, hosts: [], - users: [] + users: [], }, user: { id: overrides?.userId ?? 456, email: "test@example.com", }, attendees: [], - } + }; DB.bookings[booking.uid] = booking; return booking; -} +}; const createMockPersonalBooking = (overrides: { userId?: number; bookingUid: string }) => { const booking = { @@ -69,7 +76,7 @@ const createMockPersonalBooking = (overrides: { userId?: number; bookingUid: str }; DB.bookings[booking.uid] = booking; return booking; -} +}; const createMockMembership = ({ userId, teamId }: { userId: number; teamId: number }) => { const key = `${userId}-${teamId}`; @@ -78,25 +85,49 @@ const createMockMembership = ({ userId, teamId }: { userId: number; teamId: numb type MockPermissionCheckService = { checkPermission: Mock; -} +}; -const provideReadTeamAuditLogsPermission = ({ mockPermissionCheckService, value, targetUserId, targetTeamId }: { mockPermissionCheckService: MockPermissionCheckService, value: boolean, targetUserId: number, targetTeamId: number }) => { - mockPermissionCheckService.checkPermission.mockImplementation(({ userId, teamId, permission, _fallbackRoles }) => { - if (permission === "booking.readTeamAuditLogs" && userId === targetUserId && teamId === targetTeamId) { - return Promise.resolve(value); +const provideReadTeamAuditLogsPermission = ({ + mockPermissionCheckService, + value, + targetUserId, + targetTeamId, +}: { + mockPermissionCheckService: MockPermissionCheckService; + value: boolean; + targetUserId: number; + targetTeamId: number; +}) => { + mockPermissionCheckService.checkPermission.mockImplementation( + ({ userId, teamId, permission, _fallbackRoles }) => { + if (permission === "booking.readTeamAuditLogs" && userId === targetUserId && teamId === targetTeamId) { + return Promise.resolve(value); + } + return Promise.resolve(false); } - return Promise.resolve(false); - }); -} + ); +}; -const provideReadOrgAuditLogsPermission = ({ mockPermissionCheckService, value, targetUserId, targetTeamId }: { mockPermissionCheckService: MockPermissionCheckService, value: boolean, targetUserId: number, targetTeamId: number }) => { - mockPermissionCheckService.checkPermission.mockImplementation(({ userId, teamId, permission, _fallbackRoles }) => { - if (permission === "booking.readOrgAuditLogs" && userId === targetUserId && teamId === targetTeamId) { - return Promise.resolve(value); +const provideReadOrgAuditLogsPermission = ({ + mockPermissionCheckService, + value, + targetUserId, + targetTeamId, +}: { + mockPermissionCheckService: MockPermissionCheckService; + value: boolean; + targetUserId: number; + targetTeamId: number; +}) => { + mockPermissionCheckService.checkPermission.mockImplementation( + ({ userId, teamId, permission, _fallbackRoles }) => { + if (permission === "booking.readOrgAuditLogs" && userId === targetUserId && teamId === targetTeamId) { + return Promise.resolve(value); + } + return Promise.resolve(false); } - return Promise.resolve(false); - }); -} + ); +}; const mockBookingRepository: { findByUidIncludeEventType: Mock; @@ -130,9 +161,15 @@ describe("BookingAuditAccessService - Permission Checks", () => { checkPermission: vi.fn(), }; - vi.mocked(BookingRepository).mockImplementation(function() { return mockBookingRepository as unknown as BookingRepository; }); - vi.mocked(MembershipRepository).mockImplementation(function() { return mockMembershipRepository as unknown as MembershipRepository; }); - vi.mocked(PermissionCheckService).mockImplementation(function() { return mockPermissionCheckService as unknown as PermissionCheckService; }); + vi.mocked(BookingRepository).mockImplementation(function () { + return mockBookingRepository as unknown as BookingRepository; + }); + vi.mocked(MembershipRepository).mockImplementation(function () { + return mockMembershipRepository as unknown as MembershipRepository; + }); + vi.mocked(PermissionCheckService).mockImplementation(function () { + return mockPermissionCheckService as unknown as PermissionCheckService; + }); service = new BookingAuditAccessService({ bookingRepository: mockBookingRepository as unknown as BookingRepository, @@ -146,9 +183,16 @@ describe("BookingAuditAccessService - Permission Checks", () => { const userId = 123; const teamId = 100; createMockTeamBooking({ teamId, bookingUid }); - provideReadTeamAuditLogsPermission({ mockPermissionCheckService, value: true, targetUserId: userId, targetTeamId: teamId }); + provideReadTeamAuditLogsPermission({ + mockPermissionCheckService, + value: true, + targetUserId: userId, + targetTeamId: teamId, + }); - await expect(service.assertPermissions({ bookingUid, userId, organizationId: 200 })).resolves.not.toThrow(); + await expect( + service.assertPermissions({ bookingUid, userId, organizationId: 200 }) + ).resolves.not.toThrow(); }); it("should throw PERMISSION_DENIED error when user lacks booking.readTeamAuditLogs permission and also doesn't even have a membership in the organization for the booking's team", async () => { @@ -158,8 +202,18 @@ describe("BookingAuditAccessService - Permission Checks", () => { const organizationId = 200; createMockTeamBooking({ teamId, bookingUid, userId: 456 }); createMockMembership({ userId: 456, teamId: organizationId }); - provideReadTeamAuditLogsPermission({ mockPermissionCheckService, value: false, targetUserId: userId, targetTeamId: teamId }); - provideReadOrgAuditLogsPermission({ mockPermissionCheckService, value: false, targetUserId: userId, targetTeamId: organizationId }); + provideReadTeamAuditLogsPermission({ + mockPermissionCheckService, + value: false, + targetUserId: userId, + targetTeamId: teamId, + }); + provideReadOrgAuditLogsPermission({ + mockPermissionCheckService, + value: false, + targetUserId: userId, + targetTeamId: organizationId, + }); const promise = service.assertPermissions({ bookingUid, userId, organizationId }); await expect(promise).rejects.toThrow(BookingAuditPermissionError); await expect(promise).rejects.toThrow(BookingAuditErrorCode.PERMISSION_DENIED); @@ -173,7 +227,12 @@ describe("BookingAuditAccessService - Permission Checks", () => { const organizationId = 200; createMockPersonalBooking({ userId: 456, bookingUid }); createMockMembership({ userId: 456, teamId: organizationId }); - provideReadOrgAuditLogsPermission({ mockPermissionCheckService, value: true, targetUserId: userId, targetTeamId: organizationId }); + provideReadOrgAuditLogsPermission({ + mockPermissionCheckService, + value: true, + targetUserId: userId, + targetTeamId: organizationId, + }); await service.assertPermissions({ bookingUid, userId, organizationId }); @@ -192,7 +251,12 @@ describe("BookingAuditAccessService - Permission Checks", () => { const userId = 123; const parentTeamId = 500; createMockTeamBooking({ teamId: null, parentTeamId, bookingUid }); - provideReadTeamAuditLogsPermission({ mockPermissionCheckService, value: true, targetUserId: userId, targetTeamId: parentTeamId }); + provideReadTeamAuditLogsPermission({ + mockPermissionCheckService, + value: true, + targetUserId: userId, + targetTeamId: parentTeamId, + }); await service.assertPermissions({ bookingUid, userId, organizationId: 200 }); @@ -211,25 +275,45 @@ describe("BookingAuditAccessService - Permission Checks", () => { const organizationId = 200; createMockTeamBooking({ teamId: null, parentTeamId, bookingUid, userId: 456 }); createMockMembership({ userId: 456, teamId: organizationId }); - provideReadTeamAuditLogsPermission({ mockPermissionCheckService, value: false, targetUserId: userId, targetTeamId: parentTeamId }); - provideReadOrgAuditLogsPermission({ mockPermissionCheckService, value: false, targetUserId: userId, targetTeamId: organizationId }); + provideReadTeamAuditLogsPermission({ + mockPermissionCheckService, + value: false, + targetUserId: userId, + targetTeamId: parentTeamId, + }); + provideReadOrgAuditLogsPermission({ + mockPermissionCheckService, + value: false, + targetUserId: userId, + targetTeamId: organizationId, + }); - await expect(service.assertPermissions({ bookingUid, userId, organizationId })).rejects.toThrow(BookingAuditPermissionError); + await expect(service.assertPermissions({ bookingUid, userId, organizationId })).rejects.toThrow( + BookingAuditPermissionError + ); }); }); describe("assertPermissions - Edge Cases", () => { it("should throw error when organizationId is null", async () => { - await expect(service.assertPermissions({ bookingUid: "test-booking-uid", userId: 123, organizationId: null })).rejects.toThrow(BookingAuditPermissionError); - await expect(service.assertPermissions({ bookingUid: "test-booking-uid", userId: 123, organizationId: null })).rejects.toThrow(BookingAuditErrorCode.ORGANIZATION_ID_REQUIRED); + await expect( + service.assertPermissions({ bookingUid: "test-booking-uid", userId: 123, organizationId: null }) + ).rejects.toThrow(BookingAuditPermissionError); + await expect( + service.assertPermissions({ bookingUid: "test-booking-uid", userId: 123, organizationId: null }) + ).rejects.toThrow(BookingAuditErrorCode.ORGANIZATION_ID_REQUIRED); }); it("should throw error when booking not found", async () => { const bookingUid = "non-existent-booking-uid"; // Don't create any booking in DB - await expect(service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 })).rejects.toThrow(BookingAuditPermissionError); - await expect(service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 })).rejects.toThrow(BookingAuditErrorCode.BOOKING_NOT_FOUND_OR_PERMISSION_DENIED); + await expect( + service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 }) + ).rejects.toThrow(BookingAuditPermissionError); + await expect( + service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 }) + ).rejects.toThrow(BookingAuditErrorCode.BOOKING_NOT_FOUND_OR_PERMISSION_DENIED); }); it("should throw error when booking has no userId", async () => { @@ -251,8 +335,12 @@ describe("BookingAuditAccessService - Permission Checks", () => { }; DB.bookings[bookingUid] = booking; - await expect(service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 })).rejects.toThrow(BookingAuditPermissionError); - await expect(service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 })).rejects.toThrow(BookingAuditErrorCode.BOOKING_HAS_NO_OWNER); + await expect( + service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 }) + ).rejects.toThrow(BookingAuditPermissionError); + await expect( + service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 }) + ).rejects.toThrow(BookingAuditErrorCode.BOOKING_HAS_NO_OWNER); }); it("should throw error when booking owner is not member of organization", async () => { @@ -260,8 +348,12 @@ describe("BookingAuditAccessService - Permission Checks", () => { createMockPersonalBooking({ userId: 456, bookingUid }); // Don't create membership for userId 456 in organization 200 - await expect(service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 })).rejects.toThrow(BookingAuditPermissionError); - await expect(service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 })).rejects.toThrow(BookingAuditErrorCode.OWNER_NOT_IN_ORGANIZATION); + await expect( + service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 }) + ).rejects.toThrow(BookingAuditPermissionError); + await expect( + service.assertPermissions({ bookingUid, userId: 123, organizationId: 200 }) + ).rejects.toThrow(BookingAuditErrorCode.OWNER_NOT_IN_ORGANIZATION); }); }); }); diff --git a/packages/features/booking-audit/lib/service/__tests__/BookingAuditViewerService.test.ts b/packages/features/booking-audit/lib/service/__tests__/BookingAuditViewerService.test.ts index 2ecf8ac483..8879e7a76d 100644 --- a/packages/features/booking-audit/lib/service/__tests__/BookingAuditViewerService.test.ts +++ b/packages/features/booking-audit/lib/service/__tests__/BookingAuditViewerService.test.ts @@ -158,7 +158,13 @@ type MockUser = { const createMockUser = ( uuid?: string, - overrides?: Partial<{ id: number; uuid: string; name: string | null; email: string; avatarUrl: string | null }> + overrides?: Partial<{ + id: number; + uuid: string; + name: string | null; + email: string; + avatarUrl: string | null; + }> ) => { const userUuid = uuid ?? overrides?.uuid ?? `user-uuid-${overrides?.id ?? 123}`; const user: MockUser = { diff --git a/packages/features/booking-audit/lib/service/__tests__/EnrichmentDataStore.test.ts b/packages/features/booking-audit/lib/service/__tests__/EnrichmentDataStore.test.ts index 6d0b16f66f..cd4f81ce98 100644 --- a/packages/features/booking-audit/lib/service/__tests__/EnrichmentDataStore.test.ts +++ b/packages/features/booking-audit/lib/service/__tests__/EnrichmentDataStore.test.ts @@ -31,10 +31,7 @@ describe("EnrichmentDataStore", () => { describe("constructor", () => { it("should pre-populate usersByUuid map with nulls for declared userUuids", () => { - const store = new EnrichmentDataStore( - { userUuids: ["uuid-1", "uuid-2"] }, - repositories - ); + const store = new EnrichmentDataStore({ userUuids: ["uuid-1", "uuid-2"] }, repositories); expect(() => store.getUserByUuid("uuid-1")).not.toThrow(); expect(() => store.getUserByUuid("uuid-2")).not.toThrow(); @@ -43,10 +40,7 @@ describe("EnrichmentDataStore", () => { }); it("should pre-populate attendeesById map with nulls for declared attendeeIds", () => { - const store = new EnrichmentDataStore( - { attendeeIds: [1, 2, 3] }, - repositories - ); + const store = new EnrichmentDataStore({ attendeeIds: [1, 2, 3] }, repositories); expect(() => store.getAttendeeById(1)).not.toThrow(); expect(() => store.getAttendeeById(2)).not.toThrow(); @@ -55,10 +49,7 @@ describe("EnrichmentDataStore", () => { }); it("should pre-populate credentialsById map with nulls for declared credentialIds", () => { - const store = new EnrichmentDataStore( - { credentialIds: [10, 20] }, - repositories - ); + const store = new EnrichmentDataStore({ credentialIds: [10, 20] }, repositories); expect(() => store.getCredentialById(10)).not.toThrow(); expect(() => store.getCredentialById(20)).not.toThrow(); @@ -82,10 +73,7 @@ describe("EnrichmentDataStore", () => { ]; vi.mocked(mockUserRepository.findByUuids).mockResolvedValue(mockUsers); - const store = new EnrichmentDataStore( - { userUuids: ["uuid-1", "uuid-2"] }, - repositories - ); + const store = new EnrichmentDataStore({ userUuids: ["uuid-1", "uuid-2"] }, repositories); await store.fetch(); expect(mockUserRepository.findByUuids).toHaveBeenCalledWith({ uuids: ["uuid-1", "uuid-2"] }); @@ -100,10 +88,7 @@ describe("EnrichmentDataStore", () => { ]; vi.mocked(mockAttendeeRepository.findByIds).mockResolvedValue(mockAttendees); - const store = new EnrichmentDataStore( - { attendeeIds: [1, 2] }, - repositories - ); + const store = new EnrichmentDataStore({ attendeeIds: [1, 2] }, repositories); await store.fetch(); expect(mockAttendeeRepository.findByIds).toHaveBeenCalledWith({ ids: [1, 2] }); @@ -118,10 +103,7 @@ describe("EnrichmentDataStore", () => { ]; vi.mocked(mockCredentialRepository.findByIds).mockResolvedValue(mockCredentials); - const store = new EnrichmentDataStore( - { credentialIds: [10, 20] }, - repositories - ); + const store = new EnrichmentDataStore({ credentialIds: [10, 20] }, repositories); await store.fetch(); expect(mockCredentialRepository.findByIds).toHaveBeenCalledWith({ ids: [10, 20] }); @@ -136,9 +118,7 @@ describe("EnrichmentDataStore", () => { vi.mocked(mockAttendeeRepository.findByIds).mockResolvedValue([ { id: 1, name: "Attendee", email: "attendee@example.com" }, ]); - vi.mocked(mockCredentialRepository.findByIds).mockResolvedValue([ - { id: 10, appId: "app" }, - ]); + vi.mocked(mockCredentialRepository.findByIds).mockResolvedValue([{ id: 10, appId: "app" }]); const store = new EnrichmentDataStore( { userUuids: ["uuid-1"], attendeeIds: [1], credentialIds: [10] }, @@ -165,10 +145,7 @@ describe("EnrichmentDataStore", () => { { id: 1, uuid: "uuid-1", name: "User 1", email: "user1@example.com", avatarUrl: null }, ]); - const store = new EnrichmentDataStore( - { userUuids: ["uuid-1", "uuid-2", "uuid-3"] }, - repositories - ); + const store = new EnrichmentDataStore({ userUuids: ["uuid-1", "uuid-2", "uuid-3"] }, repositories); await store.fetch(); expect(store.getUserByUuid("uuid-1")).not.toBeNull(); @@ -179,10 +156,7 @@ describe("EnrichmentDataStore", () => { describe("getUserByUuid", () => { it("should throw error when accessing undeclared UUID", () => { - const store = new EnrichmentDataStore( - { userUuids: ["uuid-1"] }, - repositories - ); + const store = new EnrichmentDataStore({ userUuids: ["uuid-1"] }, repositories); expect(() => store.getUserByUuid("undeclared-uuid")).toThrow( 'EnrichmentDataStore: getUserByUuid("undeclared-uuid") called but was not declared in getDataRequirements' @@ -192,23 +166,23 @@ describe("EnrichmentDataStore", () => { it("should return null for declared UUID that does not exist in database", async () => { vi.mocked(mockUserRepository.findByUuids).mockResolvedValue([]); - const store = new EnrichmentDataStore( - { userUuids: ["uuid-1"] }, - repositories - ); + const store = new EnrichmentDataStore({ userUuids: ["uuid-1"] }, repositories); await store.fetch(); expect(store.getUserByUuid("uuid-1")).toBeNull(); }); it("should return user data for declared UUID that exists in database", async () => { - const mockUser = { id: 1, uuid: "uuid-1", name: "Test User", email: "test@example.com", avatarUrl: null }; + const mockUser = { + id: 1, + uuid: "uuid-1", + name: "Test User", + email: "test@example.com", + avatarUrl: null, + }; vi.mocked(mockUserRepository.findByUuids).mockResolvedValue([mockUser]); - const store = new EnrichmentDataStore( - { userUuids: ["uuid-1"] }, - repositories - ); + const store = new EnrichmentDataStore({ userUuids: ["uuid-1"] }, repositories); await store.fetch(); expect(store.getUserByUuid("uuid-1")).toEqual(mockUser); @@ -217,10 +191,7 @@ describe("EnrichmentDataStore", () => { describe("getAttendeeById", () => { it("should throw error when accessing undeclared attendee ID", () => { - const store = new EnrichmentDataStore( - { attendeeIds: [1] }, - repositories - ); + const store = new EnrichmentDataStore({ attendeeIds: [1] }, repositories); expect(() => store.getAttendeeById(999)).toThrow( "EnrichmentDataStore: getAttendeeById(999) called but was not declared in getDataRequirements" @@ -230,10 +201,7 @@ describe("EnrichmentDataStore", () => { it("should return null for declared ID that does not exist in database", async () => { vi.mocked(mockAttendeeRepository.findByIds).mockResolvedValue([]); - const store = new EnrichmentDataStore( - { attendeeIds: [1] }, - repositories - ); + const store = new EnrichmentDataStore({ attendeeIds: [1] }, repositories); await store.fetch(); expect(store.getAttendeeById(1)).toBeNull(); @@ -243,10 +211,7 @@ describe("EnrichmentDataStore", () => { const mockAttendee = { id: 1, name: "Test Attendee", email: "attendee@example.com" }; vi.mocked(mockAttendeeRepository.findByIds).mockResolvedValue([mockAttendee]); - const store = new EnrichmentDataStore( - { attendeeIds: [1] }, - repositories - ); + const store = new EnrichmentDataStore({ attendeeIds: [1] }, repositories); await store.fetch(); expect(store.getAttendeeById(1)).toEqual(mockAttendee); @@ -255,10 +220,7 @@ describe("EnrichmentDataStore", () => { describe("getCredentialById", () => { it("should throw error when accessing undeclared credential ID", () => { - const store = new EnrichmentDataStore( - { credentialIds: [10] }, - repositories - ); + const store = new EnrichmentDataStore({ credentialIds: [10] }, repositories); expect(() => store.getCredentialById(999)).toThrow( "EnrichmentDataStore: getCredentialById(999) called but was not declared in getDataRequirements" @@ -268,10 +230,7 @@ describe("EnrichmentDataStore", () => { it("should return null for declared ID that does not exist in database", async () => { vi.mocked(mockCredentialRepository.findByIds).mockResolvedValue([]); - const store = new EnrichmentDataStore( - { credentialIds: [10] }, - repositories - ); + const store = new EnrichmentDataStore({ credentialIds: [10] }, repositories); await store.fetch(); expect(store.getCredentialById(10)).toBeNull(); @@ -281,10 +240,7 @@ describe("EnrichmentDataStore", () => { const mockCredential = { id: 10, appId: "google-calendar" }; vi.mocked(mockCredentialRepository.findByIds).mockResolvedValue([mockCredential]); - const store = new EnrichmentDataStore( - { credentialIds: [10] }, - repositories - ); + const store = new EnrichmentDataStore({ credentialIds: [10] }, repositories); await store.fetch(); expect(store.getCredentialById(10)).toEqual(mockCredential); @@ -300,10 +256,7 @@ describe("EnrichmentDataStore", () => { ]; vi.mocked(mockUserRepository.findByUuids).mockResolvedValue(mockUsers); - const store = new EnrichmentDataStore( - { userUuids: ["uuid-1", "uuid-2", "uuid-3"] }, - repositories - ); + const store = new EnrichmentDataStore({ userUuids: ["uuid-1", "uuid-2", "uuid-3"] }, repositories); await store.fetch(); expect(store.getUserByUuid("uuid-1")).toEqual(mockUsers[0]); diff --git a/packages/features/booking-audit/lib/service/__tests__/cancelled-action.integration-test.ts b/packages/features/booking-audit/lib/service/__tests__/cancelled-action.integration-test.ts index 61b45b615c..b52f81a474 100644 --- a/packages/features/booking-audit/lib/service/__tests__/cancelled-action.integration-test.ts +++ b/packages/features/booking-audit/lib/service/__tests__/cancelled-action.integration-test.ts @@ -123,4 +123,3 @@ describe("Cancelled Action Integration", () => { }); }); }); - diff --git a/packages/features/booking-audit/lib/types/actionSource.ts b/packages/features/booking-audit/lib/types/actionSource.ts index c03a743d38..085c2adb60 100644 --- a/packages/features/booking-audit/lib/types/actionSource.ts +++ b/packages/features/booking-audit/lib/types/actionSource.ts @@ -2,10 +2,25 @@ import { z } from "zod"; // This is the schema for DB value, here we use UNKNOWN in case client didn't pass an explicit action source. // SYSTEM is used for background jobs (tasker tasks, trigger.dev, etc.) -export const ActionSourceSchema = z.enum(["API_V1", "API_V2", "WEBAPP", "WEBHOOK", "MAGIC_LINK", "SYSTEM", "UNKNOWN"]); +export const ActionSourceSchema = z.enum([ + "API_V1", + "API_V2", + "WEBAPP", + "WEBHOOK", + "MAGIC_LINK", + "SYSTEM", + "UNKNOWN", +]); export type ActionSource = z.infer; // We don't keep UNKNOWN here because we don't want clients to pass UNKNOWN. // SYSTEM is used for background jobs (tasker tasks, trigger.dev, etc.) -export const ValidActionSourceSchema = z.enum(["API_V1", "API_V2", "WEBAPP", "WEBHOOK", "MAGIC_LINK", "SYSTEM"]); +export const ValidActionSourceSchema = z.enum([ + "API_V1", + "API_V2", + "WEBAPP", + "WEBHOOK", + "MAGIC_LINK", + "SYSTEM", +]); export type ValidActionSource = z.infer; diff --git a/packages/features/booking-audit/lib/types/bookingAuditTask.ts b/packages/features/booking-audit/lib/types/bookingAuditTask.ts index 3ebc9a61f7..5d6916ba83 100644 --- a/packages/features/booking-audit/lib/types/bookingAuditTask.ts +++ b/packages/features/booking-audit/lib/types/bookingAuditTask.ts @@ -8,19 +8,19 @@ import { ActionSourceSchema } from "./actionSource"; * Used for runtime validation of action field */ const BookingAuditActionSchema = z.enum([ - "CREATED", - "RESCHEDULED", - "ACCEPTED", - "CANCELLED", - "RESCHEDULE_REQUESTED", - "ATTENDEE_ADDED", - "REJECTED", - "ATTENDEE_REMOVED", - "REASSIGNMENT", - "LOCATION_CHANGED", - "NO_SHOW_UPDATED", - "SEAT_BOOKED", - "SEAT_RESCHEDULED", + "CREATED", + "RESCHEDULED", + "ACCEPTED", + "CANCELLED", + "RESCHEDULE_REQUESTED", + "ATTENDEE_ADDED", + "REJECTED", + "ATTENDEE_REMOVED", + "REASSIGNMENT", + "LOCATION_CHANGED", + "NO_SHOW_UPDATED", + "SEAT_BOOKED", + "SEAT_RESCHEDULED", ]); export type BookingAuditAction = z.infer; @@ -28,48 +28,48 @@ export type BookingAuditAction = z.infer; const actionAgnosticDataSchema = z.unknown(); const bookingAuditPayloadSchema = z.object({ - bookingUid: z.string(), - data: actionAgnosticDataSchema, + bookingUid: z.string(), + data: actionAgnosticDataSchema, }); export const SingleBookingAuditTaskConsumerSchema = z.object({ - isBulk: z.literal(false), - ...bookingAuditPayloadSchema.shape, - actor: PiiFreeActorSchema, - organizationId: z.number().nullable(), - timestamp: z.number(), - action: BookingAuditActionSchema, - source: ActionSourceSchema.default("UNKNOWN"), - operationId: z.string(), - context: BookingAuditContextSchema.optional(), + isBulk: z.literal(false), + ...bookingAuditPayloadSchema.shape, + actor: PiiFreeActorSchema, + organizationId: z.number().nullable(), + timestamp: z.number(), + action: BookingAuditActionSchema, + source: ActionSourceSchema.default("UNKNOWN"), + operationId: z.string(), + context: BookingAuditContextSchema.optional(), }); export type SingleBookingAuditTaskConsumerPayload = z.infer; /** * Bulk booking audit task payload schema - * + * * Used for operations that affect multiple bookings in a single action. * Contains an array of bookings, each with bookingUid and action-specific data. * All bookings share the same actor, organizationId, timestamp, action, source, and operationId. */ export const BulkBookingAuditTaskConsumerSchema = z.object({ - isBulk: z.literal(true), - bookings: z.array(bookingAuditPayloadSchema).min(1), - actor: PiiFreeActorSchema, - organizationId: z.number().nullable(), - timestamp: z.number(), - action: BookingAuditActionSchema, - source: ActionSourceSchema.default("UNKNOWN"), - operationId: z.string(), - context: BookingAuditContextSchema.optional(), + isBulk: z.literal(true), + bookings: z.array(bookingAuditPayloadSchema).min(1), + actor: PiiFreeActorSchema, + organizationId: z.number().nullable(), + timestamp: z.number(), + action: BookingAuditActionSchema, + source: ActionSourceSchema.default("UNKNOWN"), + operationId: z.string(), + context: BookingAuditContextSchema.optional(), }); export type BulkBookingAuditTaskConsumerPayload = z.infer; export const BookingAuditTaskConsumerSchema = z.discriminatedUnion("isBulk", [ - SingleBookingAuditTaskConsumerSchema, - BulkBookingAuditTaskConsumerSchema, + SingleBookingAuditTaskConsumerSchema, + BulkBookingAuditTaskConsumerSchema, ]); export type BookingAuditTaskConsumerPayload = z.infer; diff --git a/packages/features/bookings/Booker/hooks/useBookerLayout.ts b/packages/features/bookings/Booker/hooks/useBookerLayout.ts index ec5d345cd2..27fd8f7cc8 100644 --- a/packages/features/bookings/Booker/hooks/useBookerLayout.ts +++ b/packages/features/bookings/Booker/hooks/useBookerLayout.ts @@ -103,4 +103,4 @@ export const useBookerLayout = ( slotsViewOnSmallScreen, bookerLayouts, }; -}; \ No newline at end of file +}; diff --git a/packages/features/bookings/Booker/hooks/useBookerTime.ts b/packages/features/bookings/Booker/hooks/useBookerTime.ts index 2b34d8614d..9b9a50782d 100644 --- a/packages/features/bookings/Booker/hooks/useBookerTime.ts +++ b/packages/features/bookings/Booker/hooks/useBookerTime.ts @@ -17,4 +17,4 @@ export const useBookerTime = () => { timezoneFromBookerStore, timezoneFromTimePreferences, }; -}; \ No newline at end of file +}; diff --git a/packages/features/bookings/Booker/hooks/useBookingForm.ts b/packages/features/bookings/Booker/hooks/useBookingForm.ts index 2d2dd92691..44a7c24d38 100644 --- a/packages/features/bookings/Booker/hooks/useBookingForm.ts +++ b/packages/features/bookings/Booker/hooks/useBookingForm.ts @@ -127,4 +127,4 @@ export const useBookingForm = ({ formErrors: errors, errors, }; -}; \ No newline at end of file +}; diff --git a/packages/features/bookings/Booker/hooks/useInitialFormValues.test.ts b/packages/features/bookings/Booker/hooks/useInitialFormValues.test.ts index 6834f94fb0..f358cd603a 100644 --- a/packages/features/bookings/Booker/hooks/useInitialFormValues.test.ts +++ b/packages/features/bookings/Booker/hooks/useInitialFormValues.test.ts @@ -291,4 +291,4 @@ describe("useInitialFormValues - Autofill Disable Feature", () => { expect(result.current.values.responses?.phone).toBeUndefined(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/features/bookings/Booker/hooks/useInitialFormValues.ts b/packages/features/bookings/Booker/hooks/useInitialFormValues.ts index 98d5217576..31091140b2 100644 --- a/packages/features/bookings/Booker/hooks/useInitialFormValues.ts +++ b/packages/features/bookings/Booker/hooks/useInitialFormValues.ts @@ -212,4 +212,4 @@ export function useInitialFormValues({ ]); return initialValuesState; -} \ No newline at end of file +} diff --git a/packages/features/bookings/Booker/hooks/useLocalSet.tsx b/packages/features/bookings/Booker/hooks/useLocalSet.tsx index 7709f7bcad..67a8609c8c 100644 --- a/packages/features/bookings/Booker/hooks/useLocalSet.tsx +++ b/packages/features/bookings/Booker/hooks/useLocalSet.tsx @@ -68,4 +68,4 @@ export function useLocalSet(key: string, initialValue: }; return { set, addValue, removeById, toggleValue, hasItem, clearSet }; -} \ No newline at end of file +} diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index 4f45f258ae..e436eb42ba 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -9,11 +9,7 @@ import { BookerLayouts } from "@calcom/prisma/zod-utils"; import type { GetBookingType } from "../lib/get-booking"; import type { BookerState, BookerLayout } from "./types"; -import { - updateQueryParam, - getQueryParam, - removeQueryParam, -} from "./utils/query-param"; +import { updateQueryParam, getQueryParam, removeQueryParam } from "./utils/query-param"; const _iso_3166_1_alpha_2_codes = [ "ad", @@ -357,9 +353,7 @@ export type BookerStore = { * Multiple Selected Dates and Times */ selectedDatesAndTimes: { [key: string]: { [key: string]: string[] } } | null; - setSelectedDatesAndTimes: (selectedDatesAndTimes: { - [key: string]: { [key: string]: string[] }; - }) => void; + setSelectedDatesAndTimes: (selectedDatesAndTimes: { [key: string]: { [key: string]: string[] } }) => void; /** * Multiple duration configuration */ @@ -457,10 +451,7 @@ export const createBookerStore = () => setLayout: (layout: BookerLayout) => { // If we switch to a large layout and don't have a date selected yet, // we selected it here, so week title is rendered properly. - if ( - ["week_view", "column_view"].includes(layout) && - !get().selectedDate - ) { + if (["week_view", "column_view"].includes(layout) && !get().selectedDate) { set({ selectedDate: dayjs().format("YYYY-MM-DD") }); } if (!get().isPlatform || get().allowUpdatingUrlParams) { @@ -469,11 +460,7 @@ export const createBookerStore = () => return set({ layout }); }, selectedDate: getQueryParam("date") || null, - setSelectedDate: ({ - date: selectedDate, - omitUpdatingParams = false, - preventMonthSwitching = false, - }) => { + setSelectedDate: ({ date: selectedDate, omitUpdatingParams = false, preventMonthSwitching = false }) => { // unset selected date if (!selectedDate) { removeQueryParam("date"); @@ -483,24 +470,15 @@ export const createBookerStore = () => const currentSelection = dayjs(get().selectedDate); const newSelection = dayjs(selectedDate); set({ selectedDate }); - if ( - !omitUpdatingParams && - (!get().isPlatform || get().allowUpdatingUrlParams) - ) { + if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { updateQueryParam("date", selectedDate ?? ""); } // Setting month make sure small calendar in fullscreen layouts also updates. // preventMonthSwitching is true in monthly view - if ( - !preventMonthSwitching && - newSelection.month() !== currentSelection.month() - ) { + if (!preventMonthSwitching && newSelection.month() !== currentSelection.month()) { set({ month: newSelection.format("YYYY-MM") }); - if ( - !omitUpdatingParams && - (!get().isPlatform || get().allowUpdatingUrlParams) - ) { + if (!omitUpdatingParams && (!get().isPlatform || get().allowUpdatingUrlParams)) { updateQueryParam("month", newSelection.format("YYYY-MM")); } } @@ -561,8 +539,7 @@ export const createBookerStore = () => } get().setSelectedDate({ date: null }); }, - dayCount: - BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null, + dayCount: BOOKER_NUMBER_OF_DAYS_TO_LOAD > 0 ? BOOKER_NUMBER_OF_DAYS_TO_LOAD : null, setDayCount: (dayCount: number | null) => { set({ dayCount }); }, @@ -644,9 +621,7 @@ export const createBookerStore = () => // Preselect today's date in week / column view, since they use this to show the week title. selectedDate: selectedDateInStore || - (["week_view", "column_view"].includes(layout) - ? dayjs().format("YYYY-MM-DD") - : null), + (["week_view", "column_view"].includes(layout) ? dayjs().format("YYYY-MM-DD") : null), teamMemberEmail, crmOwnerRecordType, crmAppSlug, @@ -707,24 +682,14 @@ export const createBookerStore = () => set({ rescheduleUid }); }, recurringEventCount: null, - setRecurringEventCount: (recurringEventCount: number | null) => - set({ recurringEventCount }), - recurringEventCountQueryParam: - Number(getQueryParam("recurringEventCount")) || null, - setRecurringEventCountQueryParam: ( - recurringEventCountQueryParam: number | null - ) => { + setRecurringEventCount: (recurringEventCount: number | null) => set({ recurringEventCount }), + recurringEventCountQueryParam: Number(getQueryParam("recurringEventCount")) || null, + setRecurringEventCountQueryParam: (recurringEventCountQueryParam: number | null) => { // Guard: only update state if value is valid (not NaN or null) - if ( - recurringEventCountQueryParam !== null && - !isNaN(recurringEventCountQueryParam) - ) { + if (recurringEventCountQueryParam !== null && !isNaN(recurringEventCountQueryParam)) { set({ recurringEventCountQueryParam }); if (!get().isPlatform || get().allowUpdatingUrlParams) { - updateQueryParam( - "recurringEventCount", - recurringEventCountQueryParam - ); + updateQueryParam("recurringEventCount", recurringEventCountQueryParam); } } // If invalid, don't update state or URL - just ignore the call @@ -755,9 +720,7 @@ export const createBookerStore = () => allowUpdatingUrlParams: true, defaultPhoneCountry: null, isSlotSelectionModalVisible: false, - setIsSlotSelectionModalVisible: ( - isSlotSelectionModalVisible: boolean - ) => { + setIsSlotSelectionModalVisible: (isSlotSelectionModalVisible: boolean) => { set({ isSlotSelectionModalVisible }); }, })); diff --git a/packages/features/bookings/Booker/utils/dates.tsx b/packages/features/bookings/Booker/utils/dates.tsx index d7d7b9e666..64f65c53d9 100644 --- a/packages/features/bookings/Booker/utils/dates.tsx +++ b/packages/features/bookings/Booker/utils/dates.tsx @@ -34,10 +34,7 @@ export const formatEventFromTime = ({ date, timeFormat, timeZone, language }: Ev return { date: formattedDate, - time: - timeFormat === TimeFormat.TWELVE_HOUR - ? formattedTime.toLowerCase() - : formattedTime, + time: timeFormat === TimeFormat.TWELVE_HOUR ? formattedTime.toLowerCase() : formattedTime, }; }; @@ -68,10 +65,7 @@ export const formatEventFromToTime = ({ return { date: formattedDate, - time: - timeFormat === TimeFormat.TWELVE_HOUR - ? formattedTime.toLowerCase() - : formattedTime, + time: timeFormat === TimeFormat.TWELVE_HOUR ? formattedTime.toLowerCase() : formattedTime, }; }; diff --git a/packages/features/bookings/Booker/utils/getPrefetchMonthCount.test.ts b/packages/features/bookings/Booker/utils/getPrefetchMonthCount.test.ts index 2db3076a28..77bd38595b 100644 --- a/packages/features/bookings/Booker/utils/getPrefetchMonthCount.test.ts +++ b/packages/features/bookings/Booker/utils/getPrefetchMonthCount.test.ts @@ -84,7 +84,13 @@ describe("getPrefetchMonthCount", () => { }); it("should return undefined when both months are invalid", () => { - const result = getPrefetchMonthCount(BookerLayouts.COLUMN_VIEW, "selecting_time", Infinity, -Infinity, false); + const result = getPrefetchMonthCount( + BookerLayouts.COLUMN_VIEW, + "selecting_time", + Infinity, + -Infinity, + false + ); expect(result).toBe(undefined); }); }); diff --git a/packages/features/bookings/di/BookingEventHandlerService.container.ts b/packages/features/bookings/di/BookingEventHandlerService.container.ts index c8b542287d..5a1df5dd78 100644 --- a/packages/features/bookings/di/BookingEventHandlerService.container.ts +++ b/packages/features/bookings/di/BookingEventHandlerService.container.ts @@ -12,4 +12,3 @@ export function getBookingEventHandlerService() { return container.get(bookingEventHandlerServiceModule.token); } - diff --git a/packages/features/bookings/lib/EventManager.ts b/packages/features/bookings/lib/EventManager.ts index 1633883406..3668cafbd4 100644 --- a/packages/features/bookings/lib/EventManager.ts +++ b/packages/features/bookings/lib/EventManager.ts @@ -399,7 +399,7 @@ export default class EventManager { return { type: result.type, - uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "", + uid: createdEventObj ? createdEventObj.id : (result.createdEvent?.id?.toString() ?? ""), thirdPartyRecurringEventId: isCalendarType ? thirdPartyRecurringEventId : undefined, meetingId: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString(), meetingPassword: createdEventObj ? createdEventObj.password : result.createdEvent?.password, @@ -688,9 +688,7 @@ export default class EventManager { if (evt.requiresConfirmation) { if (!skipDeleteEventsAndMeetings) { - log.debug( - "RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking" - ); + log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking"); // As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking. await this.deleteEventsAndMeetings({ event: { diff --git a/packages/features/bookings/lib/buildEventUrlFromBooking.ts b/packages/features/bookings/lib/buildEventUrlFromBooking.ts index cf5e375485..24c757f17c 100644 --- a/packages/features/bookings/lib/buildEventUrlFromBooking.ts +++ b/packages/features/bookings/lib/buildEventUrlFromBooking.ts @@ -24,7 +24,7 @@ export function getOrganizationIdOfBooking(booking: { const { eventType, profileEnrichedBookingUser } = booking; return eventType.team ? eventType.team.parentId - : profileEnrichedBookingUser?.profile.organizationId ?? null; + : (profileEnrichedBookingUser?.profile.organizationId ?? null); } export async function buildEventUrlFromBooking(booking: { diff --git a/packages/features/bookings/lib/get-booking.ts b/packages/features/bookings/lib/get-booking.ts index 5f4b71e48a..14f17dcfc1 100644 --- a/packages/features/bookings/lib/get-booking.ts +++ b/packages/features/bookings/lib/get-booking.ts @@ -26,8 +26,7 @@ function getResponsesFromOldBooking( ) { const customInputs = rawBooking.customInputs || {}; const responses = Object.keys(customInputs).reduce((acc, label) => { - acc[slugify(label) as keyof typeof acc] = - customInputs[label as keyof typeof customInputs]; + acc[slugify(label) as keyof typeof acc] = customInputs[label as keyof typeof customInputs]; return acc; }, {}); return { @@ -46,11 +45,7 @@ function getResponsesFromOldBooking( }; } -async function getBooking( - prisma: PrismaClient, - uid: string, - isSeatedEvent?: boolean -) { +async function getBooking(prisma: PrismaClient, uid: string, isSeatedEvent?: boolean) { const rawBooking = await prisma.booking.findUnique({ where: { uid, @@ -102,12 +97,8 @@ async function getBooking( if (booking) { // @NOTE: had to do this because Server side cant return [Object objects] // probably fixable with json.stringify -> json.parse - booking["startTime"] = ( - booking?.startTime as Date - )?.toISOString() as unknown as Date; - booking["endTime"] = ( - booking?.endTime as Date - )?.toISOString() as unknown as Date; + booking["startTime"] = (booking?.startTime as Date)?.toISOString() as unknown as Date; + booking["endTime"] = (booking?.endTime as Date)?.toISOString() as unknown as Date; } return booking; @@ -120,16 +111,14 @@ export const getBookingWithResponses = < select: BookingSelect & { responses: true; }; - }> + }>, >( booking: T, isSeatedEvent?: boolean ) => { return { ...booking, - responses: isSeatedEvent - ? booking.responses - : booking.responses || getResponsesFromOldBooking(booking), + responses: isSeatedEvent ? booking.responses : booking.responses || getResponsesFromOldBooking(booking), } as Omit & { responses: Record }; }; @@ -211,16 +200,10 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => { // If we have the booking and not bookingSeat, we need to make sure the booking belongs to the userLoggedIn // Otherwise, we return null here. let hasOwnershipOnBooking = false; - if ( - theBooking && - theBooking?.eventType?.seatsPerTimeSlot && - bookingSeatReferenceUid === null - ) { + if (theBooking && theBooking?.eventType?.seatsPerTimeSlot && bookingSeatReferenceUid === null) { const isOwnerOfBooking = theBooking.userId === userId; - const isHostOfEventType = theBooking?.eventType?.hosts.some( - (host) => host.userId === userId - ); + const isHostOfEventType = theBooking?.eventType?.hosts.some((host) => host.userId === userId); const isUserIdInBooking = theBooking.userId === userId; @@ -235,13 +218,7 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => { }); } - if ( - !isOwnerOfBooking && - !isHostOfEventType && - !isUserIdInBooking && - !hasOrgAccess - ) - return null; + if (!isOwnerOfBooking && !isHostOfEventType && !isUserIdInBooking && !hasOrgAccess) return null; hasOwnershipOnBooking = true; } @@ -249,27 +226,21 @@ export const getBookingForReschedule = async (uid: string, userId?: number) => { // and we return null here. if (!theBooking && !rescheduleUid) return null; - const booking = await getBooking( - prisma, - rescheduleUid || uid, - bookingSeatReferenceUid ? true : false - ); + const booking = await getBooking(prisma, rescheduleUid || uid, bookingSeatReferenceUid ? true : false); if (!booking) return null; if (bookingSeatReferenceUid) { booking["description"] = bookingSeatData?.description ?? null; - booking["responses"] = bookingResponsesDbSchema.parse( - bookingSeatData?.responses ?? {} - ); + booking["responses"] = bookingResponsesDbSchema.parse(bookingSeatData?.responses ?? {}); } return { ...booking, attendees: rescheduleUid ? booking.attendees.filter((attendee) => attendee.email === attendeeEmail) : hasOwnershipOnBooking - ? [] - : booking.attendees, + ? [] + : booking.attendees, }; }; @@ -349,7 +320,6 @@ export const getMultipleDurationValue = ( defaultValue: number ) => { if (!multipleDurationConfig) return null; - if (multipleDurationConfig.includes(Number(queryDuration))) - return Number(queryDuration); + if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration); return defaultValue; }; diff --git a/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts b/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts index e8ccf58b09..3d6e48722b 100644 --- a/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts +++ b/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts @@ -11,9 +11,11 @@ import { UserRepository } from "@calcom/features/users/repositories/UserReposito vi.mock("@calcom/features/users/repositories/UserRepository", () => { return { - UserRepository: vi.fn().mockImplementation(function() { return { - enrichUserWithItsProfile: vi.fn(), - }; }), + UserRepository: vi.fn().mockImplementation(function () { + return { + enrichUserWithItsProfile: vi.fn(), + }; + }), }; }); diff --git a/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts b/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts index 46cb8f8a09..a9d0961fd0 100644 --- a/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts +++ b/packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts @@ -24,7 +24,7 @@ export const getAllCredentialsIncludeServiceAccountKey = async ( eventType: EventType ) => { let allCredentials = Array.isArray(user.credentials) ? user.credentials : []; - + if (eventType?.team?.id) { const teamCredentialsQuery = await prisma.credential.findMany({ where: { @@ -33,10 +33,10 @@ export const getAllCredentialsIncludeServiceAccountKey = async ( select: credentialForCalendarServiceSelect, }); if (Array.isArray(teamCredentialsQuery)) { - allCredentials.push(...teamCredentialsQuery); + allCredentials.push(...teamCredentialsQuery); } } - + if (eventType?.parentId) { const teamCredentialsQuery = await prisma.team.findFirst({ where: { @@ -60,7 +60,7 @@ export const getAllCredentialsIncludeServiceAccountKey = async ( const { profile } = await new UserRepository(prisma).enrichUserWithItsProfile({ user: user, }); - + if (profile?.organizationId) { const org = await prisma.team.findUnique({ where: { diff --git a/packages/features/bookings/lib/getAssignmentReasonCategory.test.ts b/packages/features/bookings/lib/getAssignmentReasonCategory.test.ts index 423de32c05..27dfa5b2b0 100644 --- a/packages/features/bookings/lib/getAssignmentReasonCategory.test.ts +++ b/packages/features/bookings/lib/getAssignmentReasonCategory.test.ts @@ -26,7 +26,9 @@ describe("getAssignmentReasonCategory", () => { }); it("returns 'salesforce_assigned' for SALESFORCE_ASSIGNMENT", () => { - expect(getAssignmentReasonCategory(AssignmentReasonEnum.SALESFORCE_ASSIGNMENT)).toBe("salesforce_assigned"); + expect(getAssignmentReasonCategory(AssignmentReasonEnum.SALESFORCE_ASSIGNMENT)).toBe( + "salesforce_assigned" + ); }); it("returns 'routed' for unknown enum values (default case)", () => { diff --git a/packages/features/bookings/lib/getBookingFields.ts b/packages/features/bookings/lib/getBookingFields.ts index b572cb797c..59a9e4bdd7 100644 --- a/packages/features/bookings/lib/getBookingFields.ts +++ b/packages/features/bookings/lib/getBookingFields.ts @@ -36,7 +36,7 @@ export const getSmsReminderNumberField = () => defaultLabel: "number_text_notifications", defaultPlaceholder: "enter_phone_number", editable: "system", - } as const); + }) as const; export const getSmsReminderNumberSource = ({ workflowId, @@ -59,7 +59,7 @@ export const getAIAgentCallPhoneNumberField = () => defaultLabel: "phone_number_for_ai_call", defaultPlaceholder: "enter_phone_number", editable: "system", - } as const); + }) as const; export const getAIAgentCallPhoneNumberSource = ({ workflowId, diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts index dde596199f..a96f65b21a 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.test.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.test.ts @@ -1876,7 +1876,7 @@ describe("require email/domain validation", () => { ] as z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">, view: "ALL_VIEWS", }); - + const parsedResponses = await schema.safeParseAsync({ name: "John Doe", email: "john@example.com", diff --git a/packages/features/bookings/lib/getBookingResponsesSchema.ts b/packages/features/bookings/lib/getBookingResponsesSchema.ts index d9fff69530..8f06060bc2 100644 --- a/packages/features/bookings/lib/getBookingResponsesSchema.ts +++ b/packages/features/bookings/lib/getBookingResponsesSchema.ts @@ -6,19 +6,11 @@ import { fieldTypesSchemaMap } from "@calcom/features/form-builder/schema"; import { dbReadResponseSchema } from "@calcom/lib/dbReadResponseSchema"; import logger from "@calcom/lib/logger"; import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils"; -import { - bookingResponses, - emailSchemaRefinement, -} from "@calcom/prisma/zod-utils"; +import { bookingResponses, emailSchemaRefinement } from "@calcom/prisma/zod-utils"; type View = ALL_VIEWS | (string & {}); -type BookingFields = - | (z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">) - | null; -type TranslationFunction = ( - key: string, - options?: Record -) => string; +type BookingFields = (z.infer & z.BRAND<"HAS_SYSTEM_FIELDS">) | null; +type TranslationFunction = (key: string, options?: Record) => string; type CommonParams = { bookingFields: BookingFields; view: View; @@ -58,11 +50,7 @@ const doesEmailMatchEntry = (bookerEmail: string, entry: string): boolean => { return bookerEmailLower.endsWith("@" + entry.toLowerCase()); }; -export const getBookingResponsesPartialSchema = ({ - bookingFields, - view, - translateFn, -}: CommonParams) => { +export const getBookingResponsesPartialSchema = ({ bookingFields, view, translateFn }: CommonParams) => { const schema = bookingResponses.unwrap().partial().and(catchAllSchema); return preprocess({ @@ -77,11 +65,7 @@ export const getBookingResponsesPartialSchema = ({ // Should be used when we know that not all fields responses are present // - Can happen when we are parsing the prefill query string // - Can happen when we are parsing a booking's responses (which was created before we added a new required field) -export default function getBookingResponsesSchema({ - bookingFields, - view, - translateFn, -}: CommonParams) { +export default function getBookingResponsesSchema({ bookingFields, view, translateFn }: CommonParams) { const schema = bookingResponses.and(z.record(z.any())); return preprocess({ schema, @@ -129,8 +113,7 @@ function preprocess({ const log = logger.getSubLogger({ prefix: ["getBookingResponsesSchema"] }); const preprocessed = z.preprocess( (responses) => { - const parsedResponses = - z.record(z.any()).nullable().parse(responses) || {}; + const parsedResponses = z.record(z.any()).nullable().parse(responses) || {}; const newResponses = {} as typeof parsedResponses; // if eventType has been deleted, we won't have bookingFields and thus we can't preprocess or validate them. if (!bookingFields) return parsedResponses; @@ -142,17 +125,12 @@ function preprocess({ } const views = field.views; const isFieldApplicableToCurrentView = - currentView === "ALL_VIEWS" - ? true - : views - ? views.find((view) => view.id === currentView) - : true; + currentView === "ALL_VIEWS" ? true : views ? views.find((view) => view.id === currentView) : true; if (!isFieldApplicableToCurrentView) { // If the field is not applicable in the current view, then we don't need to do any processing return; } - const fieldTypeSchema = - fieldTypesSchemaMap[field.type as keyof typeof fieldTypesSchemaMap]; + const fieldTypeSchema = fieldTypesSchemaMap[field.type as keyof typeof fieldTypesSchemaMap]; // TODO: Move all the schemas along with their respective types to fieldTypeSchema, that would make schemas shared across Routing Forms builder and Booking Question Formm builder if (fieldTypeSchema) { newResponses[field.name] = fieldTypeSchema.preprocess({ @@ -167,11 +145,7 @@ function preprocess({ newResponses[field.name] = value === "true" || value === true; } // Make sure that the value is an array - else if ( - field.type === "multiemail" || - field.type === "checkbox" || - field.type === "multiselect" - ) { + else if (field.type === "multiemail" || field.type === "checkbox" || field.type === "multiselect") { newResponses[field.name] = value instanceof Array ? value : [value]; } // Parse JSON @@ -183,17 +157,12 @@ function preprocess({ try { parsedValue = JSON.parse(value); } catch (e) { - log.error( - `Failed to parse JSON for field ${field.name}`, - e - ); + log.error(`Failed to parse JSON for field ${field.name}`, e); } const optionsInputs = field.optionsInputs; const optionInputField = optionsInputs?.[parsedValue.value]; if (optionInputField && optionInputField.type === "phone") { - parsedValue.optionValue = ensureValidPhoneNumber( - parsedValue.optionValue - ); + parsedValue.optionValue = ensureValidPhoneNumber(parsedValue.optionValue); } newResponses[field.name] = parsedValue; } else if (field.type === "phone") { @@ -214,9 +183,7 @@ function preprocess({ return; } - const attendeePhoneNumberField = bookingFields.find( - (field) => field.name === "attendeePhoneNumber" - ); + const attendeePhoneNumberField = bookingFields.find((field) => field.name === "attendeePhoneNumber"); const isAttendeePhoneNumberFieldHidden = attendeePhoneNumberField?.hidden; const emailField = bookingFields.find((field) => field.name === "email"); @@ -230,9 +197,7 @@ function preprocess({ for (const bookingField of bookingFields) { const value = responses[bookingField.name]; const stringSchema = z.string(); - const emailSchema = isPartialSchema - ? z.string() - : z.string().refine(emailSchemaRefinement); + const emailSchema = isPartialSchema ? z.string() : z.string().refine(emailSchemaRefinement); const phoneSchema = isPartialSchema ? z.string() : z.string().refine(async (val) => { @@ -240,18 +205,12 @@ function preprocess({ }); // Tag the message with the input name so that the message can be shown at appropriate place const m = (message: string, options?: Record) => { - const translatedMessage = translateFn - ? translateFn(message, options) - : message; + const translatedMessage = translateFn ? translateFn(message, options) : message; return `{${bookingField.name}}${translatedMessage}`; }; const views = bookingField.views; const isFieldApplicableToCurrentView = - currentView === "ALL_VIEWS" - ? true - : views - ? views.find((view) => view.id === currentView) - : true; + currentView === "ALL_VIEWS" ? true : views ? views.find((view) => view.id === currentView) : true; let hidden = bookingField.hidden; const numOptions = bookingField.options?.length ?? 0; if (bookingField.hideWhenJustOneOption) { @@ -276,10 +235,7 @@ function preprocess({ } if (bookingField.type === "email") { - if ( - !bookingField.hidden && - (isRequired || (value && value.trim() !== "")) - ) { + if (!bookingField.hidden && (isRequired || (value && value.trim() !== ""))) { // Email RegExp to validate if the input is a valid email if (!emailSchema.safeParse(value).success) { ctx.addIssue({ @@ -292,9 +248,7 @@ function preprocess({ if (value) { const bookerEmail = value; const excludedEmails = - bookingField.excludeEmails - ?.split(",") - .map((domain) => domain.trim()) || []; + bookingField.excludeEmails?.split(",").map((domain) => domain.trim()) || []; const match = excludedEmails.find((excludedEntry) => doesEmailMatchEntry(bookerEmail, excludedEntry) @@ -325,10 +279,7 @@ function preprocess({ continue; } - const fieldTypeSchema = - fieldTypesSchemaMap[ - bookingField.type as keyof typeof fieldTypesSchemaMap - ]; + const fieldTypeSchema = fieldTypesSchemaMap[bookingField.type as keyof typeof fieldTypesSchemaMap]; if (fieldTypeSchema) { fieldTypeSchema.superRefine({ @@ -354,10 +305,7 @@ function preprocess({ if (!emailsParsed.success) { // If additional guests are shown but all inputs are empty then don't show any errors - if ( - bookingField.name === "guests" && - value.every((email: string) => email === "") - ) { + if (bookingField.name === "guests" && value.every((email: string) => email === "")) { // reset guests to empty array, otherwise it adds "" for every input responses[bookingField.name] = []; continue; @@ -443,9 +391,7 @@ function preprocess({ const typeOfOptionInput = optionField?.type; if ( // Either the field is required or there is a radio selected, we need to check if the optionInput is required or not. - (isRequired || value?.value) && checkOptional - ? true - : optionField?.required && !optionValue + (isRequired || value?.value) && checkOptional ? true : optionField?.required && !optionValue ) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -471,11 +417,7 @@ function preprocess({ // Use fieldTypeConfig.propsType to validate for propsType=="text" or propsType=="select" as in those cases, the response would be a string. // If say we want to do special validation for 'address' that can be added to `fieldTypesSchemaMap` - if ( - ["address", "text", "select", "number", "radio", "textarea"].includes( - bookingField.type - ) - ) { + if (["address", "text", "select", "number", "radio", "textarea"].includes(bookingField.type)) { const schema = stringSchema; if (!schema.safeParse(value).success) { ctx.addIssue({ @@ -496,10 +438,7 @@ function preprocess({ if (isPartialSchema) { // Query Params can be completely invalid, try to preprocess as much of it in correct format but in worst case simply don't prefill instead of crashing return preprocessed.catch(function (res?: { error?: unknown[] }) { - console.error( - "Failed to preprocess query params, prefilling will be skipped", - res?.error - ); + console.error("Failed to preprocess query params, prefilling will be skipped", res?.error); return {}; }); } diff --git a/packages/features/bookings/lib/getHostsAndGuests.ts b/packages/features/bookings/lib/getHostsAndGuests.ts index 1b1dd22599..c37aa7dd40 100644 --- a/packages/features/bookings/lib/getHostsAndGuests.ts +++ b/packages/features/bookings/lib/getHostsAndGuests.ts @@ -40,10 +40,12 @@ export function getHostsAndGuests(booking: BookingInput): { hosts: Host[]; guest const hostEmails = new Set(filteredHosts.map((host) => host.email)); const guests = - booking.attendees?.filter((attendee) => !hostEmails.has(attendee.email)).map((attendee) => ({ - email: attendee.email, - name: attendee.name, - })) ?? []; + booking.attendees + ?.filter((attendee) => !hostEmails.has(attendee.email)) + .map((attendee) => ({ + email: attendee.email, + name: attendee.name, + })) ?? []; return { hosts: filteredHosts, diff --git a/packages/features/bookings/lib/getLuckyUser.ts b/packages/features/bookings/lib/getLuckyUser.ts index 45d91bd9e4..6c60fe7784 100644 --- a/packages/features/bookings/lib/getLuckyUser.ts +++ b/packages/features/bookings/lib/getLuckyUser.ts @@ -370,7 +370,7 @@ export class LuckyUserService implements ILuckyUserService { private filterUsersBasedOnWeights< T extends PartialUser & { weight?: number | null; - } + }, >({ availableUsers, bookingsOfAvailableUsersOfInterval, @@ -493,7 +493,7 @@ export class LuckyUserService implements ILuckyUserService { T extends PartialUser & { priority?: number | null; weight?: number | null; - } + }, >( allRRHosts: GetLuckyUserParams["allRRHosts"], attributesQueryValueChild: Record< @@ -693,15 +693,18 @@ export class LuckyUserService implements ILuckyUserService { ) ); - return usersBusyTimesQuery.reduce((usersBusyTime, userBusyTimeQuery, index) => { - if (userBusyTimeQuery.success) { - usersBusyTime.push({ - userId: usersWithCredentials[index].id, - busyTimes: userBusyTimeQuery.data, - }); - } - return usersBusyTime; - }, [] as { userId: number; busyTimes: Awaited>["data"] }[]); + return usersBusyTimesQuery.reduce( + (usersBusyTime, userBusyTimeQuery, index) => { + if (userBusyTimeQuery.success) { + usersBusyTime.push({ + userId: usersWithCredentials[index].id, + busyTimes: userBusyTimeQuery.data, + }); + } + return usersBusyTime; + }, + [] as { userId: number; busyTimes: Awaited>["data"] }[] + ); } private async getBookingsOfInterval({ @@ -736,7 +739,7 @@ export class LuckyUserService implements ILuckyUserService { T extends PartialUser & { priority?: number | null; weight?: number | null; - } + }, >(getLuckyUserParams: GetLuckyUserParams): Promise { const startTime = performance.now(); @@ -763,9 +766,8 @@ export class LuckyUserService implements ILuckyUserService { ); })(); - const { attributeWeights, virtualQueuesData } = await this.prepareQueuesAndAttributesData( - getLuckyUserParams - ); + const { attributeWeights, virtualQueuesData } = + await this.prepareQueuesAndAttributesData(getLuckyUserParams); const interval = eventType.isRRWeightsEnabled && getLuckyUserParams.eventType.team?.rrResetInterval @@ -917,7 +919,7 @@ export class LuckyUserService implements ILuckyUserService { T extends PartialUser & { priority?: number | null; weight?: number | null; - } + }, >(getLuckyUserParams: GetLuckyUserParams) { // Early return if only one available user to avoid unnecessary data fetching if (getLuckyUserParams.availableUsers.length === 1) { @@ -938,7 +940,7 @@ export class LuckyUserService implements ILuckyUserService { T extends PartialUser & { priority?: number | null; weight?: number | null; - } + }, >({ availableUsers, ...getLuckyUserParams }: GetLuckyUserParams & FetchedData) { const { eventType, diff --git a/packages/features/bookings/lib/handleNewBooking/createBooking.ts b/packages/features/bookings/lib/handleNewBooking/createBooking.ts index e33790cec9..5c8011d4d6 100644 --- a/packages/features/bookings/lib/handleNewBooking/createBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking/createBooking.ts @@ -143,7 +143,14 @@ async function saveBooking( const createBookingObj = { include: { user: { - select: { uuid: true, email: true, name: true, timeZone: true, username: true, isPlatformManaged: true }, + select: { + uuid: true, + email: true, + name: true, + timeZone: true, + username: true, + isPlatformManaged: true, + }, }, attendees: true, payment: true, @@ -258,8 +265,8 @@ function buildNewBookingData(params: CreateBookingParams) { destinationCalendar: evt.destinationCalendar && evt.destinationCalendar.length > 0 ? { - connect: { id: evt.destinationCalendar[0].id }, - } + connect: { id: evt.destinationCalendar[0].id }, + } : undefined, routedFromRoutingFormReponse: routingFormResponseId diff --git a/packages/features/bookings/lib/handleNewBooking/getAuditActionSource.ts b/packages/features/bookings/lib/handleNewBooking/getAuditActionSource.ts index c61e0d37db..5b327d1d88 100644 --- a/packages/features/bookings/lib/handleNewBooking/getAuditActionSource.ts +++ b/packages/features/bookings/lib/handleNewBooking/getAuditActionSource.ts @@ -2,14 +2,26 @@ import { CreationSource } from "@calcom/prisma/enums"; import type { ActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; import { criticalLogger } from "@calcom/lib/logger.server"; -export const getAuditActionSource = ({ creationSource, eventTypeId, rescheduleUid }: { creationSource: CreationSource | null | undefined, eventTypeId: number, rescheduleUid: string | null }): ActionSource => { - if (creationSource === CreationSource.API_V1 || creationSource === CreationSource.API_V2 || creationSource === CreationSource.WEBAPP) { - return creationSource; - } - // Unknown creationSource - log for tracking and fix - criticalLogger.warn("Unknown booking creationSource detected", { - eventTypeId, - rescheduleUid, - }); - return "UNKNOWN"; -}; \ No newline at end of file +export const getAuditActionSource = ({ + creationSource, + eventTypeId, + rescheduleUid, +}: { + creationSource: CreationSource | null | undefined; + eventTypeId: number; + rescheduleUid: string | null; +}): ActionSource => { + if ( + creationSource === CreationSource.API_V1 || + creationSource === CreationSource.API_V2 || + creationSource === CreationSource.WEBAPP + ) { + return creationSource; + } + // Unknown creationSource - log for tracking and fix + criticalLogger.warn("Unknown booking creationSource detected", { + eventTypeId, + rescheduleUid, + }); + return "UNKNOWN"; +}; diff --git a/packages/features/bookings/lib/handleNewBooking/getCustomInputsResponses.ts b/packages/features/bookings/lib/handleNewBooking/getCustomInputsResponses.ts index c790836ebe..1c9df0c727 100644 --- a/packages/features/bookings/lib/handleNewBooking/getCustomInputsResponses.ts +++ b/packages/features/bookings/lib/handleNewBooking/getCustomInputsResponses.ts @@ -16,10 +16,13 @@ type RequestBody = { function mapCustomInputs( customInputs: { label: string; value: CustomInputs[number]["value"] }[] ): Record { - return customInputs.reduce((acc, { label, value }) => { - acc[label] = value; - return acc; - }, {} as Record); + return customInputs.reduce( + (acc, { label, value }) => { + acc[label] = value; + return acc; + }, + {} as Record + ); } function mapResponsesToCustomInputs( @@ -27,13 +30,16 @@ function mapResponsesToCustomInputs( eventTypeCustomInputs: getEventTypeResponse["customInputs"] ): NonNullable { // Backward Compatibility: Map new `responses` to old `customInputs` format so that webhooks can still receive same values. - return Object.entries(responses).reduce((acc, [fieldName, fieldValue]) => { - const foundInput = eventTypeCustomInputs.find((input) => slugify(input.label) === fieldName); - if (foundInput) { - acc[foundInput.label] = fieldValue; - } - return acc; - }, {} as NonNullable); + return Object.entries(responses).reduce( + (acc, [fieldName, fieldValue]) => { + const foundInput = eventTypeCustomInputs.find((input) => slugify(input.label) === fieldName); + if (foundInput) { + acc[foundInput.label] = fieldValue; + } + return acc; + }, + {} as NonNullable + ); } export function getCustomInputsResponses( diff --git a/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts index 743d76e30c..0eedda6a1a 100644 --- a/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts +++ b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts @@ -20,7 +20,7 @@ export const _getLocationValuesForDb = < username: string | null; metadata: Prisma.JsonValue; credentials: CredentialForCalendarService[]; - } + }, >({ dynamicUserList, users, diff --git a/packages/features/bookings/lib/handleNewBooking/handleAppsStatus.ts b/packages/features/bookings/lib/handleNewBooking/handleAppsStatus.ts index b90237ca75..c96d3719df 100644 --- a/packages/features/bookings/lib/handleNewBooking/handleAppsStatus.ts +++ b/packages/features/bookings/lib/handleNewBooking/handleAppsStatus.ts @@ -46,16 +46,19 @@ function calculateAggregatedAppsStatus( // From down here we can assume reqAppsStatus is not undefined anymore // Other status exist, so this is the last booking of a series, // proceeding to prepare the info for the event - const aggregatedStatus = reqAppsStatus.concat(resultStatus).reduce((acc, curr) => { - if (acc[curr.type]) { - acc[curr.type].success += curr.success; - acc[curr.type].errors = acc[curr.type].errors.concat(curr.errors); - acc[curr.type].warnings = acc[curr.type].warnings?.concat(curr.warnings || []); - } else { - acc[curr.type] = curr; - } - return acc; - }, {} as { [key: string]: AppsStatus }); + const aggregatedStatus = reqAppsStatus.concat(resultStatus).reduce( + (acc, curr) => { + if (acc[curr.type]) { + acc[curr.type].success += curr.success; + acc[curr.type].errors = acc[curr.type].errors.concat(curr.errors); + acc[curr.type].warnings = acc[curr.type].warnings?.concat(curr.warnings || []); + } else { + acc[curr.type] = curr; + } + return acc; + }, + {} as { [key: string]: AppsStatus } + ); return Object.values(aggregatedStatus); } diff --git a/packages/features/bookings/lib/handleNewBooking/test/buildEventForTeamEventType.test.ts b/packages/features/bookings/lib/handleNewBooking/test/buildEventForTeamEventType.test.ts index d61f58c267..1a8b6e68e6 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/buildEventForTeamEventType.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/buildEventForTeamEventType.test.ts @@ -25,7 +25,7 @@ vi.mock("@calcom/features/CalendarEventBuilder", () => { fromEvent: vi.fn().mockImplementation((_evt) => ({ withDestinationCalendar: withDestinationCalendarSpy, withTeam: withTeamSpy, - build: vi.fn().mockImplementation(function() { + build: vi.fn().mockImplementation(function () { return { destinationCalendar: [], team: {}, // <- you won't use this result anyway 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 7b71cd8e50..8ee00d1d73 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -88,7 +88,10 @@ async function mockPaymentSuccessWebhookFromStripe({ externalId }: { externalId: let webhookResponse = null; try { const traceContext = distributedTracing.createTrace("test_stripe_webhook"); - await handleStripePaymentSuccess(getMockedStripePaymentEvent({ paymentIntentId: externalId }), traceContext); + await handleStripePaymentSuccess( + getMockedStripePaymentEvent({ paymentIntentId: externalId }), + traceContext + ); } catch (e) { log.silly("mockPaymentSuccessWebhookFromStripe:catch", JSON.stringify(e)); webhookResponse = e as HttpError; @@ -1392,7 +1395,11 @@ describe("handleNewBooking", () => { }, ], organizer, - apps: [TestData.apps["google-meet"], TestData.apps["daily-video"], TestData.apps["office365-calendar"]], + apps: [ + TestData.apps["google-meet"], + TestData.apps["daily-video"], + TestData.apps["office365-calendar"], + ], }); mockSuccessfulVideoMeetingCreation({ @@ -1481,7 +1488,11 @@ describe("handleNewBooking", () => { }, ], organizer, - apps: [TestData.apps["google-calendar"], TestData.apps["google-meet"], TestData.apps["daily-video"]], + apps: [ + TestData.apps["google-calendar"], + TestData.apps["google-meet"], + TestData.apps["daily-video"], + ], }); mockSuccessfulVideoMeetingCreation({ diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts index b4017dec06..6998a459ee 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/round-robin.test.ts @@ -1089,154 +1089,151 @@ describe("Round Robin handleNewBooking", () => { }); describe("Round Robin with requiresConfirmation", () => { - test( - "should not create calendar events for unconfirmed round robin bookings on first booking and reschedule", - async () => { - const handleNewBooking = getNewBookingHandler(); - const booker = getBooker({ - email: "booker@example.com", - name: "Booker", - }); + test("should not create calendar events for unconfirmed round robin bookings on first booking and reschedule", async () => { + const handleNewBooking = getNewBookingHandler(); + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); - const roundRobinHost1 = getOrganizer({ - name: "RR Host 1", - email: "rrhost1@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - destinationCalendar: { - integration: TestData.apps["google-calendar"].type, - externalId: "rrhost1@google-calendar.com", - }, - }); + const roundRobinHost1 = getOrganizer({ + name: "RR Host 1", + email: "rrhost1@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "rrhost1@google-calendar.com", + }, + }); - const roundRobinHost2 = getOrganizer({ - name: "RR Host 2", - email: "rrhost2@example.com", - id: 102, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - destinationCalendar: { - integration: TestData.apps["google-calendar"].type, - externalId: "rrhost2@google-calendar.com", - }, - }); + const roundRobinHost2 = getOrganizer({ + name: "RR Host 2", + email: "rrhost2@example.com", + id: 102, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "rrhost2@google-calendar.com", + }, + }); - const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); - await createBookingScenario( - getScenarioData({ - eventTypes: [ - { - id: 1, - slotInterval: 15, - length: 15, - requiresConfirmation: true, - schedulingType: SchedulingType.ROUND_ROBIN, - users: [ - { - id: 101, - }, - { - id: 102, - }, - ], - hosts: [ - { userId: 101, isFixed: false }, - { userId: 102, isFixed: false }, - ], - schedule: TestData.schedules.IstWorkHours, - }, - ], - organizer: roundRobinHost1, - usersApartFromOrganizer: [roundRobinHost2], - apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], - }) - ); - - // Mock calendar - we should NOT see calendar events created for unconfirmed bookings - mockCalendarToHaveNoBusySlots("googlecalendar", { - create: { - uid: "MOCK_ID", - iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", - }, - }); - - // First booking with first host - should be PENDING and NO calendar events - const firstBookingData = getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - user: roundRobinHost1.name, - start: `${plus1DateString}T05:00:00.000Z`, - end: `${plus1DateString}T05:15:00.000Z`, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.CalVideo }, + await createBookingScenario( + getScenarioData({ + eventTypes: [ + { + id: 1, + slotInterval: 15, + length: 15, + requiresConfirmation: true, + schedulingType: SchedulingType.ROUND_ROBIN, + users: [ + { + id: 101, + }, + { + id: 102, + }, + ], + hosts: [ + { userId: 101, isFixed: false }, + { userId: 102, isFixed: false }, + ], + schedule: TestData.schedules.IstWorkHours, }, + ], + organizer: roundRobinHost1, + usersApartFromOrganizer: [roundRobinHost2], + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + // Mock calendar - we should NOT see calendar events created for unconfirmed bookings + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + uid: "MOCK_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + // First booking with first host - should be PENDING and NO calendar events + const firstBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + user: roundRobinHost1.name, + start: `${plus1DateString}T05:00:00.000Z`, + end: `${plus1DateString}T05:15:00.000Z`, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, }, - }); + }, + }); - const firstBooking = await handleNewBooking({ - bookingData: firstBookingData, - }); + const firstBooking = await handleNewBooking({ + bookingData: firstBookingData, + }); - // Verify first booking is PENDING - expect(firstBooking.status).toBe(BookingStatus.PENDING); + // Verify first booking is PENDING + expect(firstBooking.status).toBe(BookingStatus.PENDING); - // Verify first booking has NO calendar references (no calendar events created) - const firstBookingInDb = await prisma.booking.findUnique({ - where: { - id: firstBooking.id, + // Verify first booking has NO calendar references (no calendar events created) + const firstBookingInDb = await prisma.booking.findUnique({ + where: { + id: firstBooking.id, + }, + include: { + references: true, + }, + }); + + expect(firstBookingInDb?.references).toHaveLength(0); + expect(firstBookingInDb?.status).toBe(BookingStatus.PENDING); + + // Now reschedule with second host - should still be PENDING and NO calendar events + const rescheduleBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + user: roundRobinHost2.name, + rescheduleUid: firstBooking.uid, + start: `${plus1DateString}T06:00:00.000Z`, + end: `${plus1DateString}T06:15:00.000Z`, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, }, - include: { - references: true, - }, - }); + rescheduledBy: booker.email, + }, + }); - expect(firstBookingInDb?.references).toHaveLength(0); - expect(firstBookingInDb?.status).toBe(BookingStatus.PENDING); + const rescheduledBooking = await handleNewBooking({ + bookingData: rescheduleBookingData, + }); - // Now reschedule with second host - should still be PENDING and NO calendar events - const rescheduleBookingData = getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - user: roundRobinHost2.name, - rescheduleUid: firstBooking.uid, - start: `${plus1DateString}T06:00:00.000Z`, - end: `${plus1DateString}T06:15:00.000Z`, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: BookingLocations.CalVideo }, - }, - rescheduledBy: booker.email, - }, - }); + // Verify rescheduled booking is still PENDING + expect(rescheduledBooking.status).toBe(BookingStatus.PENDING); - const rescheduledBooking = await handleNewBooking({ - bookingData: rescheduleBookingData, - }); + // Verify rescheduled booking has NO calendar references (no calendar events created) + // This is the key fix: rescheduling unconfirmed bookings should NOT create calendar events + const rescheduledBookingInDb = await prisma.booking.findUnique({ + where: { + id: rescheduledBooking.id, + }, + include: { + references: true, + }, + }); - // Verify rescheduled booking is still PENDING - expect(rescheduledBooking.status).toBe(BookingStatus.PENDING); - - // Verify rescheduled booking has NO calendar references (no calendar events created) - // This is the key fix: rescheduling unconfirmed bookings should NOT create calendar events - const rescheduledBookingInDb = await prisma.booking.findUnique({ - where: { - id: rescheduledBooking.id, - }, - include: { - references: true, - }, - }); - - expect(rescheduledBookingInDb?.references).toHaveLength(0); - expect(rescheduledBookingInDb?.status).toBe(BookingStatus.PENDING); - } - ); + expect(rescheduledBookingInDb?.references).toHaveLength(0); + expect(rescheduledBookingInDb?.status).toBe(BookingStatus.PENDING); + }); }); }); diff --git a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts index df638c2960..5b7489d406 100644 --- a/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts +++ b/packages/features/bookings/lib/handleNewBooking/validateBookingTimeIsNotOutOfBounds.ts @@ -39,7 +39,7 @@ const _validateBookingTimeIsNotOutOfBounds = async unknown } => +const isPaymentService = ( + x: unknown +): x is { BuildPaymentService: (credentials: { key: unknown }) => unknown } => !!x && typeof x === "object" && "BuildPaymentService" in x && typeof x.BuildPaymentService === "function"; const handlePayment = async ({ @@ -141,8 +143,8 @@ const handlePayment = async ({ const selectedValues = Array.isArray(responseValue) ? responseValue : responseValue - ? [responseValue] - : []; + ? [responseValue] + : []; selectedValues.forEach((value) => { const option = typedInput.options?.find((opt) => opt.value === value); diff --git a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts index 638469a7c5..0360b2fca0 100644 --- a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts +++ b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts @@ -18,11 +18,7 @@ import { BookingStatus } from "@calcom/prisma/enums"; import { findBookingQuery } from "../../handleNewBooking/findBookingQuery"; import type { IEventTypePaymentCredentialType } from "../../handleNewBooking/types"; -import type { - SeatedBooking, - NewSeatedBookingObject, - HandleSeatsResultBooking, -} from "../types"; +import type { SeatedBooking, NewSeatedBookingObject, HandleSeatsResultBooking } from "../types"; export type AddSeatInput = { bookingUid: string; @@ -48,10 +44,7 @@ export type AddSeatInput = { * Uses a transaction with a fresh read to prevent TOCTOU race conditions * where concurrent requests could exceed the seat limit. */ -export async function addSeatToBooking( - input: AddSeatInput, - prismaClient: PrismaClient = prisma -) { +export async function addSeatToBooking(input: AddSeatInput, prismaClient: PrismaClient = prisma) { const referenceUid = uuid(); return prismaClient.$transaction(async (tx) => { @@ -80,9 +73,7 @@ export async function addSeatToBooking( // Check seat availability with fresh data // Only enforce the limit when seatsPerTimeSlot > 0 (matching original behavior // where falsy seatsPerTimeSlot would skip this check entirely) - const currentSeatCount = freshBooking.attendees.filter( - (attendee) => !!attendee.bookingSeat - ).length; + const currentSeatCount = freshBooking.attendees.filter((attendee) => !!attendee.bookingSeat).length; if (input.seatsPerTimeSlot > 0 && input.seatsPerTimeSlot <= currentSeatCount) { throw new HttpError({ statusCode: 409, @@ -159,9 +150,7 @@ const createNewSeat = async ( }; }); - const videoCallReference = seatedBooking.references.find((reference) => - reference.type.includes("_video") - ); + const videoCallReference = seatedBooking.references.find((reference) => reference.type.includes("_video")); if (videoCallReference) { evt.videoCallData = { @@ -218,20 +207,16 @@ const createNewSeat = async ( let isHostConfirmationEmailsDisabled = false; let isAttendeeConfirmationEmailDisabled = false; - isHostConfirmationEmailsDisabled = - eventType.metadata?.disableStandardEmails?.confirmation?.host || false; + isHostConfirmationEmailsDisabled = eventType.metadata?.disableStandardEmails?.confirmation?.host || false; isAttendeeConfirmationEmailDisabled = - eventType.metadata?.disableStandardEmails?.confirmation?.attendee || - false; + eventType.metadata?.disableStandardEmails?.confirmation?.attendee || false; if (isHostConfirmationEmailsDisabled) { - isHostConfirmationEmailsDisabled = - allowDisablingHostConfirmationEmails(workflows); + isHostConfirmationEmailsDisabled = allowDisablingHostConfirmationEmails(workflows); } if (isAttendeeConfirmationEmailDisabled) { - isAttendeeConfirmationEmailDisabled = - allowDisablingAttendeeConfirmationEmails(workflows); + isAttendeeConfirmationEmailDisabled = allowDisablingAttendeeConfirmationEmails(workflows); } await sendScheduledSeatsEmailsAndSMS( copyEvent, @@ -244,27 +229,16 @@ const createNewSeat = async ( ); } const credentials = await refreshCredentials(allCredentials); - const apps = eventTypeAppMetadataOptionalSchema.parse( - eventType?.metadata?.apps - ); - const eventManager = new EventManager( - { ...organizerUser, credentials }, - apps - ); + const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps); + const eventManager = new EventManager({ ...organizerUser, credentials }, apps); await eventManager.updateCalendarAttendees(evt, seatedBooking); const foundBooking = await findBookingQuery(seatedBooking.id); - if ( - !Number.isNaN(paymentAppData.price) && - paymentAppData.price > 0 && - !!seatedBooking - ) { + if (!Number.isNaN(paymentAppData.price) && paymentAppData.price > 0 && !!seatedBooking) { const credentialPaymentAppCategories = await prisma.credential.findMany({ where: { - ...(paymentAppData.credentialId - ? { id: paymentAppData.credentialId } - : { userId: organizerUser.id }), + ...(paymentAppData.credentialId ? { id: paymentAppData.credentialId } : { userId: organizerUser.id }), app: { categories: { hasSome: ["payment"], @@ -283,11 +257,9 @@ const createNewSeat = async ( }, }); - const eventTypePaymentAppCredential = credentialPaymentAppCategories.find( - (credential) => { - return credential.appId === paymentAppData.appId; - } - ); + const eventTypePaymentAppCredential = credentialPaymentAppCategories.find((credential) => { + return credential.appId === paymentAppData.appId; + }); if (!eventTypePaymentAppCredential) { throw new HttpError({ @@ -313,8 +285,7 @@ const createNewSeat = async ( } : {}, }, - paymentAppCredentials: - eventTypePaymentAppCredential as IEventTypePaymentCredentialType, + paymentAppCredentials: eventTypePaymentAppCredential as IEventTypePaymentCredentialType, booking: seatedBooking, bookerName: fullName, bookerEmail, diff --git a/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts b/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts index 8da45bb2b3..f0bcf7bfe1 100644 --- a/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts +++ b/packages/features/bookings/lib/handleSeats/lib/lastAttendeeDeleteBooking.ts @@ -1,4 +1,3 @@ - import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import { getAllDelegationCredentialsForUserIncludeServiceAccountKey } from "@calcom/app-store/delegationCredential"; import { getDelegationCredentialOrFindRegularCredential } from "@calcom/app-store/delegationCredential"; diff --git a/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts b/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts index b2563373fd..2907fe8471 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts @@ -1,4 +1,3 @@ - import { cloneDeep } from "lodash"; import { sendRescheduledSeatEmailAndSMS } from "@calcom/emails/email-manager"; @@ -107,7 +106,11 @@ const attendeeRescheduleSeatedBooking = async ( ? addVideoCallDataToEvent(newTimeSlotBooking.references, copyEvent) : copyEvent; - await sendRescheduledSeatEmailAndSMS(copyEventWithVideoCallData, seatAttendee as Person, eventType.metadata); + await sendRescheduledSeatEmailAndSMS( + copyEventWithVideoCallData, + seatAttendee as Person, + eventType.metadata + ); const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => { return attendee.email !== bookerEmail; }); diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts index 85475ac823..f393c0fb11 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts @@ -1,4 +1,3 @@ - import { cloneDeep } from "lodash"; import { uuid } from "short-uuid"; diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts index d0994d60a5..69e8e6b8c3 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts @@ -1,4 +1,3 @@ - import { cloneDeep } from "lodash"; import { sendRescheduledEmailsAndSMS } from "@calcom/emails/email-manager"; @@ -13,7 +12,17 @@ import { handleAppsStatus } from "../../../handleNewBooking/handleAppsStatus"; import type { createLoggerWithEventDetails } from "../../../handleNewBooking/logger"; import type { SeatedBooking, RescheduleSeatedBookingObject } from "../../types"; -async function updateBooking({ bookingId, startTime, endTime, cancellationReason }: { bookingId: number, startTime: string, endTime: string, cancellationReason: string }): Promise<(Booking & { appsStatus?: AppsStatus[] })> { +async function updateBooking({ + bookingId, + startTime, + endTime, + cancellationReason, +}: { + bookingId: number; + startTime: string; + endTime: string; + cancellationReason: string; +}): Promise { const booking = await prisma.booking.update({ where: { id: bookingId, @@ -51,7 +60,12 @@ const moveSeatedBookingToNewTimeSlot = async ( } = rescheduleSeatedBookingObject; let { evt } = rescheduleSeatedBookingObject; - const newBooking = await updateBooking({ bookingId: seatedBooking.id, startTime: evt.startTime, endTime: evt.endTime, cancellationReason: rescheduleReason }); + const newBooking = await updateBooking({ + bookingId: seatedBooking.id, + startTime: evt.startTime, + endTime: evt.endTime, + cancellationReason: rescheduleReason, + }); evt = { ...addVideoCallDataToEvent(newBooking.references, evt), bookerUrl: evt.bookerUrl }; diff --git a/packages/features/bookings/lib/host-filtering/filterHostsBySameRoundRobinHost.ts b/packages/features/bookings/lib/host-filtering/filterHostsBySameRoundRobinHost.ts index 398f5b9a4a..5cbed94c2f 100644 --- a/packages/features/bookings/lib/host-filtering/filterHostsBySameRoundRobinHost.ts +++ b/packages/features/bookings/lib/host-filtering/filterHostsBySameRoundRobinHost.ts @@ -12,7 +12,7 @@ export class FilterHostsService { T extends { isFixed: false; // ensure no fixed hosts are passed. user: { id: number; email: string }; - } + }, >({ hosts, rescheduleUid, diff --git a/packages/features/bookings/lib/host-filtering/findQualifiedHostsWithDelegationCredentials.ts b/packages/features/bookings/lib/host-filtering/findQualifiedHostsWithDelegationCredentials.ts index c53be6d3d7..d4b935d31b 100644 --- a/packages/features/bookings/lib/host-filtering/findQualifiedHostsWithDelegationCredentials.ts +++ b/packages/features/bookings/lib/host-filtering/findQualifiedHostsWithDelegationCredentials.ts @@ -61,9 +61,9 @@ const isWithinRRHostSubset = { if (rrHostSubsetIds.length === 0 || !rrHostSubsetEnabled || schedulingType !== SchedulingType.ROUND_ROBIN) { return true; @@ -72,7 +72,7 @@ const isWithinRRHostSubset = + } & Record, >({ eventType, rescheduleUid, diff --git a/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts b/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts index 24d27fa883..10d54a7843 100644 --- a/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts +++ b/packages/features/bookings/lib/onBookingEvents/BookingEventHandlerService.ts @@ -147,8 +147,16 @@ export class BookingEventHandlerService { } async onBookingAccepted(params: OnBookingAcceptedParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueAcceptedAudit({ bookingUid, actor, @@ -162,8 +170,16 @@ export class BookingEventHandlerService { } async onBookingCancelled(params: OnBookingCancelledParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueCancelledAudit({ bookingUid, actor, @@ -177,8 +193,16 @@ export class BookingEventHandlerService { } async onRescheduleRequested(params: OnRescheduleRequestedParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueRescheduleRequestedAudit({ bookingUid, actor, @@ -192,8 +216,16 @@ export class BookingEventHandlerService { } async onAttendeeAdded(params: OnAttendeeAddedParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueAttendeeAddedAudit({ bookingUid, actor, @@ -207,8 +239,16 @@ export class BookingEventHandlerService { } async onNoShowUpdated(params: OnNoShowUpdatedParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueNoShowUpdatedAudit({ bookingUid, actor, @@ -222,8 +262,16 @@ export class BookingEventHandlerService { } async onBookingRejected(params: OnBookingRejectedParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueRejectedAudit({ bookingUid, actor, @@ -237,8 +285,16 @@ export class BookingEventHandlerService { } async onAttendeeRemoved(params: OnAttendeeRemovedParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueAttendeeRemovedAudit({ bookingUid, actor, @@ -252,8 +308,16 @@ export class BookingEventHandlerService { } async onReassignment(params: OnReassignmentParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueReassignmentAudit({ bookingUid, actor, @@ -268,8 +332,16 @@ export class BookingEventHandlerService { async onLocationChanged(params: OnLocationChangedParams) { try { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueLocationChangedAudit({ bookingUid, actor, @@ -286,8 +358,16 @@ export class BookingEventHandlerService { } async onSeatBooked(params: OnSeatBookedParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueSeatBookedAudit({ bookingUid, actor, @@ -301,8 +381,16 @@ export class BookingEventHandlerService { } async onSeatRescheduled(params: OnSeatRescheduledParams) { - const { bookingUid, actor, organizationId, auditData, source, operationId, context, isBookingAuditEnabled } = - params; + const { + bookingUid, + actor, + organizationId, + auditData, + source, + operationId, + context, + isBookingAuditEnabled, + } = params; await this.bookingAuditProducerService.queueSeatRescheduledAudit({ bookingUid, actor, diff --git a/packages/features/bookings/lib/payment/handleNoShowFee.test.ts b/packages/features/bookings/lib/payment/handleNoShowFee.test.ts index 6d0ff342f5..dfcaf3e54a 100644 --- a/packages/features/bookings/lib/payment/handleNoShowFee.test.ts +++ b/packages/features/bookings/lib/payment/handleNoShowFee.test.ts @@ -52,9 +52,11 @@ vi.mock("@calcom/features/membership/repositories/MembershipRepository", () => ( })); vi.mock("@calcom/features/ee/teams/repositories/TeamRepository", () => ({ - TeamRepository: vi.fn().mockImplementation(function() { return { - findParentOrganizationByTeamId: vi.fn(), - }; }), + TeamRepository: vi.fn().mockImplementation(function () { + return { + findParentOrganizationByTeamId: vi.fn(), + }; + }), })); vi.mock("@calcom/prisma", () => ({ @@ -71,7 +73,9 @@ describe("handleNoShowFee", () => { }; const paymentServiceModule = await PaymentServiceMap.stripepayment; - vi.mocked(paymentServiceModule.BuildPaymentService).mockImplementation(function() { return mockPaymentService; }); + vi.mocked(paymentServiceModule.BuildPaymentService).mockImplementation(function () { + return mockPaymentService; + }); }); const mockBooking = { @@ -220,7 +224,9 @@ describe("handleNoShowFee", () => { const mockTeamRepository = { findParentOrganizationByTeamId: vi.fn().mockResolvedValue({ id: 2 }), }; - vi.mocked(TeamRepository).mockImplementation(function() { return mockTeamRepository; }); + vi.mocked(TeamRepository).mockImplementation(function () { + return mockTeamRepository; + }); const result = await handleNoShowFee({ booking: teamBooking, diff --git a/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts b/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts index 7a7c804b19..c54b5343ef 100644 --- a/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts +++ b/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts @@ -38,27 +38,27 @@ const createTestBooking = (overrides?: { const defaultEndTime = futureDate(5); const defaultStartTime = new Date(defaultEndTime.getTime() - 30 * 60 * 1000); // 30 minutes before endTime return { - uid: overrides?.uid || "test-booking-uid", - status: overrides?.status || BookingStatus.ACCEPTED, + uid: overrides?.uid || "test-booking-uid", + status: overrides?.status || BookingStatus.ACCEPTED, startTime: overrides?.startTime !== undefined ? overrides.startTime : defaultStartTime, endTime: overrides?.endTime !== undefined ? overrides.endTime : defaultEndTime, - responses: overrides?.responses || { - name: "John Doe", - email: "john.doe@example.com", - }, - userId: overrides?.userId !== undefined ? overrides.userId : 1, - eventType: - overrides?.eventType !== undefined - ? overrides.eventType - : // Default values from DB - { - disableRescheduling: false, - allowReschedulingPastBookings: false, - allowBookingFromCancelledBookingReschedule: false, + responses: overrides?.responses || { + name: "John Doe", + email: "john.doe@example.com", + }, + userId: overrides?.userId !== undefined ? overrides.userId : 1, + eventType: + overrides?.eventType !== undefined + ? overrides.eventType + : // Default values from DB + { + disableRescheduling: false, + allowReschedulingPastBookings: false, + allowBookingFromCancelledBookingReschedule: false, minimumRescheduleNotice: null, - teamId: null, - }, - dynamicEventSlugRef: overrides?.dynamicEventSlugRef !== undefined ? overrides.dynamicEventSlugRef : null, + teamId: null, + }, + dynamicEventSlugRef: overrides?.dynamicEventSlugRef !== undefined ? overrides.dynamicEventSlugRef : null, }; }; diff --git a/packages/features/bookings/lib/service/InstantBookingCreateService.ts b/packages/features/bookings/lib/service/InstantBookingCreateService.ts index b609813d1a..0b46539772 100644 --- a/packages/features/bookings/lib/service/InstantBookingCreateService.ts +++ b/packages/features/bookings/lib/service/InstantBookingCreateService.ts @@ -225,16 +225,19 @@ export async function handler( }, ]; - const guests = (reqBody.guests || []).reduce((guestArray, guest) => { - guestArray.push({ - email: guest, - name: "", - timeZone: attendeeTimezone, - locale: "en", - phoneNumber: null, - }); - return guestArray; - }, [] as typeof invitee); + const guests = (reqBody.guests || []).reduce( + (guestArray, guest) => { + guestArray.push({ + email: guest, + name: "", + timeZone: attendeeTimezone, + locale: "en", + phoneNumber: null, + }); + return guestArray; + }, + [] as typeof invitee + ); const attendeesList = [...invitee, ...guests]; const calVideoMeeting = await createInstantMeetingWithCalVideo(dayjs.utc(reqBody.end).toISOString()); diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index f71f6a2b88..3892eabdd6 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -1354,13 +1354,13 @@ async function handler( const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata); const organizerMetadata = metadataParseResult.success ? metadataParseResult.data : undefined; const defaultApp = organizerMetadata?.defaultConferencingApp; - + if (defaultApp?.appSlug) { const app = getAppFromSlug(defaultApp.appSlug); locationBodyString = app?.appData?.location?.type || locationBodyString; - + const mainHostCalendar = eventType.destinationCalendar || organizerUser.destinationCalendar; - + if (locationBodyString === MeetLocationType && mainHostCalendar?.integration !== "google_calendar") { locationBodyString = "integrations:daily"; organizerOrFirstDynamicGroupMemberDefaultLocationUrl = undefined; @@ -1371,7 +1371,6 @@ async function handler( locationBodyString = organizationDefaultLocation || "integrations:daily"; } } - const invitee: Invitee = [ { diff --git a/packages/features/bookings/repositories/BookingRepository.integration-test.ts b/packages/features/bookings/repositories/BookingRepository.integration-test.ts index 7774f8e33a..b1ce9d4f73 100644 --- a/packages/features/bookings/repositories/BookingRepository.integration-test.ts +++ b/packages/features/bookings/repositories/BookingRepository.integration-test.ts @@ -339,9 +339,7 @@ describe("BookingRepository (Integration Tests)", () => { }); expect(bookings).toHaveLength(1); - expect(bookings[0].startTime.toISOString()).toBe( - "2025-06-26T00:00:00.000Z" - ); + expect(bookings[0].startTime.toISOString()).toBe("2025-06-26T00:00:00.000Z"); }); }); -}); \ No newline at end of file +}); diff --git a/packages/features/bookings/repositories/IAttendeeRepository.ts b/packages/features/bookings/repositories/IAttendeeRepository.ts index 223b6f4a7d..d3290e5e73 100644 --- a/packages/features/bookings/repositories/IAttendeeRepository.ts +++ b/packages/features/bookings/repositories/IAttendeeRepository.ts @@ -1,10 +1,9 @@ /** * Attendee lookup interface - * + * * This interface provides methods for looking up attendee information. */ export interface IAttendeeRepository { - findById(id: number): Promise<{ name: string; email: string } | null>; - findByIds({ ids }: { ids: number[] }): Promise<{ id: number; name: string; email: string }[]>; + findById(id: number): Promise<{ name: string; email: string } | null>; + findByIds({ ids }: { ids: number[] }): Promise<{ id: number; name: string; email: string }[]>; } - diff --git a/packages/features/bookings/repositories/IBookingRepository.ts b/packages/features/bookings/repositories/IBookingRepository.ts index 15303ef159..b5eca6b6b9 100644 --- a/packages/features/bookings/repositories/IBookingRepository.ts +++ b/packages/features/bookings/repositories/IBookingRepository.ts @@ -12,11 +12,11 @@ export interface BookingWhereInput { export type BookingWhereUniqueInput = | { - id: number; - } + id: number; + } | { - uid: string; - }; + uid: string; + }; export interface BookingUpdateData { status?: BookingStatus; diff --git a/packages/features/bookings/services/BookingAccessService.test.ts b/packages/features/bookings/services/BookingAccessService.test.ts index 2365de50cf..c417177f5c 100644 --- a/packages/features/bookings/services/BookingAccessService.test.ts +++ b/packages/features/bookings/services/BookingAccessService.test.ts @@ -40,8 +40,12 @@ describe("BookingAccessService", () => { checkPermission: vi.fn(), }; - vi.mocked(BookingRepository).mockImplementation(function() { return mockBookingRepo as any; }); - vi.mocked(UserRepository).mockImplementation(function() { return mockUserRepo as any; }); + vi.mocked(BookingRepository).mockImplementation(function () { + return mockBookingRepo as any; + }); + vi.mocked(UserRepository).mockImplementation(function () { + return mockUserRepo as any; + }); service = new BookingAccessService(mockPrismaClient); diff --git a/packages/features/bookings/services/BookingAccessService.ts b/packages/features/bookings/services/BookingAccessService.ts index eac7821262..be035b9f7c 100644 --- a/packages/features/bookings/services/BookingAccessService.ts +++ b/packages/features/bookings/services/BookingAccessService.ts @@ -64,8 +64,8 @@ export class BookingAccessService { const booking = bookingUid ? await bookingRepo.findByUidIncludeEventType({ bookingUid }) : bookingId - ? await bookingRepo.findByIdIncludeEventType({ bookingId }) - : null; + ? await bookingRepo.findByIdIncludeEventType({ bookingId }) + : null; if (!booking) return false; diff --git a/packages/features/calAIPhone/AIPhoneServiceRegistry.test.ts b/packages/features/calAIPhone/AIPhoneServiceRegistry.test.ts index eb4e0b2c1b..ea0d2fe732 100644 --- a/packages/features/calAIPhone/AIPhoneServiceRegistry.test.ts +++ b/packages/features/calAIPhone/AIPhoneServiceRegistry.test.ts @@ -16,7 +16,7 @@ import { AIPhoneServiceProviderType } from "./interfaces/AIPhoneService.interfac vi.stubEnv("RETELL_AI_KEY", "test-api-key"); vi.mock("./providers/retellAI", () => ({ - RetellAIPhoneServiceProviderFactory: vi.fn().mockImplementation(function() { + RetellAIPhoneServiceProviderFactory: vi.fn().mockImplementation(function () { return { create: vi.fn().mockReturnValue({ setupConfiguration: vi.fn(), diff --git a/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts b/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts index 6751e5fc17..7d68bfd06e 100644 --- a/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts +++ b/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts @@ -49,7 +49,7 @@ export type AIPhoneServiceConfiguration = AIPhoneServiceProviderTypeMap[T]["UpdateModelParams"]; export type AIPhoneServiceModel = @@ -65,37 +65,37 @@ export type AIPhoneServicePhoneNumber = AIPhoneServiceProviderTypeMap[T]["UpdatePhoneNumberParams"]; export type AIPhoneServiceCreatePhoneNumberParams< - T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType + T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType, > = AIPhoneServiceProviderTypeMap[T]["CreatePhoneNumberParams"]; export type AIPhoneServiceImportPhoneNumberParams< - T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType + T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType, > = AIPhoneServiceProviderTypeMap[T]["ImportPhoneNumberParams"]; export type AIPhoneServiceUpdateAgentParams< - T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType + T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType, > = AIPhoneServiceProviderTypeMap[T]["UpdateAgentParams"]; export type AIPhoneServiceTools = AIPhoneServiceProviderTypeMap[T]["Tools"]; export type AIPhoneServiceCreatePhoneCallParams< - T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType + T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType, > = AIPhoneServiceProviderTypeMap[T]["CreatePhoneCallParams"]; export type AIPhoneServiceAgentWithDetails< - T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType + T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType, > = AIPhoneServiceProviderTypeMap[T]["AgentWithDetails"]; export type AIPhoneServiceListCallsParams = AIPhoneServiceProviderTypeMap[T]["ListCallsParams"]; export type AIPhoneServiceListCallsResponse< - T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType + T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType, > = AIPhoneServiceProviderTypeMap[T]["ListCallsResponse"]; export type AIPhoneServiceVoice = @@ -120,7 +120,7 @@ export type AIPhoneServiceCallData = AIPhoneServiceImportPhoneNumberParams & { userId: number; teamId?: number; @@ -420,7 +420,7 @@ export interface AIPhoneServiceProviderConfig { * Factory interface for creating AI phone service providers */ export interface AIPhoneServiceProviderFactory< - T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType + T extends AIPhoneServiceProviderType = AIPhoneServiceProviderType, > { create(config: AIPhoneServiceProviderConfig): AIPhoneServiceProvider; } diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProviderFactory.test.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProviderFactory.test.ts index e391195894..f1c58f54f3 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProviderFactory.test.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProviderFactory.test.ts @@ -66,37 +66,43 @@ vi.mock("./RetellAIPhoneServiceProvider", () => ({ })); vi.mock("../adapters/PrismaAgentRepositoryAdapter", () => ({ - PrismaAgentRepositoryAdapter: vi.fn().mockImplementation(function() { return { - canManageTeamResources: vi.fn(), - findByIdWithUserAccess: vi.fn(), - findByProviderAgentIdWithUserAccess: vi.fn(), - findManyWithUserAccess: vi.fn(), - findByIdWithUserAccessAndDetails: vi.fn(), - create: vi.fn(), - findByIdWithAdminAccess: vi.fn(), - findByIdWithCallAccess: vi.fn(), - delete: vi.fn(), - linkOutboundAgentToWorkflow: vi.fn(), - }; }), + PrismaAgentRepositoryAdapter: vi.fn().mockImplementation(function () { + return { + canManageTeamResources: vi.fn(), + findByIdWithUserAccess: vi.fn(), + findByProviderAgentIdWithUserAccess: vi.fn(), + findManyWithUserAccess: vi.fn(), + findByIdWithUserAccessAndDetails: vi.fn(), + create: vi.fn(), + findByIdWithAdminAccess: vi.fn(), + findByIdWithCallAccess: vi.fn(), + delete: vi.fn(), + linkOutboundAgentToWorkflow: vi.fn(), + }; + }), })); vi.mock("../adapters/PrismaPhoneNumberRepositoryAdapter", () => ({ - PrismaPhoneNumberRepositoryAdapter: vi.fn().mockImplementation(function() { return { - findByPhoneNumberAndUserId: vi.fn(), - findByPhoneNumberAndTeamId: vi.fn(), - findByIdAndUserId: vi.fn(), - findByIdWithTeamAccess: vi.fn(), - createPhoneNumber: vi.fn(), - deletePhoneNumber: vi.fn(), - updateSubscriptionStatus: vi.fn(), - updateAgents: vi.fn(), - }; }), + PrismaPhoneNumberRepositoryAdapter: vi.fn().mockImplementation(function () { + return { + findByPhoneNumberAndUserId: vi.fn(), + findByPhoneNumberAndTeamId: vi.fn(), + findByIdAndUserId: vi.fn(), + findByIdWithTeamAccess: vi.fn(), + createPhoneNumber: vi.fn(), + deletePhoneNumber: vi.fn(), + updateSubscriptionStatus: vi.fn(), + updateAgents: vi.fn(), + }; + }), })); vi.mock("../adapters/PrismaTransactionAdapter", () => ({ - PrismaTransactionAdapter: vi.fn().mockImplementation(function() { return { - executeInTransaction: vi.fn(), - }; }), + PrismaTransactionAdapter: vi.fn().mockImplementation(function () { + return { + executeInTransaction: vi.fn(), + }; + }), })); describe("RetellAIPhoneServiceProviderFactory", () => { diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIService.test.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIService.test.ts index 50fcc09fb8..52408f690d 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellAIService.test.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellAIService.test.ts @@ -34,7 +34,7 @@ vi.mock("@calcom/features/ee/payments/server/stripe", () => ({ const mockGetAllCredits = vi.fn(); const mockHasAvailableCredits = vi.fn(); -const mockCreditService = vi.fn().mockImplementation(function() { +const mockCreditService = vi.fn().mockImplementation(function () { return { getAllCredits: mockGetAllCredits, hasAvailableCredits: mockHasAvailableCredits, @@ -851,7 +851,7 @@ describe("RetellAIService", () => { const { CreditService } = await import("@calcom/features/ee/billing/credit-service"); const mockHasAvailableCredits = vi.fn().mockResolvedValue(true); - (CreditService as any).mockImplementation(function() { + (CreditService as any).mockImplementation(function () { return { hasAvailableCredits: mockHasAvailableCredits, }; @@ -935,7 +935,7 @@ describe("RetellAIService", () => { // Mock credit service to return false (no credits) const mockHasAvailableCredits = vi.fn().mockResolvedValue(false); - (CreditService as any).mockImplementation(function() { + (CreditService as any).mockImplementation(function () { return { hasAvailableCredits: mockHasAvailableCredits, }; @@ -958,7 +958,7 @@ describe("RetellAIService", () => { // Mock sufficient credits to get past credit check const mockHasAvailableCredits = vi.fn().mockResolvedValue(true); - (CreditService as any).mockImplementation(function() { + (CreditService as any).mockImplementation(function () { return { hasAvailableCredits: mockHasAvailableCredits, }; @@ -982,7 +982,7 @@ describe("RetellAIService", () => { // Mock sufficient credits const mockHasAvailableCredits = vi.fn().mockResolvedValue(true); - (CreditService as any).mockImplementation(function() { + (CreditService as any).mockImplementation(function () { return { hasAvailableCredits: mockHasAvailableCredits, }; @@ -1008,7 +1008,7 @@ describe("RetellAIService", () => { // Mock sufficient credits const mockHasAvailableCredits = vi.fn().mockResolvedValue(true); - (CreditService as any).mockImplementation(function() { + (CreditService as any).mockImplementation(function () { return { hasAvailableCredits: mockHasAvailableCredits, }; diff --git a/packages/features/calAIPhone/providers/retellAI/RetellSDKClient.test.ts b/packages/features/calAIPhone/providers/retellAI/RetellSDKClient.test.ts index 48a925bcd4..2a224ca2f3 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellSDKClient.test.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellSDKClient.test.ts @@ -16,7 +16,7 @@ import type { let RetellSDKClient: typeof import("./RetellSDKClient").RetellSDKClient; vi.mock("retell-sdk", () => ({ - Retell: vi.fn().mockImplementation(function() { + Retell: vi.fn().mockImplementation(function () { return { llm: { create: vi.fn(), @@ -109,7 +109,9 @@ describe("RetellSDKClient", () => { }, }; - (Retell as any).mockImplementation(function() { return mockRetellInstance; }); + (Retell as any).mockImplementation(function () { + return mockRetellInstance; + }); }); describe("constructor", () => { diff --git a/packages/features/calAIPhone/providers/retellAI/errors.ts b/packages/features/calAIPhone/providers/retellAI/errors.ts index 8e9ad61294..691b3f6b40 100644 --- a/packages/features/calAIPhone/providers/retellAI/errors.ts +++ b/packages/features/calAIPhone/providers/retellAI/errors.ts @@ -1,5 +1,9 @@ export class RetellAIError extends Error { - constructor(message: string, public readonly operation: string, public readonly originalError?: unknown) { + constructor( + message: string, + public readonly operation: string, + public readonly originalError?: unknown + ) { super(message); this.name = "RetellAIError"; } diff --git a/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts b/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts index 0156189a8c..4002455393 100644 --- a/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts +++ b/packages/features/calAIPhone/providers/retellAI/services/AgentService.ts @@ -131,11 +131,11 @@ export class AgentService { const apiKey = RETELL_AI_TEST_MODE && process.env.RETELL_AI_TEST_CAL_API_KEY ? process.env.RETELL_AI_TEST_CAL_API_KEY - : reusableKey ?? + : (reusableKey ?? (await this.createApiKey({ userId: data.userId, teamId: data.teamId || undefined, - })); + }))); const newEventTools: NonNullable> = []; if (!hasCheck) { diff --git a/packages/features/calAIPhone/providers/retellAI/services/BillingService.ts b/packages/features/calAIPhone/providers/retellAI/services/BillingService.ts index abb25f5783..81cda4dfef 100644 --- a/packages/features/calAIPhone/providers/retellAI/services/BillingService.ts +++ b/packages/features/calAIPhone/providers/retellAI/services/BillingService.ts @@ -26,7 +26,7 @@ export class BillingService { phoneNumberRepository: PhoneNumberRepositoryInterface; retellRepository: RetellAIRepository; } - ) { } + ) {} async generatePhoneNumberCheckoutSession({ userId, @@ -83,8 +83,14 @@ export class BillingService { agentId: agentId || "", workflowId: workflowId || "", type: CHECKOUT_SESSION_TYPES.PHONE_NUMBER_SUBSCRIPTION, - ...(tracking?.googleAds?.gclid && { gclid: tracking.googleAds.gclid, campaignId: tracking.googleAds.campaignId }), - ...(tracking?.linkedInAds?.liFatId && { liFatId: tracking.linkedInAds.liFatId, linkedInCampaignId: tracking.linkedInAds?.campaignId }), + ...(tracking?.googleAds?.gclid && { + gclid: tracking.googleAds.gclid, + campaignId: tracking.googleAds.campaignId, + }), + ...(tracking?.linkedInAds?.liFatId && { + liFatId: tracking.linkedInAds.liFatId, + linkedInCampaignId: tracking.linkedInAds?.campaignId, + }), }, subscription_data: { metadata: { @@ -119,14 +125,14 @@ export class BillingService { // Find phone number with proper team authorization const phoneNumber = teamId ? await this.deps.phoneNumberRepository.findByIdWithTeamAccess({ - id: phoneNumberId, - teamId, - userId, - }) + id: phoneNumberId, + teamId, + userId, + }) : await this.deps.phoneNumberRepository.findByIdAndUserId({ - id: phoneNumberId, - userId, - }); + id: phoneNumberId, + userId, + }); if (!phoneNumber) { throw new HttpError({ diff --git a/packages/features/calAIPhone/providers/retellAI/services/__tests__/test-utils.ts b/packages/features/calAIPhone/providers/retellAI/services/__tests__/test-utils.ts index cae376afda..46e47b3405 100644 --- a/packages/features/calAIPhone/providers/retellAI/services/__tests__/test-utils.ts +++ b/packages/features/calAIPhone/providers/retellAI/services/__tests__/test-utils.ts @@ -192,14 +192,20 @@ export const setupRateLimitMocks = () => { // Test Error Classes export class TestError extends Error { - constructor(message: string, public code?: string) { + constructor( + message: string, + public code?: string + ) { super(message); this.name = "TestError"; } } export class TestRetellAPIError extends Error { - constructor(message: string, public statusCode: number = 500) { + constructor( + message: string, + public statusCode: number = 500 + ) { super(message); this.name = "TestRetellAPIError"; } diff --git a/packages/features/calAIPhone/repositories/PrismaAgentRepository.ts b/packages/features/calAIPhone/repositories/PrismaAgentRepository.ts index d562e00fae..6e11a21931 100644 --- a/packages/features/calAIPhone/repositories/PrismaAgentRepository.ts +++ b/packages/features/calAIPhone/repositories/PrismaAgentRepository.ts @@ -293,21 +293,24 @@ export class PrismaAgentRepository { : []; // Map phone numbers to agents - const phoneNumbersByAgent = phoneNumbers.reduce((acc, pn) => { - const agentId = pn.outboundAgentId; - if (agentId) { - if (!acc[agentId]) { - acc[agentId] = []; + const phoneNumbersByAgent = phoneNumbers.reduce( + (acc, pn) => { + const agentId = pn.outboundAgentId; + if (agentId) { + if (!acc[agentId]) { + acc[agentId] = []; + } + acc[agentId].push({ + id: pn.id, + phoneNumber: pn.phoneNumber, + subscriptionStatus: pn.subscriptionStatus, + provider: pn.provider, + }); } - acc[agentId].push({ - id: pn.id, - phoneNumber: pn.phoneNumber, - subscriptionStatus: pn.subscriptionStatus, - provider: pn.provider, - }); - } - return acc; - }, {} as Record); + return acc; + }, + {} as Record + ); // Transform results to match expected format return agents.map((agent) => ({ diff --git a/packages/features/calAIPhone/repositories/PrismaPhoneNumberRepository.ts b/packages/features/calAIPhone/repositories/PrismaPhoneNumberRepository.ts index c543f06e8a..bb5b479ef5 100644 --- a/packages/features/calAIPhone/repositories/PrismaPhoneNumberRepository.ts +++ b/packages/features/calAIPhone/repositories/PrismaPhoneNumberRepository.ts @@ -104,26 +104,29 @@ export class PrismaPhoneNumberRepository { ` : []; - const agentsByPhoneNumber = agents.reduce((acc, agent) => { - const phoneNumberId = agent.phoneNumberId; - if (!acc[phoneNumberId]) { - acc[phoneNumberId] = { inbound: null, outbound: null }; - } + const agentsByPhoneNumber = agents.reduce( + (acc, agent) => { + const phoneNumberId = agent.phoneNumberId; + if (!acc[phoneNumberId]) { + acc[phoneNumberId] = { inbound: null, outbound: null }; + } - const agentData = { - id: agent.id, - name: agent.name, - providerAgentId: agent.providerAgentId, - }; + const agentData = { + id: agent.id, + name: agent.name, + providerAgentId: agent.providerAgentId, + }; - if (agent.agentType === "inbound") { - acc[phoneNumberId].inbound = agentData; - } else if (agent.agentType === "outbound") { - acc[phoneNumberId].outbound = agentData; - } + if (agent.agentType === "inbound") { + acc[phoneNumberId].inbound = agentData; + } else if (agent.agentType === "outbound") { + acc[phoneNumberId].outbound = agentData; + } - return acc; - }, {} as Record); + return acc; + }, + {} as Record + ); return phoneNumbers.map((pn) => ({ id: pn.id, @@ -392,26 +395,29 @@ export class PrismaPhoneNumberRepository { ` : []; - const agentsByPhoneNumber = agents.reduce((acc, agent) => { - const phoneNumberId = agent.phoneNumberId; - if (!acc[phoneNumberId]) { - acc[phoneNumberId] = { inbound: null, outbound: null }; - } + const agentsByPhoneNumber = agents.reduce( + (acc, agent) => { + const phoneNumberId = agent.phoneNumberId; + if (!acc[phoneNumberId]) { + acc[phoneNumberId] = { inbound: null, outbound: null }; + } - const agentData = { - id: agent.id, - name: agent.name, - providerAgentId: agent.providerAgentId, - }; + const agentData = { + id: agent.id, + name: agent.name, + providerAgentId: agent.providerAgentId, + }; - if (agent.agentType === "inbound") { - acc[phoneNumberId].inbound = agentData; - } else if (agent.agentType === "outbound") { - acc[phoneNumberId].outbound = agentData; - } + if (agent.agentType === "inbound") { + acc[phoneNumberId].inbound = agentData; + } else if (agent.agentType === "outbound") { + acc[phoneNumberId].outbound = agentData; + } - return acc; - }, {} as Record); + return acc; + }, + {} as Record + ); return phoneNumbers.map((pn) => ({ id: pn.id, diff --git a/packages/features/calAIPhone/retellAIService.ts b/packages/features/calAIPhone/retellAIService.ts index 7436751b31..ad7bf574c4 100644 --- a/packages/features/calAIPhone/retellAIService.ts +++ b/packages/features/calAIPhone/retellAIService.ts @@ -123,7 +123,10 @@ class GetRetellLLMCommand implements Command { } class UpdateRetellLLMCommand implements Command { - constructor(private props: initProps, private llmId: string) {} + constructor( + private props: initProps, + private llmId: string + ) {} async execute(): Promise { try { @@ -167,7 +170,10 @@ export const validatePhoneNumber = (phoneNumber: string) => { }; class CreateRetellPhoneCallCommand implements Command { - constructor(private props: initProps, private numberToCall: string) {} + constructor( + private props: initProps, + private numberToCall: string + ) {} async execute(): Promise { try { diff --git a/packages/features/calendar-subscription/adapters/GoogleCalendarSubscription.adapter.ts b/packages/features/calendar-subscription/adapters/GoogleCalendarSubscription.adapter.ts index 01f1d00291..74c18e26d1 100644 --- a/packages/features/calendar-subscription/adapters/GoogleCalendarSubscription.adapter.ts +++ b/packages/features/calendar-subscription/adapters/GoogleCalendarSubscription.adapter.ts @@ -174,14 +174,14 @@ export class GoogleCalendarSubscriptionAdapter implements ICalendarSubscriptionP const start = event.start?.dateTime ? new Date(event.start.dateTime) : event.start?.date - ? new Date(event.start.date) - : new Date(); + ? new Date(event.start.date) + : new Date(); const end = event.end?.dateTime ? new Date(event.end.dateTime) : event.end?.date - ? new Date(event.end.date) - : new Date(); + ? new Date(event.end.date) + : new Date(); return { id: event.id as string, @@ -201,8 +201,8 @@ export class GoogleCalendarSubscriptionAdapter implements ICalendarSubscriptionP originalStartDate: event.originalStartTime?.dateTime ? new Date(event.originalStartTime.dateTime) : event.originalStartTime?.date - ? new Date(event.originalStartTime.date) - : null, + ? new Date(event.originalStartTime.date) + : null, createdAt: event.created ? new Date(event.created) : null, updatedAt: event.updated ? new Date(event.updated) : null, }; diff --git a/packages/features/calendar-subscription/adapters/__tests__/GoogleCalendarSubscriptionAdapter.test.ts b/packages/features/calendar-subscription/adapters/__tests__/GoogleCalendarSubscriptionAdapter.test.ts index 5bf687ce04..0a9a205879 100644 --- a/packages/features/calendar-subscription/adapters/__tests__/GoogleCalendarSubscriptionAdapter.test.ts +++ b/packages/features/calendar-subscription/adapters/__tests__/GoogleCalendarSubscriptionAdapter.test.ts @@ -96,7 +96,7 @@ describe("GoogleCalendarSubscriptionAdapter", () => { }; const { CalendarAuth } = await import("../__mocks__/CalendarAuth"); - vi.mocked(CalendarAuth).mockImplementation(function() { + vi.mocked(CalendarAuth).mockImplementation(function () { return { getClient: vi.fn().mockResolvedValue(mockClient), }; diff --git a/packages/features/calendar-subscription/lib/telemetry/CalendarTelemetryWrapper.ts b/packages/features/calendar-subscription/lib/telemetry/CalendarTelemetryWrapper.ts index b5dde69cf5..00051b0604 100644 --- a/packages/features/calendar-subscription/lib/telemetry/CalendarTelemetryWrapper.ts +++ b/packages/features/calendar-subscription/lib/telemetry/CalendarTelemetryWrapper.ts @@ -151,7 +151,7 @@ export class CalendarTelemetryWrapper implements Calendar { cache: this.deps.cacheEnabled ? "on" : "off", calendarType: this.deps.calendarType, mode: String(this.deps.mode), - } + }, }); metrics.distribution("calendar.getAvailabilityWithTimeZones.duration_ms", totalFetchDurationMs, { @@ -159,16 +159,15 @@ export class CalendarTelemetryWrapper implements Calendar { cache: this.deps.cacheEnabled ? "on" : "off", calendarType: this.deps.calendarType, mode: String(this.deps.mode), - } + }, }); metrics.distribution("calendar.getAvailabilityWithTimeZones.events_count", results?.length ?? 0, { - attributes: { - cache: this.deps.cacheEnabled ? "on" : "off", - calendarType: this.deps.calendarType, - }, - } - ); + attributes: { + cache: this.deps.cacheEnabled ? "on" : "off", + calendarType: this.deps.calendarType, + }, + }); log.debug("Calendar fetch with timezones completed", { calendarCount: selectedCalendars.length, diff --git a/packages/features/calendars/components/DatePicker.tsx b/packages/features/calendars/components/DatePicker.tsx index efea5d8551..846ff07291 100644 --- a/packages/features/calendars/components/DatePicker.tsx +++ b/packages/features/calendars/components/DatePicker.tsx @@ -82,28 +82,23 @@ const Day = ({ const buttonContent = (
)} @@ -195,8 +188,7 @@ const Days = ({ const isSecondWeekOver = today.isAfter(firstDayOfMonth.add(2, "week")); let days: (Dayjs | null)[] = []; - const getPadding = (day: number) => - (browsingDate.set("date", day).day() - weekStart + 7) % 7; + const getPadding = (day: number) => (browsingDate.set("date", day).day() - weekStart + 7) % 7; const totalDays = daysInMonth(browsingDate); const showNextMonthDays = isSecondWeekOver && !isCompact; @@ -229,18 +221,12 @@ const Days = ({ } } - const [selectedDatesAndTimes] = useBookerStoreContext( - (state) => [state.selectedDatesAndTimes], - shallow - ); + const [selectedDatesAndTimes] = useBookerStoreContext((state) => [state.selectedDatesAndTimes], shallow); const isActive = (day: dayjs.Dayjs) => { // for selecting a range of dates if (Array.isArray(selected)) { - return ( - Array.isArray(selected) && - selected?.some((e) => yyyymmdd(e) === yyyymmdd(day)) - ); + return Array.isArray(selected) && selected?.some((e) => yyyymmdd(e) === yyyymmdd(day)); } if (selected && yyyymmdd(selected) === yyyymmdd(day)) { @@ -254,11 +240,9 @@ const Days = ({ selectedDatesAndTimes[eventSlug as string] && Object.keys(selectedDatesAndTimes[eventSlug as string]).length > 0 ) { - return Object.keys(selectedDatesAndTimes[eventSlug as string]).some( - (date) => { - return yyyymmdd(dayjs(date)) === yyyymmdd(day); - } - ); + return Object.keys(selectedDatesAndTimes[eventSlug as string]).some((date) => { + return yyyymmdd(dayjs(date)) === yyyymmdd(day); + }); } return false; @@ -272,24 +256,18 @@ const Days = ({ const oooInfo = daySlots.find((slot) => slot.away) || null; const isNextMonth = day.month() !== browsingDate.month(); - const isFirstDayOfNextMonth = - isSecondWeekOver && !isCompact && isNextMonth && day.date() === 1; + const isFirstDayOfNextMonth = isSecondWeekOver && !isCompact && isNextMonth && day.date() === 1; const included = includedDates?.includes(dateKey); const excluded = excludedDates.includes(dateKey); const hasAvailableSlots = daySlots.some((slot) => !slot.away); - const isOOOAllDay = - daySlots.length > 0 && daySlots.every((slot) => slot.away); + const isOOOAllDay = daySlots.length > 0 && daySlots.every((slot) => slot.away); const away = isOOOAllDay; // OOO dates are selectable only if there's a redirect user OR the note is public const oooIsSelectable = oooInfo?.toUser || oooInfo?.showNotePublicly; - const disabled = away - ? !oooIsSelectable - : isNextMonth - ? !hasAvailableSlots - : !included || excluded; + const disabled = away ? !oooIsSelectable : isNextMonth ? !hasAvailableSlots : !included || excluded; return { day, @@ -314,13 +292,10 @@ const Days = ({ if (selected instanceof Array) { return; } - const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find( - (day) => !day.disabled - )?.day; + const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day; const isSelectedDateAvailable = selected ? daysToRenderForTheMonth.some(({ day, disabled }) => { - if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) - return true; + if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true; }) : false; if (!isSelectedDateAvailable && firstAvailableDateOfTheMonth) { @@ -340,48 +315,38 @@ const Days = ({ return ( <> - {daysToRenderForTheMonth.map( - ({ day, disabled, away, emoji, isFirstDayOfNextMonth }, idx) => ( -
- {day === null ? ( -
- ) : props.isLoading ? ( - - ) : ( - { - props.onChange(day); - props?.scrollToTimeSlots?.(); - }} - disabled={disabled} - active={isActive(day)} - away={away} - emoji={emoji} - showMonthTooltip={ - showNextMonthDays && - !disabled && - day.month() !== browsingDate.month() - } - isFirstDayOfNextMonth={isFirstDayOfNextMonth} - /> - )} -
- ) - )} + {daysToRenderForTheMonth.map(({ day, disabled, away, emoji, isFirstDayOfNextMonth }, idx) => ( +
+ {day === null ? ( +
+ ) : props.isLoading ? ( + + ) : ( + { + props.onChange(day); + props?.scrollToTimeSlots?.(); + }} + disabled={disabled} + active={isActive(day)} + away={away} + emoji={emoji} + showMonthTooltip={showNextMonthDays && !disabled && day.month() !== browsingDate.month()} + isFirstDayOfNextMonth={isFirstDayOfNextMonth} + /> + )} +
+ ))} {!props.isLoading && !isBookingInPast && includedDates && @@ -429,18 +394,13 @@ const DatePicker = ({ scrollToTimeSlots?: () => void; }) => { const minDate = passThroughProps.minDate; - const rawBrowsingDate = - passThroughProps.browsingDate || dayjs().startOf("month"); + const rawBrowsingDate = passThroughProps.browsingDate || dayjs().startOf("month"); const browsingDate = - minDate && rawBrowsingDate.valueOf() < minDate.valueOf() - ? dayjs(minDate) - : rawBrowsingDate; + minDate && rawBrowsingDate.valueOf() < minDate.valueOf() ? dayjs(minDate) : rawBrowsingDate; const { i18n, t } = useLocale(); const bookingData = useBookerStoreContext((state) => state.bookingData); - const isBookingInPast = bookingData - ? new Date(bookingData.endTime) < new Date() - : false; + const isBookingInPast = bookingData ? new Date(bookingData.endTime) < new Date() : false; const changeMonth = (newMonth: number) => { if (onMonthChange) { onMonthChange(browsingDate.add(newMonth, "month")); @@ -457,24 +417,12 @@ const DatePicker = ({
{browsingDate ? ( -
))} diff --git a/packages/features/calendars/components/DestinationCalendarSelector.tsx b/packages/features/calendars/components/DestinationCalendarSelector.tsx index 59da48591c..20a08a5d1b 100644 --- a/packages/features/calendars/components/DestinationCalendarSelector.tsx +++ b/packages/features/calendars/components/DestinationCalendarSelector.tsx @@ -10,7 +10,6 @@ import { useEffect, useState } from "react"; import type { OptionProps, SingleValueProps } from "react-select"; import { components } from "react-select"; - type CalendarWithPrimary = Omit & { primary?: boolean | null; isSelected?: boolean; diff --git a/packages/features/calendars/lib/CalendarManager.ts b/packages/features/calendars/lib/CalendarManager.ts index f1eb527d83..355f119cde 100644 --- a/packages/features/calendars/lib/CalendarManager.ts +++ b/packages/features/calendars/lib/CalendarManager.ts @@ -52,7 +52,8 @@ export const processEvent = (calEvent: CalendarEvent): CalendarServiceEvent => { .some((domain) => calEvent.organizer.email.toLowerCase().endsWith(domain.toLowerCase())); // Zoho Calendar requires at least one attendee, so don't empty attendees array for Zoho Calendar - const hasZohoCalendar = calEvent.destinationCalendar?.some((cal) => cal.integration === "zoho_calendar") ?? false; + const hasZohoCalendar = + calEvent.destinationCalendar?.some((cal) => cal.integration === "zoho_calendar") ?? false; if (calEvent.hideOrganizerEmail && !isOrganizerExempt && !isMeetLocationType && !hasZohoCalendar) { calendarEvent.attendees = []; diff --git a/packages/features/calendars/lib/tasker/CalendarsSyncTasker.ts b/packages/features/calendars/lib/tasker/CalendarsSyncTasker.ts index 531459c40f..70d6a3d051 100644 --- a/packages/features/calendars/lib/tasker/CalendarsSyncTasker.ts +++ b/packages/features/calendars/lib/tasker/CalendarsSyncTasker.ts @@ -9,9 +9,7 @@ export interface ICalendarsSyncTaskerDependencies { } export class CalendarsSyncTasker implements ICalendarsTasker { - constructor( - public readonly dependencies: ITaskerDependencies & ICalendarsSyncTaskerDependencies - ) {} + constructor(public readonly dependencies: ITaskerDependencies & ICalendarsSyncTaskerDependencies) {} async ensureDefaultCalendars( payload: Parameters[0] diff --git a/packages/features/calendars/weeklyview/utils/overlap.ts b/packages/features/calendars/weeklyview/utils/overlap.ts index db092ff249..a3e75a954a 100644 --- a/packages/features/calendars/weeklyview/utils/overlap.ts +++ b/packages/features/calendars/weeklyview/utils/overlap.ts @@ -29,7 +29,7 @@ const DEFAULT_CONFIG: Required = { * Calculates variable widths for each event in a cascade based on position * Leftmost (longest) events get more width, rightmost (shortest) get less width * Uses anchor points for 2-4 events and smooth easing curve for 5+ events - * + * * @param groupSize - Number of overlapping events in the group * @param minWidthPercent - Minimum width to maintain readability (default 25%) * @param curveExponent - Easing curve exponent for width distribution (default 1.3) @@ -43,11 +43,11 @@ function calculateVariableWidths( if (groupSize <= 1) { return [100]; // Single event gets full width (100%) } - + // Define anchor points for first and last widths based on group size let wFirst: number; let wLast: number; - + if (groupSize === 2) { wFirst = 80; wLast = 50; @@ -61,7 +61,7 @@ function calculateVariableWidths( wFirst = Math.max(30, 40 - 3 * (groupSize - 4)); wLast = minWidthPercent; } - + const widths: number[] = []; for (let i = 0; i < groupSize; i++) { const t = groupSize > 1 ? i / (groupSize - 1) : 0; @@ -69,7 +69,7 @@ function calculateVariableWidths( const width = wLast + (wFirst - wLast) * easedT; widths.push(Math.max(minWidthPercent, width)); } - + return widths; } @@ -95,11 +95,11 @@ export function sortEvents(events: CalendarEvent[]): CalendarEvent[] { const startA = dayjs(a.start); const startB = dayjs(b.start); const startDiff = startA.diff(startB); - + if (startDiff !== 0) { return startDiff; } - + const endA = dayjs(a.end); const endB = dayjs(b.end); return endB.diff(endA); @@ -161,10 +161,10 @@ export function calculateEventLayouts( groups.forEach((group, groupIndex) => { const groupSize = group.length; - + const widths = calculateVariableWidths(groupSize, minWidthPercent, curveExponent); const Rmax = 100 - safetyMarginPercent; - + if (groupSize === 1) { const width = floor3(Math.min(widths[0], Rmax)); layouts.push({ @@ -177,13 +177,13 @@ export function calculateEventLayouts( }); } else { const Rmin = widths[0]; - + group.forEach((event, indexInGroup) => { const t = indexInGroup / (groupSize - 1); const ri = Rmin + (Rmax - Rmin) * t; const leftRaw = ri - widths[indexInGroup]; const left = round3(leftRaw); - + const maxWidthCap = Rmax - left; const widthCap = Math.min(widths[indexInGroup], maxWidthCap); const width = floor3(Math.max(0, widthCap)); diff --git a/packages/features/credentials/deleteCredential.test.ts b/packages/features/credentials/deleteCredential.test.ts index 8c0b10e71a..a38295b84a 100644 --- a/packages/features/credentials/deleteCredential.test.ts +++ b/packages/features/credentials/deleteCredential.test.ts @@ -1,7 +1,4 @@ -import { - addEventTypesToDb, - mockNoTranslations, -} from "@calcom/testing/lib/bookingScenario/bookingScenario"; +import { addEventTypesToDb, mockNoTranslations } from "@calcom/testing/lib/bookingScenario/bookingScenario"; import { describe, test, expect, beforeEach } from "vitest"; diff --git a/packages/features/credentials/handleDeleteCredential.ts b/packages/features/credentials/handleDeleteCredential.ts index 3122113504..ee9b7f71cf 100644 --- a/packages/features/credentials/handleDeleteCredential.ts +++ b/packages/features/credentials/handleDeleteCredential.ts @@ -363,8 +363,8 @@ const handleDeleteCredential = async ({ destinationCalendar: booking.destinationCalendar ? [booking.destinationCalendar] : booking.user?.destinationCalendar - ? [booking.user?.destinationCalendar] - : [], + ? [booking.user?.destinationCalendar] + : [], cancellationReason: "Payment method removed by organizer", seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot, seatsShowAttendees: booking.eventType?.seatsShowAttendees, @@ -493,12 +493,15 @@ const removeAppFromEventTypeMetadata = ( } ) => { const appMetadata = eventTypeMetadata?.apps - ? Object.entries(eventTypeMetadata.apps).reduce((filteredApps, [appName, appData]) => { - if (appName !== appSlugToDelete) { - filteredApps[appName as keyof typeof eventTypeMetadata.apps] = appData; - } - return filteredApps; - }, {} as z.infer) + ? Object.entries(eventTypeMetadata.apps).reduce( + (filteredApps, [appName, appData]) => { + if (appName !== appSlugToDelete) { + filteredApps[appName as keyof typeof eventTypeMetadata.apps] = appData; + } + return filteredApps; + }, + {} as z.infer + ) : {}; return appMetadata; diff --git a/packages/features/credentials/repositories/CredentialRepository.ts b/packages/features/credentials/repositories/CredentialRepository.ts index 04100a5fc7..95b98c7ffa 100644 --- a/packages/features/credentials/repositories/CredentialRepository.ts +++ b/packages/features/credentials/repositories/CredentialRepository.ts @@ -59,13 +59,7 @@ export class CredentialRepository { }); return buildNonDelegationCredential(credential); } - static async findByAppIdAndUserId({ - appId, - userId, - }: { - appId: string; - userId: number; - }) { + static async findByAppIdAndUserId({ appId, userId }: { appId: string; userId: number }) { const credential = await prisma.credential.findFirst({ where: { appId, @@ -97,13 +91,7 @@ export class CredentialRepository { return buildNonDelegationCredential(credential); } - static async findFirstByAppIdAndUserId({ - appId, - userId, - }: { - appId: string; - userId: number; - }) { + static async findFirstByAppIdAndUserId({ appId, userId }: { appId: string; userId: number }) { return await prisma.credential.findFirst({ where: { appId, @@ -112,13 +100,7 @@ export class CredentialRepository { }); } - static async findFirstByUserIdAndType({ - userId, - type, - }: { - userId: number; - type: string; - }) { + static async findFirstByUserIdAndType({ userId, type }: { userId: number; type: string }) { const credential = await prisma.credential.findFirst({ where: { userId, type }, }); @@ -129,13 +111,7 @@ export class CredentialRepository { await prisma.credential.delete({ where: { id } }); } - static async updateCredentialById({ - id, - data, - }: { - id: number; - data: CredentialUpdateInput; - }) { + static async updateCredentialById({ id, data }: { id: number; data: CredentialUpdateInput }) { await prisma.credential.update({ where: { id }, data, @@ -198,13 +174,7 @@ export class CredentialRepository { }); } - static async findAllDelegationByTypeIncludeUserAndTake({ - type, - take, - }: { - type: string; - take: number; - }) { + static async findAllDelegationByTypeIncludeUserAndTake({ type, take }: { type: string; take: number }) { const delegationUserCredentials = await prisma.credential.findMany({ where: { delegationCredentialId: { not: null }, @@ -220,16 +190,14 @@ export class CredentialRepository { }, take, }); - return delegationUserCredentials.map( - ({ delegationCredentialId, ...rest }) => { - return { - ...rest, - // We queried only those where delegationCredentialId is not null + return delegationUserCredentials.map(({ delegationCredentialId, ...rest }) => { + return { + ...rest, + // We queried only those where delegationCredentialId is not null - delegationCredentialId: delegationCredentialId!, - }; - } - ); + delegationCredentialId: delegationCredentialId!, + }; + }); } static async findUniqueByUserIdAndDelegationCredentialId({ @@ -249,13 +217,10 @@ export class CredentialRepository { if (delegationUserCredentials.length > 1) { // Instead of crashing use the first one and log for observability // TODO: Plan to add a unique constraint on userId and delegationCredentialId - log.error( - `DelegationCredential: Multiple delegation user credentials found - this should not happen`, - { - userId, - delegationCredentialId, - } - ); + log.error(`DelegationCredential: Multiple delegation user credentials found - this should not happen`, { + userId, + delegationCredentialId, + }); } return delegationUserCredentials[0]; @@ -301,13 +266,7 @@ export class CredentialRepository { }); } - static async updateWhereId({ - id, - data, - }: { - id: number; - data: { key: Prisma.InputJsonValue }; - }) { + static async updateWhereId({ id, data }: { id: number; data: { key: Prisma.InputJsonValue } }) { return prisma.credential.update({ where: { id }, data }); } @@ -446,12 +405,15 @@ export class CredentialRepository { } const key = credential.key as Record; - const filteredKey = keyFields.reduce((acc, field) => { - if (field in key) { - acc[field] = key[field]; - } - return acc; - }, {} as Record); + const filteredKey = keyFields.reduce( + (acc, field) => { + if (field in key) { + acc[field] = key[field]; + } + return acc; + }, + {} as Record + ); return { ...credential, key: filteredKey }; } diff --git a/packages/features/credentials/services/CredentialAccessService.test.ts b/packages/features/credentials/services/CredentialAccessService.test.ts index 14875ee704..a8430a5a82 100644 --- a/packages/features/credentials/services/CredentialAccessService.test.ts +++ b/packages/features/credentials/services/CredentialAccessService.test.ts @@ -27,9 +27,11 @@ vi.mock("@calcom/features/credentials/repositories/CredentialRepository", () => vi.mock("@calcom/features/users/repositories/UserRepository", () => { return { - UserRepository: vi.fn().mockImplementation(function() { return { - getUserOrganizationAndTeams: vi.fn(), - }; }), + UserRepository: vi.fn().mockImplementation(function () { + return { + getUserOrganizationAndTeams: vi.fn(), + }; + }), }; }); @@ -123,7 +125,9 @@ describe("CredentialAccessService", () => { }), }; - vi.mocked(UserRepository).mockImplementation(function() { return mockUserRepo as any; }); + vi.mocked(UserRepository).mockImplementation(function () { + return mockUserRepo as any; + }); const service = new CredentialAccessService(); await expect( @@ -168,7 +172,9 @@ describe("CredentialAccessService", () => { }), }; - vi.mocked(UserRepository).mockImplementation(function() { return mockUserRepo as any; }); + vi.mocked(UserRepository).mockImplementation(function () { + return mockUserRepo as any; + }); const service = new CredentialAccessService(); await expect( @@ -207,7 +213,9 @@ describe("CredentialAccessService", () => { }), }; - vi.mocked(UserRepository).mockImplementation(function() { return mockUserRepo as any; }); + vi.mocked(UserRepository).mockImplementation(function () { + return mockUserRepo as any; + }); const service = new CredentialAccessService(); await expect( @@ -273,7 +281,9 @@ describe("CredentialAccessService", () => { }), }; - vi.mocked(UserRepository).mockImplementation(function() { return mockUserRepo as any; }); + vi.mocked(UserRepository).mockImplementation(function () { + return mockUserRepo as any; + }); const service = new CredentialAccessService(); const error = await service diff --git a/packages/features/data-table/hooks/useDebouncedWidth.ts b/packages/features/data-table/hooks/useDebouncedWidth.ts index 60db166b9e..378189308e 100644 --- a/packages/features/data-table/hooks/useDebouncedWidth.ts +++ b/packages/features/data-table/hooks/useDebouncedWidth.ts @@ -1,6 +1,5 @@ "use client"; - import debounce from "lodash/debounce"; import { useState, useEffect } from "react"; diff --git a/packages/features/data-table/hooks/useFilterValue.ts b/packages/features/data-table/hooks/useFilterValue.ts index 1db4ddf8cc..468fc74af9 100644 --- a/packages/features/data-table/hooks/useFilterValue.ts +++ b/packages/features/data-table/hooks/useFilterValue.ts @@ -7,7 +7,7 @@ import { useDataTable } from "./useDataTable"; export function useFilterValue< T extends FilterType, - TSchema extends FilterValueSchema | typeof ZFilterValue + TSchema extends FilterValueSchema | typeof ZFilterValue, >(columnId: string, schema: TSchema) { const { activeFilters } = useDataTable(); return useMemo(() => { diff --git a/packages/features/data-table/lib/types.ts b/packages/features/data-table/lib/types.ts index 695df96238..3892c3ab56 100644 --- a/packages/features/data-table/lib/types.ts +++ b/packages/features/data-table/lib/types.ts @@ -140,27 +140,26 @@ export const ZColumnFilter = z.object({ export type FilterValueSchema = T extends ColumnFilterTypeValues["SINGLE_SELECT"] ? typeof ZSingleSelectFilterValue : T extends ColumnFilterTypeValues["MULTI_SELECT"] - ? typeof ZMultiSelectFilterValue - : T extends ColumnFilterTypeValues["TEXT"] - ? typeof ZTextFilterValue - : T extends ColumnFilterTypeValues["NUMBER"] - ? typeof ZNumberFilterValue - : T extends ColumnFilterTypeValues["DATE_RANGE"] - ? typeof ZDateRangeFilterValue - : never; + ? typeof ZMultiSelectFilterValue + : T extends ColumnFilterTypeValues["TEXT"] + ? typeof ZTextFilterValue + : T extends ColumnFilterTypeValues["NUMBER"] + ? typeof ZNumberFilterValue + : T extends ColumnFilterTypeValues["DATE_RANGE"] + ? typeof ZDateRangeFilterValue + : never; -export type FilterValue = - T extends ColumnFilterTypeValues["SINGLE_SELECT"] - ? SingleSelectFilterValue - : T extends ColumnFilterTypeValues["MULTI_SELECT"] +export type FilterValue = T extends ColumnFilterTypeValues["SINGLE_SELECT"] + ? SingleSelectFilterValue + : T extends ColumnFilterTypeValues["MULTI_SELECT"] ? MultiSelectFilterValue : T extends ColumnFilterTypeValues["TEXT"] - ? TextFilterValue - : T extends ColumnFilterTypeValues["NUMBER"] - ? NumberFilterValue - : T extends ColumnFilterTypeValues["DATE_RANGE"] - ? DateRangeFilterValue - : never; + ? TextFilterValue + : T extends ColumnFilterTypeValues["NUMBER"] + ? NumberFilterValue + : T extends ColumnFilterTypeValues["DATE_RANGE"] + ? DateRangeFilterValue + : never; export type TypedColumnFilter = { id: string; diff --git a/packages/features/data-table/repositories/filterSegment.ts b/packages/features/data-table/repositories/filterSegment.ts index 9351c05b0e..821bd516c2 100644 --- a/packages/features/data-table/repositories/filterSegment.ts +++ b/packages/features/data-table/repositories/filterSegment.ts @@ -131,8 +131,8 @@ export class FilterSegmentRepository implements IFilterSegmentRepository { const preferredSegmentId = preference?.segmentId ? { id: preference.segmentId, type: "user" as const } : preference?.systemSegmentId - ? { id: preference.systemSegmentId, type: "system" as const } - : null; + ? { id: preference.systemSegmentId, type: "system" as const } + : null; return { segments: parsedSegments, diff --git a/packages/features/di/containers/BookingAccessService.ts b/packages/features/di/containers/BookingAccessService.ts index db13566a72..5bd19d4669 100644 --- a/packages/features/di/containers/BookingAccessService.ts +++ b/packages/features/di/containers/BookingAccessService.ts @@ -9,5 +9,3 @@ export function getBookingAccessService() { bookingAccessServiceModuleLoader.loadModule(container); return container.get(bookingAccessServiceModuleLoader.token); } - - diff --git a/packages/features/di/containers/BookingAuditViewerService.container.ts b/packages/features/di/containers/BookingAuditViewerService.container.ts index a5dae8c300..0df435546b 100644 --- a/packages/features/di/containers/BookingAuditViewerService.container.ts +++ b/packages/features/di/containers/BookingAuditViewerService.container.ts @@ -13,6 +13,5 @@ bookingAuditRepositoryModuleLoader.loadModule(container); bookingAuditViewerServiceModuleLoader.loadModule(container); export function getBookingAuditViewerService() { - return container.get(BOOKING_AUDIT_DI_TOKENS.BOOKING_AUDIT_VIEWER_SERVICE); + return container.get(BOOKING_AUDIT_DI_TOKENS.BOOKING_AUDIT_VIEWER_SERVICE); } - diff --git a/packages/features/di/containers/FeaturesRepository.ts b/packages/features/di/containers/FeaturesRepository.ts index 5070f8faad..65a016d239 100644 --- a/packages/features/di/containers/FeaturesRepository.ts +++ b/packages/features/di/containers/FeaturesRepository.ts @@ -1,5 +1,8 @@ import { createContainer } from "../di"; -import { type FeaturesRepository, moduleLoader as featuresRepositoryModuleLoader } from "../modules/FeaturesRepository"; +import { + type FeaturesRepository, + moduleLoader as featuresRepositoryModuleLoader, +} from "../modules/FeaturesRepository"; const featuresRepositoryContainer = createContainer(); diff --git a/packages/features/di/containers/ManagedEventReassignment.ts b/packages/features/di/containers/ManagedEventReassignment.ts index 5611b3863f..e6f61b3199 100644 --- a/packages/features/di/containers/ManagedEventReassignment.ts +++ b/packages/features/di/containers/ManagedEventReassignment.ts @@ -7,8 +7,5 @@ const container = createContainer(); export function getManagedEventReassignmentService() { managedEventReassignmentServiceModuleLoader.loadModule(container); - return container.get( - managedEventReassignmentServiceModuleLoader.token - ); + return container.get(managedEventReassignmentServiceModuleLoader.token); } - diff --git a/packages/features/di/modules/AssignmentReason.ts b/packages/features/di/modules/AssignmentReason.ts index 7fe3e1f966..c5c705e3e8 100644 --- a/packages/features/di/modules/AssignmentReason.ts +++ b/packages/features/di/modules/AssignmentReason.ts @@ -19,4 +19,3 @@ export const moduleLoader: ModuleLoader = { token, loadModule, }; - diff --git a/packages/features/di/modules/BookingAccessService.ts b/packages/features/di/modules/BookingAccessService.ts index 06cfe743bf..a5f92aefa7 100644 --- a/packages/features/di/modules/BookingAccessService.ts +++ b/packages/features/di/modules/BookingAccessService.ts @@ -19,5 +19,3 @@ export const moduleLoader: ModuleLoader = { token, loadModule, }; - - diff --git a/packages/features/di/modules/Credential.ts b/packages/features/di/modules/Credential.ts index c375541730..096f552ff0 100644 --- a/packages/features/di/modules/Credential.ts +++ b/packages/features/di/modules/Credential.ts @@ -19,4 +19,3 @@ export const moduleLoader: ModuleLoader = { token, loadModule, }; - diff --git a/packages/features/di/modules/Holiday.ts b/packages/features/di/modules/Holiday.ts index 2fd7d973ce..7b4233fdcf 100644 --- a/packages/features/di/modules/Holiday.ts +++ b/packages/features/di/modules/Holiday.ts @@ -14,4 +14,3 @@ export const moduleLoader = { container.load(moduleToken, holidayRepositoryModule); }, }; - diff --git a/packages/features/di/modules/ManagedEventReassignment.ts b/packages/features/di/modules/ManagedEventReassignment.ts index 9e5e894899..f0fcf7199e 100644 --- a/packages/features/di/modules/ManagedEventReassignment.ts +++ b/packages/features/di/modules/ManagedEventReassignment.ts @@ -29,4 +29,3 @@ export const moduleLoader: ModuleLoader = { }; export type { ManagedEventReassignmentService }; - diff --git a/packages/features/di/shared/services/tasker.service.ts b/packages/features/di/shared/services/tasker.service.ts index 0f90fea382..5769ea7b12 100644 --- a/packages/features/di/shared/services/tasker.service.ts +++ b/packages/features/di/shared/services/tasker.service.ts @@ -20,4 +20,4 @@ const loadModule = (container: Container) => { export const moduleLoader = { token, loadModule, -}; \ No newline at end of file +}; diff --git a/packages/features/ee/api-keys/lib/autoLock.test.ts b/packages/features/ee/api-keys/lib/autoLock.test.ts index 65f0bbff51..fb8cecf0e7 100644 --- a/packages/features/ee/api-keys/lib/autoLock.test.ts +++ b/packages/features/ee/api-keys/lib/autoLock.test.ts @@ -34,7 +34,9 @@ describe("autoLock", () => { beforeEach(() => { // Reset all mocks before each test vi.clearAllMocks(); - vi.mocked(RedisService).mockImplementation(function() { return mockRedis as any; }); + vi.mocked(RedisService).mockImplementation(function () { + return mockRedis as any; + }); // Mock environment variables process.env.UPSTASH_REDIS_REST_TOKEN = "test-token"; diff --git a/packages/features/ee/api-keys/services/ApiKeyService.ts b/packages/features/ee/api-keys/services/ApiKeyService.ts index 2853359ce0..c19b9013b2 100644 --- a/packages/features/ee/api-keys/services/ApiKeyService.ts +++ b/packages/features/ee/api-keys/services/ApiKeyService.ts @@ -4,22 +4,24 @@ type Deps = { apiKeyRepo: PrismaApiKeyRepository; }; -type VerifyKeyResult = { - valid: true; - userId: number; - user: { - uuid: string; - role: string; - locked: boolean; - email: string; - }; -} | { - valid: false; - error: string; -} +type VerifyKeyResult = + | { + valid: true; + userId: number; + user: { + uuid: string; + role: string; + locked: boolean; + email: string; + }; + } + | { + valid: false; + error: string; + }; export class ApiKeyService { - constructor(private readonly deps: Deps) { } + constructor(private readonly deps: Deps) {} async verifyKeyByHashedKey(hashedKey: string): Promise { const apiKey = await this.deps.apiKeyRepo.findByHashedKey(hashedKey); diff --git a/packages/features/ee/billing/active-user/services/ActiveUserBillingService.ts b/packages/features/ee/billing/active-user/services/ActiveUserBillingService.ts index 29a9e2fb58..6a6c143fc8 100644 --- a/packages/features/ee/billing/active-user/services/ActiveUserBillingService.ts +++ b/packages/features/ee/billing/active-user/services/ActiveUserBillingService.ts @@ -20,18 +20,15 @@ export class ActiveUserBillingService { periodEnd: Date ): Promise { const managedUserEmails = - await this.deps.activeUserBillingRepository.getManagedUserEmailsBySubscriptionId( - subscriptionId - ); + await this.deps.activeUserBillingRepository.getManagedUserEmailsBySubscriptionId(subscriptionId); if (managedUserEmails.length === 0) return 0; - const activeHosts = - await this.deps.activeUserBillingRepository.getActivePlatformUsersAsHost( - subscriptionId, - periodStart, - periodEnd - ); + const activeHosts = await this.deps.activeUserBillingRepository.getActivePlatformUsersAsHost( + subscriptionId, + periodStart, + periodEnd + ); const activeHostEmails = new Set(activeHosts.map((h) => h.email)); @@ -41,12 +38,11 @@ export class ActiveUserBillingService { if (nonHostEmails.length === 0) return activeHostEmails.size; - const activeAttendees = - await this.deps.activeUserBillingRepository.getActiveUsersAsAttendee( - nonHostEmails, - periodStart, - periodEnd - ); + const activeAttendees = await this.deps.activeUserBillingRepository.getActiveUsersAsAttendee( + nonHostEmails, + periodStart, + periodEnd + ); return activeHostEmails.size + activeAttendees.length; } @@ -55,15 +51,8 @@ export class ActiveUserBillingService { * Count active users for a regular organization (uses org/team ID). * A user is "active" if they hosted or attended at least one booking in the period. */ - async getActiveUserCountForOrg( - orgId: number, - periodStart: Date, - periodEnd: Date - ): Promise { - const memberEmails = - await this.deps.activeUserBillingRepository.getOrgMemberEmailsByOrgId( - orgId - ); + async getActiveUserCountForOrg(orgId: number, periodStart: Date, periodEnd: Date): Promise { + const memberEmails = await this.deps.activeUserBillingRepository.getOrgMemberEmailsByOrgId(orgId); return this.countActiveUsers( memberEmails.map((u) => u.email), @@ -79,27 +68,23 @@ export class ActiveUserBillingService { ): Promise { if (candidateEmails.length === 0) return 0; - const activeHosts = - await this.deps.activeUserBillingRepository.getActiveUsersAsHost( - candidateEmails, - periodStart, - periodEnd - ); + const activeHosts = await this.deps.activeUserBillingRepository.getActiveUsersAsHost( + candidateEmails, + periodStart, + periodEnd + ); const activeHostEmails = new Set(activeHosts.map((h) => h.email)); - const nonHostEmails = candidateEmails.filter( - (email) => !activeHostEmails.has(email) - ); + const nonHostEmails = candidateEmails.filter((email) => !activeHostEmails.has(email)); if (nonHostEmails.length === 0) return activeHostEmails.size; - const activeAttendees = - await this.deps.activeUserBillingRepository.getActiveUsersAsAttendee( - nonHostEmails, - periodStart, - periodEnd - ); + const activeAttendees = await this.deps.activeUserBillingRepository.getActiveUsersAsAttendee( + nonHostEmails, + periodStart, + periodEnd + ); return activeHostEmails.size + activeAttendees.length; } diff --git a/packages/features/ee/billing/api/webhook/__handler.test.ts b/packages/features/ee/billing/api/webhook/__handler.test.ts index a9f1320a4b..45ddcf7ff0 100644 --- a/packages/features/ee/billing/api/webhook/__handler.test.ts +++ b/packages/features/ee/billing/api/webhook/__handler.test.ts @@ -30,7 +30,7 @@ describe("stripeWebhookHandler", () => { ({ headers, body, - } as unknown as NextApiRequest); + }) as unknown as NextApiRequest; it("should throw an error if stripe-signature header is missing", async () => { const { req } = createMocks({ diff --git a/packages/features/ee/billing/api/webhook/_invoice.payment_failed.test.ts b/packages/features/ee/billing/api/webhook/_invoice.payment_failed.test.ts index 9cc7ddb663..d564ebb210 100644 --- a/packages/features/ee/billing/api/webhook/_invoice.payment_failed.test.ts +++ b/packages/features/ee/billing/api/webhook/_invoice.payment_failed.test.ts @@ -40,10 +40,7 @@ describe("invoice.payment_failed webhook", () => { expect(getPaymentIntentFailureReason).toHaveBeenCalledWith("pi_123"); expect(createBySubscriptionId).toHaveBeenCalledWith("sub_123"); - expect(onPaymentFailed).toHaveBeenCalledWith( - { lines: data.object.lines }, - "card_declined" - ); + expect(onPaymentFailed).toHaveBeenCalledWith({ lines: data.object.lines }, "card_declined"); expect(result).toEqual({ success: true, handled: true }); }); @@ -82,10 +79,7 @@ describe("invoice.payment_failed webhook", () => { const result = await handler(data); expect(getPaymentIntentFailureReason).not.toHaveBeenCalled(); - expect(onPaymentFailed).toHaveBeenCalledWith( - { lines: data.object.lines }, - "open" - ); + expect(onPaymentFailed).toHaveBeenCalledWith({ lines: data.object.lines }, "open"); expect(result).toEqual({ success: true, handled: true }); }); }); diff --git a/packages/features/ee/billing/api/webhook/_invoice.payment_failed.ts b/packages/features/ee/billing/api/webhook/_invoice.payment_failed.ts index 81b17d4169..8255b1e3a0 100644 --- a/packages/features/ee/billing/api/webhook/_invoice.payment_failed.ts +++ b/packages/features/ee/billing/api/webhook/_invoice.payment_failed.ts @@ -1,4 +1,7 @@ -import { getBillingProviderService, getSeatBillingStrategyFactory } from "@calcom/ee/billing/di/containers/Billing"; +import { + getBillingProviderService, + getSeatBillingStrategyFactory, +} from "@calcom/ee/billing/di/containers/Billing"; import logger from "@calcom/lib/logger"; import type { SWHMap } from "./__handler"; diff --git a/packages/features/ee/billing/api/webhook/hwm-webhook-utils.ts b/packages/features/ee/billing/api/webhook/hwm-webhook-utils.ts index a4111ca890..3b562b1ba3 100644 --- a/packages/features/ee/billing/api/webhook/hwm-webhook-utils.ts +++ b/packages/features/ee/billing/api/webhook/hwm-webhook-utils.ts @@ -20,17 +20,13 @@ export function validateInvoiceLinesForHwm( log: Logger = defaultLogger ): { isValid: boolean; periodStart?: number } { if (!linesData || linesData.length === 0) { - log.warn( - `Invoice has no line items for subscription ${subscriptionId}, cannot process HWM` - ); + log.warn(`Invoice has no line items for subscription ${subscriptionId}, cannot process HWM`); return { isValid: false }; } const periodStart = linesData[0]?.period?.start; if (!periodStart) { - log.warn( - `Invoice line item missing period.start for subscription ${subscriptionId}, cannot process HWM` - ); + log.warn(`Invoice line item missing period.start for subscription ${subscriptionId}, cannot process HWM`); return { isValid: false }; } @@ -49,9 +45,7 @@ export async function handleHwmResetAfterRenewal( log: Logger = defaultLogger ): Promise { if (!periodStartTimestamp) { - log.warn( - `No period start timestamp for subscription ${subscriptionId}, skipping HWM reset` - ); + log.warn(`No period start timestamp for subscription ${subscriptionId}, skipping HWM reset`); return { success: false, error: "No period start timestamp" }; } diff --git a/packages/features/ee/billing/credit-service.test.ts b/packages/features/ee/billing/credit-service.test.ts index 14a8565c76..b790ae39ab 100644 --- a/packages/features/ee/billing/credit-service.test.ts +++ b/packages/features/ee/billing/credit-service.test.ts @@ -469,7 +469,9 @@ describe("CreditService", () => { members: [{ accepted: true }], }), }; - vi.mocked(TeamRepository).mockImplementation(function() { return mockTeamRepo as unknown as TeamRepository; }); + vi.mocked(TeamRepository).mockImplementation(function () { + return mockTeamRepo as unknown as TeamRepository; + }); const mockTeamBillingService = { getSubscriptionStatus: vi.fn().mockResolvedValue(SubscriptionStatus.TRIALING), @@ -493,7 +495,9 @@ describe("CreditService", () => { members: [{ accepted: true }, { accepted: true }, { accepted: true }], }), }; - vi.mocked(TeamRepository).mockImplementation(function() { return mockTeamRepo as unknown as TeamRepository; }); + vi.mocked(TeamRepository).mockImplementation(function () { + return mockTeamRepo as unknown as TeamRepository; + }); const mockTeamBillingService = { getSubscriptionStatus: vi.fn().mockResolvedValue(SubscriptionStatus.ACTIVE), @@ -524,7 +528,9 @@ describe("CreditService", () => { members: [{ accepted: true }, { accepted: true }], }), }; - vi.mocked(TeamRepository).mockImplementation(function() { return mockTeamRepo as unknown as TeamRepository; }); + vi.mocked(TeamRepository).mockImplementation(function () { + return mockTeamRepo as unknown as TeamRepository; + }); const mockTeamBillingService = { getSubscriptionStatus: vi.fn().mockResolvedValue(SubscriptionStatus.ACTIVE), @@ -549,7 +555,9 @@ describe("CreditService", () => { members: [{ accepted: true }, { accepted: true }, { accepted: true }], }), }; - vi.mocked(TeamRepository).mockImplementation(function() { return mockTeamRepo as unknown as TeamRepository; }); + vi.mocked(TeamRepository).mockImplementation(function () { + return mockTeamRepo as unknown as TeamRepository; + }); const mockTeamBillingService = { getSubscriptionStatus: vi.fn().mockResolvedValue(SubscriptionStatus.ACTIVE), @@ -842,9 +850,9 @@ describe("CreditService", () => { ]); const mockTeamRepoInstance = { - findTeamsForCreditCheck: vi.fn().mockResolvedValue([ - { id: 2, isOrganization: false, parentId: null, parent: null }, - ]), + findTeamsForCreditCheck: vi + .fn() + .mockResolvedValue([{ id: 2, isOrganization: false, parentId: null, parent: null }]), }; vi.mocked(TeamRepository).mockImplementation(function () { return mockTeamRepoInstance as unknown as TeamRepository; @@ -960,9 +968,9 @@ describe("CreditService", () => { ]); const mockTeamRepoInstance = { - findTeamsForCreditCheck: vi.fn().mockResolvedValue([ - { id: 2, isOrganization: false, parentId: null, parent: null }, - ]), + findTeamsForCreditCheck: vi + .fn() + .mockResolvedValue([{ id: 2, isOrganization: false, parentId: null, parent: null }]), }; vi.mocked(TeamRepository).mockImplementation(function () { return mockTeamRepoInstance as unknown as TeamRepository; diff --git a/packages/features/ee/billing/repository/highWaterMark/HighWaterMarkRepository.ts b/packages/features/ee/billing/repository/highWaterMark/HighWaterMarkRepository.ts index be42f930c9..277347d7c0 100644 --- a/packages/features/ee/billing/repository/highWaterMark/HighWaterMarkRepository.ts +++ b/packages/features/ee/billing/repository/highWaterMark/HighWaterMarkRepository.ts @@ -74,9 +74,7 @@ export class HighWaterMarkRepository { if (!team) return null; - const billing = team.isOrganization - ? team.organizationBilling - : team.teamBilling; + const billing = team.isOrganization ? team.organizationBilling : team.teamBilling; if (!billing) return null; return { @@ -85,9 +83,7 @@ export class HighWaterMarkRepository { }; } - async getBySubscriptionId( - subscriptionId: string - ): Promise { + async getBySubscriptionId(subscriptionId: string): Promise { // Try team billing first const teamBilling = await this.prisma.teamBilling.findUnique({ where: { subscriptionId }, @@ -170,9 +166,7 @@ export class HighWaterMarkRepository { throw new Error(`No team found for teamId ${teamId}`); } - const billing = isOrganization - ? team.organizationBilling - : team.teamBilling; + const billing = isOrganization ? team.organizationBilling : team.teamBilling; if (!billing) { throw new Error(`No billing record found for team ${teamId}`); } @@ -181,13 +175,10 @@ export class HighWaterMarkRepository { const currentPeriodStart = billing.highWaterMarkPeriodStart; // Check if we're in a new period (period start changed) - const isNewPeriod = - !currentPeriodStart || - periodStart.getTime() !== currentPeriodStart.getTime(); + const isNewPeriod = !currentPeriodStart || periodStart.getTime() !== currentPeriodStart.getTime(); // If new period, always update. Otherwise, only update if higher. - const shouldUpdate = - isNewPeriod || currentHwm === null || newSeatCount > currentHwm; + const shouldUpdate = isNewPeriod || currentHwm === null || newSeatCount > currentHwm; if (!shouldUpdate) { return { updated: false, previousHighWaterMark: currentHwm }; diff --git a/packages/features/ee/billing/repository/proration/MonthlyProrationTeamRepository.ts b/packages/features/ee/billing/repository/proration/MonthlyProrationTeamRepository.ts index 45c23aed90..03f582f4b6 100644 --- a/packages/features/ee/billing/repository/proration/MonthlyProrationTeamRepository.ts +++ b/packages/features/ee/billing/repository/proration/MonthlyProrationTeamRepository.ts @@ -70,9 +70,7 @@ export class MonthlyProrationTeamRepository { if (!team) return null; - let billing: BillingInfo | null = team.isOrganization - ? team.organizationBilling - : team.teamBilling; + let billing: BillingInfo | null = team.isOrganization ? team.organizationBilling : team.teamBilling; if (!billing) { billing = this.extractBillingFromMetadata(team.metadata); diff --git a/packages/features/ee/billing/seed.ts b/packages/features/ee/billing/seed.ts index a9939dedb9..a73d4b8183 100644 --- a/packages/features/ee/billing/seed.ts +++ b/packages/features/ee/billing/seed.ts @@ -124,7 +124,12 @@ async function main() { if (args.includes("--all")) { process.exit(await seedAll(args.includes("--cleanup"))); } - if (args.includes("--cleanup") && !args.includes("--hwm") && !args.includes("--proration") && !args.includes("--all")) { + if ( + args.includes("--cleanup") && + !args.includes("--hwm") && + !args.includes("--proration") && + !args.includes("--all") + ) { process.exit(await cleanupAll()); } diff --git a/packages/features/ee/billing/service/dueInvoice/seed-proration-test.ts b/packages/features/ee/billing/service/dueInvoice/seed-proration-test.ts index a461489da5..758fde9396 100644 --- a/packages/features/ee/billing/service/dueInvoice/seed-proration-test.ts +++ b/packages/features/ee/billing/service/dueInvoice/seed-proration-test.ts @@ -183,7 +183,10 @@ async function cleanupStripeResources(stripe: Stripe) { }); for (const product of products.data) { - if ((product.name.startsWith("Proration Test") || product.name.startsWith("Trigger Test")) && product.active) { + if ( + (product.name.startsWith("Proration Test") || product.name.startsWith("Trigger Test")) && + product.active + ) { await stripe.products.update(product.id, { active: false }); console.log(` Archived product: ${product.id}`); } diff --git a/packages/features/ee/billing/service/highWaterMark/__tests__/HighWaterMarkStripe.integration-test.ts b/packages/features/ee/billing/service/highWaterMark/__tests__/HighWaterMarkStripe.integration-test.ts index a3a680de7e..ef2c14cc5e 100644 --- a/packages/features/ee/billing/service/highWaterMark/__tests__/HighWaterMarkStripe.integration-test.ts +++ b/packages/features/ee/billing/service/highWaterMark/__tests__/HighWaterMarkStripe.integration-test.ts @@ -23,8 +23,7 @@ const STRIPE_PRIVATE_KEY = process.env.STRIPE_PRIVATE_KEY; const STRIPE_TEAM_MONTHLY_PRICE_ID = process.env.STRIPE_TEAM_MONTHLY_PRICE_ID; // Skip if Stripe is not configured -const describeIfStripe = - STRIPE_PRIVATE_KEY && STRIPE_TEAM_MONTHLY_PRICE_ID ? describe : describe.skip; +const describeIfStripe = STRIPE_PRIVATE_KEY && STRIPE_TEAM_MONTHLY_PRICE_ID ? describe : describe.skip; describeIfStripe("HighWaterMark Stripe E2E Test", () => { let stripe: Stripe; @@ -139,9 +138,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { subscription = await stripe.subscriptions.create({ customer: customer.id, - items: [ - { price: STRIPE_TEAM_MONTHLY_PRICE_ID!, quantity: currentMemberCount }, - ], + items: [{ price: STRIPE_TEAM_MONTHLY_PRICE_ID!, quantity: currentMemberCount }], metadata: { testData: "true", teamId: testTeam.id.toString(), @@ -163,9 +160,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { status: "ACTIVE", planName: "TEAM", highWaterMark: peakMemberCount, - highWaterMarkPeriodStart: new Date( - subscription.current_period_start * 1000 - ), + highWaterMarkPeriodStart: new Date(subscription.current_period_start * 1000), }, }); @@ -175,11 +170,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { console.log(`HWM (Peak): ${peakMemberCount}`); console.log(`Subscription: ${subscription.id}`); console.log(`Test Clock: ${testClock.id}`); - console.log( - `Period End: ${new Date( - subscription.current_period_end * 1000 - ).toISOString()}` - ); + console.log(`Period End: ${new Date(subscription.current_period_end * 1000).toISOString()}`); }); afterAll(async () => { @@ -208,10 +199,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { console.log("E2E Test cleanup complete"); }); - async function waitForClockAdvancement( - clockId: string, - maxWaitMs = 60000 - ): Promise { + async function waitForClockAdvancement(clockId: string, maxWaitMs = 60000): Promise { const startTime = Date.now(); const pollIntervalMs = 2000; @@ -219,20 +207,12 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { const clock = await stripe.testHelpers.testClocks.retrieve(clockId); if (clock.status === "ready") { - console.log( - `Test clock ready at: ${new Date( - clock.frozen_time * 1000 - ).toISOString()}` - ); + console.log(`Test clock ready at: ${new Date(clock.frozen_time * 1000).toISOString()}`); return; } if (clock.status === "advancing") { - console.log( - `Clock still advancing... (${Math.round( - (Date.now() - startTime) / 1000 - )}s elapsed)` - ); + console.log(`Clock still advancing... (${Math.round((Date.now() - startTime) / 1000)}s elapsed)`); await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); continue; } @@ -257,11 +237,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { return; } - console.log( - `Waiting for ${description}... (${Math.round( - (Date.now() - startTime) / 1000 - )}s elapsed)` - ); + console.log(`Waiting for ${description}... (${Math.round((Date.now() - startTime) / 1000)}s elapsed)`); await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); } @@ -295,21 +271,14 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { where: { teamId: testTeam.id }, select: { paidSeats: true, highWaterMark: true }, }); - console.log( - `Reset state: paidSeats=${resetBilling?.paidSeats}, HWM=${resetBilling?.highWaterMark}` - ); + console.log(`Reset state: paidSeats=${resetBilling?.paidSeats}, HWM=${resetBilling?.highWaterMark}`); expect(resetBilling?.paidSeats).toBe(4); expect(resetBilling?.highWaterMark).toBe(6); // Advance clock to 3 days before period end (triggers invoice.upcoming) - const invoiceUpcomingTime = - subscription.current_period_end - 3 * 24 * 60 * 60; - console.log( - `Advancing clock to: ${new Date( - invoiceUpcomingTime * 1000 - ).toISOString()}` - ); + const invoiceUpcomingTime = subscription.current_period_end - 3 * 24 * 60 * 60; + console.log(`Advancing clock to: ${new Date(invoiceUpcomingTime * 1000).toISOString()}`); await stripe.testHelpers.testClocks.advance(testClock.id, { frozen_time: invoiceUpcomingTime, @@ -320,9 +289,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { // The webhook fires during clock advancement but fails because Stripe blocks // modifications while advancing. Now that clock is ready, manually trigger scale-up. - console.log( - "\\nManually triggering scale-up (webhook fails during clock advancement)..." - ); + console.log("\\nManually triggering scale-up (webhook fails during clock advancement)..."); const billingService = new StripeBillingService(stripe); const hwmService = new HighWaterMarkService({ @@ -347,9 +314,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { }, 120000); // 2 minute timeout it("should scale DOWN subscription quantity to current members after renewal", async () => { - console.log( - "\n=== Step 2: Advance clock past renewal to trigger scale down ===" - ); + console.log("\n=== Step 2: Advance clock past renewal to trigger scale down ==="); // Get the updated subscription (period may have changed) const currentSub = await stripe.subscriptions.retrieve(subscription.id); @@ -357,12 +322,8 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { // Advance clock past the billing period end (triggers renewal + subscription.updated) const renewalTime = periodEnd + 60; // 1 minute after period end - console.log( - `Current period end: ${new Date(periodEnd * 1000).toISOString()}` - ); - console.log( - `Advancing clock to: ${new Date(renewalTime * 1000).toISOString()}` - ); + console.log(`Current period end: ${new Date(periodEnd * 1000).toISOString()}`); + console.log(`Advancing clock to: ${new Date(renewalTime * 1000).toISOString()}`); await stripe.testHelpers.testClocks.advance(testClock.id, { frozen_time: renewalTime, @@ -372,16 +333,12 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { await waitForClockAdvancement(testClock.id); // Debug: Check state immediately after clock advancement - const subAfterRenewal = await stripe.subscriptions.retrieve( - subscription.id - ); + const subAfterRenewal = await stripe.subscriptions.retrieve(subscription.id); console.log("Stripe subscription after renewal:"); console.log(` - Status: ${subAfterRenewal.status}`); console.log(` - Quantity: ${subAfterRenewal.items.data[0].quantity}`); console.log( - ` - Period: ${new Date( - subAfterRenewal.current_period_start * 1000 - ).toISOString()} to ${new Date( + ` - Period: ${new Date(subAfterRenewal.current_period_start * 1000).toISOString()} to ${new Date( subAfterRenewal.current_period_end * 1000 ).toISOString()}` ); @@ -397,9 +354,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { // The webhook fires during clock advancement but fails because Stripe blocks // modifications while advancing. Now that clock is ready, manually trigger reset. // This simulates what would happen if we had retry logic or a separate job. - console.log( - "\\nManually triggering reset (webhook fails during clock advancement)..." - ); + console.log("\\nManually triggering reset (webhook fails during clock advancement)..."); const billingService = new StripeBillingService(stripe); const hwmService = new HighWaterMarkService({ @@ -407,9 +362,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { billingService, }); - const newPeriodStart = new Date( - subAfterRenewal.current_period_start * 1000 - ); + const newPeriodStart = new Date(subAfterRenewal.current_period_start * 1000); await hwmService.resetSubscriptionAfterRenewal({ subscriptionId: subscription.id, newPeriodStart, @@ -430,11 +383,7 @@ describeIfStripe("HighWaterMark Stripe E2E Test", () => { const updatedSub = await stripe.subscriptions.retrieve(subscription.id); expect(updatedSub.items.data[0].quantity).toBe(4); - console.log( - "Scale DOWN verified: subscription quantity = 4 (current members)" - ); - console.log( - `Final state: paidSeats=${billing?.paidSeats}, HWM=${billing?.highWaterMark}` - ); + console.log("Scale DOWN verified: subscription quantity = 4 (current members)"); + console.log(`Final state: paidSeats=${billing?.paidSeats}, HWM=${billing?.highWaterMark}`); }, 180000); // 3 minute timeout }); diff --git a/packages/features/ee/billing/service/highWaterMark/seed-hwm-test.ts b/packages/features/ee/billing/service/highWaterMark/seed-hwm-test.ts index d6a787dba0..ddbe5bd80f 100644 --- a/packages/features/ee/billing/service/highWaterMark/seed-hwm-test.ts +++ b/packages/features/ee/billing/service/highWaterMark/seed-hwm-test.ts @@ -270,7 +270,12 @@ async function cleanupDatabaseResources() { console.log("Cleaning up database test resources..."); // Delete test users (cascade will handle related records) - const testEmails = [HWM_TEAM_ADMIN_EMAIL, ...HWM_TEAM_MEMBER_EMAILS, HWM_ORG_ADMIN_EMAIL, ...HWM_ORG_MEMBER_EMAILS]; + const testEmails = [ + HWM_TEAM_ADMIN_EMAIL, + ...HWM_TEAM_MEMBER_EMAILS, + HWM_ORG_ADMIN_EMAIL, + ...HWM_ORG_MEMBER_EMAILS, + ]; const deleteResult = await prisma.user.deleteMany({ where: { email: { in: testEmails } }, }); @@ -445,7 +450,9 @@ async function seedHwmTeam(stripe: Stripe | null): Promise { }, }); - console.log(` TeamBilling created (MONTHLY, $${teamPricePerSeatCents / 100}/seat, ${paidSeats} paid, HWM=${peakMemberCount})`); + console.log( + ` TeamBilling created (MONTHLY, $${teamPricePerSeatCents / 100}/seat, ${paidSeats} paid, HWM=${peakMemberCount})` + ); // Get billing record for linking seat changes const teamBilling = await prisma.teamBilling.findUnique({ where: { teamId: team.id } }); @@ -654,7 +661,9 @@ async function seedHwmOrg(stripe: Stripe | null): Promise { }, }); - console.log(` OrganizationBilling created (MONTHLY, $${orgPricePerSeatCents / 100}/seat, ${paidSeats} paid, HWM=${peakMemberCount})`); + console.log( + ` OrganizationBilling created (MONTHLY, $${orgPricePerSeatCents / 100}/seat, ${paidSeats} paid, HWM=${peakMemberCount})` + ); // Get billing record for linking seat changes const orgBilling = await prisma.organizationBilling.findUnique({ where: { teamId: org.id } }); @@ -812,12 +821,8 @@ async function main() { const formatDate = (d: Date | null) => (d ? d.toISOString().split("T")[0] : "N/A"); console.log("=== Summary Table ===\n"); - console.log( - "| Entity | Qty | Members | HWM | invoice.upcoming | After Renewal | Price |" - ); - console.log( - "|--------|-----|---------|-----|------------------|---------------|-------|" - ); + console.log("| Entity | Qty | Members | HWM | invoice.upcoming | After Renewal | Price |"); + console.log("|--------|-----|---------|-----|------------------|---------------|-------|"); console.log( `| Team (ID:${result.team.team.id}) | ${result.team.paidSeats} | ${result.team.memberCount} | ${result.team.highWaterMark} | qty → ${result.team.highWaterMark} | qty → ${result.team.memberCount} (scale down) | $6.99 |` ); @@ -826,8 +831,12 @@ async function main() { ); console.log("\n=== Test Clock Dates ===\n"); - console.log(` 1. Advance to ${formatDate(teamInvoiceUpcomingDate)} → triggers invoice.upcoming (scale UP to HWM)`); - console.log(` 2. Advance to ${formatDate(teamPeriodEnd)} → triggers renewal (scale DOWN to current members)`); + console.log( + ` 1. Advance to ${formatDate(teamInvoiceUpcomingDate)} → triggers invoice.upcoming (scale UP to HWM)` + ); + console.log( + ` 2. Advance to ${formatDate(teamPeriodEnd)} → triggers renewal (scale DOWN to current members)` + ); console.log("\n=== Stripe Resources ===\n"); console.log("Team:"); @@ -853,7 +862,9 @@ async function main() { console.log(" Or use Stripe CLI:"); if (result.team.stripe.testClock) { const advanceTime = Math.floor(Date.now() / 1000) + 27 * 24 * 60 * 60; - console.log(` stripe test_clocks advance ${result.team.stripe.testClock.id} --frozen-time ${advanceTime}`); + console.log( + ` stripe test_clocks advance ${result.team.stripe.testClock.id} --frozen-time ${advanceTime}` + ); } console.log(""); console.log("2. invoice.upcoming webhook flow:"); @@ -875,8 +886,12 @@ async function main() { console.log(" - Org: subscription quantity reset from 8 to 6, HWM reset to 6"); console.log(""); console.log("5. To verify HWM in database:"); - console.log(` SELECT "highWaterMark", "highWaterMarkPeriodStart", "paidSeats" FROM "TeamBilling" WHERE "teamId" = ${result.team.team.id};`); - console.log(` SELECT "highWaterMark", "highWaterMarkPeriodStart", "paidSeats" FROM "OrganizationBilling" WHERE "teamId" = ${result.organization.organization.id};`); + console.log( + ` SELECT "highWaterMark", "highWaterMarkPeriodStart", "paidSeats" FROM "TeamBilling" WHERE "teamId" = ${result.team.team.id};` + ); + console.log( + ` SELECT "highWaterMark", "highWaterMarkPeriodStart", "paidSeats" FROM "OrganizationBilling" WHERE "teamId" = ${result.organization.organization.id};` + ); console.log(""); console.log("=== Cleanup ===\n"); diff --git a/packages/features/ee/billing/service/proration/MonthlyProrationService.ts b/packages/features/ee/billing/service/proration/MonthlyProrationService.ts index eb3476301b..90aa80e886 100644 --- a/packages/features/ee/billing/service/proration/MonthlyProrationService.ts +++ b/packages/features/ee/billing/service/proration/MonthlyProrationService.ts @@ -66,7 +66,8 @@ export class MonthlyProrationService { async processMonthlyProrations(params: ProcessMonthlyProrationsParams) { const { monthKey, teamIds } = params; - const isFeatureEnabled = await this.featuresRepository.checkIfFeatureIsEnabledGlobally("monthly-proration"); + const isFeatureEnabled = + await this.featuresRepository.checkIfFeatureIsEnabledGlobally("monthly-proration"); if (!isFeatureEnabled) { this.logger.info("Monthly proration feature is not enabled, skipping batch processing", { monthKey }); return []; @@ -400,7 +401,12 @@ export class MonthlyProrationService { await this.billingService.deleteInvoiceItem(invoiceItemId); } } catch (cleanupError) { - this.logger.error("Failed to clean up Stripe artifacts", { prorationId, invoiceId, invoiceItemId, error: cleanupError }); + this.logger.error("Failed to clean up Stripe artifacts", { + prorationId, + invoiceId, + invoiceItemId, + error: cleanupError, + }); } } diff --git a/packages/features/ee/billing/service/proration/ProrationEmailService.ts b/packages/features/ee/billing/service/proration/ProrationEmailService.ts index 1f60a235cd..3e01d389e9 100644 --- a/packages/features/ee/billing/service/proration/ProrationEmailService.ts +++ b/packages/features/ee/billing/service/proration/ProrationEmailService.ts @@ -37,9 +37,7 @@ export class ProrationEmailService { async sendInvoiceEmail(params: SendInvoiceEmailParams): Promise { const { prorationId, teamId, isAutoCharge } = params; - log.debug( - `Processing invoice email for prorationId ${prorationId}, teamId ${teamId}` - ); + log.debug(`Processing invoice email for prorationId ${prorationId}, teamId ${teamId}`); const proration = await this.prorationRepository.findForEmail(prorationId); if (!proration) { @@ -58,17 +56,13 @@ export class ProrationEmailService { } if (team.adminAndOwners.length === 0) { - log.warn( - `No users with billing permission found for team ${teamId}, skipping invoice email` - ); + log.warn(`No users with billing permission found for team ${teamId}, skipping invoice email`); return; } const invoiceUrl = await this.getInvoiceUrl(proration.invoiceId); - const { sendProrationInvoiceEmails } = await import( - "@calcom/emails/billing-email-service" - ); + const { sendProrationInvoiceEmails } = await import("@calcom/emails/billing-email-service"); await sendProrationInvoiceEmails({ team: { id: team.id, name: team.name }, proration: { @@ -81,17 +75,13 @@ export class ProrationEmailService { adminAndOwners: team.adminAndOwners, }); - log.debug( - `Successfully sent proration invoice emails for prorationId ${prorationId}` - ); + log.debug(`Successfully sent proration invoice emails for prorationId ${prorationId}`); } async sendReminderEmail(params: SendReminderEmailParams): Promise { const { prorationId, teamId } = params; - log.debug( - `Processing reminder email for prorationId ${prorationId}, teamId ${teamId}` - ); + log.debug(`Processing reminder email for prorationId ${prorationId}, teamId ${teamId}`); const proration = await this.prorationRepository.findForEmail(prorationId); if (!proration) { @@ -100,9 +90,7 @@ export class ProrationEmailService { } if (proration.status === "CHARGED") { - log.debug( - `Proration ${prorationId} already charged, skipping reminder email` - ); + log.debug(`Proration ${prorationId} already charged, skipping reminder email`); return; } @@ -117,9 +105,7 @@ export class ProrationEmailService { } if (team.adminAndOwners.length === 0) { - log.warn( - `No users with billing permission found for team ${teamId}, skipping reminder email` - ); + log.warn(`No users with billing permission found for team ${teamId}, skipping reminder email`); return; } @@ -129,52 +115,41 @@ export class ProrationEmailService { "@calcom/emails/templates/proration-reminder-email" ); - const emailPromises = team.adminAndOwners.map( - async (user: UserWithBillingAccess) => { - const email = new ProrationReminderEmail({ - user: { - name: user.name, - email: user.email, - t: user.t, - }, - team: { - id: team.id, - name: team.name, - }, - proration: { - monthKey: proration.monthKey, - netSeatIncrease: proration.netSeatIncrease, - proratedAmount: proration.proratedAmount, - }, - invoiceUrl, - }); - return email.sendEmail(); - } - ); + const emailPromises = team.adminAndOwners.map(async (user: UserWithBillingAccess) => { + const email = new ProrationReminderEmail({ + user: { + name: user.name, + email: user.email, + t: user.t, + }, + team: { + id: team.id, + name: team.name, + }, + proration: { + monthKey: proration.monthKey, + netSeatIncrease: proration.netSeatIncrease, + proratedAmount: proration.proratedAmount, + }, + invoiceUrl, + }); + return email.sendEmail(); + }); await Promise.all(emailPromises); - log.debug( - `Successfully sent proration reminder emails for prorationId ${prorationId}` - ); + log.debug(`Successfully sent proration reminder emails for prorationId ${prorationId}`); } - private async getInvoiceUrl( - invoiceId: string | null - ): Promise { + private async getInvoiceUrl(invoiceId: string | null): Promise { if (!invoiceId) return null; try { - const { default: stripe } = await import( - "@calcom/features/ee/payments/server/stripe" - ); + const { default: stripe } = await import("@calcom/features/ee/payments/server/stripe"); const invoice = await stripe.invoices.retrieve(invoiceId); return invoice.hosted_invoice_url ?? null; } catch (error) { - log.warn( - `Failed to retrieve invoice URL for ${invoiceId}`, - safeStringify(error) - ); + log.warn(`Failed to retrieve invoice URL for ${invoiceId}`, safeStringify(error)); return null; } } diff --git a/packages/features/ee/billing/service/proration/__tests__/MonthlyProrationService.integration-test.ts b/packages/features/ee/billing/service/proration/__tests__/MonthlyProrationService.integration-test.ts index d5565b48e9..05eca3b5b7 100644 --- a/packages/features/ee/billing/service/proration/__tests__/MonthlyProrationService.integration-test.ts +++ b/packages/features/ee/billing/service/proration/__tests__/MonthlyProrationService.integration-test.ts @@ -26,14 +26,10 @@ function getTestDates() { } const mockBillingService: IBillingProviderService = { - createInvoiceItem: vi - .fn() - .mockResolvedValue({ invoiceItemId: "ii_test_123" }), + createInvoiceItem: vi.fn().mockResolvedValue({ invoiceItemId: "ii_test_123" }), deleteInvoiceItem: vi.fn().mockResolvedValue(undefined), createInvoice: vi.fn().mockResolvedValue({ invoiceId: "in_test_123" }), - finalizeInvoice: vi - .fn() - .mockResolvedValue({ invoiceUrl: "https://invoice.stripe.com/test" }), + finalizeInvoice: vi.fn().mockResolvedValue({ invoiceUrl: "https://invoice.stripe.com/test" }), getSubscription: vi.fn().mockResolvedValue({ items: [ { @@ -54,12 +50,8 @@ const mockBillingService: IBillingProviderService = { handleSubscriptionCancel: vi.fn().mockResolvedValue(undefined), handleSubscriptionCreation: vi.fn().mockResolvedValue(undefined), handleEndTrial: vi.fn().mockResolvedValue(undefined), - createCustomer: vi - .fn() - .mockResolvedValue({ stripeCustomerId: "cus_test_123" }), - createPaymentIntent: vi - .fn() - .mockResolvedValue({ id: "pi_test_123", client_secret: "secret_123" }), + createCustomer: vi.fn().mockResolvedValue({ stripeCustomerId: "cus_test_123" }), + createPaymentIntent: vi.fn().mockResolvedValue({ id: "pi_test_123", client_secret: "secret_123" }), createSubscriptionCheckout: vi.fn().mockResolvedValue({ checkoutUrl: "https://checkout.test", sessionId: "cs_test_123", @@ -158,10 +150,7 @@ describe("MonthlyProrationService Integration Tests", () => { }); it("should process end-to-end proration for annual team with seat additions", async () => { - const prorationService = new MonthlyProrationService( - undefined, - mockBillingService - ); + const prorationService = new MonthlyProrationService(undefined, mockBillingService); const timestamp = Date.now(); const randomSuffix = Math.random().toString(36).substring(7); @@ -231,17 +220,12 @@ describe("MonthlyProrationService Integration Tests", () => { }); expect(seatChanges).toHaveLength(2); - expect( - seatChanges.every((sc) => sc.processedInProrationId === proration?.id) - ).toBe(true); + expect(seatChanges.every((sc) => sc.processedInProrationId === proration?.id)).toBe(true); }); it("should create a $0 proration for team with no net change", async () => { const seatTracker = new SeatChangeTrackingService(); - const prorationService = new MonthlyProrationService( - undefined, - mockBillingService - ); + const prorationService = new MonthlyProrationService(undefined, mockBillingService); await seatTracker.logSeatAddition({ teamId: testTeam.id, @@ -344,21 +328,14 @@ describe("MonthlyProrationService Integration Tests", () => { monthKey, }); - const filteredResults = results.filter((r) => - [testTeam.id, testTeam2.id].includes(r.teamId) - ); + const filteredResults = results.filter((r) => [testTeam.id, testTeam2.id].includes(r.teamId)); expect(filteredResults).toHaveLength(2); - expect(filteredResults.every((r) => r.status === "INVOICE_CREATED")).toBe( - true - ); + expect(filteredResults.every((r) => r.status === "INVOICE_CREATED")).toBe(true); }); it("should handle payment success callback", async () => { const seatTracker = new SeatChangeTrackingService(); - const prorationService = new MonthlyProrationService( - undefined, - mockBillingService - ); + const prorationService = new MonthlyProrationService(undefined, mockBillingService); await seatTracker.logSeatAddition({ teamId: testTeam.id, @@ -387,10 +364,7 @@ describe("MonthlyProrationService Integration Tests", () => { it("should handle payment failure callback", async () => { const seatTracker = new SeatChangeTrackingService(); - const prorationService = new MonthlyProrationService( - undefined, - mockBillingService - ); + const prorationService = new MonthlyProrationService(undefined, mockBillingService); await seatTracker.logSeatAddition({ teamId: testTeam.id, @@ -422,10 +396,7 @@ describe("MonthlyProrationService Integration Tests", () => { it("should call handleSubscriptionUpdate when updating subscription quantity", async () => { const seatTracker = new SeatChangeTrackingService(); - const prorationService = new MonthlyProrationService( - undefined, - mockBillingService - ); + const prorationService = new MonthlyProrationService(undefined, mockBillingService); // Reset the mock to track calls vi.mocked(mockBillingService.handleSubscriptionUpdate).mockClear(); @@ -460,14 +431,9 @@ describe("MonthlyProrationService Integration Tests", () => { const seatTracker = new SeatChangeTrackingService(); const failingBillingService = { ...mockBillingService, - handleSubscriptionUpdate: vi - .fn() - .mockRejectedValue(new Error("Subscription not found")), + handleSubscriptionUpdate: vi.fn().mockRejectedValue(new Error("Subscription not found")), }; - const prorationService = new MonthlyProrationService( - undefined, - failingBillingService - ); + const prorationService = new MonthlyProrationService(undefined, failingBillingService); await seatTracker.logSeatAddition({ teamId: testTeam.id, @@ -483,8 +449,8 @@ describe("MonthlyProrationService Integration Tests", () => { }); // Should throw when trying to update subscription - await expect( - prorationService.handleProrationPaymentSuccess(proration!.id) - ).rejects.toThrow("Subscription not found"); + await expect(prorationService.handleProrationPaymentSuccess(proration!.id)).rejects.toThrow( + "Subscription not found" + ); }); }); diff --git a/packages/features/ee/billing/service/proration/tasker/ProrationEmailSyncTasker.ts b/packages/features/ee/billing/service/proration/tasker/ProrationEmailSyncTasker.ts index 3971c8fdce..e4a8362542 100644 --- a/packages/features/ee/billing/service/proration/tasker/ProrationEmailSyncTasker.ts +++ b/packages/features/ee/billing/service/proration/tasker/ProrationEmailSyncTasker.ts @@ -28,10 +28,9 @@ export class ProrationEmailSyncTasker implements IProrationEmailTasker { const runId = `sync_${nanoid(10)}`; // In sync mode, reminders are not scheduled (they require Trigger.dev) // so cancellation is a no-op - this.logger.info( - `[ProrationEmailSyncTasker] cancelReminder runId=${runId} - no-op in sync mode`, - { prorationId: payload.prorationId } - ); + this.logger.info(`[ProrationEmailSyncTasker] cancelReminder runId=${runId} - no-op in sync mode`, { + prorationId: payload.prorationId, + }); return { runId }; } } diff --git a/packages/features/ee/billing/service/proration/tasker/trigger/scheduleMonthlyProration.ts b/packages/features/ee/billing/service/proration/tasker/trigger/scheduleMonthlyProration.ts index f8b73103c2..0b9d0f32ba 100644 --- a/packages/features/ee/billing/service/proration/tasker/trigger/scheduleMonthlyProration.ts +++ b/packages/features/ee/billing/service/proration/tasker/trigger/scheduleMonthlyProration.ts @@ -13,24 +13,18 @@ export const scheduleMonthlyProration = schedules.task({ run: async (payload) => { const { subMonths } = await import("date-fns"); const { TriggerDevLogger } = await import("@calcom/lib/triggerDevLogger"); - const { formatMonthKey, isValidMonthKey } = await import( - "@calcom/features/ee/billing/lib/month-key" - ); + const { formatMonthKey, isValidMonthKey } = await import("@calcom/features/ee/billing/lib/month-key"); const { MonthlyProrationTeamRepository } = await import( "@calcom/features/ee/billing/repository/proration/MonthlyProrationTeamRepository" ); - const { getFeaturesRepository } = await import( - "@calcom/features/di/containers/FeaturesRepository" - ); + const { getFeaturesRepository } = await import("@calcom/features/di/containers/FeaturesRepository"); const triggerDevLogger = new TriggerDevLogger(); const log = triggerDevLogger.getSubLogger({ name: "MonthlyProrationSchedule", }); const featuresRepository = getFeaturesRepository(); - const isEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally( - "monthly-proration" - ); + const isEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("monthly-proration"); if (!isEnabled) { log.info("Monthly proration feature is disabled"); @@ -38,9 +32,7 @@ export const scheduleMonthlyProration = schedules.task({ } const externalIdMonthKey = - payload.externalId && isValidMonthKey(payload.externalId) - ? payload.externalId - : null; + payload.externalId && isValidMonthKey(payload.externalId) ? payload.externalId : null; let monthKey: string; if (externalIdMonthKey) { @@ -48,9 +40,7 @@ export const scheduleMonthlyProration = schedules.task({ log.info(`Using monthKey from externalId: ${monthKey}`); } else { const now = new Date(); - const startOfCurrentMonthUtc = new Date( - Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1) - ); + const startOfCurrentMonthUtc = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); const previousMonthUtc = subMonths(startOfCurrentMonthUtc, 1); monthKey = formatMonthKey(previousMonthUtc); } @@ -58,9 +48,7 @@ export const scheduleMonthlyProration = schedules.task({ log.info(`Starting monthly proration for ${monthKey}`); const teamRepository = new MonthlyProrationTeamRepository(); - const teamIdsList = await teamRepository.getAnnualTeamsWithSeatChanges( - monthKey - ); + const teamIdsList = await teamRepository.getAnnualTeamsWithSeatChanges(monthKey); if (teamIdsList.length === 0) { log.info(`No teams with seat changes found for ${monthKey}`); diff --git a/packages/features/ee/billing/service/seatBillingStrategy/SeatBillingStrategyFactory.ts b/packages/features/ee/billing/service/seatBillingStrategy/SeatBillingStrategyFactory.ts index 7a42872fff..54ef46e2f8 100644 --- a/packages/features/ee/billing/service/seatBillingStrategy/SeatBillingStrategyFactory.ts +++ b/packages/features/ee/billing/service/seatBillingStrategy/SeatBillingStrategyFactory.ts @@ -41,23 +41,16 @@ export class SeatBillingStrategyFactory { } async createByTeamId(teamId: number): Promise { - const info = await this.deps.billingPeriodService.getBillingPeriodInfo( - teamId - ); + const info = await this.deps.billingPeriodService.getBillingPeriodInfo(teamId); if (!info.isInTrial && info.subscriptionStart) { if (info.billingPeriod === "ANNUALLY") { const enabled = - await this.deps.featuresRepository.checkIfFeatureIsEnabledGlobally( - "monthly-proration" - ); + await this.deps.featuresRepository.checkIfFeatureIsEnabledGlobally("monthly-proration"); if (enabled) return this.prorationStrategy; } if (info.billingPeriod === "MONTHLY") { - const enabled = - await this.deps.featuresRepository.checkIfFeatureIsEnabledGlobally( - "hwm-seating" - ); + const enabled = await this.deps.featuresRepository.checkIfFeatureIsEnabledGlobally("hwm-seating"); if (enabled) return this.hwmStrategy; } } @@ -65,16 +58,10 @@ export class SeatBillingStrategyFactory { return this.fallback; } - async createBySubscriptionId( - subscriptionId: string - ): Promise { - const team = await this.deps.teamBillingDataRepository.findBySubscriptionId( - subscriptionId - ); + async createBySubscriptionId(subscriptionId: string): Promise { + const team = await this.deps.teamBillingDataRepository.findBySubscriptionId(subscriptionId); if (!team) { - log.warn( - `No team found for subscription ${subscriptionId}, using fallback strategy` - ); + log.warn(`No team found for subscription ${subscriptionId}, using fallback strategy`); return this.fallback; } return this.createByTeamId(team.id); diff --git a/packages/features/ee/billing/service/seatBillingStrategy/__tests__/SeatBillingStrategyFactory.test.ts b/packages/features/ee/billing/service/seatBillingStrategy/__tests__/SeatBillingStrategyFactory.test.ts index 17270db639..7eb15f95f4 100644 --- a/packages/features/ee/billing/service/seatBillingStrategy/__tests__/SeatBillingStrategyFactory.test.ts +++ b/packages/features/ee/billing/service/seatBillingStrategy/__tests__/SeatBillingStrategyFactory.test.ts @@ -99,10 +99,7 @@ describe("SeatBillingStrategyFactory", () => { }); it("returns HighWaterMarkStrategy for monthly plan with HWM enabled", async () => { - const factory = createFactory( - { ...baseBillingInfo, billingPeriod: "MONTHLY" }, - { "hwm-seating": true } - ); + const factory = createFactory({ ...baseBillingInfo, billingPeriod: "MONTHLY" }, { "hwm-seating": true }); const strategy = await factory.createByTeamId(1); expect(strategy).toBeInstanceOf(HighWaterMarkStrategy); @@ -119,10 +116,7 @@ describe("SeatBillingStrategyFactory", () => { }); it("returns ImmediateUpdateStrategy for monthly plan with HWM disabled", async () => { - const factory = createFactory( - { ...baseBillingInfo, billingPeriod: "MONTHLY" }, - { "hwm-seating": false } - ); + const factory = createFactory({ ...baseBillingInfo, billingPeriod: "MONTHLY" }, { "hwm-seating": false }); const strategy = await factory.createByTeamId(1); expect(strategy).toBeInstanceOf(ImmediateUpdateStrategy); diff --git a/packages/features/ee/billing/service/teams/ITeamBillingService.ts b/packages/features/ee/billing/service/teams/ITeamBillingService.ts index 5077a17638..6b2bdc83ad 100644 --- a/packages/features/ee/billing/service/teams/ITeamBillingService.ts +++ b/packages/features/ee/billing/service/teams/ITeamBillingService.ts @@ -1,6 +1,9 @@ import type { Team } from "@calcom/prisma/client"; -import type { IBillingRepositoryCreateArgs, SubscriptionStatus } from "../../repository/billing/IBillingRepository"; +import type { + IBillingRepositoryCreateArgs, + SubscriptionStatus, +} from "../../repository/billing/IBillingRepository"; import type { SeatChangeType } from "../seatBillingStrategy/ISeatBillingStrategy"; export type TeamBillingInput = Pick; diff --git a/packages/features/ee/dsync/lib/assignValueToUser.ts b/packages/features/ee/dsync/lib/assignValueToUser.ts index 4b4fbaa9f1..21207c810b 100644 --- a/packages/features/ee/dsync/lib/assignValueToUser.ts +++ b/packages/features/ee/dsync/lib/assignValueToUser.ts @@ -312,7 +312,7 @@ const buildPrismaQueryForAttributeOptionCreation = ({ }; const createMissingOptionsAndReturnAlongWithExisting = async < - TattributeOptionsToAssign extends AttributeOptionsToAssign + TattributeOptionsToAssign extends AttributeOptionsToAssign, >({ attributeOptionsToAssignIncludingNonExistentOptions, orgId, diff --git a/packages/features/ee/dsync/lib/handleUserEvents.test.ts b/packages/features/ee/dsync/lib/handleUserEvents.test.ts index 42a592d771..a13fe2071f 100644 --- a/packages/features/ee/dsync/lib/handleUserEvents.test.ts +++ b/packages/features/ee/dsync/lib/handleUserEvents.test.ts @@ -48,9 +48,11 @@ vi.mock("./removeUserFromOrg", () => ({ })); vi.mock("@calcom/features/users/repositories/UserRepository", () => ({ - UserRepository: vi.fn().mockImplementation(function() { return { - isAMemberOfOrganization: vi.fn().mockResolvedValue(false), - }; }), + UserRepository: vi.fn().mockImplementation(function () { + return { + isAMemberOfOrganization: vi.fn().mockResolvedValue(false), + }; + }), })); async function createMockOrganization({ id, name, slug }: { id: number; name: string; slug: string }) { diff --git a/packages/features/ee/integration-attribute-sync/di/AttributeSyncRuleService.container.ts b/packages/features/ee/integration-attribute-sync/di/AttributeSyncRuleService.container.ts index c123000ccc..dbcbead9e6 100644 --- a/packages/features/ee/integration-attribute-sync/di/AttributeSyncRuleService.container.ts +++ b/packages/features/ee/integration-attribute-sync/di/AttributeSyncRuleService.container.ts @@ -10,5 +10,7 @@ const attributeSyncRuleServiceContainer = createContainer(); export function getAttributeSyncRuleService(): AttributeSyncRuleService { attributeSyncRuleServiceModule.loadModule(attributeSyncRuleServiceContainer); - return attributeSyncRuleServiceContainer.get(attributeSyncRuleServiceModule.token); + return attributeSyncRuleServiceContainer.get( + attributeSyncRuleServiceModule.token + ); } diff --git a/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts b/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts index 31be6a54f4..fe571fa50b 100644 --- a/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts +++ b/packages/features/ee/integration-attribute-sync/repositories/IIntegrationAttributeSyncRepository.ts @@ -123,31 +123,15 @@ export interface IUpdateAttributeSyncInput { } export interface IIntegrationAttributeSyncRepository { - getByOrganizationId( - organizationId: number - ): Promise; + getByOrganizationId(organizationId: number): Promise; getById(id: string): Promise; - getSyncFieldMappings( - integrationAttributeSyncId: string - ): Promise; - getMappedAttributeIdsByOrganization( - organizationId: number, - excludeSyncId?: string - ): Promise; - getAttributeIdsByOrganization( - organizationId: number, - attributeIds: string[] - ): Promise; - create( - params: IIntegrationAttributeSyncCreateParams - ): Promise; - updateTransactionWithRuleAndMappings( - params: IIntegrationAttributeSyncUpdateParams - ): Promise; + getSyncFieldMappings(integrationAttributeSyncId: string): Promise; + getMappedAttributeIdsByOrganization(organizationId: number, excludeSyncId?: string): Promise; + getAttributeIdsByOrganization(organizationId: number, attributeIds: string[]): Promise; + create(params: IIntegrationAttributeSyncCreateParams): Promise; + updateTransactionWithRuleAndMappings(params: IIntegrationAttributeSyncUpdateParams): Promise; deleteById(id: string): Promise; - getAllByCredentialId( - credentialId: number - ): Promise; + getAllByCredentialId(credentialId: number): Promise; } export interface IIntegrationAttributeSyncUpdateParams { diff --git a/packages/features/ee/integration-attribute-sync/schemas/zod.test.ts b/packages/features/ee/integration-attribute-sync/schemas/zod.test.ts index 71e45559ca..3a2a7e03d5 100644 --- a/packages/features/ee/integration-attribute-sync/schemas/zod.test.ts +++ b/packages/features/ee/integration-attribute-sync/schemas/zod.test.ts @@ -1,10 +1,6 @@ import { describe, it, expect } from "vitest"; -import { - attributeSyncRuleConditionSchema, - attributeSyncRuleSchema, - syncFormDataSchema, -} from "./zod"; +import { attributeSyncRuleConditionSchema, attributeSyncRuleSchema, syncFormDataSchema } from "./zod"; describe("attributeSyncRuleConditionSchema", () => { describe("team conditions", () => { diff --git a/packages/features/ee/integration-attribute-sync/schemas/zod.ts b/packages/features/ee/integration-attribute-sync/schemas/zod.ts index 2741d5eb55..deff5523d5 100644 --- a/packages/features/ee/integration-attribute-sync/schemas/zod.ts +++ b/packages/features/ee/integration-attribute-sync/schemas/zod.ts @@ -38,8 +38,10 @@ const attributeConditionSchema = baseConditionSchema.extend({ }); // Discriminated union for conditions -export const attributeSyncRuleConditionSchema: z.ZodType = - z.discriminatedUnion("identifier", [teamConditionSchema, attributeConditionSchema]); +export const attributeSyncRuleConditionSchema: z.ZodType = z.discriminatedUnion( + "identifier", + [teamConditionSchema, attributeConditionSchema] +); export const attributeSyncRuleSchema: z.ZodType = z.object({ operator: ruleOperatorSchema, diff --git a/packages/features/ee/integration-attribute-sync/services/AttributeSyncFieldMappingService.ts b/packages/features/ee/integration-attribute-sync/services/AttributeSyncFieldMappingService.ts index 1eb42a1bf8..9159639dff 100644 --- a/packages/features/ee/integration-attribute-sync/services/AttributeSyncFieldMappingService.ts +++ b/packages/features/ee/integration-attribute-sync/services/AttributeSyncFieldMappingService.ts @@ -43,18 +43,14 @@ export class AttributeSyncFieldMappingService { }); if (!membership) { - log.warn( - `No membership found for user ${userId} in org ${organizationId}` - ); + log.warn(`No membership found for user ${userId} in org ${organizationId}`); return; } const memberId = membership.id; const enabledSyncFieldMappings = syncFieldMappings.filter( - (mapping) => - mapping.enabled && - integrationFields[mapping.integrationFieldName] !== undefined + (mapping) => mapping.enabled && integrationFields[mapping.integrationFieldName] !== undefined ); if (enabledSyncFieldMappings.length === 0) { @@ -64,22 +60,20 @@ export class AttributeSyncFieldMappingService { const attributeIds = enabledSyncFieldMappings.map((m) => m.attributeId); - const attributes = - await this.deps.attributeRepository.findManyByIdsAndOrgIdWithOptions({ - attributeIds, - orgId: organizationId, - }); + const attributes = await this.deps.attributeRepository.findManyByIdsAndOrgIdWithOptions({ + attributeIds, + orgId: organizationId, + }); const attributeMap = new Map(attributes.map((a) => [a.id, a])); - const { attributeIdsToSync, optionsToCreate, assignmentsToCreate } = - this.processMappings({ - enabledMappings: enabledSyncFieldMappings, - attributeMap, - integrationFields, - memberId, - orgId: organizationId, - }); + const { attributeIdsToSync, optionsToCreate, assignmentsToCreate } = this.processMappings({ + enabledMappings: enabledSyncFieldMappings, + attributeMap, + integrationFields, + memberId, + orgId: organizationId, + }); if (optionsToCreate.length > 0) { const newAssignments = await this.createOptionsAndGetAssignments({ @@ -103,9 +97,7 @@ export class AttributeSyncFieldMappingService { if (assignmentsToCreate.length > 0) { await this.deps.attributeToUserRepository.createManySkipDuplicates(assignmentsToCreate); - log.info( - `Synced ${assignmentsToCreate.length} attribute(s) for member ${memberId}` - ); + log.info(`Synced ${assignmentsToCreate.length} attribute(s) for member ${memberId}`); } } @@ -156,9 +148,7 @@ export class AttributeSyncFieldMappingService { continue; } - const rawFieldValue = String( - integrationFields[mapping.integrationFieldName] - ); + const rawFieldValue = String(integrationFields[mapping.integrationFieldName]); if (hasOptions({ attribute })) { // SINGLE_SELECT / MULTI_SELECT - must find existing option @@ -243,12 +233,9 @@ export class AttributeSyncFieldMappingService { orgId, }); - const optionLookup = new Map( - allOptions.map((o) => [`${o.attributeId}:${o.value.toLowerCase()}`, o]) - ); + const optionLookup = new Map(allOptions.map((o) => [`${o.attributeId}:${o.value.toLowerCase()}`, o])); - const assignments: Array<{ memberId: number; attributeOptionId: string }> = - []; + const assignments: Array<{ memberId: number; attributeOptionId: string }> = []; for (const newOption of optionsToCreate) { const key = `${newOption.attributeId}:${newOption.value.toLowerCase()}`; @@ -260,9 +247,7 @@ export class AttributeSyncFieldMappingService { attributeOptionId: createdOption.id, }); } else { - log.error( - `Failed to find newly created option for attribute ${newOption.attributeId}` - ); + log.error(`Failed to find newly created option for attribute ${newOption.attributeId}`); } } diff --git a/packages/features/ee/integration-attribute-sync/services/AttributeSyncRuleService.ts b/packages/features/ee/integration-attribute-sync/services/AttributeSyncRuleService.ts index 86c63c4c6e..9e67f1d478 100644 --- a/packages/features/ee/integration-attribute-sync/services/AttributeSyncRuleService.ts +++ b/packages/features/ee/integration-attribute-sync/services/AttributeSyncRuleService.ts @@ -52,15 +52,12 @@ export class AttributeSyncRuleService { userId: number; teamConditions: ITeamCondition[]; }) { - const userMemberships = - await this.deps.membershipRepository.findAllByUserId({ - userId, - filters: { accepted: true }, - }); + const userMemberships = await this.deps.membershipRepository.findAllByUserId({ + userId, + filters: { accepted: true }, + }); - const userTeamIdSet = new Set( - userMemberships.map((membership) => membership.teamId) - ); + const userTeamIdSet = new Set(userMemberships.map((membership) => membership.teamId)); const teamConditionEvaluated: boolean[] = []; @@ -90,11 +87,10 @@ export class AttributeSyncRuleService { }): Promise { const attributeService = getAttributeService(); - const userAttributes = - await attributeService.getUsersAttributesByOrgMembershipId({ - userId: user.id, - orgId: user.organizationId, - }); + const userAttributes = await attributeService.getUsersAttributesByOrgMembershipId({ + userId: user.id, + orgId: user.organizationId, + }); const attributeConditionResults: boolean[] = []; @@ -114,10 +110,7 @@ export class AttributeSyncRuleService { const { operator } = condition; if (!userAttribute) { - if ( - operator === ConditionOperatorEnum.IN || - operator === ConditionOperatorEnum.EQUALS - ) { + if (operator === ConditionOperatorEnum.IN || operator === ConditionOperatorEnum.EQUALS) { return false; } // For NOT_IN/NOT_EQUALS: user doesn't have attribute, condition passes @@ -125,17 +118,9 @@ export class AttributeSyncRuleService { } if (userAttribute.type === "MULTI_SELECT") { - return this.evaluateMultiSelectCondition( - userAttribute, - operator, - condition - ); + return this.evaluateMultiSelectCondition(userAttribute, operator, condition); } else { - return this.evaluateSingleValueCondition( - userAttribute, - operator, - condition - ); + return this.evaluateSingleValueCondition(userAttribute, operator, condition); } } @@ -149,14 +134,10 @@ export class AttributeSyncRuleService { switch (operator) { case ConditionOperatorEnum.IN: - return conditionAttributeOptionIds.every((id) => - userAttributeOptionIds.has(id) - ); + return conditionAttributeOptionIds.every((id) => userAttributeOptionIds.has(id)); case ConditionOperatorEnum.NOT_IN: - return !conditionAttributeOptionIds.some((id) => - userAttributeOptionIds.has(id) - ); + return !conditionAttributeOptionIds.some((id) => userAttributeOptionIds.has(id)); default: return false; @@ -164,10 +145,7 @@ export class AttributeSyncRuleService { } private evaluateSingleValueCondition( - userAttribute: Extract< - UserAttribute, - { type: "TEXT" | "NUMBER" | "SINGLE_SELECT" } - >, + userAttribute: Extract, operator: ConditionOperatorEnum, condition: IAttributeCondition ): boolean { @@ -175,13 +153,11 @@ export class AttributeSyncRuleService { // For TEXT/NUMBER: condition stores actual values, compare against value (case-insensitive) const isSingleSelect = userAttribute.type === "SINGLE_SELECT"; - const userValue = isSingleSelect - ? userAttribute.optionId - : userAttribute.value?.toLowerCase() ?? null; + const userValue = isSingleSelect ? userAttribute.optionId : (userAttribute.value?.toLowerCase() ?? null); const conditionValue = isSingleSelect - ? condition.value[0] ?? null - : condition.value[0]?.toLowerCase() ?? null; + ? (condition.value[0] ?? null) + : (condition.value[0]?.toLowerCase() ?? null); switch (operator) { case ConditionOperatorEnum.EQUALS: diff --git a/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.test.ts b/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.test.ts index d3a5ced66f..8340dded78 100644 --- a/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.test.ts +++ b/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.test.ts @@ -180,14 +180,21 @@ describe("IntegrationAttributeSyncService", () => { }; mockIntegrationAttributeSyncRepository.getMappedAttributeIdsByOrganization.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue(["attr-1", "attr-2"]); + mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue([ + "attr-1", + "attr-2", + ]); mockIntegrationAttributeSyncRepository.getSyncFieldMappings.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue(undefined); + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue( + undefined + ); mockTeamRepository.findTeamsNotBelongingToOrgByIds.mockResolvedValue([]); await service.updateIncludeRulesAndMappings(formData); - expect(mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings).toHaveBeenCalledWith({ + expect( + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings + ).toHaveBeenCalledWith({ integrationAttributeSync: { id: formData.id, name: formData.name, @@ -215,17 +222,24 @@ describe("IntegrationAttributeSyncService", () => { }; mockIntegrationAttributeSyncRepository.getMappedAttributeIdsByOrganization.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue(["attr-1", "attr-2"]); + mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue([ + "attr-1", + "attr-2", + ]); mockIntegrationAttributeSyncRepository.getSyncFieldMappings.mockResolvedValue([ { id: "mapping-1", integrationFieldName: "field1", attributeId: "attr-1", enabled: true }, { id: "mapping-2", integrationFieldName: "field2", attributeId: "attr-2", enabled: true }, ]); - mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue(undefined); + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue( + undefined + ); mockTeamRepository.findTeamsNotBelongingToOrgByIds.mockResolvedValue([]); await service.updateIncludeRulesAndMappings(formData); - expect(mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings).toHaveBeenCalledWith({ + expect( + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings + ).toHaveBeenCalledWith({ integrationAttributeSync: { id: formData.id, name: formData.name, @@ -258,12 +272,16 @@ describe("IntegrationAttributeSyncService", () => { { id: "mapping-2", integrationFieldName: "field2", attributeId: "attr-2", enabled: true }, { id: "mapping-3", integrationFieldName: "field3", attributeId: "attr-3", enabled: true }, ]); - mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue(undefined); + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue( + undefined + ); mockTeamRepository.findTeamsNotBelongingToOrgByIds.mockResolvedValue([]); await service.updateIncludeRulesAndMappings(formData); - expect(mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings).toHaveBeenCalledWith({ + expect( + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings + ).toHaveBeenCalledWith({ integrationAttributeSync: { id: formData.id, name: formData.name, @@ -291,17 +309,24 @@ describe("IntegrationAttributeSyncService", () => { }; mockIntegrationAttributeSyncRepository.getMappedAttributeIdsByOrganization.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue(["attr-1", "attr-new"]); + mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue([ + "attr-1", + "attr-new", + ]); mockIntegrationAttributeSyncRepository.getSyncFieldMappings.mockResolvedValue([ { id: "mapping-1", integrationFieldName: "field1", attributeId: "attr-1", enabled: true }, { id: "mapping-2", integrationFieldName: "field2", attributeId: "attr-2", enabled: true }, ]); - mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue(undefined); + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue( + undefined + ); mockTeamRepository.findTeamsNotBelongingToOrgByIds.mockResolvedValue([]); await service.updateIncludeRulesAndMappings(formData); - expect(mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings).toHaveBeenCalledWith({ + expect( + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings + ).toHaveBeenCalledWith({ integrationAttributeSync: { id: formData.id, name: formData.name, @@ -343,11 +368,15 @@ describe("IntegrationAttributeSyncService", () => { mockIntegrationAttributeSyncRepository.getMappedAttributeIdsByOrganization.mockResolvedValue([]); mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue([]); mockIntegrationAttributeSyncRepository.getSyncFieldMappings.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue(undefined); + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue( + undefined + ); await service.updateIncludeRulesAndMappings(formData); - expect(mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings).toHaveBeenCalledWith( + expect( + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings + ).toHaveBeenCalledWith( expect.objectContaining({ attributeSyncRule: { id: formData.ruleId, @@ -391,7 +420,9 @@ describe("IntegrationAttributeSyncService", () => { mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue(["attr-1"]); mockIntegrationAttributeSyncRepository.getSyncFieldMappings.mockResolvedValue([]); - await expect(service.updateIncludeRulesAndMappings(formData)).rejects.toThrow(UnauthorizedAttributeError); + await expect(service.updateIncludeRulesAndMappings(formData)).rejects.toThrow( + UnauthorizedAttributeError + ); }); it("should include invalid attribute ids in the error", async () => { @@ -410,7 +441,10 @@ describe("IntegrationAttributeSyncService", () => { await expect(service.updateIncludeRulesAndMappings(formData)).rejects.toSatisfy((error) => { expect(error).toBeInstanceOf(UnauthorizedAttributeError); - expect((error as UnauthorizedAttributeError).attributeIds).toEqual(["attr-invalid-1", "attr-invalid-2"]); + expect((error as UnauthorizedAttributeError).attributeIds).toEqual([ + "attr-invalid-1", + "attr-invalid-2", + ]); return true; }); }); @@ -425,9 +459,14 @@ describe("IntegrationAttributeSyncService", () => { }; mockIntegrationAttributeSyncRepository.getMappedAttributeIdsByOrganization.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue(["attr-1", "attr-2"]); + mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue([ + "attr-1", + "attr-2", + ]); mockIntegrationAttributeSyncRepository.getSyncFieldMappings.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue(undefined); + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue( + undefined + ); mockTeamRepository.findTeamsNotBelongingToOrgByIds.mockResolvedValue([]); await expect(service.updateIncludeRulesAndMappings(formData)).resolves.not.toThrow(); @@ -442,7 +481,9 @@ describe("IntegrationAttributeSyncService", () => { mockIntegrationAttributeSyncRepository.getMappedAttributeIdsByOrganization.mockResolvedValue([]); mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue([]); mockIntegrationAttributeSyncRepository.getSyncFieldMappings.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue(undefined); + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue( + undefined + ); mockTeamRepository.findTeamsNotBelongingToOrgByIds.mockResolvedValue([]); await expect(service.updateIncludeRulesAndMappings(formData)).resolves.not.toThrow(); @@ -520,7 +561,9 @@ describe("IntegrationAttributeSyncService", () => { mockIntegrationAttributeSyncRepository.getMappedAttributeIdsByOrganization.mockResolvedValue([]); mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue([]); mockIntegrationAttributeSyncRepository.getSyncFieldMappings.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue(undefined); + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue( + undefined + ); await service.updateIncludeRulesAndMappings(formData); @@ -546,7 +589,9 @@ describe("IntegrationAttributeSyncService", () => { mockIntegrationAttributeSyncRepository.getMappedAttributeIdsByOrganization.mockResolvedValue([]); mockIntegrationAttributeSyncRepository.getAttributeIdsByOrganization.mockResolvedValue([]); mockIntegrationAttributeSyncRepository.getSyncFieldMappings.mockResolvedValue([]); - mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue(undefined); + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings.mockResolvedValue( + undefined + ); await service.updateIncludeRulesAndMappings(formData); @@ -554,7 +599,9 @@ describe("IntegrationAttributeSyncService", () => { teamIds: [1, 2], orgId: 123, }); - expect(mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings).toHaveBeenCalled(); + expect( + mockIntegrationAttributeSyncRepository.updateTransactionWithRuleAndMappings + ).toHaveBeenCalled(); }); }); }); diff --git a/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts b/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts index 3b111c9c68..3d3ce37133 100644 --- a/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts +++ b/packages/features/ee/integration-attribute-sync/services/IntegrationAttributeSyncService.ts @@ -51,37 +51,26 @@ export class IntegrationAttributeSyncService { private extractTeamIdsFromRule(rule: IAttributeSyncRule): number[] { return rule.conditions - .filter( - (c): c is ITeamCondition => - c.identifier === ConditionIdentifierEnum.TEAM_ID - ) + .filter((c): c is ITeamCondition => c.identifier === ConditionIdentifierEnum.TEAM_ID) .flatMap((c) => c.value); } - private async validateTeamsBelongToOrg( - teamIds: number[], - organizationId: number - ): Promise { + private async validateTeamsBelongToOrg(teamIds: number[], organizationId: number): Promise { if (teamIds.length === 0) return; - const invalidTeams = - await this.deps.teamRepository.findTeamsNotBelongingToOrgByIds({ - teamIds, - orgId: organizationId, - }); + const invalidTeams = await this.deps.teamRepository.findTeamsNotBelongingToOrgByIds({ + teamIds, + orgId: organizationId, + }); if (invalidTeams.length > 0) { throw new Error( - `Teams do not belong to this organization: ${invalidTeams - .map((t) => t.id) - .join(", ")}` + `Teams do not belong to this organization: ${invalidTeams.map((t) => t.id).join(", ")}` ); } } - private validateWithinSyncUniqueness( - mappings: { attributeId: string }[] - ): void { + private validateWithinSyncUniqueness(mappings: { attributeId: string }[]): void { const seenAttributes = new Set(); for (const mapping of mappings) { if (seenAttributes.has(mapping.attributeId)) { @@ -110,10 +99,7 @@ export class IntegrationAttributeSyncService { } } - private async validateAttributeOwnership( - organizationId: number, - attributeIds: string[] - ): Promise { + private async validateAttributeOwnership(organizationId: number, attributeIds: string[]): Promise { const validAttributeIds = await this.deps.integrationAttributeSyncRepository.getAttributeIdsByOrganization( organizationId, @@ -135,19 +121,14 @@ export class IntegrationAttributeSyncService { } async getAllIntegrationAttributeSyncs(organizationId: number) { - return this.deps.integrationAttributeSyncRepository.getByOrganizationId( - organizationId - ); + return this.deps.integrationAttributeSyncRepository.getByOrganizationId(organizationId); } async getById(id: string) { return this.deps.integrationAttributeSyncRepository.getById(id); } - async createAttributeSync( - input: ZCreateAttributeSyncSchema, - organizationId: number - ) { + async createAttributeSync(input: ZCreateAttributeSyncSchema, organizationId: number) { const credential = await this.deps.credentialRepository.findByIdAndTeamId({ id: input.credentialId, teamId: organizationId, @@ -163,20 +144,13 @@ export class IntegrationAttributeSyncService { await this.validateTeamsBelongToOrg(teamIds, organizationId); const integrationValue = credential.app?.slug || credential.type; - if ( - !Object.values(AttributeSyncIntegrations).includes( - integrationValue as AttributeSyncIntegrations - ) - ) { + if (!Object.values(AttributeSyncIntegrations).includes(integrationValue as AttributeSyncIntegrations)) { throw new Error(`Unsupported integration type: ${integrationValue}`); } this.validateWithinSyncUniqueness(input.syncFieldMappings); - await this.validateCrossSyncUniqueness( - organizationId, - input.syncFieldMappings - ); + await this.validateCrossSyncUniqueness(organizationId, input.syncFieldMappings); const attributeIds = input.syncFieldMappings.map((m) => m.attributeId); await this.validateAttributeOwnership(organizationId, attributeIds); @@ -193,24 +167,18 @@ export class IntegrationAttributeSyncService { } async updateIncludeRulesAndMappings(data: ISyncFormData) { - const { syncFieldMappings, rule, ruleId, ...integrationAttributeSync } = - data; + const { syncFieldMappings, rule, ruleId, ...integrationAttributeSync } = data; this.validateWithinSyncUniqueness(syncFieldMappings); - await this.validateCrossSyncUniqueness( - data.organizationId, - syncFieldMappings, - data.id - ); + await this.validateCrossSyncUniqueness(data.organizationId, syncFieldMappings, data.id); const attributeIds = syncFieldMappings.map((m) => m.attributeId); await this.validateAttributeOwnership(data.organizationId, attributeIds); - const existingFieldMappings = - await this.deps.integrationAttributeSyncRepository.getSyncFieldMappings( - data.id - ); + const existingFieldMappings = await this.deps.integrationAttributeSyncRepository.getSyncFieldMappings( + data.id + ); const parsedRule = attributeSyncRuleSchema.parse(rule); @@ -229,30 +197,24 @@ export class IntegrationAttributeSyncService { .map((mapping) => mapping.id); const fieldMappingsToCreate = syncFieldMappings.filter((m) => !("id" in m)); - const fieldMappingsToUpdate = syncFieldMappings.filter( - (m): m is typeof m & { id: string } => "id" in m - ); + const fieldMappingsToUpdate = syncFieldMappings.filter((m): m is typeof m & { id: string } => "id" in m); - await this.deps.integrationAttributeSyncRepository.updateTransactionWithRuleAndMappings( - { - integrationAttributeSync, - attributeSyncRule: { - id: ruleId, - rule: parsedRule, - }, - fieldMappingsToCreate, - fieldMappingsToUpdate, - fieldMappingsToDelete, - } - ); + await this.deps.integrationAttributeSyncRepository.updateTransactionWithRuleAndMappings({ + integrationAttributeSync, + attributeSyncRule: { + id: ruleId, + rule: parsedRule, + }, + fieldMappingsToCreate, + fieldMappingsToUpdate, + fieldMappingsToDelete, + }); } async deleteById(id: string) { return this.deps.integrationAttributeSyncRepository.deleteById(id); } async getAllByCredentialId(credentialId: number) { - return this.deps.integrationAttributeSyncRepository.getAllByCredentialId( - credentialId - ); + return this.deps.integrationAttributeSyncRepository.getAllByCredentialId(credentialId); } } diff --git a/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts b/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts index fb2fa83291..09f889ceac 100644 --- a/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts +++ b/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts @@ -288,7 +288,8 @@ export default async function handleChildrenEventTypes({ disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false, disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false, enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false, - enableAutomaticRecordingForOrganizer: calVideoSettings.enableAutomaticRecordingForOrganizer ?? false, + enableAutomaticRecordingForOrganizer: + calVideoSettings.enableAutomaticRecordingForOrganizer ?? false, disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false, disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false, redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null, @@ -322,8 +323,8 @@ export default async function handleChildrenEventTypes({ key === "afterBufferTime" ? "afterEventBuffer" : key === "beforeBufferTime" - ? "beforeEventBuffer" - : key; + ? "beforeEventBuffer" + : key; // @ts-expect-error Element implicitly has any type acc[mappedKey] = true; return acc; @@ -474,7 +475,8 @@ export default async function handleChildrenEventTypes({ enableAutomaticRecordingForOrganizer: calVideoSettings.enableAutomaticRecordingForOrganizer ?? false, disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false, - disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false, + disableTranscriptionForOrganizer: + calVideoSettings.disableTranscriptionForOrganizer ?? false, redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null, requireEmailForGuests: calVideoSettings.requireEmailForGuests ?? false, updatedAt: new Date(), @@ -487,7 +489,8 @@ export default async function handleChildrenEventTypes({ enableAutomaticRecordingForOrganizer: calVideoSettings.enableAutomaticRecordingForOrganizer ?? false, disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false, - disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false, + disableTranscriptionForOrganizer: + calVideoSettings.disableTranscriptionForOrganizer ?? false, redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null, requireEmailForGuests: calVideoSettings.requireEmailForGuests ?? false, }, diff --git a/packages/features/ee/managed-event-types/reassignment/index.ts b/packages/features/ee/managed-event-types/reassignment/index.ts index 8a9ec5a348..ef0213b905 100644 --- a/packages/features/ee/managed-event-types/reassignment/index.ts +++ b/packages/features/ee/managed-event-types/reassignment/index.ts @@ -1,4 +1,3 @@ export { managedEventManualReassignment } from "./managedEventManualReassignment"; export { managedEventReassignment } from "./managedEventReassignment"; export * from "./utils"; - diff --git a/packages/features/ee/managed-event-types/reassignment/managedEventManualReassignment.integration-test.ts b/packages/features/ee/managed-event-types/reassignment/managedEventManualReassignment.integration-test.ts index abc9ad518f..2731ae5533 100644 --- a/packages/features/ee/managed-event-types/reassignment/managedEventManualReassignment.integration-test.ts +++ b/packages/features/ee/managed-event-types/reassignment/managedEventManualReassignment.integration-test.ts @@ -468,7 +468,7 @@ describe("managedEventManualReassignment - Integration Tests", () => { const newBooking = await prisma.booking.findFirst({ where: { userId: newUser.id, eventTypeId: childEventTypes[1].id }, }); - + expect(newBooking).toBeTruthy(); expect(newBooking?.userId).toBe(newUser.id); }); @@ -499,4 +499,3 @@ describe("managedEventManualReassignment - Integration Tests", () => { ).rejects.toThrow(); }); }); - diff --git a/packages/features/ee/managed-event-types/reassignment/managedEventManualReassignment.ts b/packages/features/ee/managed-event-types/reassignment/managedEventManualReassignment.ts index 0434418886..a10de2e4cc 100644 --- a/packages/features/ee/managed-event-types/reassignment/managedEventManualReassignment.ts +++ b/packages/features/ee/managed-event-types/reassignment/managedEventManualReassignment.ts @@ -5,7 +5,7 @@ import { createManagedEventManualReassignmentService } from "./services/containe /** * Entry point for manual managed event reassignment - * + * * This delegates to the service layer without direct repository knowledge. * The container handles all dependency injection. */ diff --git a/packages/features/ee/managed-event-types/reassignment/managedEventReassignment.integration-test.ts b/packages/features/ee/managed-event-types/reassignment/managedEventReassignment.integration-test.ts index fb68798890..00a7181c77 100644 --- a/packages/features/ee/managed-event-types/reassignment/managedEventReassignment.integration-test.ts +++ b/packages/features/ee/managed-event-types/reassignment/managedEventReassignment.integration-test.ts @@ -155,9 +155,9 @@ afterEach(async () => { }, select: { id: true }, }); - + const testBookingIds = testBookings.map((b) => b.id); - + if (testBookingIds.length > 0) { await prisma.bookingReference.deleteMany({ where: { bookingId: { in: testBookingIds } }, @@ -172,7 +172,7 @@ afterEach(async () => { where: { id: { in: testBookingIds } }, }); } - + bookingIds.splice(0, bookingIds.length); if (eventTypeIds.length > 0) { @@ -393,7 +393,7 @@ describe("managedEventReassignment - Integration Tests", () => { const newBooking = await prisma.booking.findFirst({ where: { userId: user2.id, startTime }, }); - + expect(newBooking).toBeTruthy(); expect(newBooking?.userId).toBe(user2.id); }); @@ -417,4 +417,3 @@ describe("managedEventReassignment - Integration Tests", () => { ).rejects.toThrow(); }); }); - diff --git a/packages/features/ee/managed-event-types/reassignment/managedEventReassignment.ts b/packages/features/ee/managed-event-types/reassignment/managedEventReassignment.ts index 5a86c86a31..b9594bb056 100644 --- a/packages/features/ee/managed-event-types/reassignment/managedEventReassignment.ts +++ b/packages/features/ee/managed-event-types/reassignment/managedEventReassignment.ts @@ -27,4 +27,3 @@ export async function managedEventReassignment({ } export default managedEventReassignment; - diff --git a/packages/features/ee/managed-event-types/reassignment/services/ManagedEventAssignmentReasonRecorder.ts b/packages/features/ee/managed-event-types/reassignment/services/ManagedEventAssignmentReasonRecorder.ts index 09cf7c78bd..9304f47f12 100644 --- a/packages/features/ee/managed-event-types/reassignment/services/ManagedEventAssignmentReasonRecorder.ts +++ b/packages/features/ee/managed-event-types/reassignment/services/ManagedEventAssignmentReasonRecorder.ts @@ -28,7 +28,7 @@ export class ManagedEventAssignmentReasonService { /** * Record a managed event reassignment reason - * + * * @param newBookingId - The ID of the NEW booking created during reassignment * @param reassignById - The ID of the user who performed the reassignment * @param reassignReason - Optional reason for the reassignment diff --git a/packages/features/ee/managed-event-types/reassignment/services/ManagedEventManualReassignmentService.ts b/packages/features/ee/managed-event-types/reassignment/services/ManagedEventManualReassignmentService.ts index a3d198e708..b462c6818d 100644 --- a/packages/features/ee/managed-event-types/reassignment/services/ManagedEventManualReassignmentService.ts +++ b/packages/features/ee/managed-event-types/reassignment/services/ManagedEventManualReassignmentService.ts @@ -28,7 +28,6 @@ import logger from "@calcom/lib/logger"; import type loggerType from "@calcom/lib/logger"; import type { PrismaClient } from "@calcom/prisma"; - import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import { @@ -111,7 +110,6 @@ export class ManagedEventManualReassignmentService { reassignedById, }); - const { newBooking, cancelledBooking } = await this.executeBookingReassignmentTransaction({ originalBookingFull, newBookingPlan, @@ -241,17 +239,13 @@ export class ManagedEventManualReassignmentService { * Resolves and validates all entities needed for reassignment */ private async resolveTargetEntities(bookingId: number, newUserId: number, reassignLogger: typeof logger) { - const { - currentChildEventType, - parentEventType, - targetChildEventType, - originalBooking, - } = await findTargetChildEventType({ - bookingId, - newUserId, - bookingRepository: this.bookingRepository, - eventTypeRepository: this.eventTypeRepository, - }); + const { currentChildEventType, parentEventType, targetChildEventType, originalBooking } = + await findTargetChildEventType({ + bookingId, + newUserId, + bookingRepository: this.bookingRepository, + eventTypeRepository: this.eventTypeRepository, + }); reassignLogger.info("Found target child event type", { currentChildId: currentChildEventType.id, @@ -284,9 +278,8 @@ export class ManagedEventManualReassignmentService { throw new Error("Original booking user not found"); } - const originalBookingFull = await this.bookingRepository.findByIdWithAttendeesPaymentAndReferences( - bookingId - ); + const originalBookingFull = + await this.bookingRepository.findByIdWithAttendeesPaymentAndReferences(bookingId); if (!originalBookingFull) { throw new Error("Original booking not found"); @@ -612,9 +605,7 @@ export class ManagedEventManualReassignmentService { data: { location: bookingLocation, metadata: { - ...(typeof newBooking.metadata === "object" && newBooking.metadata - ? newBooking.metadata - : {}), + ...(typeof newBooking.metadata === "object" && newBooking.metadata ? newBooking.metadata : {}), ...bookingMetadataUpdate, }, referencesToCreate: referencesToCreateForDb, @@ -644,7 +635,6 @@ export class ManagedEventManualReassignmentService { }; } - /** * Sends reassignment notification emails to all parties */ diff --git a/packages/features/ee/managed-event-types/reassignment/services/ManagedEventReassignmentService.ts b/packages/features/ee/managed-event-types/reassignment/services/ManagedEventReassignmentService.ts index 1f23889670..75c3a2b469 100644 --- a/packages/features/ee/managed-event-types/reassignment/services/ManagedEventReassignmentService.ts +++ b/packages/features/ee/managed-event-types/reassignment/services/ManagedEventReassignmentService.ts @@ -51,7 +51,7 @@ export class ManagedEventReassignmentService { this.luckyUserService = deps.luckyUserService; this.log = logger.getSubLogger({ prefix: ["ManagedEventReassignmentService"] }); } - + async executeAutoReassignment({ bookingId, orgId, @@ -216,4 +216,3 @@ export class ManagedEventReassignmentService { return luckyUser; } } - diff --git a/packages/features/ee/managed-event-types/reassignment/services/container.ts b/packages/features/ee/managed-event-types/reassignment/services/container.ts index 4990a9efac..e4e44353f6 100644 --- a/packages/features/ee/managed-event-types/reassignment/services/container.ts +++ b/packages/features/ee/managed-event-types/reassignment/services/container.ts @@ -10,7 +10,7 @@ import { ManagedEventAssignmentReasonService } from "./ManagedEventAssignmentRea /** * Dependency Injection Container for Reassignment Services - * + * * This ensures: * - Repositories are only instantiated once per request * - Services receive explicit dependencies via constructor @@ -97,4 +97,3 @@ export function createManagedEventReassignmentService( const container = new ReassignmentServiceContainer(prisma); return container.getManagedEventReassignmentService(luckyUserService); } - diff --git a/packages/features/ee/managed-event-types/reassignment/utils/buildNewBookingPlan.ts b/packages/features/ee/managed-event-types/reassignment/utils/buildNewBookingPlan.ts index 647a091a83..5712c114e4 100644 --- a/packages/features/ee/managed-event-types/reassignment/utils/buildNewBookingPlan.ts +++ b/packages/features/ee/managed-event-types/reassignment/utils/buildNewBookingPlan.ts @@ -93,4 +93,3 @@ export function buildNewBookingPlan({ iCalSequence: 0, }; } - diff --git a/packages/features/ee/managed-event-types/reassignment/utils/findTargetChildEventType.ts b/packages/features/ee/managed-event-types/reassignment/utils/findTargetChildEventType.ts index 478a929f65..9ac38b1df1 100644 --- a/packages/features/ee/managed-event-types/reassignment/utils/findTargetChildEventType.ts +++ b/packages/features/ee/managed-event-types/reassignment/utils/findTargetChildEventType.ts @@ -31,9 +31,9 @@ interface FindTargetChildEventTypeResult { /** * Pure function that finds and validates the target child event type for managed event reassignment - * + * * Dependencies are injected via parameters - no direct instantiation of repositories - * + * * @param bookingRepository - Injected booking repository * @param eventTypeRepository - Injected event type repository * @throws Error if booking is not on a managed event type @@ -77,12 +77,15 @@ export async function findTargetChildEventType({ throw new Error("Parent event type must be a MANAGED type"); } - const targetChildEventType = await eventTypeRepository.findByIdTargetChildEventType(newUserId, currentChildEventType.parentId); + const targetChildEventType = await eventTypeRepository.findByIdTargetChildEventType( + newUserId, + currentChildEventType.parentId + ); if (!targetChildEventType) { throw new Error( `User ${newUserId} does not have a child event type for this managed event. ` + - `Only users who are assigned to the parent managed event can be reassigned to.` + `Only users who are assigned to the parent managed event can be reassigned to.` ); } @@ -109,4 +112,3 @@ export async function findTargetChildEventType({ originalBooking: booking, }; } - diff --git a/packages/features/ee/managed-event-types/reassignment/utils/validateManagedEventReassignment.ts b/packages/features/ee/managed-event-types/reassignment/utils/validateManagedEventReassignment.ts index 3a345d4473..a8bf1b97bf 100644 --- a/packages/features/ee/managed-event-types/reassignment/utils/validateManagedEventReassignment.ts +++ b/packages/features/ee/managed-event-types/reassignment/utils/validateManagedEventReassignment.ts @@ -16,11 +16,9 @@ interface ValidateReassignmentResult { }; } - - /** * Validates that a booking can be reassigned - * + * * @throws Error if booking is already cancelled * @throws Error if booking has already ended * @throws Error if booking is recurring (Phase 1 limitation) @@ -29,7 +27,6 @@ export async function validateManagedEventReassignment({ bookingId, bookingRepository, }: ValidateReassignmentParams): Promise { - const booking = await bookingRepository.findByIdForReassignmentValidation(bookingId); if (!booking) { @@ -45,11 +42,8 @@ export async function validateManagedEventReassignment({ } if (booking.recurringEventId) { - throw new Error( - "Reassignment of recurring bookings is not yet supported for managed events" - ); + throw new Error("Reassignment of recurring bookings is not yet supported for managed events"); } return { booking }; } - diff --git a/packages/features/ee/organizations/__mocks__/organizationMock.ts b/packages/features/ee/organizations/__mocks__/organizationMock.ts index b22cfd7870..6b66a0e3c4 100644 --- a/packages/features/ee/organizations/__mocks__/organizationMock.ts +++ b/packages/features/ee/organizations/__mocks__/organizationMock.ts @@ -18,16 +18,14 @@ export const organizationScenarios = { findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail: { // eslint-disable-next-line @typescript-eslint/no-explicit-any fakeReturnOrganization: (org: any, forInput: any) => { - mockedSingleton.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.mockImplementation( - (arg) => { - if (forInput.email === arg.email) { - return org; - } - const errorMsg = "Mock Error-fakeReturnOrganization: Unhandled input"; - console.log(errorMsg, { arg, forInput }); - throw new Error(errorMsg); + mockedSingleton.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.mockImplementation((arg) => { + if (forInput.email === arg.email) { + return org; } - ); + const errorMsg = "Mock Error-fakeReturnOrganization: Unhandled input"; + console.log(errorMsg, { arg, forInput }); + throw new Error(errorMsg); + }); }, fakeNoMatch: () => { mockedSingleton.findUniqueNonPlatformOrgsByMatchingAutoAcceptEmail.mockResolvedValue(null); diff --git a/packages/features/ee/organizations/di/OrganizationMembershipService.module.ts b/packages/features/ee/organizations/di/OrganizationMembershipService.module.ts index 22a2256e61..a469de17b8 100644 --- a/packages/features/ee/organizations/di/OrganizationMembershipService.module.ts +++ b/packages/features/ee/organizations/di/OrganizationMembershipService.module.ts @@ -9,19 +9,18 @@ const token = ORGANIZATION_DI_TOKENS.ORGANIZATION_MEMBERSHIP_SERVICE; const moduleToken = ORGANIZATION_DI_TOKENS.ORGANIZATION_MEMBERSHIP_SERVICE_MODULE; const loadModule = bindModuleToClassOnToken({ - module: thisModule, - moduleToken, - token, - classs: OrganizationMembershipService, - depsMap: { - organizationRepository: organizationRepositoryModuleLoader, - }, + module: thisModule, + moduleToken, + token, + classs: OrganizationMembershipService, + depsMap: { + organizationRepository: organizationRepositoryModuleLoader, + }, }); export const moduleLoader = { - token, - loadModule, + token, + loadModule, }; export type { OrganizationMembershipService }; - diff --git a/packages/features/ee/organizations/di/OrganizationRepository.module.ts b/packages/features/ee/organizations/di/OrganizationRepository.module.ts index ba41eb1a7e..f4a4c9d560 100644 --- a/packages/features/ee/organizations/di/OrganizationRepository.module.ts +++ b/packages/features/ee/organizations/di/OrganizationRepository.module.ts @@ -8,19 +8,18 @@ export const organizationRepositoryModule = createModule(); const token = ORGANIZATION_DI_TOKENS.ORGANIZATION_REPOSITORY; const moduleToken = ORGANIZATION_DI_TOKENS.ORGANIZATION_REPOSITORY_MODULE; const loadModule = bindModuleToClassOnToken({ - module: organizationRepositoryModule, - moduleToken, - token, - classs: OrganizationRepository, - depsMap: { - prismaClient: prismaModuleLoader, - }, + module: organizationRepositoryModule, + moduleToken, + token, + classs: OrganizationRepository, + depsMap: { + prismaClient: prismaModuleLoader, + }, }); export const moduleLoader: ModuleLoader = { - token, - loadModule, + token, + loadModule, }; export type { OrganizationRepository }; - diff --git a/packages/features/ee/organizations/di/tasker/tokens.ts b/packages/features/ee/organizations/di/tasker/tokens.ts index 0e25ff0dd7..330287ac23 100644 --- a/packages/features/ee/organizations/di/tasker/tokens.ts +++ b/packages/features/ee/organizations/di/tasker/tokens.ts @@ -6,7 +6,9 @@ export const PLATFORM_BILLING_TASKER_DI_TOKENS = { PLATFORM_ORGANIZATION_BILLING_SYNC_TASKER: Symbol("PlatformOrganizationBillingSyncTasker"), PLATFORM_ORGANIZATION_BILLING_SYNC_TASKER_MODULE: Symbol("PlatformOrganizationBillingSyncTaskerModule"), PLATFORM_ORGANIZATION_BILLING_TRIGGER_TASKER: Symbol("PlatformOrganizationBillingTriggerTasker"), - PLATFORM_ORGANIZATION_BILLING_TRIGGER_TASKER_MODULE: Symbol("PlatformOrganizationBillingTriggerTaskerModule"), + PLATFORM_ORGANIZATION_BILLING_TRIGGER_TASKER_MODULE: Symbol( + "PlatformOrganizationBillingTriggerTaskerModule" + ), PLATFORM_ORGANIZATION_BILLING_TASKER: Symbol("PlatformOrganizationBillingTasker"), PLATFORM_ORGANIZATION_BILLING_TASKER_MODULE: Symbol("PlatformOrganizationBillingTaskerModule"), }; diff --git a/packages/features/ee/organizations/di/tokens.ts b/packages/features/ee/organizations/di/tokens.ts index c6b23698ee..23fb5e7ffa 100644 --- a/packages/features/ee/organizations/di/tokens.ts +++ b/packages/features/ee/organizations/di/tokens.ts @@ -1,6 +1,6 @@ export const ORGANIZATION_DI_TOKENS = { - ORGANIZATION_REPOSITORY: Symbol("OrganizationRepository"), - ORGANIZATION_REPOSITORY_MODULE: Symbol("OrganizationRepositoryModule"), - ORGANIZATION_MEMBERSHIP_SERVICE: Symbol("OrganizationMembershipService"), - ORGANIZATION_MEMBERSHIP_SERVICE_MODULE: Symbol("OrganizationMembershipServiceModule"), + ORGANIZATION_REPOSITORY: Symbol("OrganizationRepository"), + ORGANIZATION_REPOSITORY_MODULE: Symbol("OrganizationRepositoryModule"), + ORGANIZATION_MEMBERSHIP_SERVICE: Symbol("OrganizationMembershipService"), + ORGANIZATION_MEMBERSHIP_SERVICE_MODULE: Symbol("OrganizationMembershipServiceModule"), }; diff --git a/packages/features/ee/organizations/lib/server/orgCreationUtils.ts b/packages/features/ee/organizations/lib/server/orgCreationUtils.ts index fdc1f033b1..5915c6667d 100644 --- a/packages/features/ee/organizations/lib/server/orgCreationUtils.ts +++ b/packages/features/ee/organizations/lib/server/orgCreationUtils.ts @@ -205,9 +205,8 @@ export async function assertCanCreateOrg({ errorOnUserAlreadyPartOfOrg?: boolean; }) { const featuresRepository = new FeaturesRepository(prisma); - const emailVerificationEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally( - "email-verification" - ); + const emailVerificationEnabled = + await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification"); const verifiedUser = emailVerificationEnabled ? !!orgOwner.emailVerified : true; if (!verifiedUser) { diff --git a/packages/features/ee/organizations/lib/service/OrganizationMembershipService.ts b/packages/features/ee/organizations/lib/service/OrganizationMembershipService.ts index 2a25883655..fe4941a7c5 100644 --- a/packages/features/ee/organizations/lib/service/OrganizationMembershipService.ts +++ b/packages/features/ee/organizations/lib/service/OrganizationMembershipService.ts @@ -1,37 +1,36 @@ import type { IOrganizationRepository } from "../repository/IOrganizationRepository"; export interface IOrganizationMembershipServiceDependencies { - organizationRepository: IOrganizationRepository; + organizationRepository: IOrganizationRepository; } export class OrganizationMembershipService { - constructor(private readonly deps: IOrganizationMembershipServiceDependencies) { } + constructor(private readonly deps: IOrganizationMembershipServiceDependencies) {} - /** - * Determines if user should be auto-accepted to an organization or its sub-teams based on email domain - */ - async shouldAutoAccept({ - organizationId, - userEmail, - }: { - organizationId: number; - userEmail: string; - }): Promise { - const orgSettings = await this.deps.organizationRepository.getOrganizationAutoAcceptSettings( - organizationId - ); + /** + * Determines if user should be auto-accepted to an organization or its sub-teams based on email domain + */ + async shouldAutoAccept({ + organizationId, + userEmail, + }: { + organizationId: number; + userEmail: string; + }): Promise { + const orgSettings = + await this.deps.organizationRepository.getOrganizationAutoAcceptSettings(organizationId); - if (!orgSettings) return false; + if (!orgSettings) return false; - const { orgAutoAcceptEmail, isOrganizationVerified } = orgSettings; + const { orgAutoAcceptEmail, isOrganizationVerified } = orgSettings; - if (!isOrganizationVerified || !orgAutoAcceptEmail) return false; + if (!isOrganizationVerified || !orgAutoAcceptEmail) return false; - // Case-insensitive comparison (email domains are case-insensitive per RFC) - const emailDomain = userEmail.split("@")[1]?.trim().toLowerCase(); - const autoAcceptEmailDomain = orgAutoAcceptEmail.trim().toLowerCase(); + // Case-insensitive comparison (email domains are case-insensitive per RFC) + const emailDomain = userEmail.split("@")[1]?.trim().toLowerCase(); + const autoAcceptEmailDomain = orgAutoAcceptEmail.trim().toLowerCase(); - if (!emailDomain) return false; + if (!emailDomain) return false; - return emailDomain === autoAcceptEmailDomain; - } + return emailDomain === autoAcceptEmailDomain; + } } diff --git a/packages/features/ee/organizations/lib/service/onboarding/__tests__/BaseOnboardingService.test.ts b/packages/features/ee/organizations/lib/service/onboarding/__tests__/BaseOnboardingService.test.ts index 6823792acf..e0b0aa6c5d 100644 --- a/packages/features/ee/organizations/lib/service/onboarding/__tests__/BaseOnboardingService.test.ts +++ b/packages/features/ee/organizations/lib/service/onboarding/__tests__/BaseOnboardingService.test.ts @@ -184,7 +184,11 @@ describe("BaseOnboardingService", () => { it("should handle undefined teams and invites", async () => { const service = new TestableBaseOnboardingService(mockUser); - const { teamsData, invitedMembersData } = await service.testBuildTeamsAndInvites("test-org", undefined, undefined); + const { teamsData, invitedMembersData } = await service.testBuildTeamsAndInvites( + "test-org", + undefined, + undefined + ); expect(teamsData).toEqual([]); expect(invitedMembersData).toEqual([]); @@ -203,7 +207,11 @@ describe("BaseOnboardingService", () => { { email: "user2@example.com", teamId: -1, teamName: "Sales", role: "ADMIN" }, ]; - const { teamsData, invitedMembersData } = await service.testBuildTeamsAndInvites("test-org", teams, invites); + const { teamsData, invitedMembersData } = await service.testBuildTeamsAndInvites( + "test-org", + teams, + invites + ); expect(teamsData).toHaveLength(2); expect(invitedMembersData).toHaveLength(2); @@ -227,7 +235,11 @@ describe("BaseOnboardingService", () => { { email: "eng@example.com", teamName: "Engineering", teamId: 42, role: "ADMIN" }, ]; - const { teamsData, invitedMembersData } = await service.testBuildTeamsAndInvites("test-org", teams, invites); + const { teamsData, invitedMembersData } = await service.testBuildTeamsAndInvites( + "test-org", + teams, + invites + ); expect(teamsData).toHaveLength(2); expect(invitedMembersData).toHaveLength(3); diff --git a/packages/features/ee/organizations/lib/utils.ts b/packages/features/ee/organizations/lib/utils.ts index d7ecd3ff34..eeba8d3f09 100644 --- a/packages/features/ee/organizations/lib/utils.ts +++ b/packages/features/ee/organizations/lib/utils.ts @@ -3,7 +3,9 @@ export function extractDomainFromEmail(email: string) { try { const match = email.match(/^(?:.*?:\/\/)?.*?([\w-]*(?:\.\w{2,}|\.\w{2,}\.\w{2}))(?:[/?#:]|$)/); out = (match && match[1]) ?? ""; - } catch { /* empty */ } + } catch { + /* empty */ + } return out.split(".")[0]; } diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index 8f95076572..2c3979b53c 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -8,13 +8,8 @@ import { eventTypeMetaDataSchemaWithTypedApps, eventTypeAppMetadataOptionalSchema, } from "@calcom/app-store/zod-utils"; -import { - sendAttendeeRequestEmailAndSMS, - sendOrganizerRequestEmail, -} from "@calcom/emails/email-manager"; -import EventManager, { - placeholderCreatedEvent, -} from "@calcom/features/bookings/lib/EventManager"; +import { sendAttendeeRequestEmailAndSMS, sendOrganizerRequestEmail } from "@calcom/emails/email-manager"; +import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; import { doesBookingRequireConfirmation } from "@calcom/features/bookings/lib/doesBookingRequireConfirmation"; import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials"; import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; @@ -41,10 +36,7 @@ export const config = { }, }; -export async function handleStripePaymentSuccess( - event: Stripe.Event, - traceContext: TraceContext -) { +export async function handleStripePaymentSuccess(event: Stripe.Event, traceContext: TraceContext) { const paymentIntent = event.data.object as Stripe.PaymentIntent; const payment = await prisma.payment.findFirst({ where: { @@ -57,15 +49,10 @@ export async function handleStripePaymentSuccess( }); if (!payment?.bookingId) { - log.error( - "Stripe: Payment Not Found", - safeStringify(paymentIntent), - safeStringify(payment) - ); + log.error("Stripe: Payment Not Found", safeStringify(paymentIntent), safeStringify(payment)); throw new HttpCode({ statusCode: 204, message: "Payment not found" }); } - if (!payment?.bookingId) - throw new HttpCode({ statusCode: 204, message: "Payment not found" }); + if (!payment?.bookingId) throw new HttpCode({ statusCode: 204, message: "Payment not found" }); await handlePaymentSuccess({ paymentId: payment.id, @@ -75,10 +62,7 @@ export async function handleStripePaymentSuccess( }); } -const handleSetupSuccess = async ( - event: Stripe.Event, - traceContext: TraceContext -) => { +const handleSetupSuccess = async (event: Stripe.Event, traceContext: TraceContext) => { const setupIntent = event.data.object as Stripe.SetupIntent; const payment = await prisma.payment.findFirst({ where: { @@ -86,8 +70,7 @@ const handleSetupSuccess = async ( }, }); - if (!payment?.data || !payment?.id) - throw new HttpCode({ statusCode: 204, message: "Payment not found" }); + if (!payment?.data || !payment?.id) throw new HttpCode({ statusCode: 204, message: "Payment not found" }); const { booking, user, evt, eventType } = await getBooking(payment.bookingId); @@ -108,9 +91,7 @@ const handleSetupSuccess = async ( }, }); - const metadata = eventTypeMetaDataSchemaWithTypedApps.parse( - eventType?.metadata - ); + const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventType?.metadata); const allCredentials = await getAllCredentialsIncludeServiceAccountKey(user, { ...booking.eventType, metadata, @@ -120,15 +101,11 @@ const handleSetupSuccess = async ( const platformOAuthClient = user.isPlatformManaged ? await platformOAuthClientRepository.getByUserId(user.id) : null; - const areCalendarEventsEnabled = - platformOAuthClient?.areCalendarEventsEnabled ?? true; + const areCalendarEventsEnabled = platformOAuthClient?.areCalendarEventsEnabled ?? true; const areEmailsEnabled = platformOAuthClient?.areEmailsEnabled ?? true; if (!requiresConfirmation) { - const eventManager = new EventManager( - { ...user, credentials: allCredentials }, - metadata?.apps - ); + const eventManager = new EventManager({ ...user, credentials: allCredentials }, metadata?.apps); const scheduleResult = areCalendarEventsEnabled ? await eventManager.create(evt) : placeholderCreatedEvent; @@ -165,27 +142,18 @@ const handleSetupSuccess = async ( bookingId: booking.id, booking, paid: true, - platformClientParams: platformOAuthClient - ? getPlatformParams(platformOAuthClient) - : undefined, + platformClientParams: platformOAuthClient ? getPlatformParams(platformOAuthClient) : undefined, traceContext: updatedTraceContext, actionSource: "WEBHOOK", actor, }); } else if (areEmailsEnabled) { await sendOrganizerRequestEmail({ ...evt }, eventType.metadata); - await sendAttendeeRequestEmailAndSMS( - { ...evt }, - evt.attendees[0], - eventType.metadata - ); + await sendAttendeeRequestEmailAndSMS({ ...evt }, evt.attendees[0], eventType.metadata); } }; -type WebhookHandler = ( - event: Stripe.Event, - traceContext: TraceContext -) => Promise; +type WebhookHandler = (event: Stripe.Event, traceContext: TraceContext) => Promise; const webhookHandlers: Record = { "payment_intent.succeeded": handleStripePaymentSuccess, @@ -197,10 +165,7 @@ const webhookHandlers: Record = { * We need to create a PaymentManager in `@calcom/lib` * to prevent circular dependencies on App Store migration */ -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { +export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { if (req.method !== "POST") { throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" }); @@ -233,11 +198,7 @@ export default async function handler( let event: Stripe.Event; try { - event = stripe.webhooks.constructEvent( - payload, - sig, - process.env.STRIPE_WEBHOOK_SECRET - ); + event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { log.error("Stripe webhook signature verification failed", { stripeEventId, diff --git a/packages/features/ee/round-robin/handleRescheduleEventManager.ts b/packages/features/ee/round-robin/handleRescheduleEventManager.ts index eb9a6f71d0..071a3135fe 100644 --- a/packages/features/ee/round-robin/handleRescheduleEventManager.ts +++ b/packages/features/ee/round-robin/handleRescheduleEventManager.ts @@ -124,7 +124,7 @@ export const handleRescheduleEventManager = async ({ const googleHangoutLink = Array.isArray(googleCalResult?.updatedEvent) ? googleCalResult.updatedEvent[0]?.hangoutLink - : googleCalResult?.updatedEvent?.hangoutLink ?? googleCalResult?.createdEvent?.hangoutLink; + : (googleCalResult?.updatedEvent?.hangoutLink ?? googleCalResult?.createdEvent?.hangoutLink); if (googleHangoutLink) { results.push({ @@ -154,7 +154,7 @@ export const handleRescheduleEventManager = async ({ } const createdOrUpdatedEvent = Array.isArray(results[0]?.updatedEvent) ? results[0]?.updatedEvent[0] - : results[0]?.updatedEvent ?? results[0]?.createdEvent; + : (results[0]?.updatedEvent ?? results[0]?.createdEvent); metadata.hangoutLink = createdOrUpdatedEvent?.hangoutLink; metadata.conferenceData = createdOrUpdatedEvent?.conferenceData; metadata.entryPoints = createdOrUpdatedEvent?.entryPoints; diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts index 20dfe81586..4f9965227b 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts @@ -268,9 +268,7 @@ export const roundRobinManualReassignment = async ({ organizationId: orgId, source: actionSource, auditData: { - ...(hasOrganizerChanged - ? { organizerUuid: { old: originalOrganizer.uuid, new: newUser.uuid } } - : null), + ...(hasOrganizerChanged ? { organizerUuid: { old: originalOrganizer.uuid, new: newUser.uuid } } : null), ...(attendeeUpdatedId ? { hostAttendeeUpdated: { diff --git a/packages/features/ee/teams/lib/getTeamMemberEmailFromCrm.ts b/packages/features/ee/teams/lib/getTeamMemberEmailFromCrm.ts index 5f9b57b2c6..8b72c58b07 100644 --- a/packages/features/ee/teams/lib/getTeamMemberEmailFromCrm.ts +++ b/packages/features/ee/teams/lib/getTeamMemberEmailFromCrm.ts @@ -79,8 +79,8 @@ async function getAttributeRoutingConfig( const routingFormResponseQuery = routingFormResponseId ? await routingFormResponseRepository.findFormResponseIncludeForm({ routingFormResponseId }) : queuedFormResponseId - ? await routingFormResponseRepository.findQueuedFormResponseIncludeForm({ queuedFormResponseId }) - : null; + ? await routingFormResponseRepository.findQueuedFormResponseIncludeForm({ queuedFormResponseId }) + : null; if (!routingFormResponseQuery || !routingFormResponseQuery?.form.routes) return null; const parsedRoutes = routesSchema.safeParse(routingFormResponseQuery?.form.routes); @@ -232,8 +232,8 @@ async function getTeamMemberEmailForResponseOrContact({ routingFormResponseId || queuedFormResponseId ? { routingFormResponseId, queuedFormResponseId, eventTypeId } : chosenRoute - ? { route: chosenRoute } - : null; + ? { route: chosenRoute } + : null; // If we have found crmAppSlug, it means that the CRM App in the routing-form will handle the logic if (attributeRoutingConfigGetterData && crmAppSlug) { diff --git a/packages/features/ee/teams/lib/payments.ts b/packages/features/ee/teams/lib/payments.ts index d8f78ad9ac..4d59578ecc 100644 --- a/packages/features/ee/teams/lib/payments.ts +++ b/packages/features/ee/teams/lib/payments.ts @@ -68,15 +68,15 @@ export const generateTeamCheckoutSession = async ({ mode: "subscription", ...(dubCustomer?.discount?.couponId ? { - discounts: [ - { - coupon: - process.env.NODE_ENV !== "production" && dubCustomer.discount.couponTestId - ? dubCustomer.discount.couponTestId - : dubCustomer.discount.couponId, - }, - ], - } + discounts: [ + { + coupon: + process.env.NODE_ENV !== "production" && dubCustomer.discount.couponTestId + ? dubCustomer.discount.couponTestId + : dubCustomer.discount.couponId, + }, + ], + } : { allow_promotion_codes: true }), success_url: `${WEBAPP_URL}/api/teams/create?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${WEBAPP_URL}/settings/my-account/profile`, @@ -108,8 +108,14 @@ export const generateTeamCheckoutSession = async ({ userId, dubCustomerId: userId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe ...(isOnboarding !== undefined && { isOnboarding: isOnboarding.toString() }), - ...(tracking?.googleAds?.gclid && { gclid: tracking.googleAds.gclid, campaignId: tracking.googleAds?.campaignId }), - ...(tracking?.linkedInAds?.liFatId && { liFatId: tracking.linkedInAds.liFatId, linkedInCampaignId: tracking.linkedInAds?.campaignId }), + ...(tracking?.googleAds?.gclid && { + gclid: tracking.googleAds.gclid, + campaignId: tracking.googleAds?.campaignId, + }), + ...(tracking?.linkedInAds?.liFatId && { + liFatId: tracking.linkedInAds.liFatId, + linkedInCampaignId: tracking.linkedInAds?.campaignId, + }), }, }); return session; @@ -160,10 +166,7 @@ export const purchaseTeamOrOrgSubscription = async (input: { let priceId: string | undefined; if (pricePerSeat) { - if ( - isOrg && - pricePerSeat === ORGANIZATION_SELF_SERVE_PRICE - ) { + if (isOrg && pricePerSeat === ORGANIZATION_SELF_SERVE_PRICE) { priceId = fixedPrice as string; } else { const customPriceObj = await getPriceObject(fixedPrice); @@ -201,8 +204,14 @@ export const purchaseTeamOrOrgSubscription = async (input: { }, metadata: { teamId, - ...(tracking?.googleAds?.gclid && { gclid: tracking.googleAds.gclid, campaignId: tracking.googleAds.campaignId }), - ...(tracking?.linkedInAds?.liFatId && { liFatId: tracking.linkedInAds.liFatId, linkedInCampaignId: tracking.linkedInAds?.campaignId }), + ...(tracking?.googleAds?.gclid && { + gclid: tracking.googleAds.gclid, + campaignId: tracking.googleAds.campaignId, + }), + ...(tracking?.linkedInAds?.liFatId && { + liFatId: tracking.linkedInAds.liFatId, + linkedInCampaignId: tracking.linkedInAds?.campaignId, + }), }, subscription_data: { metadata: { diff --git a/packages/features/ee/teams/repositories/TeamRepository.ts b/packages/features/ee/teams/repositories/TeamRepository.ts index 830bdf1839..e3ed188a8c 100644 --- a/packages/features/ee/teams/repositories/TeamRepository.ts +++ b/packages/features/ee/teams/repositories/TeamRepository.ts @@ -10,10 +10,7 @@ import { MembershipRole } from "@calcom/prisma/enums"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; type TeamGetPayloadWithParsedMetadata = - | (Omit< - Prisma.TeamGetPayload<{ select: TeamSelect }>, - "metadata" | "isOrganization" - > & { + | (Omit, "metadata" | "isOrganization"> & { metadata: z.infer; isOrganization: boolean; }) @@ -51,17 +48,14 @@ async function getTeamOrOrg({ forOrgWithSlug: forOrgWithSlug, isOrg, teamSelect, -}: GetTeamOrOrgArg): Promise< - TeamGetPayloadWithParsedMetadata -> { +}: GetTeamOrOrgArg): Promise> { const where: Prisma.TeamFindFirstArgs["where"] = {}; teamSelect = { ...teamSelect, metadata: true, isOrganization: true, } satisfies TeamSelect; - if (lookupBy.havingMemberWithId) - where.members = { some: { userId: lookupBy.havingMemberWithId } }; + if (lookupBy.havingMemberWithId) where.members = { some: { userId: lookupBy.havingMemberWithId } }; if ("id" in lookupBy) { where.id = lookupBy.id; @@ -114,20 +108,17 @@ async function getTeamOrOrg({ }); if (teamsWithParsedMetadata.length > 1) { - log.error( - "Found more than one team/Org. We should be doing something wrong.", - { - isOrgView: isOrg, - where, - teams: teamsWithParsedMetadata.map((team) => { - const t = team as unknown as { id: number; slug: string }; - return { - id: t.id, - slug: t.slug, - }; - }), - } - ); + log.error("Found more than one team/Org. We should be doing something wrong.", { + isOrgView: isOrg, + where, + teams: teamsWithParsedMetadata.map((team) => { + const t = team as unknown as { id: number; slug: string }; + return { + id: t.id, + slug: t.slug, + }; + }), + }); } const team = teamsWithParsedMetadata[0]; @@ -141,9 +132,7 @@ export async function getTeam({ lookupBy, forOrgWithSlug: forOrgWithSlug, teamSelect, -}: Omit, "isOrg">): Promise< - TeamGetPayloadWithParsedMetadata -> { +}: Omit, "isOrg">): Promise> { return getTeamOrOrg({ lookupBy, forOrgWithSlug: forOrgWithSlug, @@ -156,9 +145,7 @@ export async function getOrg({ lookupBy, forOrgWithSlug: forOrgWithSlug, teamSelect, -}: Omit, "isOrg">): Promise< - TeamGetPayloadWithParsedMetadata -> { +}: Omit, "isOrg">): Promise> { return getTeamOrOrg({ lookupBy, forOrgWithSlug: forOrgWithSlug, @@ -254,9 +241,7 @@ export class TeamRepository { return await this.prismaClient.team.findFirst({ where: { slug, - parent: parentSlug - ? whereClauseForOrgWithSlugOrRequestedSlug(parentSlug) - : null, + parent: parentSlug ? whereClauseForOrgWithSlugOrRequestedSlug(parentSlug) : null, }, select, }); @@ -307,13 +292,7 @@ export class TeamRepository { }); } - async findTeamsByUserId({ - userId, - includeOrgs, - }: { - userId: number; - includeOrgs?: boolean; - }) { + async findTeamsByUserId({ userId, includeOrgs }: { userId: number; includeOrgs?: boolean }) { const memberships = await this.prismaClient.membership.findMany({ where: { // Show all the teams this user belongs to regardless of the team being part of the user's org or not @@ -356,10 +335,7 @@ export class TeamRepository { // Only return inviteToken if user is OWNER or ADMIN const inviteToken = membership.role === "OWNER" || membership.role === "ADMIN" - ? inviteTokens.find( - (token) => - token.identifier === `invite-link-for-teamId-${team.id}` - ) + ? inviteTokens.find((token) => token.identifier === `invite-link-for-teamId-${team.id}`) : null; return { @@ -396,9 +372,7 @@ export class TeamRepository { }, }); - return memberships - .filter((mmship) => !mmship.team.isOrganization) - .map((mmship) => mmship.team); + return memberships.filter((mmship) => !mmship.team.isOrganization).map((mmship) => mmship.team); } async findTeamWithOrganizationSettings(teamId: number) { @@ -473,13 +447,7 @@ export class TeamRepository { }); } - async findOrganization({ - teamId, - userId, - }: { - teamId?: number; - userId: number; - }) { + async findOrganization({ teamId, userId }: { teamId?: number; userId: number }) { return await this.prismaClient.team.findFirst({ where: { isOrganization: true, @@ -501,11 +469,7 @@ export class TeamRepository { }); } - async findOrganizationIdBySlug({ - slug, - }: { - slug: string; - }): Promise { + async findOrganizationIdBySlug({ slug }: { slug: string }): Promise { const org = await this.prismaClient.team.findFirst({ where: { slug, @@ -545,13 +509,7 @@ export class TeamRepository { return !conflictingTeam; } - async getTeamByIdIfUserIsAdmin({ - userId, - teamId, - }: { - userId: number; - teamId: number; - }) { + async getTeamByIdIfUserIsAdmin({ userId, teamId }: { userId: number; teamId: number }) { return await this.prismaClient.team.findUnique({ where: { id: teamId, @@ -571,13 +529,7 @@ export class TeamRepository { }); } - async findOrgTeamsExcludingTeam({ - parentId, - excludeTeamId, - }: { - parentId: number; - excludeTeamId: number; - }) { + async findOrgTeamsExcludingTeam({ parentId, excludeTeamId }: { parentId: number; excludeTeamId: number }) { return await this.prismaClient.team.findMany({ where: { parentId, @@ -660,13 +612,7 @@ export class TeamRepository { return { resource, action }; } - async findTeamsNotBelongingToOrgByIds({ - teamIds, - orgId, - }: { - teamIds: number[]; - orgId: number; - }) { + async findTeamsNotBelongingToOrgByIds({ teamIds, orgId }: { teamIds: number[]; orgId: number }) { return await this.prismaClient.team.findMany({ where: { id: { in: teamIds }, @@ -677,13 +623,7 @@ export class TeamRepository { }); } - async findByIdsAndOrgId({ - teamIds, - orgId, - }: { - teamIds: number[]; - orgId: number; - }) { + async findByIdsAndOrgId({ teamIds, orgId }: { teamIds: number[]; orgId: number }) { return await this.prismaClient.team.findMany({ where: { id: { in: teamIds }, diff --git a/packages/features/ee/teams/services/teamService.alternative.test.ts b/packages/features/ee/teams/services/teamService.alternative.test.ts index ad619ffbcd..c0f210b5b3 100644 --- a/packages/features/ee/teams/services/teamService.alternative.test.ts +++ b/packages/features/ee/teams/services/teamService.alternative.test.ts @@ -50,7 +50,9 @@ const mockTeamRepo = { throw new Error(`Team with id ${id} not found`); }), }; -vi.mocked(TeamRepository).mockImplementation(function() { return mockTeamRepo; }); +vi.mocked(TeamRepository).mockImplementation(function () { + return mockTeamRepo; +}); vi.mocked(deleteDomain).mockImplementation(async (slug) => { database.domains.delete(slug); @@ -63,7 +65,7 @@ vi.mocked(WorkflowService.deleteWorkflowRemindersOfRemovedTeam).mockImplementati describe("TeamService", () => { beforeEach(async () => { database.clear(); - + const { getTeamBillingServiceFactory } = await import("@calcom/ee/billing/di/containers/Billing"); vi.mocked(getTeamBillingServiceFactory).mockReturnValue({ findAndInit: vi.fn().mockImplementation(async (teamId) => { diff --git a/packages/features/ee/teams/services/teamService.integration-test.ts b/packages/features/ee/teams/services/teamService.integration-test.ts index 4e7df20612..9eab1ab067 100644 --- a/packages/features/ee/teams/services/teamService.integration-test.ts +++ b/packages/features/ee/teams/services/teamService.integration-test.ts @@ -34,7 +34,9 @@ const createTestUser = async (overrides?: { data: { email: overrides?.email ?? `test-user-${timestamp}-${randomSuffix}@example.com`, username: - overrides?.username === null ? null : overrides?.username ?? `testuser-${timestamp}-${randomSuffix}`, + overrides?.username === null + ? null + : (overrides?.username ?? `testuser-${timestamp}-${randomSuffix}`), name: "Test User", organizationId: overrides?.organizationId ?? undefined, }, diff --git a/packages/features/ee/tsconfig.json b/packages/features/ee/tsconfig.json index 68f3705dfe..ec89ff18af 100644 --- a/packages/features/ee/tsconfig.json +++ b/packages/features/ee/tsconfig.json @@ -15,5 +15,13 @@ "../../types/business-days-plugin.d.ts", "../../types/window.d.ts" ], - "exclude": ["dist", "build", "**/node_modules/**", "**/*.test.*", "**/__mocks__/*", "**/__tests__/*", "billing"] + "exclude": [ + "dist", + "build", + "**/node_modules/**", + "**/*.test.*", + "**/__mocks__/*", + "**/__tests__/*", + "billing" + ] } diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index b680b62a48..c683667325 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -531,4 +531,4 @@ function shouldHideBranding(booking: { return !!booking.user?.hideBranding; } -export {handler}; \ No newline at end of file +export { handler }; diff --git a/packages/features/ee/workflows/hooks/useVoicePreview.ts b/packages/features/ee/workflows/hooks/useVoicePreview.ts index 6cf66ddc61..aceb080d05 100644 --- a/packages/features/ee/workflows/hooks/useVoicePreview.ts +++ b/packages/features/ee/workflows/hooks/useVoicePreview.ts @@ -48,7 +48,8 @@ export function useVoicePreview() { setCurrentAudio(audio); audioRef.current = audio; - audio.play() + audio + .play() .then(() => { setPlayingVoiceId(voiceId || null); }) diff --git a/packages/features/ee/workflows/lib/getOptions.ts b/packages/features/ee/workflows/lib/getOptions.ts index 22063ccf3a..4c045577c4 100644 --- a/packages/features/ee/workflows/lib/getOptions.ts +++ b/packages/features/ee/workflows/lib/getOptions.ts @@ -75,7 +75,12 @@ function convertToTemplateOptions( value: template, needsTeamsUpgrade, upgradeTeamsBadgeProps: needsTeamsUpgrade ? { hasPaidPlan, hasActiveTeamPlan, isTrial } : undefined, - } as { label: string; value: WorkflowTemplates; needsTeamsUpgrade: boolean; upgradeTeamsBadgeProps?: PlanState }; + } as { + label: string; + value: WorkflowTemplates; + needsTeamsUpgrade: boolean; + upgradeTeamsBadgeProps?: PlanState; + }; }); } @@ -93,8 +98,8 @@ export function getWorkflowTemplateOptions( action && isWhatsappAction(action) ? WHATSAPP_WORKFLOW_TEMPLATES : action && isEmailToAttendeeAction(action) - ? ATTENDEE_WORKFLOW_TEMPLATES - : BASIC_WORKFLOW_TEMPLATES; + ? ATTENDEE_WORKFLOW_TEMPLATES + : BASIC_WORKFLOW_TEMPLATES; return convertToTemplateOptions(t, planState, TEMPLATES); } diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index eb6ed7c93e..7cae2f734d 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -78,9 +78,8 @@ const getReminderPhoneNumber = async ( if (seatReferenceUid) { const bookingSeatRepository = new BookingSeatRepository(prisma); - const seatAttendeeData = await bookingSeatRepository.getByReferenceUidWithAttendeeDetails( - seatReferenceUid - ); + const seatAttendeeData = + await bookingSeatRepository.getByReferenceUidWithAttendeeDetails(seatReferenceUid); return seatAttendeeData?.attendee?.phoneNumber || smsReminderNumber; } diff --git a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts index 889489dfe9..0c5bf3768a 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts @@ -128,13 +128,13 @@ const customTemplate = ( const attendeeFirstName = variables.attendeeFirstName ? variables.attendeeFirstName - : attendeeNameWords?.[0] ?? ""; + : (attendeeNameWords?.[0] ?? ""); const attendeeLastName = variables.attendeeLastName ? variables.attendeeLastName : attendeeNameWordCount > 1 - ? attendeeNameWords![attendeeNameWordCount - 1] - : ""; + ? attendeeNameWords![attendeeNameWordCount - 1] + : ""; let dynamicText = text .replaceAll("{EVENT_NAME}", variables.eventName || "") diff --git a/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts b/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts index 93b92e3d20..c55b3dd8bc 100644 --- a/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts +++ b/packages/features/ee/workflows/lib/service/EmailWorkflowService.ts @@ -21,7 +21,11 @@ import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { CalendarEvent } from "@calcom/types/Calendar"; import type { WorkflowReminderRepository } from "../../repositories/WorkflowReminderRepository"; -import { isEmailAction, getTemplateBodyForAction, getTemplateSubjectForAction } from "../actionHelperFunctions"; +import { + isEmailAction, + getTemplateBodyForAction, + getTemplateSubjectForAction, +} from "../actionHelperFunctions"; import { detectMatchedTemplate } from "../detectMatchedTemplate"; import { getWorkflowRecipientEmail } from "../getWorkflowReminders"; import type { VariablesType } from "../reminders/templates/customTemplate"; @@ -414,7 +418,7 @@ export class EmailWorkflowService { if (matchedTemplate === WorkflowTemplates.REMINDER) { const t = await getTranslation(locale, "common"); - const meetingUrl = + const meetingUrl = getVideoCallUrlFromCalEvent({ videoCallData: evt.videoCallData, uid: evt.uid, @@ -547,10 +551,9 @@ export class EmailWorkflowService { }; const shouldIncludeCalendarEvent = - includeCalendarEvent && - triggerEvent !== WorkflowTriggerEvents.BOOKING_REQUESTED; + includeCalendarEvent && triggerEvent !== WorkflowTriggerEvents.BOOKING_REQUESTED; - const attachments = shouldIncludeCalendarEvent + const attachments = shouldIncludeCalendarEvent ? [ { content: @@ -578,4 +581,4 @@ export class EmailWorkflowService { sender, }; } -} \ No newline at end of file +} diff --git a/packages/features/ee/workflows/lib/service/WorkflowService.test.ts b/packages/features/ee/workflows/lib/service/WorkflowService.test.ts index 89574df00b..67b9053640 100644 --- a/packages/features/ee/workflows/lib/service/WorkflowService.test.ts +++ b/packages/features/ee/workflows/lib/service/WorkflowService.test.ts @@ -20,9 +20,11 @@ vi.mock("@calcom/features/profile/lib/hideBranding", () => ({ const mockWorkflowReminderCreate = vi.fn(); vi.mock("@calcom/features/ee/workflows/repositories/WorkflowReminderRepository", () => ({ - WorkflowReminderRepository: vi.fn().mockImplementation(function() { return { - create: mockWorkflowReminderCreate, - }; }), + WorkflowReminderRepository: vi.fn().mockImplementation(function () { + return { + create: mockWorkflowReminderCreate, + }; + }), })); vi.mock("@calcom/prisma", () => ({ diff --git a/packages/features/ee/workflows/lib/service/workflowOptOutService.ts b/packages/features/ee/workflows/lib/service/workflowOptOutService.ts index 59f2bdb6ef..c8c73f0a71 100644 --- a/packages/features/ee/workflows/lib/service/workflowOptOutService.ts +++ b/packages/features/ee/workflows/lib/service/workflowOptOutService.ts @@ -8,9 +8,8 @@ export class WorkflowOptOutService { static async optOutPhoneNumber(phoneNumber: string) { await WorkflowOptOutContactRepository.addPhoneNumber(phoneNumber); // Delete scheduled workflows - const scheduledReminders = await WorkflowReminderRepository.getFutureScheduledAttendeeSMSReminders( - phoneNumber - ); + const scheduledReminders = + await WorkflowReminderRepository.getFutureScheduledAttendeeSMSReminders(phoneNumber); // Get twilio scheduled workflows reminders await deleteMultipleScheduledSMS( diff --git a/packages/features/ee/workflows/lib/test/compareReminderBodyToTemplate.test.ts b/packages/features/ee/workflows/lib/test/compareReminderBodyToTemplate.test.ts index 74ca692ff6..a193e9406d 100644 --- a/packages/features/ee/workflows/lib/test/compareReminderBodyToTemplate.test.ts +++ b/packages/features/ee/workflows/lib/test/compareReminderBodyToTemplate.test.ts @@ -4,8 +4,6 @@ import { getTranslation } from "@calcom/lib/server/i18n"; import { TimeFormat } from "@calcom/lib/timeFormat"; import { WorkflowActions, WorkflowTemplates } from "@calcom/prisma/enums"; - - import { getTemplateBodyForAction } from "../actionHelperFunctions"; import compareReminderBodyToTemplate from "../compareReminderBodyToTemplate"; import plainTextReminderTemplates from "../reminders/templates/plainTextTemplates"; diff --git a/packages/features/ee/workflows/lib/test/twilioWebhook.test.ts b/packages/features/ee/workflows/lib/test/twilioWebhook.test.ts index 129add17b5..2820761d46 100644 --- a/packages/features/ee/workflows/lib/test/twilioWebhook.test.ts +++ b/packages/features/ee/workflows/lib/test/twilioWebhook.test.ts @@ -18,10 +18,12 @@ vi.mock("../reminders/providers/twilioProvider", () => ({ const mockChargeCredits = vi.fn().mockResolvedValue({ teamId: 1 }); vi.mock("@calcom/features/ee/billing/credit-service", () => ({ - CreditService: vi.fn().mockImplementation(function() { return { - chargeCredits: mockChargeCredits, - calculateCreditsFromPrice: vi.fn().mockReturnValue(1), - }; }), + CreditService: vi.fn().mockImplementation(function () { + return { + chargeCredits: mockChargeCredits, + calculateCreditsFromPrice: vi.fn().mockReturnValue(1), + }; + }), })); const mockFindFirst = vi.fn(); diff --git a/packages/features/ee/workflows/lib/urlScanner.ts b/packages/features/ee/workflows/lib/urlScanner.ts index 98208156b0..d0649806af 100644 --- a/packages/features/ee/workflows/lib/urlScanner.ts +++ b/packages/features/ee/workflows/lib/urlScanner.ts @@ -295,12 +295,15 @@ async function getScanResult(scanId: string): Promise { } try { - const response = await fetch(`${CLOUDFLARE_API_BASE}/accounts/${accountId}/urlscanner/v2/result/${scanId}`, { - method: "GET", - headers: { - Authorization: `Bearer ${apiToken}`, - }, - }); + const response = await fetch( + `${CLOUDFLARE_API_BASE}/accounts/${accountId}/urlscanner/v2/result/${scanId}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + } + ); // 404 means scan is still in progress if (response.status === 404) { diff --git a/packages/features/ee/workflows/lib/variableTranslations.ts b/packages/features/ee/workflows/lib/variableTranslations.ts index 7e030bc36a..7bdc0c740f 100644 --- a/packages/features/ee/workflows/lib/variableTranslations.ts +++ b/packages/features/ee/workflows/lib/variableTranslations.ts @@ -16,8 +16,8 @@ export function getTranslatedText(text: string, language: { locale: string; t: T let translatedVariable = DYNAMIC_TEXT_VARIABLES.includes(variable.toLowerCase()) ? language.t(variable.toLowerCase().concat("_variable")).replace(/ /g, "_").toLocaleUpperCase() : DYNAMIC_TEXT_VARIABLES.includes(variable.toLowerCase().concat("_name")) //for the old variables names (ORGANIZER_NAME, ATTENDEE_NAME) - ? language.t(variable.toLowerCase().concat("_name_variable")).replace(/ /g, "_").toLocaleUpperCase() - : variable; + ? language.t(variable.toLowerCase().concat("_name_variable")).replace(/ /g, "_").toLocaleUpperCase() + : variable; // this takes care of translating formatted variables (e.g. {EVENT_DATE_DD MM YYYY}) const formattedVarToTranslate = FORMATTED_DYNAMIC_TEXT_VARIABLES.map((formattedVar) => { diff --git a/packages/features/ee/workflows/repositories/WorkflowRepository.test.ts b/packages/features/ee/workflows/repositories/WorkflowRepository.test.ts index 05228bc504..c3c527d1d0 100644 --- a/packages/features/ee/workflows/repositories/WorkflowRepository.test.ts +++ b/packages/features/ee/workflows/repositories/WorkflowRepository.test.ts @@ -1,4 +1,3 @@ - import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; import { describe, it, expect } from "vitest"; diff --git a/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx b/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx index 05716e0aab..3337403791 100644 --- a/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx +++ b/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx @@ -73,7 +73,9 @@ export function BulkEditDefaultForEventsModal({
)} {eventTypes.map((eventType) => ( -
+
(); - const hasTeamLimits = () => { - const team = eventType.team as { bookingLimits?: IntervalLimit | null; includeManagedEventsInLimits?: boolean } | null | undefined; - const parentTeam = (eventType.parent as { team?: { bookingLimits?: IntervalLimit | null; includeManagedEventsInLimits?: boolean } } | null | undefined)?.team; + const team = eventType.team as + | { bookingLimits?: IntervalLimit | null; includeManagedEventsInLimits?: boolean } + | null + | undefined; + const parentTeam = ( + eventType.parent as + | { team?: { bookingLimits?: IntervalLimit | null; includeManagedEventsInLimits?: boolean } } + | null + | undefined + )?.team; - const teamHasLimits = - !!team?.bookingLimits && Object.keys(team.bookingLimits).length > 0; + const teamHasLimits = !!team?.bookingLimits && Object.keys(team.bookingLimits).length > 0; const parentTeamHasLimits = - !!parentTeam?.bookingLimits && - Object.keys(parentTeam.bookingLimits).length > 0; + !!parentTeam?.bookingLimits && Object.keys(parentTeam.bookingLimits).length > 0; - const includeManaged = - !!parentTeam?.includeManagedEventsInLimits || - !!team?.includeManagedEventsInLimits; + const includeManaged = !!parentTeam?.includeManagedEventsInLimits || !!team?.includeManagedEventsInLimits; if (teamHasLimits) { - if(eventType.schedulingType === SchedulingType.MANAGED) return includeManaged; + if (eventType.schedulingType === SchedulingType.MANAGED) return includeManaged; return true; } return parentTeamHasLimits && includeManaged; }; - const TeamLimitsBadge = ({ isManagedChild, teamId }: { isManagedChild: boolean, teamId?: number | null }) => { + const TeamLimitsBadge = ({ + isManagedChild, + teamId, + }: { + isManagedChild: boolean; + teamId?: number | null; + }) => { const badge = ( {t("team_limits_apply")} @@ -438,11 +447,7 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr if (teamId) { return ( - + {badge} ); @@ -654,7 +659,8 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr title={t("limit_booking_frequency")} Badge={ hasTeamLimits() - ? TeamLimitsBadge({ isManagedChild: !!eventType.parent, teamId: eventType.team?.id }) : null + ? TeamLimitsBadge({ isManagedChild: !!eventType.parent, teamId: eventType.team?.id }) + : null } {...bookingLimitsLocked} description={ diff --git a/packages/features/eventtypes/components/tabs/recurring/RecurringEventController.tsx b/packages/features/eventtypes/components/tabs/recurring/RecurringEventController.tsx index 8f697463d8..60589a3f01 100644 --- a/packages/features/eventtypes/components/tabs/recurring/RecurringEventController.tsx +++ b/packages/features/eventtypes/components/tabs/recurring/RecurringEventController.tsx @@ -106,8 +106,8 @@ export default function RecurringEventController({ isSeatsOffered ? t("seats_doesnt_support_recurring") : hasBookingLimitPerBooker - ? t("booking_limit_per_booker_doesnt_support_recurring") - : undefined + ? t("booking_limit_per_booker_doesnt_support_recurring") + : undefined } onCheckedChange={(e) => { if (!e) { diff --git a/packages/features/eventtypes/lib/getEventTypeById.integration-test.ts b/packages/features/eventtypes/lib/getEventTypeById.integration-test.ts index 337f7158a1..03c3e3f614 100644 --- a/packages/features/eventtypes/lib/getEventTypeById.integration-test.ts +++ b/packages/features/eventtypes/lib/getEventTypeById.integration-test.ts @@ -1,4 +1,4 @@ -import { prisma } from "@calcom/prisma" +import { prisma } from "@calcom/prisma"; import type { PrismaClient } from "@calcom/prisma"; import i18nMock from "@calcom/testing/lib/__mocks__/libServerI18n"; @@ -52,14 +52,14 @@ describe("getRawEventType", () => { organizationId: overrides?.organizationId, ...(overrides?.withProfile && overrides.organizationId && { - profiles: { - create: { - organizationId: overrides.organizationId, - uid: username, - username, + profiles: { + create: { + organizationId: overrides.organizationId, + uid: username, + username, + }, }, - }, - }), + }), }, }); createdResources.users.push(user.id); @@ -119,10 +119,7 @@ describe("getRawEventType", () => { return membership; }; - const createTestEventType = async ( - userId: number, - overrides?: { slug?: string; title?: string } - ) => { + const createTestEventType = async (userId: number, overrides?: { slug?: string; title?: string }) => { const timestamp = Date.now() + Math.random(); const eventType = await prisma.eventType.create({ data: { diff --git a/packages/features/eventtypes/lib/getPublicEvent.ts b/packages/features/eventtypes/lib/getPublicEvent.ts index 9fe8a0d386..4f36bc6ebe 100644 --- a/packages/features/eventtypes/lib/getPublicEvent.ts +++ b/packages/features/eventtypes/lib/getPublicEvent.ts @@ -129,10 +129,10 @@ export const getPublicEventSelect = (fetchAllUsers: boolean) => { }, }, }, - successRedirectUrl: true, - forwardParamsSuccessRedirect: true, - redirectUrlOnNoRoutingFormResponse: true, - workflows: { + successRedirectUrl: true, + forwardParamsSuccessRedirect: true, + redirectUrlOnNoRoutingFormResponse: true, + workflows: { include: { workflow: { include: { @@ -661,7 +661,7 @@ export async function getUsersFromEvent( // getOwnerFromUsersArray is used here for backward compatibility when team event type has users[] but not hosts[] return eventHosts.length ? eventHosts.filter((host) => host.user.username).map(mapHostsToUsers) - : (await getOwnerFromUsersArray(prisma, id)) ?? []; + : ((await getOwnerFromUsersArray(prisma, id)) ?? []); } if (!owner) { return null; diff --git a/packages/features/flags/features.repository.mock.ts b/packages/features/flags/features.repository.mock.ts index 4db074a4ab..418f8cfc87 100644 --- a/packages/features/flags/features.repository.mock.ts +++ b/packages/features/flags/features.repository.mock.ts @@ -6,13 +6,8 @@ export class MockFeaturesRepository implements IFeaturesRepository { return slug === "mock-feature"; } - async getUserFeaturesStatus( - _userId: number, - slugs: string[] - ): Promise> { - return Object.fromEntries( - slugs.map((slug) => [slug, slug === "mock-feature"]) - ); + async getUserFeaturesStatus(_userId: number, slugs: string[]): Promise> { + return Object.fromEntries(slugs.map((slug) => [slug, slug === "mock-feature"])); } async checkIfUserHasFeatureNonHierarchical(_userId: number, slug: string) { @@ -82,9 +77,7 @@ export class MockFeaturesRepository implements IFeaturesRepository { return false; } - async getTeamsAutoOptIn( - _teamIds: number[] - ): Promise> { + async getTeamsAutoOptIn(_teamIds: number[]): Promise> { // Mock implementation - return empty (all teams default to false) return {}; } diff --git a/packages/features/flags/features.repository.ts b/packages/features/flags/features.repository.ts index 651b979a24..9a8c8a3cf4 100644 --- a/packages/features/flags/features.repository.ts +++ b/packages/features/flags/features.repository.ts @@ -21,7 +21,7 @@ export class FeaturesRepository implements IFeaturesRepository { // eslint-disable-next-line @typescript-eslint/no-explicit-any private static featuresCache: { data: any[]; expiry: number } | null = null; - constructor(private prismaClient: PrismaClient) { } + constructor(private prismaClient: PrismaClient) {} private clearCache() { FeaturesRepository.featuresCache = null; diff --git a/packages/features/form-builder/utils/variantsConfig.ts b/packages/features/form-builder/utils/variantsConfig.ts index 0e0b9db233..26d623baec 100644 --- a/packages/features/form-builder/utils/variantsConfig.ts +++ b/packages/features/form-builder/utils/variantsConfig.ts @@ -16,28 +16,31 @@ function getTranslatedConfigVariants( ) { const fieldTypeVariantsConfig = fieldTypesConfigMap[fieldType]?.variantsConfig?.variants; - return Object.entries(configVariants).reduce((variantsConfigVariants, [variantName, variant]) => { - const variantFieldsMap = fieldTypeVariantsConfig?.[variantName as keyof typeof fieldTypeVariantsConfig] - ?.fieldsMap; + return Object.entries(configVariants).reduce( + (variantsConfigVariants, [variantName, variant]) => { + const variantFieldsMap = + fieldTypeVariantsConfig?.[variantName as keyof typeof fieldTypeVariantsConfig]?.fieldsMap; - const translatedFields = variant.fields.map((field) => { - const defaultLabel = - variantFieldsMap?.[field.name as keyof typeof variantFieldsMap]?.defaultLabel ?? ""; - const label = (field.label?.trim() ? field.label : defaultLabel) ?? ""; - const placeholder = field.placeholder ?? ""; - return { - ...field, - label: translate(label), - placeholder: translate(placeholder), + const translatedFields = variant.fields.map((field) => { + const defaultLabel = + variantFieldsMap?.[field.name as keyof typeof variantFieldsMap]?.defaultLabel ?? ""; + const label = (field.label?.trim() ? field.label : defaultLabel) ?? ""; + const placeholder = field.placeholder ?? ""; + return { + ...field, + label: translate(label), + placeholder: translate(placeholder), + }; + }); + variantsConfigVariants[variantName] = { + ...variant, + fields: translatedFields, }; - }); - variantsConfigVariants[variantName] = { - ...variant, - fields: translatedFields, - }; - return variantsConfigVariants; - }, {} as typeof configVariants); + return variantsConfigVariants; + }, + {} as typeof configVariants + ); } /** diff --git a/packages/features/form/components/Select.tsx b/packages/features/form/components/Select.tsx index 8702cc4a59..17c74f8c99 100644 --- a/packages/features/form/components/Select.tsx +++ b/packages/features/form/components/Select.tsx @@ -8,7 +8,7 @@ import classNames from "@calcom/ui/classNames"; export type SelectProps< Option, IsMulti extends boolean = false, - Group extends GroupBase
- } - > + }> {installCalendarAction && (
-
@@ -127,15 +114,11 @@ export function CalendarToggleContainerComponent({ return (
-

- {t("calendars_were_checking_for_conflicts")} -

+

{t("calendars_were_checking_for_conflicts")}

{hasConnectedCalendars && !isLoading ? ( <> {connectedCalendars.map((calendar) => { - const foundPrimary = calendar.calendars?.find( - (item) => item.primary - ); + const foundPrimary = calendar.calendars?.find((item) => item.primary); // Will be used when getAvailbility is modified to use externalId instead of appId for source. // const color = SELECTION_COLORS[idx] || "#000000"; // // Add calendar to color map using externalId (what we use on the backend to determine source) @@ -157,11 +140,7 @@ export function CalendarToggleContainerComponent({ ); })} {manageCalendarsAction && ( - )} diff --git a/packages/features/troubleshooter/components/EventTypeSelectComponent.tsx b/packages/features/troubleshooter/components/EventTypeSelectComponent.tsx index b4e0484256..aa0b9c41c8 100644 --- a/packages/features/troubleshooter/components/EventTypeSelectComponent.tsx +++ b/packages/features/troubleshooter/components/EventTypeSelectComponent.tsx @@ -27,14 +27,13 @@ export function EventTypeSelectComponent({ isPending, }: EventTypeSelectComponentProps): JSX.Element { const { t } = useLocale(); - const { event: selectedEventType, setEvent: setSelectedEventType } = - useTroubleshooterStore( - (state) => ({ - event: state.event, - setEvent: state.setEvent, - }), - shallow - ); + const { event: selectedEventType, setEvent: setSelectedEventType } = useTroubleshooterStore( + (state) => ({ + event: state.event, + setEvent: state.setEvent, + }), + shallow + ); const options = useMemo(() => { if (!eventTypes) return []; @@ -51,9 +50,7 @@ export function EventTypeSelectComponent({ if (!eventTypes || eventTypes.length === 0) return; const selectedEventIdParam = getQueryParam("eventTypeId"); - const eventTypeId = selectedEventIdParam - ? parseInt(selectedEventIdParam, 10) - : null; + const eventTypeId = selectedEventIdParam ? parseInt(selectedEventIdParam, 10) : null; // If we already have a selected event that matches the query param, don't do anything if (selectedEventType?.id === eventTypeId) return; @@ -93,10 +90,7 @@ export function EventTypeSelectComponent({ label={t("event_type")} options={options} isDisabled={isPending || options.length === 0} - value={ - options.find((option) => option.id === selectedEventType?.id) || - options[0] - } + value={options.find((option) => option.id === selectedEventType?.id) || options[0]} onChange={(option) => { if (!option) return; const foundEventType = eventTypes?.find((et) => et.id === option.id); diff --git a/packages/features/users/lib/getRoutedUsers.ts b/packages/features/users/lib/getRoutedUsers.ts index edf4e5a19a..db30dbe001 100644 --- a/packages/features/users/lib/getRoutedUsers.ts +++ b/packages/features/users/lib/getRoutedUsers.ts @@ -12,7 +12,7 @@ import type { CredentialPayload } from "@calcom/types/Credential"; const log = logger.getSubLogger({ prefix: ["[getRoutedUsers]"] }); export const getRoutedUsersWithContactOwnerAndFixedUsers = < - T extends { id: number; isFixed?: boolean; email: string } + T extends { id: number; isFixed?: boolean; email: string }, >({ routedTeamMemberIds, users, @@ -130,7 +130,7 @@ export function getNormalizedHosts + Host extends BaseHost, >({ eventType, }: { diff --git a/packages/features/users/repositories/UserRepository.test.ts b/packages/features/users/repositories/UserRepository.test.ts index 8b28134126..ac50054c35 100644 --- a/packages/features/users/repositories/UserRepository.test.ts +++ b/packages/features/users/repositories/UserRepository.test.ts @@ -1,4 +1,3 @@ - import prismock from "@calcom/testing/lib/__mocks__/prisma"; import { describe, test, vi, expect, beforeEach } from "vitest"; diff --git a/packages/features/watchlist/lib/errors/WatchlistErrors.ts b/packages/features/watchlist/lib/errors/WatchlistErrors.ts index 7c1220e484..1987c6d9b4 100644 --- a/packages/features/watchlist/lib/errors/WatchlistErrors.ts +++ b/packages/features/watchlist/lib/errors/WatchlistErrors.ts @@ -35,14 +35,11 @@ export const WatchlistErrors = { invalidIp: (message: string) => new WatchlistError(WatchlistErrorCode.INVALID_IP, message), - validationError: (message: string) => - new WatchlistError(WatchlistErrorCode.VALIDATION_ERROR, message), + validationError: (message: string) => new WatchlistError(WatchlistErrorCode.VALIDATION_ERROR, message), - permissionDenied: (message: string) => - new WatchlistError(WatchlistErrorCode.PERMISSION_DENIED, message), + permissionDenied: (message: string) => new WatchlistError(WatchlistErrorCode.PERMISSION_DENIED, message), - duplicateEntry: (message: string) => - new WatchlistError(WatchlistErrorCode.DUPLICATE_ENTRY, message), + duplicateEntry: (message: string) => new WatchlistError(WatchlistErrorCode.DUPLICATE_ENTRY, message), bulkDeletePartialFailure: (message: string) => new WatchlistError(WatchlistErrorCode.BULK_DELETE_PARTIAL_FAILURE, message), diff --git a/packages/features/watchlist/lib/repository/WatchlistRepository.ts b/packages/features/watchlist/lib/repository/WatchlistRepository.ts index 8d0aebe30e..caff46857e 100644 --- a/packages/features/watchlist/lib/repository/WatchlistRepository.ts +++ b/packages/features/watchlist/lib/repository/WatchlistRepository.ts @@ -212,10 +212,7 @@ export class WatchlistRepository implements IWatchlistRepository { }, }); - const allBookingReports = [ - ...(entry?.bookingReports ?? []), - ...(entry?.globalBookingReports ?? []), - ]; + const allBookingReports = [...(entry?.bookingReports ?? []), ...(entry?.globalBookingReports ?? [])]; return { entry: entry diff --git a/packages/features/watchlist/lib/service/OrganizationWatchlistOperationsService.ts b/packages/features/watchlist/lib/service/OrganizationWatchlistOperationsService.ts index 3a980705a0..f8765b9a3a 100644 --- a/packages/features/watchlist/lib/service/OrganizationWatchlistOperationsService.ts +++ b/packages/features/watchlist/lib/service/OrganizationWatchlistOperationsService.ts @@ -117,7 +117,10 @@ export class OrganizationWatchlistOperationsService extends WatchlistOperationsS }; } - async dismissReportByEmail(input: { email: string; userId: number }): Promise<{ success: boolean; count: number }> { + async dismissReportByEmail(input: { + email: string; + userId: number; + }): Promise<{ success: boolean; count: number }> { await this.checkPermission(input.userId, "watchlist.update"); const normalizedEmail = normalizeEmail(input.email); diff --git a/packages/features/watchlist/operations/check-user-blocking.test.ts b/packages/features/watchlist/operations/check-user-blocking.test.ts index 5f1aa4a04e..6c4293834c 100644 --- a/packages/features/watchlist/operations/check-user-blocking.test.ts +++ b/packages/features/watchlist/operations/check-user-blocking.test.ts @@ -8,7 +8,6 @@ import { type BlockingInfo, } from "./check-user-blocking"; - vi.mock("@calcom/features/di/watchlist/containers/watchlist", () => ({ getWatchlistFeature: vi.fn(), })); @@ -55,7 +54,9 @@ describe("check-user-blocking", () => { }); test("should skip users with empty emails", async () => { - mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["valid@example.com", { isBlocked: false }]])); + mockGlobalBlocking.areBlocked.mockResolvedValue( + new Map([["valid@example.com", { isBlocked: false }]]) + ); const users: BlockableUser[] = [ { email: "", locked: false }, @@ -290,9 +291,7 @@ describe("check-user-blocking", () => { }); test("should return false for non-blocked user", () => { - const blockingMap = new Map([ - ["clean@example.com", { isBlocked: false }], - ]); + const blockingMap = new Map([["clean@example.com", { isBlocked: false }]]); expect(isUserBlocked("clean@example.com", blockingMap)).toBe(false); }); @@ -343,9 +342,7 @@ describe("check-user-blocking", () => { test("should combine global and org blocking results", async () => { const emails = ["user@example.com"]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); mockOrgBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: true }]])); const result = await checkWatchlistBlocking(emails, 123); @@ -381,9 +378,7 @@ describe("check-user-blocking", () => { test("should normalize emails in result map", async () => { const emails = ["USER@EXAMPLE.COM"]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: true }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: true }]])); const result = await checkWatchlistBlocking(emails); @@ -391,4 +386,3 @@ describe("check-user-blocking", () => { }); }); }); - diff --git a/packages/features/watchlist/operations/filter-blocked-hosts.controller.test.ts b/packages/features/watchlist/operations/filter-blocked-hosts.controller.test.ts index 555bcf9f6e..3c45d96689 100644 --- a/packages/features/watchlist/operations/filter-blocked-hosts.controller.test.ts +++ b/packages/features/watchlist/operations/filter-blocked-hosts.controller.test.ts @@ -2,7 +2,6 @@ import { describe, test, expect, vi, beforeEach } from "vitest"; import { filterBlockedHosts, type HostWithEmail } from "./filter-blocked-hosts.controller"; - vi.mock("@calcom/features/di/watchlist/containers/watchlist", () => ({ getWatchlistFeature: vi.fn(), })); @@ -52,10 +51,7 @@ describe("filterBlockedHosts", () => { describe("Locked host filtering", () => { test("should filter out locked hosts", async () => { - const hosts = [ - createHost(1, "locked@example.com", true), - createHost(2, "unlocked@example.com", false), - ]; + const hosts = [createHost(1, "locked@example.com", true), createHost(2, "unlocked@example.com", false)]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["unlocked@example.com", { isBlocked: false }]]) @@ -69,10 +65,7 @@ describe("filterBlockedHosts", () => { }); test("should filter out all locked hosts", async () => { - const hosts = [ - createHost(1, "locked1@example.com", true), - createHost(2, "locked2@example.com", true), - ]; + const hosts = [createHost(1, "locked1@example.com", true), createHost(2, "locked2@example.com", true)]; const result = await filterBlockedHosts(hosts); @@ -84,10 +77,7 @@ describe("filterBlockedHosts", () => { describe("Watchlist blocking", () => { test("should filter out watchlist-blocked hosts", async () => { - const hosts = [ - createHost(1, "blocked@spam.com", false), - createHost(2, "clean@example.com", false), - ]; + const hosts = [createHost(1, "blocked@spam.com", false), createHost(2, "clean@example.com", false)]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([ @@ -106,9 +96,7 @@ describe("filterBlockedHosts", () => { test("should check org-level blocking when organizationId provided", async () => { const hosts = [createHost(1, "user@example.com", false)]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); mockOrgBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: true }]])); const result = await filterBlockedHosts(hosts, 123); @@ -157,9 +145,7 @@ describe("filterBlockedHosts", () => { }, ]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["host@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["host@example.com", { isBlocked: false }]])); const result = await filterBlockedHosts(hosts); @@ -176,9 +162,7 @@ describe("filterBlockedHosts", () => { test("should handle emails with different cases", async () => { const hosts = [createHost(1, "USER@EXAMPLE.COM", false)]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: true }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: true }]])); const result = await filterBlockedHosts(hosts); @@ -215,10 +199,7 @@ describe("filterBlockedHosts", () => { }); test("should not call watchlist for locked-only hosts", async () => { - const hosts = [ - createHost(1, "locked1@example.com", true), - createHost(2, "locked2@example.com", true), - ]; + const hosts = [createHost(1, "locked1@example.com", true), createHost(2, "locked2@example.com", true)]; await filterBlockedHosts(hosts); @@ -226,4 +207,3 @@ describe("filterBlockedHosts", () => { }); }); }); - diff --git a/packages/features/watchlist/operations/filter-blocked-users.controller.test.ts b/packages/features/watchlist/operations/filter-blocked-users.controller.test.ts index 987f3fdb14..17dc19c176 100644 --- a/packages/features/watchlist/operations/filter-blocked-users.controller.test.ts +++ b/packages/features/watchlist/operations/filter-blocked-users.controller.test.ts @@ -4,7 +4,6 @@ import type { SpanFn } from "@calcom/features/watchlist/lib/telemetry"; import { filterBlockedUsers, type UserWithEmail } from "./filter-blocked-users.controller"; - vi.mock("@calcom/features/di/watchlist/containers/watchlist", () => ({ getWatchlistFeature: vi.fn(), })); @@ -57,10 +56,7 @@ describe("filterBlockedUsers", () => { describe("Locked user filtering", () => { test("should filter out locked users", async () => { - const users = [ - createUser("locked@example.com", true, 1), - createUser("unlocked@example.com", false, 2), - ]; + const users = [createUser("locked@example.com", true, 1), createUser("unlocked@example.com", false, 2)]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([["unlocked@example.com", { isBlocked: false }]]) @@ -74,10 +70,7 @@ describe("filterBlockedUsers", () => { }); test("should filter out all locked users", async () => { - const users = [ - createUser("locked1@example.com", true, 1), - createUser("locked2@example.com", true, 2), - ]; + const users = [createUser("locked1@example.com", true, 1), createUser("locked2@example.com", true, 2)]; const result = await filterBlockedUsers(users); @@ -109,10 +102,7 @@ describe("filterBlockedUsers", () => { describe("Watchlist blocking", () => { test("should filter out watchlist-blocked users", async () => { - const users = [ - createUser("blocked@spam.com", false, 1), - createUser("clean@example.com", false, 2), - ]; + const users = [createUser("blocked@spam.com", false, 1), createUser("clean@example.com", false, 2)]; mockGlobalBlocking.areBlocked.mockResolvedValue( new Map([ @@ -131,9 +121,7 @@ describe("filterBlockedUsers", () => { test("should check org-level blocking when organizationId provided", async () => { const users = [createUser("user@example.com", false, 1)]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); mockOrgBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: true }]])); const result = await filterBlockedUsers(users, 123); @@ -146,9 +134,7 @@ describe("filterBlockedUsers", () => { test("should not check org-level blocking when organizationId is null", async () => { const users = [createUser("user@example.com", false, 1)]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); await filterBlockedUsers(users, null); @@ -231,9 +217,7 @@ describe("filterBlockedUsers", () => { }, ]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); const result = await filterBlockedUsers(users); @@ -258,9 +242,7 @@ describe("filterBlockedUsers", () => { }, ]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); const result = await filterBlockedUsers(users); @@ -273,9 +255,7 @@ describe("filterBlockedUsers", () => { test("should handle emails with different cases", async () => { const users = [createUser("USER@EXAMPLE.COM", false, 1)]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: true }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: true }]])); const result = await filterBlockedUsers(users); @@ -286,9 +266,7 @@ describe("filterBlockedUsers", () => { test("should handle emails with whitespace", async () => { const users = [createUser(" user@example.com ", false, 1)]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); const result = await filterBlockedUsers(users); @@ -324,10 +302,7 @@ describe("filterBlockedUsers", () => { }); test("should not call watchlist for locked-only users", async () => { - const users = [ - createUser("locked1@example.com", true, 1), - createUser("locked2@example.com", true, 2), - ]; + const users = [createUser("locked1@example.com", true, 1), createUser("locked2@example.com", true, 2)]; await filterBlockedUsers(users); @@ -339,9 +314,7 @@ describe("filterBlockedUsers", () => { test("should call span when provided", async () => { const users = [createUser("user@example.com", false, 1)]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); const mockSpan: SpanFn = vi.fn((_options, callback) => { return Promise.resolve(callback()); @@ -356,9 +329,7 @@ describe("filterBlockedUsers", () => { test("should work without span", async () => { const users = [createUser("user@example.com", false, 1)]; - mockGlobalBlocking.areBlocked.mockResolvedValue( - new Map([["user@example.com", { isBlocked: false }]]) - ); + mockGlobalBlocking.areBlocked.mockResolvedValue(new Map([["user@example.com", { isBlocked: false }]])); // Should not throw when span is not provided const result = await filterBlockedUsers(users); @@ -367,4 +338,3 @@ describe("filterBlockedUsers", () => { }); }); }); - diff --git a/packages/features/webhooks/lib/WebhookService.ts b/packages/features/webhooks/lib/WebhookService.ts index 24d98fa84f..6779d08531 100644 --- a/packages/features/webhooks/lib/WebhookService.ts +++ b/packages/features/webhooks/lib/WebhookService.ts @@ -10,7 +10,10 @@ const log = logger.getSubLogger({ prefix: ["[WebhookService] "] }); /** This is a WIP. With minimal methods until the API matures and stabilizes */ export class WebhookService { - private constructor(private options: GetSubscriberOptions, private webhooks: WebhookSubscriber[]) {} + private constructor( + private options: GetSubscriberOptions, + private webhooks: WebhookSubscriber[] + ) {} static async init(options: GetSubscriberOptions) { const webhooks = await getWebhooks(options); return new WebhookService(options, webhooks); diff --git a/packages/features/webhooks/lib/factory/base/BaseDelegationPayloadBuilder.ts b/packages/features/webhooks/lib/factory/base/BaseDelegationPayloadBuilder.ts index 8476b0a22d..fdc77c9cec 100644 --- a/packages/features/webhooks/lib/factory/base/BaseDelegationPayloadBuilder.ts +++ b/packages/features/webhooks/lib/factory/base/BaseDelegationPayloadBuilder.ts @@ -18,4 +18,3 @@ export abstract class BaseDelegationPayloadBuilder implements IDelegationPayload */ abstract build(dto: DelegationCredentialErrorDTO): WebhookPayload; } - diff --git a/packages/features/webhooks/lib/factory/base/BaseFormPayloadBuilder.test.ts b/packages/features/webhooks/lib/factory/base/BaseFormPayloadBuilder.test.ts index 62fe918255..4cfc767d05 100644 --- a/packages/features/webhooks/lib/factory/base/BaseFormPayloadBuilder.test.ts +++ b/packages/features/webhooks/lib/factory/base/BaseFormPayloadBuilder.test.ts @@ -8,7 +8,9 @@ describe("FormPayloadBuilder (v2021-10-20)", () => { const builder = new FormPayloadBuilder(); const createMockDTO = ( - triggerEvent: typeof WebhookTriggerEvents.FORM_SUBMITTED | typeof WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT + triggerEvent: + | typeof WebhookTriggerEvents.FORM_SUBMITTED + | typeof WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT ): FormSubmittedDTO | FormSubmittedNoEventDTO => ({ triggerEvent, diff --git a/packages/features/webhooks/lib/factory/versioned/PayloadBuilderFactory.test.ts b/packages/features/webhooks/lib/factory/versioned/PayloadBuilderFactory.test.ts index 40d89334fe..903363a580 100644 --- a/packages/features/webhooks/lib/factory/versioned/PayloadBuilderFactory.test.ts +++ b/packages/features/webhooks/lib/factory/versioned/PayloadBuilderFactory.test.ts @@ -127,14 +127,20 @@ describe("PayloadBuilderFactory", () => { }); it("should route instant meeting events to instant meeting builder", () => { - const builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.INSTANT_MEETING); + const builder = factory.getBuilder( + WebhookVersionEnum.V_2021_10_20, + WebhookTriggerEvents.INSTANT_MEETING + ); expect(builder).toBe(defaultBuilders.instantMeeting); }); }); describe("Fallback Behavior", () => { it("should fallback to default version when requested version not found", () => { - const builder = factory.getBuilder(WebhookVersionEnum.V_2099_99_99, WebhookTriggerEvents.BOOKING_CREATED); + const builder = factory.getBuilder( + WebhookVersionEnum.V_2099_99_99, + WebhookTriggerEvents.BOOKING_CREATED + ); // Should get default builder, not throw expect(builder).toBe(defaultBuilders.booking); @@ -159,12 +165,30 @@ describe("PayloadBuilderFactory", () => { describe("Type Safety", () => { it("should return correctly typed builder for each trigger", () => { // These type assertions verify compile-time type safety - const bookingBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.BOOKING_CREATED); - const formBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.FORM_SUBMITTED); - const oooBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.OOO_CREATED); - const recordingBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.RECORDING_READY); - const meetingBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.MEETING_STARTED); - const instantBuilder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.INSTANT_MEETING); + const bookingBuilder = factory.getBuilder( + WebhookVersionEnum.V_2021_10_20, + WebhookTriggerEvents.BOOKING_CREATED + ); + const formBuilder = factory.getBuilder( + WebhookVersionEnum.V_2021_10_20, + WebhookTriggerEvents.FORM_SUBMITTED + ); + const oooBuilder = factory.getBuilder( + WebhookVersionEnum.V_2021_10_20, + WebhookTriggerEvents.OOO_CREATED + ); + const recordingBuilder = factory.getBuilder( + WebhookVersionEnum.V_2021_10_20, + WebhookTriggerEvents.RECORDING_READY + ); + const meetingBuilder = factory.getBuilder( + WebhookVersionEnum.V_2021_10_20, + WebhookTriggerEvents.MEETING_STARTED + ); + const instantBuilder = factory.getBuilder( + WebhookVersionEnum.V_2021_10_20, + WebhookTriggerEvents.INSTANT_MEETING + ); // Runtime verification expect(bookingBuilder).toBeDefined(); @@ -221,7 +245,10 @@ describe("PayloadBuilderFactory", () => { }, }; - const builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.BOOKING_CREATED); + const builder = factory.getBuilder( + WebhookVersionEnum.V_2021_10_20, + WebhookTriggerEvents.BOOKING_CREATED + ); const payload = builder.build(mockDTO); expect(payload).toBeDefined(); @@ -244,8 +271,14 @@ describe("PayloadBuilderFactory", () => { factory.registerVersion(WebhookVersionEnum.V_2024_12_01, v2Builders); // Both versions should work - const v1Builder = factory.getBuilder(WebhookVersionEnum.V_2021_10_20, WebhookTriggerEvents.BOOKING_CREATED); - const v2Builder = factory.getBuilder(WebhookVersionEnum.V_2024_12_01, WebhookTriggerEvents.BOOKING_CREATED); + const v1Builder = factory.getBuilder( + WebhookVersionEnum.V_2021_10_20, + WebhookTriggerEvents.BOOKING_CREATED + ); + const v2Builder = factory.getBuilder( + WebhookVersionEnum.V_2024_12_01, + WebhookTriggerEvents.BOOKING_CREATED + ); expect(v1Builder).toBe(defaultBuilders.booking); expect(v2Builder).toBe(v2Builders.booking); @@ -253,4 +286,3 @@ describe("PayloadBuilderFactory", () => { }); }); }); - diff --git a/packages/features/webhooks/lib/factory/versioned/registry.test.ts b/packages/features/webhooks/lib/factory/versioned/registry.test.ts index 4e4316b547..7a807595cc 100644 --- a/packages/features/webhooks/lib/factory/versioned/registry.test.ts +++ b/packages/features/webhooks/lib/factory/versioned/registry.test.ts @@ -18,12 +18,24 @@ describe("Payload Builder Registry", () => { const factory = createPayloadBuilderFactory(); // Test each builder category by trigger event - const bookingBuilder = factory.getBuilder(DEFAULT_WEBHOOK_VERSION, WebhookTriggerEvents.BOOKING_CREATED); + const bookingBuilder = factory.getBuilder( + DEFAULT_WEBHOOK_VERSION, + WebhookTriggerEvents.BOOKING_CREATED + ); const formBuilder = factory.getBuilder(DEFAULT_WEBHOOK_VERSION, WebhookTriggerEvents.FORM_SUBMITTED); const oooBuilder = factory.getBuilder(DEFAULT_WEBHOOK_VERSION, WebhookTriggerEvents.OOO_CREATED); - const recordingBuilder = factory.getBuilder(DEFAULT_WEBHOOK_VERSION, WebhookTriggerEvents.RECORDING_READY); - const meetingBuilder = factory.getBuilder(DEFAULT_WEBHOOK_VERSION, WebhookTriggerEvents.MEETING_STARTED); - const instantBuilder = factory.getBuilder(DEFAULT_WEBHOOK_VERSION, WebhookTriggerEvents.INSTANT_MEETING); + const recordingBuilder = factory.getBuilder( + DEFAULT_WEBHOOK_VERSION, + WebhookTriggerEvents.RECORDING_READY + ); + const meetingBuilder = factory.getBuilder( + DEFAULT_WEBHOOK_VERSION, + WebhookTriggerEvents.MEETING_STARTED + ); + const instantBuilder = factory.getBuilder( + DEFAULT_WEBHOOK_VERSION, + WebhookTriggerEvents.INSTANT_MEETING + ); expect(bookingBuilder).toBeDefined(); expect(formBuilder).toBeDefined(); @@ -52,7 +64,10 @@ describe("Payload Builder Registry", () => { const factory = createPayloadBuilderFactory(); // Request non-existent version (cast to WebhookVersion for testing fallback) - const builder = factory.getBuilder("9999-99-99" as WebhookVersion, WebhookTriggerEvents.BOOKING_CREATED); + const builder = factory.getBuilder( + "9999-99-99" as WebhookVersion, + WebhookTriggerEvents.BOOKING_CREATED + ); // Should get default builder, not throw expect(builder).toBeDefined(); @@ -144,4 +159,3 @@ describe("Payload Builder Registry", () => { }); }); }); - diff --git a/packages/features/webhooks/lib/factory/versioned/v2021-10-20/BookingPayloadBuilder.test.ts b/packages/features/webhooks/lib/factory/versioned/v2021-10-20/BookingPayloadBuilder.test.ts index 7816527dad..8a366dac14 100644 --- a/packages/features/webhooks/lib/factory/versioned/v2021-10-20/BookingPayloadBuilder.test.ts +++ b/packages/features/webhooks/lib/factory/versioned/v2021-10-20/BookingPayloadBuilder.test.ts @@ -150,4 +150,3 @@ describe("v2021-10-20/BookingPayloadBuilder", () => { expect(() => builder.build(minimalDTO)).not.toThrow(); }); }); - diff --git a/packages/features/webhooks/lib/factory/versioned/v2021-10-20/DelegationPayloadBuilder.ts b/packages/features/webhooks/lib/factory/versioned/v2021-10-20/DelegationPayloadBuilder.ts index 5103c74999..ad388176e9 100644 --- a/packages/features/webhooks/lib/factory/versioned/v2021-10-20/DelegationPayloadBuilder.ts +++ b/packages/features/webhooks/lib/factory/versioned/v2021-10-20/DelegationPayloadBuilder.ts @@ -28,4 +28,3 @@ export class DelegationPayloadBuilder extends BaseDelegationPayloadBuilder { }; } } - diff --git a/packages/features/webhooks/lib/getWebhooks.ts b/packages/features/webhooks/lib/getWebhooks.ts index c46efe2f0c..b132388e83 100644 --- a/packages/features/webhooks/lib/getWebhooks.ts +++ b/packages/features/webhooks/lib/getWebhooks.ts @@ -15,7 +15,6 @@ export type GetSubscriberOptions = { oAuthClientId?: string | null; }; - const getWebhooks = async ( options: GetSubscriberOptions, prisma: PrismaClient = defaultPrisma diff --git a/packages/features/webhooks/lib/handleWebhookScheduledTriggers.test.ts b/packages/features/webhooks/lib/handleWebhookScheduledTriggers.test.ts index 4e4f8ef333..6a5fcc289a 100644 --- a/packages/features/webhooks/lib/handleWebhookScheduledTriggers.test.ts +++ b/packages/features/webhooks/lib/handleWebhookScheduledTriggers.test.ts @@ -81,7 +81,7 @@ describe("handleWebhookScheduledTriggers - X-Cal-Webhook-Version header", () => }; await handleWebhookScheduledTriggers(mockPrisma as unknown as PrismaClient); - + expect(mockFetch).toHaveBeenCalledTimes(1); const [, options] = mockFetch.mock.calls[0]; diff --git a/packages/features/webhooks/lib/handleWebhookScheduledTriggers.ts b/packages/features/webhooks/lib/handleWebhookScheduledTriggers.ts index b32fc28ab4..c900e9df17 100644 --- a/packages/features/webhooks/lib/handleWebhookScheduledTriggers.ts +++ b/packages/features/webhooks/lib/handleWebhookScheduledTriggers.ts @@ -69,7 +69,7 @@ export async function handleWebhookScheduledTriggers(prisma: PrismaClient) { body: job.payload, headers, // Avoid following redirect - redirect: "manual" + redirect: "manual", }).catch((error) => { console.error(`Webhook trigger for subscriber url ${job.subscriberUrl} failed with error: ${error}`); }) diff --git a/packages/features/webhooks/lib/infrastructure/mappers/WebhookOutputMapper.ts b/packages/features/webhooks/lib/infrastructure/mappers/WebhookOutputMapper.ts index c36f5288cb..697a339c8b 100644 --- a/packages/features/webhooks/lib/infrastructure/mappers/WebhookOutputMapper.ts +++ b/packages/features/webhooks/lib/infrastructure/mappers/WebhookOutputMapper.ts @@ -131,4 +131,3 @@ export class WebhookOutputMapper { }; } } - diff --git a/packages/features/webhooks/lib/service/WebhookNotificationHandler.test.ts b/packages/features/webhooks/lib/service/WebhookNotificationHandler.test.ts index 6d0342b885..87a51dd8f4 100644 --- a/packages/features/webhooks/lib/service/WebhookNotificationHandler.test.ts +++ b/packages/features/webhooks/lib/service/WebhookNotificationHandler.test.ts @@ -154,7 +154,10 @@ describe("WebhookNotificationHandler", () => { it("should use factory to build payload", async () => { await handler.handleNotification(mockDTO); - expect(mockFactory.getBuilder).toHaveBeenCalledWith(WebhookVersion.V_2021_10_20, WebhookTriggerEvents.BOOKING_CREATED); + expect(mockFactory.getBuilder).toHaveBeenCalledWith( + WebhookVersion.V_2021_10_20, + WebhookTriggerEvents.BOOKING_CREATED + ); }); it("should process webhooks with built payload", async () => { @@ -302,8 +305,10 @@ describe("WebhookNotificationHandler", () => { await handler.handleNotification(dto); - expect(mockFactory.getBuilder).toHaveBeenCalledWith(WebhookVersion.V_2021_10_20, WebhookTriggerEvents.BOOKING_CREATED); + expect(mockFactory.getBuilder).toHaveBeenCalledWith( + WebhookVersion.V_2021_10_20, + WebhookTriggerEvents.BOOKING_CREATED + ); }); }); }); - diff --git a/packages/features/webhooks/lib/service/WebhookNotificationHandler.ts b/packages/features/webhooks/lib/service/WebhookNotificationHandler.ts index d379ffbb13..95ad063f79 100644 --- a/packages/features/webhooks/lib/service/WebhookNotificationHandler.ts +++ b/packages/features/webhooks/lib/service/WebhookNotificationHandler.ts @@ -77,7 +77,10 @@ export class WebhookNotificationHandler implements IWebhookNotificationHandler { * 2. Build version-specific payloads per subscriber * 3. Group subscribers by version for efficiency */ - private createPayload(dto: WebhookEventDTO, version: WebhookVersion = DEFAULT_WEBHOOK_VERSION): WebhookPayload { + private createPayload( + dto: WebhookEventDTO, + version: WebhookVersion = DEFAULT_WEBHOOK_VERSION + ): WebhookPayload { // Get version-specific builder from factory - handles all event types const builder = this.payloadBuilderFactory.getBuilder(version, dto.triggerEvent); return builder.build(dto); diff --git a/packages/features/webhooks/lib/service/WebhookNotifier.ts b/packages/features/webhooks/lib/service/WebhookNotifier.ts index 4cc924f153..5e5a4b1640 100644 --- a/packages/features/webhooks/lib/service/WebhookNotifier.ts +++ b/packages/features/webhooks/lib/service/WebhookNotifier.ts @@ -5,7 +5,10 @@ import type { IWebhookNotifier, IWebhookNotificationHandler } from "../interface export class WebhookNotifier implements IWebhookNotifier { private readonly log: ILogger; - constructor(private readonly handler: IWebhookNotificationHandler, logger: ILogger) { + constructor( + private readonly handler: IWebhookNotificationHandler, + logger: ILogger + ) { this.log = logger.getSubLogger({ prefix: ["[WebhookNotifier]"] }); } diff --git a/packages/features/webhooks/lib/service/WebhookService.test.ts b/packages/features/webhooks/lib/service/WebhookService.test.ts index 87db5f2bfc..1e96e91da4 100644 --- a/packages/features/webhooks/lib/service/WebhookService.test.ts +++ b/packages/features/webhooks/lib/service/WebhookService.test.ts @@ -72,7 +72,11 @@ describe("WebhookService", () => { describe("X-Cal-Webhook-Version header", () => { it("should include X-Cal-Webhook-Version header when sending webhook directly", async () => { - const service = new WebhookService(mockRepository as unknown as IWebhookRepository, mockTasker as unknown as ITasker, mockLogger as unknown as ILogger); + const service = new WebhookService( + mockRepository as unknown as IWebhookRepository, + mockTasker as unknown as ITasker, + mockLogger as unknown as ILogger + ); const subscriber: WebhookSubscriber = { id: "webhook-1", @@ -101,7 +105,11 @@ describe("WebhookService", () => { }); it("should include correct version for each subscriber", async () => { - const service = new WebhookService(mockRepository as unknown as IWebhookRepository, mockTasker as unknown as ITasker, mockLogger as unknown as ILogger); + const service = new WebhookService( + mockRepository as unknown as IWebhookRepository, + mockTasker as unknown as ITasker, + mockLogger as unknown as ILogger + ); const subscriber1: WebhookSubscriber = { id: "webhook-1", @@ -128,7 +136,10 @@ describe("WebhookService", () => { payload: { test: "data", triggerEvent: WebhookTriggerEvents.BOOKING_CREATED }, } as unknown as WebhookPayload; - await service.processWebhooks(WebhookTriggerEvents.BOOKING_CREATED, payload, [subscriber1, subscriber2]); + await service.processWebhooks(WebhookTriggerEvents.BOOKING_CREATED, payload, [ + subscriber1, + subscriber2, + ]); expect(mockFetch).toHaveBeenCalledTimes(2); @@ -142,7 +153,11 @@ describe("WebhookService", () => { it("should schedule webhook with version when TASKER_ENABLE_WEBHOOKS is enabled", async () => { process.env.TASKER_ENABLE_WEBHOOKS = "1"; - const service = new WebhookService(mockRepository as unknown as IWebhookRepository, mockTasker as unknown as ITasker, mockLogger as unknown as ILogger); + const service = new WebhookService( + mockRepository as unknown as IWebhookRepository, + mockTasker as unknown as ITasker, + mockLogger as unknown as ILogger + ); const subscriber: WebhookSubscriber = { id: "webhook-1", diff --git a/packages/features/webhooks/lib/tasker/trigger/deliver-webhook.ts b/packages/features/webhooks/lib/tasker/trigger/deliver-webhook.ts index 94909275de..834af8cfa0 100644 --- a/packages/features/webhooks/lib/tasker/trigger/deliver-webhook.ts +++ b/packages/features/webhooks/lib/tasker/trigger/deliver-webhook.ts @@ -20,29 +20,33 @@ const WEBHOOK_DELIVERY_JOB_ID = "webhook.deliver" as const; * * Errors are logged and re-thrown to enable trigger.dev's retry mechanism. */ -export const deliverWebhook: TaskWithSchema = - schemaTask({ - id: WEBHOOK_DELIVERY_JOB_ID, - ...webhookDeliveryTaskConfig, - schema: webhookDeliveryTaskSchema, - run: async (payload: WebhookTaskPayload, { ctx }) => { - const { getWebhookTaskConsumer } = await import( - "@calcom/features/di/webhooks/containers/webhook" - ); +export const deliverWebhook: TaskWithSchema< + typeof WEBHOOK_DELIVERY_JOB_ID, + typeof webhookDeliveryTaskSchema +> = schemaTask({ + id: WEBHOOK_DELIVERY_JOB_ID, + ...webhookDeliveryTaskConfig, + schema: webhookDeliveryTaskSchema, + run: async (payload: WebhookTaskPayload, { ctx }) => { + const { getWebhookTaskConsumer } = await import("@calcom/features/di/webhooks/containers/webhook"); - const webhookTaskConsumer = getWebhookTaskConsumer(); - const taskId = ctx.run.id; + const webhookTaskConsumer = getWebhookTaskConsumer(); + const taskId = ctx.run.id; - try { - await webhookTaskConsumer.processWebhookTask(payload, taskId); - logger.info("Webhook delivered successfully", { operationId: payload.operationId, taskId }); - } catch (error) { - if (error instanceof Error || error instanceof ErrorWithCode) { - logger.error(error.message, { operationId: payload.operationId, taskId }); - } else { - logger.error("Unknown error in webhook delivery", { error, operationId: payload.operationId, taskId }); - } - throw error; + try { + await webhookTaskConsumer.processWebhookTask(payload, taskId); + logger.info("Webhook delivered successfully", { operationId: payload.operationId, taskId }); + } catch (error) { + if (error instanceof Error || error instanceof ErrorWithCode) { + logger.error(error.message, { operationId: payload.operationId, taskId }); + } else { + logger.error("Unknown error in webhook delivery", { + error, + operationId: payload.operationId, + taskId, + }); } - }, - }); + throw error; + } + }, +});