import { getTidyCalApi, TIDYCAL_ACCOUNT_SCHEMA, TIDYCAL_BOOKING_SCHEMA, TIDYCAL_BOOKING_TYPE_SCHEMA, TIDYCAL_CONTACT_SCHEMA, TIDYCAL_TEAM_SCHEMA, TIDYCAL_TEAM_USER_SCHEMA, TIDYCAL_TIMESLOT_SCHEMA, } from "@automate.ax/integration-contracts/tidycal" import * as z from "zod" import { defineAction } from "../../automation/actions" const ACCOUNT = "tidycal" const TIDYCAL_ACCOUNT_REQUIREMENT = { connections: [ { connectionMethodId: "oauth" }, { connectionMethodId: "personal-access-token" }, ], } as const const ID_SCHEMA = z.number().int().positive() const PAGE_INPUT_SCHEMA = z.object({ page: z.number().int().positive().prefault(1), }) const DATE_TIME_INPUT_SCHEMA = z.iso.datetime({ offset: true }) const BOOKING_TYPE_INPUT_SCHEMA = z .object({ approvalRequired: z.boolean().nullable().optional(), bookingAvailabilityIntervalMinutes: z .number() .int() .min(15) .max(1440) .prefault(15), bookingThreshold: z.number().int().min(0).max(15_000).prefault(120), bookingTypeCategoryId: ID_SCHEMA.nullable().optional(), currencyCode: z .string() .regex(/^[A-Z]{3}$/) .optional(), description: z.string(), displaySeatsRemaining: z.boolean().prefault(false), durationMinutes: z.number().int().positive(), latestAvailabilityDays: z.number().int().min(0).max(36_500).prefault(60), maxBookings: z.number().int().positive().prefault(1), maxGuestInvitesPerBooker: z.number().int().min(0).max(10).prefault(0), paddingMinutes: z.number().int().min(0).prefault(0), paymentPlatform: z.enum(["paypal", "stripe", "tidycal"]).optional(), price: z.number().nonnegative().prefault(0), private: z.boolean().prefault(false), redirectUrl: z.url().max(60_000).nullable().optional(), title: z.string().trim().min(1).max(191), urlSlug: z.string().trim().min(1).max(191), }) .refine( ({ paymentPlatform, price }) => price === 0 || paymentPlatform !== undefined, { message: "Select a payment platform when price is greater than zero.", path: ["paymentPlatform"], }, ) const BOOKING_QUESTION_INPUT_SCHEMA = z.object({ answer: z.union([z.string(), z.string().array()]), bookingTypeQuestionId: ID_SCHEMA, }) const CREATE_BOOKING_COMMON_INPUT = { bookingQuestions: BOOKING_QUESTION_INPUT_SCHEMA.array().optional(), email: z.email().max(191), name: z.string().trim().min(1).max(191), timezone: z.string().trim().min(1).max(191), } const CREATE_BOOKING_INPUT_SCHEMA = z.union([ z.object({ ...CREATE_BOOKING_COMMON_INPUT, bookings: z.undefined().optional(), startsAt: DATE_TIME_INPUT_SCHEMA, }), z.object({ ...CREATE_BOOKING_COMMON_INPUT, bookings: z.object({ startsAt: DATE_TIME_INPUT_SCHEMA }).array().min(1), startsAt: z.undefined().optional(), }), ]) const BOOKING_TYPE_RESPONSE_SCHEMA = z.object({ data: TIDYCAL_BOOKING_TYPE_SCHEMA, }) const CREATED_BOOKING_RESPONSE_SCHEMA = z.object({ data: z.union([TIDYCAL_BOOKING_SCHEMA, TIDYCAL_BOOKING_SCHEMA.array()]), }) const CONTACT_RESPONSE_SCHEMA = z.object({ data: TIDYCAL_CONTACT_SCHEMA }) const TEAM_RESPONSE_SCHEMA = z.object({ data: TIDYCAL_TEAM_SCHEMA }) const INVITE_RESPONSE_SCHEMA = z.object({ message: z.string(), teamUserId: ID_SCHEMA, }) const MESSAGE_RESPONSE_SCHEMA = z.object({ message: z.string() }) /** * Resolves the shared API client from one action account. * * @param account - Resolved TidyCal account. * @param account.connectionMethodId - OAuth or personal-access-token method. * @param account.secret - Decrypted account secret. * @param account.serviceId - TidyCal service identifier. */ function api(account: { connectionMethodId: string secret: Record serviceId: "tidycal" }) { return getTidyCalApi(account) } /** * Removes TidyCal's response envelope. * * @param response - Provider response envelope. * @param response.data - Provider response payload. */ function unwrapData(response: { data: T }) { return response.data } /** Retrieves the connected TidyCal account. */ export const getTidyCalAccount = defineAction("Get TidyCal account") .describe("Retrieves the connected TidyCal account profile.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(z.object({})) .output(TIDYCAL_ACCOUNT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(({ account }) => api(account).request("me", { responseSchema: TIDYCAL_ACCOUNT_SCHEMA }), ) /** Lists bookings visible to the connected TidyCal account. */ export const listTidyCalBookings = defineAction("List TidyCal bookings") .describe( "Lists personal bookings with date, cancellation, and team filters.", ) .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input( PAGE_INPUT_SCHEMA.extend({ cancelled: z.boolean().optional(), endsAt: z.iso.date().optional(), includeTeams: z.boolean().optional(), startsAt: z.iso.date().optional(), }), ) .output(z.object({ items: TIDYCAL_BOOKING_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ({ items: unwrapData( await api(account).request("bookings", { query: input, responseSchema: z.object({ data: TIDYCAL_BOOKING_SCHEMA.array() }), }), ), })) /** Retrieves one TidyCal booking by ID. */ export const getTidyCalBooking = defineAction("Get TidyCal booking") .describe("Retrieves one booking with its contact, questions, and payment.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(z.object({ bookingId: ID_SCHEMA })) .output(TIDYCAL_BOOKING_SCHEMA) .retry({ replaySafety: "safe" }) .handler(({ account, input }) => api(account).request(`bookings/${input.bookingId}`, { responseSchema: TIDYCAL_BOOKING_SCHEMA, }), ) /** Cancels one TidyCal booking. */ export const cancelTidyCalBooking = defineAction("Cancel TidyCal booking") .describe("Cancels one booking and optionally records a reason.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input( z.object({ bookingId: ID_SCHEMA, reason: z.string().trim().optional() }), ) .output(TIDYCAL_BOOKING_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input: { bookingId, ...body } }) => api(account).request(`bookings/${bookingId}/cancel`, { body, method: "PATCH", responseSchema: TIDYCAL_BOOKING_SCHEMA, }), ) /** Lists booking types owned by the connected TidyCal account. */ export const listTidyCalBookingTypes = defineAction( "List TidyCal booking types", ) .describe("Lists the connected account's booking types.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(PAGE_INPUT_SCHEMA) .output(z.object({ items: TIDYCAL_BOOKING_TYPE_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ({ items: unwrapData( await api(account).request("booking-types", { query: input, responseSchema: z.object({ data: TIDYCAL_BOOKING_TYPE_SCHEMA.array(), }), }), ), })) /** Creates a booking type for the connected TidyCal account. */ export const createTidyCalBookingType = defineAction( "Create TidyCal booking type", ) .describe("Creates a personal TidyCal scheduling page.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(BOOKING_TYPE_INPUT_SCHEMA) .output(TIDYCAL_BOOKING_TYPE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => api(account) .request("booking-types", { body: input, method: "POST", responseSchema: BOOKING_TYPE_RESPONSE_SCHEMA, }) .then(unwrapData), ) /** Lists available timeslots for one TidyCal booking type. */ export const listTidyCalAvailableTimeslots = defineAction( "List TidyCal available timeslots", ) .describe("Lists bookable UTC timeslots in a date-time range.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input( z.object({ bookingTypeId: ID_SCHEMA, endsAt: DATE_TIME_INPUT_SCHEMA, startsAt: DATE_TIME_INPUT_SCHEMA, }), ) .output(z.object({ items: TIDYCAL_TIMESLOT_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input: { bookingTypeId, ...query } }) => ({ items: unwrapData( await api(account).request(`booking-types/${bookingTypeId}/timeslots`, { query, responseSchema: z.object({ data: TIDYCAL_TIMESLOT_SCHEMA.array() }), }), ), })) /** Creates one booking or a package of bookings. */ export const createTidyCalBooking = defineAction("Create TidyCal booking") .describe("Books one timeslot or a package of timeslots for a contact.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input( z.object({ bookingTypeId: ID_SCHEMA }).and(CREATE_BOOKING_INPUT_SCHEMA), ) .output(z.union([TIDYCAL_BOOKING_SCHEMA, TIDYCAL_BOOKING_SCHEMA.array()])) .retry({ replaySafety: "unsafe" }) .handler(({ account, input: { bookingTypeId, ...body } }) => api(account) .request(`booking-types/${bookingTypeId}/bookings`, { body, method: "POST", responseSchema: CREATED_BOOKING_RESPONSE_SCHEMA, }) .then(unwrapData), ) /** Lists contacts visible to the connected TidyCal account. */ export const listTidyCalContacts = defineAction("List TidyCal contacts") .describe("Lists contacts collected from TidyCal bookings.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(PAGE_INPUT_SCHEMA) .output(z.object({ items: TIDYCAL_CONTACT_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ({ items: unwrapData( await api(account).request("contacts", { query: input, responseSchema: z.object({ data: TIDYCAL_CONTACT_SCHEMA.array() }), }), ), })) /** Creates one TidyCal contact. */ export const createTidyCalContact = defineAction("Create TidyCal contact") .describe("Creates a contact with an optional timezone.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input( z.object({ email: z.email(), name: z.string().trim().min(1), timezone: z.string().trim().min(1).optional(), }), ) .output(TIDYCAL_CONTACT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => api(account) .request("contacts", { body: input, method: "POST", responseSchema: CONTACT_RESPONSE_SCHEMA, }) .then(unwrapData), ) /** Lists teams visible to the connected TidyCal account. */ export const listTidyCalTeams = defineAction("List TidyCal teams") .describe("Lists teams owned by or shared with the connected account.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(PAGE_INPUT_SCHEMA) .output(z.object({ items: TIDYCAL_TEAM_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => ({ items: unwrapData( await api(account).request("teams", { query: input, responseSchema: z.object({ data: TIDYCAL_TEAM_SCHEMA.array() }), }), ), })) /** Retrieves one TidyCal team by ID. */ export const getTidyCalTeam = defineAction("Get TidyCal team") .describe("Retrieves one team visible to the connected account.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(z.object({ teamId: ID_SCHEMA })) .output(TIDYCAL_TEAM_SCHEMA) .retry({ replaySafety: "safe" }) .handler(({ account, input }) => api(account) .request(`teams/${input.teamId}`, { responseSchema: TEAM_RESPONSE_SCHEMA, }) .then(unwrapData), ) /** Lists bookings owned by one TidyCal team. */ export const listTidyCalTeamBookings = defineAction( "List TidyCal team bookings", ) .describe("Lists team bookings with date, email, host, and status filters.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input( PAGE_INPUT_SCHEMA.extend({ cancelled: z.boolean().optional(), email: z.email().optional(), endDate: z.iso.date().optional(), hostId: ID_SCHEMA.optional(), startDate: z.iso.date().optional(), teamId: ID_SCHEMA, }), ) .output(z.object({ items: TIDYCAL_BOOKING_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input: { teamId, ...query } }) => ({ items: unwrapData( await api(account).request(`teams/${teamId}/bookings`, { query, responseSchema: z.object({ data: TIDYCAL_BOOKING_SCHEMA.array() }), }), ), })) /** Lists users in one TidyCal team. */ export const listTidyCalTeamUsers = defineAction("List TidyCal team users") .describe("Lists the members of one TidyCal team.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(PAGE_INPUT_SCHEMA.extend({ teamId: ID_SCHEMA })) .output(z.object({ items: TIDYCAL_TEAM_USER_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input: { teamId, ...query } }) => ({ items: unwrapData( await api(account).request(`teams/${teamId}/users`, { query, responseSchema: z.object({ data: TIDYCAL_TEAM_USER_SCHEMA.array() }), }), ), })) /** Invites one user to a TidyCal team. */ export const addTidyCalTeamUser = defineAction("Add TidyCal team user") .describe("Invites an admin or user to one TidyCal team.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input( z.object({ email: z.email(), roleName: z.enum(["admin", "user"]).optional(), teamId: ID_SCHEMA, }), ) .output(INVITE_RESPONSE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input: { teamId, ...body } }) => api(account).request(`teams/${teamId}/users`, { body, method: "POST", responseSchema: INVITE_RESPONSE_SCHEMA, }), ) /** Removes one user from a TidyCal team. */ export const removeTidyCalTeamUser = defineAction("Remove TidyCal team user") .describe("Removes one team-user record from a TidyCal team.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(z.object({ teamId: ID_SCHEMA, teamUserId: ID_SCHEMA })) .output(MESSAGE_RESPONSE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input }) => api(account).request(`teams/${input.teamId}/users/${input.teamUserId}`, { method: "DELETE", responseSchema: MESSAGE_RESPONSE_SCHEMA, }), ) /** Lists booking types owned by one TidyCal team. */ export const listTidyCalTeamBookingTypes = defineAction( "List TidyCal team booking types", ) .describe("Lists the booking types for one TidyCal team.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(PAGE_INPUT_SCHEMA.extend({ teamId: ID_SCHEMA })) .output(z.object({ items: TIDYCAL_BOOKING_TYPE_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input: { teamId, ...query } }) => ({ items: unwrapData( await api(account).request(`teams/${teamId}/booking-types`, { query, responseSchema: z.object({ data: TIDYCAL_BOOKING_TYPE_SCHEMA.array(), }), }), ), })) /** Creates a booking type for one TidyCal team. */ export const createTidyCalTeamBookingType = defineAction( "Create TidyCal team booking type", ) .describe("Creates a scheduling page for one TidyCal team.") .account(ACCOUNT, TIDYCAL_ACCOUNT_REQUIREMENT) .input(z.object({ teamId: ID_SCHEMA }).and(BOOKING_TYPE_INPUT_SCHEMA)) .output(TIDYCAL_BOOKING_TYPE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(({ account, input: { teamId, ...body } }) => api(account) .request(`teams/${teamId}/booking-types`, { body, method: "POST", responseSchema: BOOKING_TYPE_RESPONSE_SCHEMA, }) .then(unwrapData), )