import { createClient } from 'jsr:@supabase/supabase-js@2' import { buildBookingNotificationEmail } from '../_shared/email.ts' const FROM_ADDRESS = 'bookings@braidsbymira.com' const MIRA_EMAIL = 'hello@braidsbymira.com' Deno.serve(async (req) => { const notifySecret = Deno.env.get('NOTIFY_WEBHOOK_SECRET') if (notifySecret && req.headers.get('X-Notify-Secret') !== notifySecret) { return new Response(JSON.stringify({ error: 'unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }) } let booking try { const payload = await req.json() booking = payload?.record if (!booking) { throw new Error('payload.record is missing') } } catch (err) { console.error('on-booking-created: failed to parse request body', err instanceof Error ? err.message : err) return new Response(JSON.stringify({ error: 'invalid request body' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }) } const supabaseAdmin = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!, ) // Best-effort: records a failure so it's visible somewhere a human could // plausibly check (e.g. a future admin panel view), rather than only in // Edge Function logs nobody reads. Swallow any error from this insert // itself -- we're already in a failure path and must not let logging the // failure become a second, louder failure. const recordFailure = async (message: string) => { try { await supabaseAdmin .from('notification_failures') .insert({ kind: 'booking', record_id: booking.id, error_message: message }) } catch (err) { console.error('on-booking-created: failed to record notification_failures row', err instanceof Error ? err.message : err) } } try { const { data: service } = await supabaseAdmin .from('services') .select('name') .eq('id', booking.service_id) .single() const { subject, html } = buildBookingNotificationEmail({ serviceName: service?.name ?? booking.service_id, date: booking.date, time: booking.time, contact: { name: booking.contact_name, phone: booking.contact_phone, email: booking.contact_email, notes: booking.contact_notes, }, }) const resendApiKey = Deno.env.get('RESEND_API_KEY') if (!resendApiKey) { const message = 'RESEND_API_KEY is not set' console.error(`on-booking-created: ${message}, skipping email for service ${booking.service_id}`) await recordFailure(message) } else { const resendRes = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { Authorization: `Bearer ${resendApiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: FROM_ADDRESS, to: MIRA_EMAIL, subject, html }), }) if (!resendRes.ok) { const body = await resendRes.text() console.error( `on-booking-created: Resend send failed for service ${booking.service_id} (status ${resendRes.status})`, body, ) await recordFailure(`Resend responded ${resendRes.status}: ${body}`) } } } catch (err) { // The booking itself already succeeded (this handler only runs after // the row was inserted) -- a failure here means the notification email // didn't go out, not that the booking failed. We still return 200: // pg_net is fire-and-forget (it doesn't inspect the response status or // retry on non-2xx -- the response just lands in net._http_response), // so returning 500 wouldn't trigger a retry or help anyone; nothing // consumes this status code. What it *would* do is misrepresent the // request as failed when the booking itself succeeded. The actual gap // this creates -- a failed send being invisible -- is what // notification_failures (recorded above/below) exists to close. const message = err instanceof Error ? err.message : String(err) console.error(`on-booking-created: unexpected error for service ${booking?.service_id}`, message) await recordFailure(message) } return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } }) })