import { CALENDLY_AVAILABLE_TIME_SCHEMA, CALENDLY_DATE_TIME_SCHEMA, CALENDLY_EVENT_TYPE_HOST_SCHEMA, CALENDLY_EVENT_TYPE_SCHEMA, CALENDLY_URI_SCHEMA, CALENDLY_UUID_SCHEMA, } from "@automate.ax/integration-contracts/calendly" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { CALENDLY_ACCOUNT, CALENDLY_PAGE_INFO_SCHEMA, CALENDLY_PAGE_INPUT_SCHEMA, calendlyAccountRequirement, collectionResponse, compactCalendlyInput, compactCalendlyQuery, getCalendlyApi, resourceResponse, toPage, validateCalendlyDateRange, } from "./lib" const EVENT_TYPES_ACCOUNT = calendlyAccountRequirement("event_types:read") const EVENT_TYPES_WRITE_ACCOUNT = calendlyAccountRequirement("event_types:write") const AVAILABILITY_ACCOUNT = calendlyAccountRequirement("availability:read") const EVENT_TYPE_LOCALE_SCHEMA = z.enum([ "de", "en", "es", "fr", "it", "nl", "pt", "uk", ]) const EVENT_TYPE_LOCATION_SCHEMA = z.object({ additionalInfo: z.string().optional(), kind: z.enum([ "ask_invitee", "custom", "google_conference", "gotomeeting_conference", "inbound_call", "microsoft_teams_conference", "outbound_call", "physical", "webex_conference", "zoom_conference", ]), location: z.string().optional(), phoneNumber: z.string().optional(), }) const EVENT_TYPE_FIELDS = { active: z.boolean().optional(), color: z .string() .regex(/^#[a-f\d]{6}$/i) .optional(), description: z.string().optional(), duration: z.number().int().min(1).max(720).optional(), durationOptions: z.number().int().min(1).max(720).array().max(4).optional(), locale: EVENT_TYPE_LOCALE_SCHEMA.optional(), locations: EVENT_TYPE_LOCATION_SCHEMA.array().optional(), name: z.string().trim().min(1).optional(), } const EVENT_TYPE_INPUT_SCHEMA = z .object(EVENT_TYPE_FIELDS) .superRefine(validateEventTypeDurations) const ONE_OFF_LOCATION_SCHEMA = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("custom"), location: z.string().optional() }), z.object({ kind: z.literal("google_conference") }), z.object({ kind: z.literal("gotomeeting_conference") }), z.object({ additionalInfo: z.string().optional(), kind: z.literal("physical"), location: z.string().optional(), }), z.object({ additionalInfo: z.string().optional(), kind: z.literal("inbound_call"), phoneNumber: z.string().min(1), }), z.object({ kind: z.literal("ask_invitee"), location: z.string().optional() }), z.object({ kind: z.literal("microsoft_teams_conference") }), z.object({ kind: z.literal("outbound_call") }), z.object({ kind: z.literal("webex_conference") }), z.object({ kind: z.literal("zoom_conference") }), ]) const ONE_OFF_DATE_SETTING_SCHEMA = z.discriminatedUnion("type", [ z .object({ endDate: z.iso.date(), startDate: z.iso.date(), type: z.literal("date_range"), }) .superRefine(({ endDate, startDate }, context) => { if (endDate <= startDate) { context.addIssue({ code: "custom", message: "endDate must be after startDate.", path: ["endDate"], }) } if (endDate <= new Date().toISOString().slice(0, 10)) { context.addIssue({ code: "custom", message: "endDate must be in the future.", path: ["endDate"], }) } if ( Date.parse(`${endDate}T00:00:00Z`) - Date.parse(`${startDate}T00:00:00Z`) >= 365 * 24 * 60 * 60 * 1_000 ) { context.addIssue({ code: "custom", message: "endDate must be less than 365 days after startDate.", path: ["endDate"], }) } }), z.object({ days: z.number().int().min(0).max(365), onlyWeekdays: z.boolean(), type: z.literal("days_in_future"), }), z.object({ spots: z .object({ endTime: CALENDLY_DATE_TIME_SCHEMA, startTime: CALENDLY_DATE_TIME_SCHEMA, }) .array() .min(1), type: z.literal("spots"), }), ]) export const createCalendlyEventType = defineAction( "Create Calendly event type", ) .describe("Creates a one-on-one Calendly event type for one owner.") .account(CALENDLY_ACCOUNT, EVENT_TYPES_WRITE_ACCOUNT) .input( EVENT_TYPE_INPUT_SCHEMA.safeExtend({ name: z.string().trim().min(1), owner: CALENDLY_URI_SCHEMA, }), ) .output(CALENDLY_EVENT_TYPE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => getCalendlyApi(account) .request("event_types", { body: compactCalendlyInput(input), method: "POST", responseSchema: resourceResponse(CALENDLY_EVENT_TYPE_SCHEMA), }) .then(({ resource }) => resource), ) export const createCalendlyOneOffEventType = defineAction( "Create Calendly one-off event type", ) .describe("Creates a temporary Calendly event type with a bounded schedule.") .account(CALENDLY_ACCOUNT, EVENT_TYPES_WRITE_ACCOUNT) .input( z.object({ coHosts: CALENDLY_URI_SCHEMA.array().optional(), dateSetting: ONE_OFF_DATE_SETTING_SCHEMA, duration: z.number().int().min(1).max(720), host: CALENDLY_URI_SCHEMA, location: ONE_OFF_LOCATION_SCHEMA.optional(), name: z.string().trim().min(1).max(55), timezone: z.string().trim().min(1).optional(), }), ) .output(CALENDLY_EVENT_TYPE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => getCalendlyApi(account) .request("one_off_event_types", { body: compactCalendlyInput(input), method: "POST", responseSchema: resourceResponse(CALENDLY_EVENT_TYPE_SCHEMA), }) .then(({ resource }) => resource), ) export const updateCalendlyEventType = defineAction( "Update Calendly event type", ) .describe("Updates a one-on-one Calendly event type.") .account(CALENDLY_ACCOUNT, EVENT_TYPES_WRITE_ACCOUNT) .input( EVENT_TYPE_INPUT_SCHEMA.safeExtend({ eventTypeId: CALENDLY_UUID_SCHEMA }), ) .output(CALENDLY_EVENT_TYPE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(({ account, input: { eventTypeId, ...body } }) => getCalendlyApi(account) .request(`event_types/${encodeURIComponent(eventTypeId)}`, { body: compactCalendlyInput(body), method: "PATCH", responseSchema: resourceResponse(CALENDLY_EVENT_TYPE_SCHEMA), }) .then(({ resource }) => resource), ) export const listCalendlyEventTypes = defineAction("List Calendly event types") .describe("Lists event types for an explicit Calendly organization or user.") .account(CALENDLY_ACCOUNT, EVENT_TYPES_ACCOUNT) .input( CALENDLY_PAGE_INPUT_SCHEMA.extend({ active: z.boolean().optional(), adminManaged: z.boolean().optional(), organization: CALENDLY_URI_SCHEMA.optional(), sort: z .enum([ "name:asc", "name:desc", "position:asc", "position:desc", "created_at:asc", "created_at:desc", "updated_at:asc", "updated_at:desc", ]) .prefault("name:asc"), user: CALENDLY_URI_SCHEMA.optional(), userAvailabilitySchedule: CALENDLY_URI_SCHEMA.optional(), }).refine(({ organization, user }) => Boolean(organization || user), { message: "Provide organization or user.", path: ["organization"], }), ) .output( z.object({ items: CALENDLY_EVENT_TYPE_SCHEMA.array(), pageInfo: CALENDLY_PAGE_INFO_SCHEMA, }), ) .retry({ replaySafety: "safe" }) .handler(({ account, input }) => getCalendlyApi(account) .request("event_types", { query: compactCalendlyQuery(input), responseSchema: collectionResponse(CALENDLY_EVENT_TYPE_SCHEMA), }) .then(toPage), ) export const getCalendlyEventType = defineAction("Get Calendly event type") .describe("Retrieves one Calendly event type by UUID.") .account(CALENDLY_ACCOUNT, EVENT_TYPES_ACCOUNT) .input(z.object({ eventTypeId: CALENDLY_UUID_SCHEMA })) .output(CALENDLY_EVENT_TYPE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(({ account, input }) => getCalendlyApi(account) .request(`event_types/${encodeURIComponent(input.eventTypeId)}`, { responseSchema: resourceResponse(CALENDLY_EVENT_TYPE_SCHEMA), }) .then(({ resource }) => resource), ) export const listCalendlyEventTypeHosts = defineAction( "List Calendly event type hosts", ) .describe("Lists hosts assigned to one Calendly event type.") .account(CALENDLY_ACCOUNT, EVENT_TYPES_ACCOUNT) .input(CALENDLY_PAGE_INPUT_SCHEMA.extend({ eventType: CALENDLY_URI_SCHEMA })) .output( z.object({ items: CALENDLY_EVENT_TYPE_HOST_SCHEMA.array(), pageInfo: CALENDLY_PAGE_INFO_SCHEMA, }), ) .retry({ replaySafety: "safe" }) .handler(({ account, input }) => getCalendlyApi(account) .request("event_type_memberships", { query: compactCalendlyQuery(input), responseSchema: collectionResponse(CALENDLY_EVENT_TYPE_HOST_SCHEMA), }) .then(toPage), ) export const listCalendlyEventTypeAvailableTimes = defineAction( "List Calendly event type available times", ) .describe( "Lists bookable times for an event type across at most 31 future days.", ) .account(CALENDLY_ACCOUNT, AVAILABILITY_ACCOUNT) .input( z .object({ endTime: CALENDLY_DATE_TIME_SCHEMA, eventType: CALENDLY_URI_SCHEMA, startTime: CALENDLY_DATE_TIME_SCHEMA, }) .superRefine((value, context) => validateCalendlyDateRange(value, context, 31), ), ) .output(z.object({ items: CALENDLY_AVAILABLE_TIME_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ({ items: ( await getCalendlyApi(account).request("event_type_available_times", { query: input, responseSchema: z.object({ collection: CALENDLY_AVAILABLE_TIME_SCHEMA.array(), }), }) ).collection, })) /** * Enforces Calendly's coupled duration-option invariants. * * @param value - Event type duration fields. * @param value.duration - Selected default duration. * @param value.durationOptions - Allowed booking durations. * @param context - Zod validation context. */ function validateEventTypeDurations( value: { duration?: number durationOptions?: number[] }, context: z.RefinementCtx, ) { const { duration, durationOptions } = value if (durationOptions === undefined) return if (new Set(durationOptions).size !== durationOptions.length) { context.addIssue({ code: "custom", message: "durationOptions must contain unique values.", path: ["durationOptions"], }) } if (duration !== undefined && !durationOptions.includes(duration)) { context.addIssue({ code: "custom", message: "duration must be included in durationOptions.", path: ["duration"], }) } }