import * as z from "zod" import { defineAction } from "../../automation/actions" import { getBrevoApi, parseBrevoResponse, } from "@automate.ax/integration-contracts/brevo" import { fromBrevoWire } from "./wire" const BREVO_ACCOUNT = "brevo" const EVENT_NAME_SCHEMA = z .string() .min(1) .max(255) .regex(/^[\w-]+$/, "Use only letters, numbers, underscores, and hyphens.") const EVENT_VALUE_SCHEMA = z.union([ z.string(), z.number(), z.boolean(), z.record(z.string(), z.json()), z.json().array(), ]) const EVENT_IDENTIFIERS_SCHEMA = z .object({ contactId: z.number().int().positive().optional(), emailId: z.email().optional(), extId: z.string().min(1).optional(), landlineNumberId: z.string().min(1).optional(), phoneId: z.string().min(1).optional(), whatsappId: z.string().min(1).optional(), }) .refine((identifiers) => Object.values(identifiers).some(Boolean), { message: "Provide at least one contact identifier.", }) const EVENT_OBJECT_SCHEMA = z.object({ identifiers: z.object({ extId: z.string().min(1).optional(), id: z.string().min(1).optional(), }), type: z.string().min(1), }) const CREATE_EVENT_SCHEMA = z.object({ /** Contact attributes to update while recording the event. */ contactProperties: z .record(z.string().min(1), z.union([z.string(), z.number(), z.boolean()])) .optional(), /** When the event occurred; Brevo uses receipt time when omitted. */ eventDate: z.iso.datetime({ offset: true }).optional(), /** Event name used by Brevo automation filters. */ eventName: EVENT_NAME_SCHEMA, /** Event-specific properties, limited by Brevo to 50 KB. */ eventProperties: z.record(z.string().min(1), EVENT_VALUE_SCHEMA).optional(), /** Contact associated with the event. */ identifiers: EVENT_IDENTIFIERS_SCHEMA, /** Optional custom-object record associated with the event. */ object: EVENT_OBJECT_SCHEMA.optional(), }) const EVENT_SCHEMA = z.object({ contactId: z.number().int().positive().optional(), contactProperties: z.record(z.string(), z.json()).optional(), eventDate: z.string(), eventFilterId: z.string().optional(), eventName: z.string(), eventProperties: z.record(z.string(), z.json()).optional(), objectType: z.string().optional(), }) const BATCH_EVENT_RESPONSE_SCHEMA = z.union([ z.object({ count: z.number().int().nonnegative(), message: z.string(), }), z.object({ errors: z .object({ eventIndex: z.number().int().nonnegative().array(), message: z.string(), }) .array(), failedEvents: z.number().int().nonnegative(), status: z.string(), successfulEvents: z.number().int().nonnegative(), totalEvents: z.number().int().nonnegative(), }), ]) /** Lists custom Brevo events with filters and offset pagination. */ export const listBrevoEvents = defineAction("List Brevo events") .describe("Lists custom events recorded for Brevo contacts.") .account(BREVO_ACCOUNT) .input( z .object({ contactIds: z.number().int().positive().array().optional(), endDate: z .union([z.iso.date(), z.iso.datetime({ offset: true })]) .optional(), eventNames: EVENT_NAME_SCHEMA.array().optional(), limit: z.number().int().min(1).max(10_000).prefault(100), objectTypes: z.string().min(1).array().optional(), offset: z.number().int().nonnegative().prefault(0), startDate: z .union([z.iso.date(), z.iso.datetime({ offset: true })]) .optional(), }) .refine( ({ endDate, startDate }) => Boolean(endDate) === Boolean(startDate), { message: "startDate and endDate must be provided together." }, ), ) .output( z.object({ count: z.number().int().nonnegative(), events: EVENT_SCHEMA.array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const response = await parseBrevoResponse( await getBrevoApi(account.secret).request("events", { query: { contact_id: input.contactIds, endDate: input.endDate, event_name: input.eventNames, limit: input.limit, object_type: input.objectTypes, offset: input.offset, startDate: input.startDate, }, }), z.object({ count: z.number().int().nonnegative(), events: z .object({ contact_id: z.number().int().positive().optional(), contact_properties: z.record(z.string(), z.json()).optional(), event_date: z.string(), event_filter_id: z.string().optional(), event_name: z.string(), event_properties: z.record(z.string(), z.json()).optional(), object_type: z.string().optional(), }) .array() .optional(), }), ) return { count: response.count, events: (response.events ?? []).map((event) => ({ contactId: event.contact_id, contactProperties: event.contact_properties, eventDate: event.event_date, eventFilterId: event.event_filter_id, eventName: event.event_name, eventProperties: event.event_properties, objectType: event.object_type, })), } }) /** Records one custom Brevo event for a contact. */ export const createBrevoEvent = defineAction("Create Brevo event") .describe("Records one custom event and optional contact properties.") .account(BREVO_ACCOUNT) .input(CREATE_EVENT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request("events", { body: JSON.stringify(toBrevoEvent(input)), method: "POST", }) }) /** Queues multiple custom Brevo events in one provider request. */ export const createBrevoBatchEvents = defineAction("Create Brevo batch events") .describe("Queues multiple custom events and reports partial failures.") .account(BREVO_ACCOUNT) .input(z.object({ events: CREATE_EVENT_SCHEMA.array().min(1) })) .output(BATCH_EVENT_RESPONSE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { return BATCH_EVENT_RESPONSE_SCHEMA.parse( fromBrevoWire( await ( await getBrevoApi(account.secret).request("events/batch", { body: JSON.stringify({ events: input.events.map(toBrevoEvent) }), method: "POST", }) ).json(), ), ) }) /** * Converts one public event payload to Brevo's wire contract. * * @param input - Public event payload. */ function toBrevoEvent(input: z.output) { return { contact_properties: input.contactProperties, event_date: input.eventDate, event_name: input.eventName, event_properties: input.eventProperties, identifiers: { contact_id: input.identifiers.contactId, email_id: input.identifiers.emailId, ext_id: input.identifiers.extId, landline_number_id: input.identifiers.landlineNumberId, phone_id: input.identifiers.phoneId, whatsapp_id: input.identifiers.whatsappId, }, object: input.object ? { identifiers: { ext_id: input.object.identifiers.extId, id: input.object.identifiers.id, }, type: input.object.type, } : undefined, } }