mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
183fe0a93c
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
163 lines
6.3 KiB
TypeScript
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' },
|
|
})
|
|
}
|
|
}) |