Files
2026-06-01 20:19:26 -04:00

163 lines
6.3 KiB
TypeScript

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 PUBLIC_BASE_URL = 'https://avria.cloud'
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
try {
const body = await req.json().catch(() => ({}))
const ownerIds: string[] = Array.isArray(body.owner_ids) ? body.owner_ids.filter((x: any) => typeof x === 'string') : []
const customMessage: string = typeof body.custom_message === 'string' ? body.custom_message.trim() : ''
if (ownerIds.length === 0) {
return new Response(JSON.stringify({ error: 'owner_ids required' }), {
status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
const supabaseUrl = Deno.env.get('SUPABASE_URL')!
const serviceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
const anonKey = Deno.env.get('SUPABASE_ANON_KEY')!
const authHeader = req.headers.get('Authorization') || ''
const authClient = createClient(supabaseUrl, anonKey, {
global: { headers: { Authorization: authHeader } },
})
const { data: userRes } = await authClient.auth.getUser()
if (!userRes?.user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
const userId = userRes.user.id
const admin = createClient(supabaseUrl, serviceKey)
const { data: roleRows } = await admin.from('user_roles').select('role').eq('user_id', userId)
const roles = (roleRows || []).map((r: any) => r.role)
if (!roles.includes('admin') && !roles.includes('manager')) {
return new Response(JSON.stringify({ error: 'Forbidden' }), {
status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
const { data: profile } = await admin.from('profiles').select('full_name').eq('user_id', userId).maybeSingle()
const requesterName = profile?.full_name || 'Avria Community Management'
const { data: owners, error: oErr } = await admin
.from('owners')
.select('id, first_name, last_name, email, management_contact_email, management_contact_name, business_name, owner_type, unit_id, association_id, property_address, user_id, associations(name), units(address)')
.in('id', ownerIds)
if (oErr) {
return new Response(JSON.stringify({ error: 'Failed to load owners', detail: oErr.message }), {
status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
const results: Array<{ owner_id: string; ok: boolean; link?: string; error?: string; sent_to?: string }> = []
for (const owner of owners || []) {
const ownerName = (owner as any).business_name
|| (owner as any).management_contact_name
|| [owner.first_name, owner.last_name].filter(Boolean).join(' ')
|| 'Owner'
const propertyAddress = (owner as any).units?.address || owner.property_address || ''
const associationName = (owner as any).associations?.name || ''
let recipientEmail = owner.email || (owner as any).management_contact_email || ''
// Fallback: if owner has a linked user_id, try profiles.email
if (!recipientEmail && (owner as any).user_id) {
const { data: profileRow } = await admin
.from('profiles')
.select('email')
.eq('user_id', (owner as any).user_id)
.maybeSingle()
if (profileRow?.email) {
recipientEmail = profileRow.email
}
}
if (!recipientEmail) {
results.push({ owner_id: owner.id, ok: false, error: 'No email on file' })
continue
}
const { data: reqRow, error: reqErr } = await admin
.from('tenant_info_requests')
.insert({
owner_id: owner.id,
unit_id: owner.unit_id,
association_id: owner.association_id,
sent_to_email: recipientEmail,
custom_message: customMessage || null,
created_by: userId,
})
.select('id, token, expires_at')
.single()
if (reqErr || !reqRow) {
results.push({ owner_id: owner.id, ok: false, error: reqErr?.message || 'Failed to create request' })
continue
}
const link = `${PUBLIC_BASE_URL}/tenant-info/${reqRow.token}`
const expiresAt = new Date(reqRow.expires_at).toLocaleDateString('en-US', {
month: 'long', day: 'numeric', year: 'numeric',
})
let emailErr: any = null
try {
const emailRes = await fetch(`${supabaseUrl}/functions/v1/send-transactional-email`, {
method: 'POST',
headers: {
'apikey': anonKey,
...(authHeader ? { Authorization: authHeader } : {}),
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateName: 'tenant-info-request',
recipientEmail,
idempotencyKey: `tenant-info-${reqRow.id}`,
templateData: {
ownerName,
propertyAddress,
associationName,
requesterName,
customMessage: customMessage || undefined,
link,
expiresAt,
},
}),
})
if (!emailRes.ok) {
const text = await emailRes.text()
emailErr = { message: `status ${emailRes.status}: ${text}` }
}
} catch (e) {
emailErr = e
}
if (emailErr) {
console.error('Email send failed:', emailErr)
results.push({ owner_id: owner.id, ok: false, link, error: 'Email failed to send. Share the link manually.' })
} else {
results.push({ owner_id: owner.id, ok: true, link, sent_to: recipientEmail })
}
}
const sentCount = results.filter((r) => r.ok).length
return new Response(JSON.stringify({ ok: true, sent: sentCount, total: results.length, results }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
} catch (e) {
console.error('send-tenant-info-request error:', e)
return new Response(JSON.stringify({ error: String(e) }), {
status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
})
}
})