import * as z from "zod" import { defineAction } from "../../../automation/actions" import { apolloApiKeyAccountOptions, getApolloApi } from "../lib/api" import { resolveApolloAccount, resolveApolloContact, resolveApolloReferences, resolveApolloUser, } from "../lib/references" import { APOLLO_PAGINATION_SCHEMA, APOLLO_PROVIDER_PAGINATION_SCHEMA, APOLLO_REFERENCE_SCHEMA, toApolloPagination, } from "../lib/schemas" const APOLLO_PROVIDER_CALL_SCHEMA = z.looseObject({ account_id: z.string().nullish(), contact_id: z.string().nullish(), conversation_id: z.string().nullish(), duration: z.number().nullish(), end_time: z.string().nullish(), from_number: z.string().nullish(), id: z.string(), inbound: z.boolean().nullish(), logged: z.boolean().nullish(), note: z.string().nullish(), opportunity_id: z.string().nullish(), phone_call_outcome_id: z.string().nullish(), phone_call_purpose_id: z.string().nullish(), recording_url: z.string().nullish(), start_time: z.string().nullish(), status: z.string().nullish(), to_number: z.string().nullish(), user_id: z.string().nullish(), }) const APOLLO_CALL_SCHEMA = z.object({ accountId: z.string().optional(), contactId: z.string().optional(), conversationId: z.string().optional(), durationSeconds: z.number().optional(), endTime: z.string().optional(), fromNumber: z.string().optional(), id: z.string(), inbound: z.boolean().optional(), logged: z.boolean().optional(), note: z.string().optional(), opportunityId: z.string().optional(), outcomeId: z.string().optional(), purposeId: z.string().optional(), recordingUrl: z.string().optional(), startTime: z.string().optional(), status: z.string().optional(), toNumber: z.string().optional(), userId: z.string().optional(), }) const CALL_RESPONSE_SCHEMA = z.looseObject({ phone_call: APOLLO_PROVIDER_CALL_SCHEMA, }) const CALLS_RESPONSE_SCHEMA = z.looseObject({ pagination: APOLLO_PROVIDER_PAGINATION_SCHEMA, phone_calls: APOLLO_PROVIDER_CALL_SCHEMA.array().prefault([]), }) const CALL_MUTATION_FIELDS = { account: APOLLO_REFERENCE_SCHEMA.optional(), contact: APOLLO_REFERENCE_SCHEMA.optional(), durationSeconds: z.number().int().nonnegative().optional(), endTime: z.iso.datetime({ offset: true }).optional(), fromNumber: z.string().trim().min(1).optional(), logged: z.boolean().optional(), note: z.string().optional(), outcomeId: z.string().optional(), purposeId: z.string().optional(), startTime: z.iso.datetime({ offset: true }).optional(), status: z.string().trim().min(1).optional(), toNumber: z.string().trim().min(1).optional(), users: APOLLO_REFERENCE_SCHEMA.array().optional(), } const UPDATE_CALL_INPUT_SCHEMA = z .object({ callId: z.string().trim().min(1), ...CALL_MUTATION_FIELDS }) .refine( (input) => input.account !== undefined || input.contact !== undefined || input.durationSeconds !== undefined || input.endTime !== undefined || input.fromNumber !== undefined || input.logged !== undefined || input.note !== undefined || input.outcomeId !== undefined || input.purposeId !== undefined || input.startTime !== undefined || input.status !== undefined || input.toNumber !== undefined || input.users !== undefined, "Provide at least one call field to update.", ) /** Searches phone calls logged in Apollo. */ export const searchApolloCalls = defineAction("Search Apollo calls") .describe("Searches logged phone calls with filters and explicit pagination.") .account("apollo", apolloApiKeyAccountOptions()) .input( z.object({ contactLabelIds: z.string().array().optional(), dateFrom: z.iso.date().optional(), dateTo: z.iso.date().optional(), direction: z.enum(["incoming", "outgoing"]).optional(), durationMaxSeconds: z.number().int().nonnegative().optional(), durationMinSeconds: z.number().int().nonnegative().optional(), keywords: z.string().trim().min(1).optional(), outcomeIds: z.string().array().optional(), page: z.number().int().min(1).prefault(1), perPage: z.number().int().min(1).max(100).prefault(100), purposeIds: z.string().array().optional(), users: APOLLO_REFERENCE_SCHEMA.array().optional(), }), ) .output( z.object({ calls: APOLLO_CALL_SCHEMA.array(), pagination: APOLLO_PAGINATION_SCHEMA, }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) const result = await api.request("phone_calls/search", { query: { "contact_label_ids[]": input.contactLabelIds, "date_range[max]": input.dateTo, "date_range[min]": input.dateFrom, "duration[max]": input.durationMaxSeconds, "duration[min]": input.durationMinSeconds, inbound: input.direction === undefined ? undefined : input.direction === "incoming", page: input.page, per_page: input.perPage, "phone_call_outcome_ids[]": input.outcomeIds, "phone_call_purpose_ids[]": input.purposeIds, q_keywords: input.keywords, "user_ids[]": await resolveApolloReferences( input.users ?? [], (reference) => resolveApolloUser(api, reference), ), }, responseSchema: CALLS_RESPONSE_SCHEMA, }) return { calls: result.phone_calls.map(toApolloCall), pagination: toApolloPagination(result.pagination), } }) /** Creates a phone-call activity in Apollo. */ export const createApolloCall = defineAction("Create Apollo call") .describe("Creates a phone-call activity and associates it with CRM records.") .account("apollo", apolloApiKeyAccountOptions()) .input(z.object(CALL_MUTATION_FIELDS)) .output(APOLLO_CALL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) return toApolloCall( ( await api.request("phone_calls", { method: "POST", query: await toCallMutationQuery(api, input), responseSchema: CALL_RESPONSE_SCHEMA, }) ).phone_call, ) }) /** Updates one phone-call activity in Apollo. */ export const updateApolloCall = defineAction("Update Apollo call") .describe("Updates selected fields on one phone-call activity.") .account("apollo", apolloApiKeyAccountOptions()) .input(UPDATE_CALL_INPUT_SCHEMA) .output(APOLLO_CALL_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) return toApolloCall( ( await api.request(`phone_calls/${encodeURIComponent(input.callId)}`, { method: "PUT", query: await toCallMutationQuery(api, input), responseSchema: CALL_RESPONSE_SCHEMA, }) ).phone_call, ) }) /** * Resolves semantic associations and encodes Apollo's call query fields. * * @param api - Authenticated Apollo client. * @param input - Public call mutation fields. */ async function toCallMutationQuery( api: ReturnType, input: z.output>, ) { return { account_id: input.account === undefined ? undefined : await resolveApolloAccount(api, input.account), contact_id: input.contact === undefined ? undefined : await resolveApolloContact(api, input.contact), duration: input.durationSeconds, end_time: input.endTime, from_number: input.fromNumber, logged: input.logged, note: input.note, phone_call_outcome_id: input.outcomeId, phone_call_purpose_id: input.purposeId, start_time: input.startTime, status: input.status, to_number: input.toNumber, "user_id[]": await resolveApolloReferences(input.users ?? [], (reference) => resolveApolloUser(api, reference), ), } } /** * Converts one provider call to its public representation. * * @param value - Provider call response. */ function toApolloCall(value: z.output) { return { accountId: value.account_id ?? undefined, contactId: value.contact_id ?? undefined, conversationId: value.conversation_id ?? undefined, durationSeconds: value.duration ?? undefined, endTime: value.end_time ?? undefined, fromNumber: value.from_number ?? undefined, id: value.id, inbound: value.inbound ?? undefined, logged: value.logged ?? undefined, note: value.note ?? undefined, opportunityId: value.opportunity_id ?? undefined, outcomeId: value.phone_call_outcome_id ?? undefined, purposeId: value.phone_call_purpose_id ?? undefined, recordingUrl: value.recording_url ?? undefined, startTime: value.start_time ?? undefined, status: value.status ?? undefined, toNumber: value.to_number ?? undefined, userId: value.user_id ?? undefined, } }