import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.45.0' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', } const formatEST = (iso?: string | null) => { if (!iso) return '' try { const d = new Date(iso) return new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true, }).format(d) + ' ET' } catch { return '' } } Deno.serve(async (req) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }) try { const { announcement_id } = await req.json() if (!announcement_id) { return new Response(JSON.stringify({ error: 'announcement_id required' }), { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!, ) const { data: ann, error: annErr } = await supabase .from('announcements') .select('*') .eq('id', announcement_id) .single() if (annErr || !ann) { return new Response(JSON.stringify({ error: 'announcement not found' }), { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } // Skip if expired or not active if (ann.status !== 'active') { return new Response(JSON.stringify({ ok: true, skipped: 'inactive' }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } if (ann.expires_at && new Date(ann.expires_at) <= new Date()) { return new Response(JSON.stringify({ ok: true, skipped: 'expired' }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } // public_only is community page only — do not email if (ann.visibility === 'public_only') { return new Response(JSON.stringify({ ok: true, skipped: 'public_only' }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } // Determine recipient user_ids based on visibility const recipientUserIds = new Set() // Always include staff (admin + manager) const { data: staffRows } = await supabase .from('user_roles') .select('user_id') .in('role', ['admin', 'manager']) for (const r of staffRows || []) if (r.user_id) recipientUserIds.add(r.user_id) // Board members — scope to association if set, else all let boardQuery = supabase.from('board_members').select('user_id, association_id') if (ann.association_id) boardQuery = boardQuery.eq('association_id', ann.association_id) const { data: boardRows } = await boardQuery for (const r of boardRows || []) if (r.user_id) recipientUserIds.add(r.user_id) // Homeowners (only when visibility is 'all' or 'public') const includeHomeowners = ann.visibility === 'all' || ann.visibility === 'public' let homeownerEmails: { email: string; name: string }[] = [] if (includeHomeowners) { let ownerQuery = supabase .from('owners') .select('user_id, email, first_name, last_name, status') .eq('status', 'active') if (ann.association_id) ownerQuery = ownerQuery.eq('association_id', ann.association_id) const { data: ownerRows } = await ownerQuery for (const o of ownerRows || []) { if (o.user_id) recipientUserIds.add(o.user_id) else if (o.email) { homeownerEmails.push({ email: String(o.email).trim(), name: [o.first_name, o.last_name].filter(Boolean).join(' '), }) } } } // Look up emails from auth.users for collected user_ids const userIdList = Array.from(recipientUserIds) const recipients: { email: string; name: string; id: string }[] = [] if (userIdList.length > 0) { // paginate auth users let page = 1 while (true) { const { data: authData, error: authErr } = await supabase.auth.admin.listUsers({ page, perPage: 1000 }) if (authErr || !authData?.users?.length) break for (const u of authData.users) { if (recipientUserIds.has(u.id) && u.email) { const meta: any = u.user_metadata || {} recipients.push({ email: u.email, name: meta.full_name || meta.first_name || '', id: u.id, }) } } if (authData.users.length < 1000) break page++ if (page > 10) break } } // Add homeowner emails (no auth account) for (const h of homeownerEmails) { recipients.push({ email: h.email, name: h.name, id: `owner-${h.email}` }) } // Dedupe by lowercased email const seen = new Set() const uniqueRecipients = recipients.filter(r => { const key = r.email.toLowerCase() if (seen.has(key)) return false seen.add(key) return true }) // Look up association name for context let associationName = '' if (ann.association_id) { const { data: assoc } = await supabase .from('associations') .select('name') .eq('id', ann.association_id) .single() associationName = assoc?.name || '' } const postedAt = formatEST(ann.created_at) const expiresAt = formatEST(ann.expires_at) let sent = 0 let failed = 0 for (const r of uniqueRecipients) { try { const { error: emailError } = await supabase.functions.invoke('send-transactional-email', { body: { templateName: 'announcement-broadcast', recipientEmail: r.email, idempotencyKey: `announcement-${ann.id}-${r.id}`, templateData: { recipientName: r.name || '', title: ann.title, contentHtml: ann.content || '', postedAt, expiresAt, associationName, }, }, }) if (emailError) { failed++ console.error(`Email to ${r.email} failed:`, emailError) } else { sent++ } } catch (e) { failed++ console.error(`Email exception for ${r.email}:`, e) } } return new Response( JSON.stringify({ ok: true, recipients: uniqueRecipients.length, sent, failed }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }, ) } catch (err: any) { console.error('notify-announcement error:', err) return new Response(JSON.stringify({ error: err.message || String(err) }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) } })