import * as z from "zod" import { defineAction } from "../../automation/actions" import { getBrevoApi, parseBrevoResponse, } from "@automate.ax/integration-contracts/brevo" const BREVO_ACCOUNT = "brevo" const SMS_EVENT_SCHEMA = z.enum([ "bounces", "hardBounces", "softBounces", "delivered", "sent", "accepted", "unsubscription", "replies", "blocked", "rejected", "skipped", ]) const SMS_METRICS_SCHEMA = z.object({ accepted: z.number().int().nonnegative().optional(), blocked: z.number().int().nonnegative().optional(), delivered: z.number().int().nonnegative().optional(), hardBounces: z.number().int().nonnegative().optional(), rejected: z.number().int().nonnegative().optional(), replied: z.number().int().nonnegative().optional(), requests: z.number().int().nonnegative().optional(), skipped: z.number().int().nonnegative().optional(), softBounces: z.number().int().nonnegative().optional(), unsubscribed: z.number().int().nonnegative().optional(), }) const SMS_REPORT_FILTER_SCHEMA = z .object({ days: z.number().int().min(1).max(90).optional(), endDate: z.iso.date().optional(), startDate: z.iso.date().optional(), tag: z.string().min(1).optional(), }) .refine( ({ days, endDate, startDate }) => days ? startDate === undefined && endDate === undefined : Boolean(startDate) === Boolean(endDate), { message: "Use days or provide both startDate and endDate, but not both.", }, ) const SEND_SMS_SCHEMA = z .object({ /** SMS body; long content may consume multiple SMS credits. */ content: z.string().trim().min(1).optional(), /** Brand prefix prepended to the content. */ organizationPrefix: z.string().trim().min(1).optional(), /** Template variables used when templateId is provided. */ params: z.record(z.string(), z.json()).optional(), /** Recipient phone number including country code. */ recipient: z .string() .trim() .regex(/^\+?\d{6,15}$/), /** Alphanumeric or numeric sender ID. */ sender: z .string() .trim() .refine( (sender) => /^\d{1,15}$/.test(sender) || /^[A-Za-z0-9]{1,11}$/.test(sender), "Sender must be up to 11 letters or digits, or up to 15 digits.", ), /** Reporting tag or tags. */ tags: z .union([z.string().min(1), z.string().min(1).array().min(1).max(10)]) .optional(), /** SMS template ID; overrides content when provided. */ templateId: z.number().int().positive().optional(), /** Message classification. */ type: z.enum(["transactional", "marketing"]).prefault("transactional"), /** Treat message content as Unicode. */ unicodeEnabled: z.boolean().prefault(false), /** URL Brevo calls for delivery events. */ webhookUrl: z.url().optional(), }) .refine(({ content, templateId }) => content || templateId, { message: "Provide content or templateId.", }) /** Queues a transactional SMS and returns without delivery credit details. */ export const sendBrevoSmsAsync = defineAction("Send Brevo SMS asynchronously") .describe("Queues a transactional SMS for asynchronous processing.") .account(BREVO_ACCOUNT) .input(SEND_SMS_SCHEMA) .output( z.object({ messageId: z.union([z.string(), z.number()]).transform(String), }), ) .retry({ replaySafety: "unsafe" }) .handler( async ({ account, input }) => await parseBrevoResponse( await getBrevoApi(account.secret).request("transactionalSMS/send", { body: JSON.stringify(toBrevoSms(input)), method: "POST", }), z.object({ messageId: z.union([z.string(), z.number()]).transform(String), }), ), ) /** Lists transactional SMS delivery and reply events. */ export const listBrevoSmsEvents = defineAction("List Brevo SMS events") .describe("Lists filtered transactional SMS delivery and reply events.") .account(BREVO_ACCOUNT) .input( z .object({ days: z.number().int().min(1).max(90).optional(), endDate: z.iso.date().optional(), event: SMS_EVENT_SCHEMA.optional(), limit: z.number().int().min(1).max(100).prefault(50), offset: z.number().int().nonnegative().prefault(0), phoneNumber: z .string() .trim() .regex(/^\+?\d{6,15}$/) .optional(), sort: z.enum(["asc", "desc"]).prefault("desc"), startDate: z.iso.date().optional(), tags: z.string().min(1).array().min(1).optional(), }) .refine( ({ days, endDate, startDate }) => days ? startDate === undefined && endDate === undefined : Boolean(startDate) === Boolean(endDate), { message: "Use days or provide both startDate and endDate, but not both.", }, ), ) .output( z.object({ events: z .object({ date: z.string().optional(), event: SMS_EVENT_SCHEMA.optional(), messageId: z.string().optional(), phoneNumber: z.string().optional(), reason: z.string().optional(), reply: z.string().optional(), tag: z.string().optional(), }) .array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { tags, ...query } = input const { events } = await parseBrevoResponse( await getBrevoApi(account.secret).request( "transactionalSMS/statistics/events", { query: { ...query, tags: tags ? JSON.stringify(tags) : undefined } }, ), z.object({ events: z .object({ date: z.string().optional(), event: SMS_EVENT_SCHEMA.optional(), messageId: z.string().optional(), phoneNumber: z.string().optional(), reason: z.string().optional(), reply: z.string().optional(), tag: z.string().optional(), }) .array() .optional(), }), ) return { events: events ?? [] } }) /** Retrieves aggregate transactional SMS statistics. */ export const getBrevoAggregateSmsReport = defineAction( "Get Brevo aggregate SMS report", ) .describe("Retrieves aggregate transactional SMS delivery statistics.") .account(BREVO_ACCOUNT) .input(SMS_REPORT_FILTER_SCHEMA) .output(SMS_METRICS_SCHEMA.extend({ range: z.string().optional() })) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await parseBrevoResponse( await getBrevoApi(account.secret).request( "transactionalSMS/statistics/aggregatedReport", { query: input }, ), SMS_METRICS_SCHEMA.extend({ range: z.string().optional() }), ), ) /** Retrieves daily transactional SMS statistics. */ export const getBrevoSmsReport = defineAction("Get Brevo SMS report") .describe("Retrieves daily transactional SMS delivery statistics.") .account(BREVO_ACCOUNT) .input( SMS_REPORT_FILTER_SCHEMA.extend({ sort: z.enum(["asc", "desc"]).prefault("desc"), }), ) .output( z.object({ reports: SMS_METRICS_SCHEMA.extend({ date: z.string().optional(), }).array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { reports } = await parseBrevoResponse( await getBrevoApi(account.secret).request( "transactionalSMS/statistics/reports", { query: input }, ), z.object({ reports: SMS_METRICS_SCHEMA.extend({ date: z.string().optional() }) .array() .optional(), }), ) return { reports: reports ?? [] } }) /** * Maps code-first SMS fields to Brevo’s provider request. * * @param input - Public SMS input. */ function toBrevoSms(input: z.output) { return { content: input.content, organisationPrefix: input.organizationPrefix, params: input.params, recipient: input.recipient, sender: input.sender, tag: input.tags, templateId: input.templateId, type: input.type, unicodeEnabled: input.unicodeEnabled, webUrl: input.webhookUrl, } }