import { z } from 'zod'; export const whatsappMessageTypeEnum = z.enum([ 'text', 'image', 'audio', 'interactive', 'sale_alert', 'daily_report', 'low_stock', 'customer_reply', 'remote_command', 'reservation', 'subscription_alert', 'custom', ]); export const interactiveActionSchema = z.object({ type: z.enum(['button', 'list']), buttons: z .array( z.object({ id: z.string(), title: z.string(), }), ) .optional(), sections: z .array( z.object({ title: z.string(), rows: z.array( z.object({ id: z.string(), title: z.string(), description: z.string().optional(), }), ), }), ) .optional(), }); export const sendWhatsappSchema = z.object({ messageType: whatsappMessageTypeEnum, recipient: z.string(), content: z.string().optional(), interactive: interactiveActionSchema.optional(), mediaUrl: z.string().optional(), metadata: z.record(z.unknown()).optional(), /** WhatsApp template (pre-approved). When set, use ContentSid + ContentVariables instead of content. */ templateName: z.string().optional(), templateParams: z.array(z.string()).optional(), }); /** Mobile-friendly: body with only { text } — recipient defaults to shop paired WhatsApp */ export const sendWhatsappSimpleSchema = z.object({ text: z.string() }); export const sendWhatsappBodySchema = z.union([sendWhatsappSchema, sendWhatsappSimpleSchema]); export const whatsappPairSchema = z.object({ phone: z.string().min(10).max(15), verificationCode: z.string().optional(), }); export const whatsappMessageSchema = z.object({ id: z.string().uuid(), direction: z.enum(['inbound', 'outbound']), messageType: whatsappMessageTypeEnum, recipient: z.string(), content: z.string(), status: z.enum(['queued', 'sent', 'delivered', 'failed']), metadata: z.record(z.unknown()).nullable(), sentAt: z.coerce.date().nullable(), createdAt: z.coerce.date(), }); /** Canonical JSON body (WAPI.js-style). Twilio sends From/Body/MessageSid and omits unix timestamp — we normalize via preprocess. */ export const incomingWhatsappSchema = z.preprocess((raw: unknown) => { if (!raw || typeof raw !== 'object') return raw; const r = raw as Record; if (typeof r.From === 'string') { const numMediaRaw = r.NumMedia; const numMedia = typeof numMediaRaw === 'number' ? numMediaRaw : parseInt(String(numMediaRaw ?? '0'), 10) || 0; const ct = String(r.MediaContentType0 ?? '').toLowerCase(); let type: string | undefined; if (numMedia > 0) { if (ct.startsWith('audio/')) type = 'audio'; else if (ct.startsWith('image/')) type = 'image'; else if (ct.startsWith('video/')) type = 'video'; } return { from: r.From, to: r.To, message: typeof r.Body === 'string' ? r.Body : '', timestamp: Math.floor(Date.now() / 1000), messageId: r.MessageSid, type, mediaUrl: typeof r.MediaUrl0 === 'string' ? r.MediaUrl0 : undefined, }; } return raw; }, z.object({ from: z.string(), to: z.string().optional(), message: z.string().optional(), timestamp: z.coerce.number().optional(), messageId: z.string().optional(), type: z.string().optional(), mediaUrl: z.string().optional(), })).transform((v) => ({ ...v, timestamp: v.timestamp ?? Math.floor(Date.now() / 1000), })); /** Provider delivery status callback (Gap 1). messageId = our id; externalId = provider SID (e.g. Twilio) for lookups. */ export const deliveryStatusWebhookSchema = z.object({ messageId: z.string().uuid().optional(), externalId: z.string().optional(), status: z.enum(['sent', 'delivered', 'failed', 'read']), }).refine((d) => d.messageId ?? d.externalId, { message: 'One of messageId or externalId required' }); export type WhatsappMessageType = z.infer; export type InteractiveAction = z.infer; export type SendWhatsappInput = z.infer; export type WhatsappPairInput = z.infer; export type WhatsappMessage = z.infer; export type IncomingWhatsapp = z.infer;