mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
RV/Boat Lots: request renter insurance (vendor-style flow)
Phase 4. Mirror the vendor insurance request flow for RV/boat renters: - Migration: insurance fields on rv_boat_lot_rentals + rv_renter_insurance_requests table + token-scoped lookup/submit SECURITY DEFINER RPCs (granted to anon). - Edge fn send-rv-renter-insurance-request emails the renter a secure link (reuses the vendor-insurance-request email template). - Public page /rv-insurance/:token to submit carrier/policy/expiration + COI upload. - "Request Insurance" button on each active rental + insurance status display. DB RPCs verified end-to-end (rolled-back txn): submit matches token, updates the rental, marks the request submitted. Edge function deployed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
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 { rental_id } = await req.json()
|
||||
if (!rental_id) {
|
||||
return new Response(JSON.stringify({ error: 'rental_id 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') && !roles.includes('association_management')) {
|
||||
return new Response(JSON.stringify({ error: 'Forbidden' }), {
|
||||
status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const { data: rental, error: rErr } = await admin
|
||||
.from('rv_boat_lot_rentals')
|
||||
.select('id, renter_name, renter_email')
|
||||
.eq('id', rental_id)
|
||||
.single()
|
||||
if (rErr || !rental) {
|
||||
return new Response(JSON.stringify({ error: 'Rental not found' }), {
|
||||
status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
if (!rental.renter_email) {
|
||||
return new Response(JSON.stringify({ error: 'Renter has no email on file' }), {
|
||||
status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const { data: profile } = await admin
|
||||
.from('profiles').select('full_name').eq('user_id', userId).maybeSingle()
|
||||
|
||||
const { data: reqRow, error: reqErr } = await admin
|
||||
.from('rv_renter_insurance_requests')
|
||||
.insert({ rental_id, sent_to_email: rental.renter_email, created_by: userId })
|
||||
.select('id, token, expires_at')
|
||||
.single()
|
||||
if (reqErr || !reqRow) {
|
||||
return new Response(JSON.stringify({ error: 'Failed to create request', detail: reqErr?.message }), {
|
||||
status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const link = `${PUBLIC_BASE_URL}/rv-insurance/${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: 'vendor-insurance-request',
|
||||
recipientEmail: rental.renter_email,
|
||||
idempotencyKey: `rv-renter-insurance-${reqRow.id}`,
|
||||
templateData: {
|
||||
vendorName: rental.renter_name,
|
||||
requesterName: profile?.full_name || 'Avria Community Management',
|
||||
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)
|
||||
return new Response(JSON.stringify({
|
||||
ok: false,
|
||||
request_id: reqRow.id,
|
||||
link,
|
||||
error: 'Email failed to send. Share the link manually.',
|
||||
}), { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } })
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
ok: true,
|
||||
request_id: reqRow.id,
|
||||
link,
|
||||
sent_to: rental.renter_email,
|
||||
}), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } })
|
||||
} catch (e) {
|
||||
console.error('send-rv-renter-insurance-request error:', e)
|
||||
return new Response(JSON.stringify({ error: String(e) }), {
|
||||
status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user