Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/app/api/donor-impact/programs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { TPA_PROGRAMS } from '@/lib/ai/donor-impact-engine'

// GET /api/donor-impact/programs
// Returns TPA program profiles for the recommendation UI.
// Merges static profiles with any live metrics from the DB.
export async function GET() {
try {
const supabase = await createClient()
const { data: dbPrograms } = await supabase
.from('tpa_programs')
.select('id, name, description, focus_area, status, families_served_annual, avg_outcome_score, cost_per_family_usd, roi_multiplier, evidence_level, metadata')
.order('id')

// Merge live DB data over static defaults
const programs = TPA_PROGRAMS.map(p => {
const live = dbPrograms?.find(d => d.id === p.id)
if (!live) return p
return {
...p,
name: live.name ?? p.name,
description: live.description ?? p.description,
focusArea: live.focus_area ?? p.focusArea,
status: live.status ?? p.status,
familiesServedAnnual: live.families_served_annual ?? p.familiesServedAnnual,
avgOutcomeScore: live.avg_outcome_score ?? p.avgOutcomeScore,
costPerFamilyUsd: live.cost_per_family_usd ?? p.costPerFamilyUsd,
roiMultiplier: live.roi_multiplier ?? p.roiMultiplier,
evidenceLevel: live.evidence_level ?? p.evidenceLevel,
}
})

return NextResponse.json(programs)
} catch (err) {
console.error('[donor-impact/programs] error:', err)
return NextResponse.json(TPA_PROGRAMS)
}
}
136 changes: 136 additions & 0 deletions src/app/api/donor-impact/recommendations/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import {
generateAllocationRecommendation,
TPA_PROGRAMS,
type DonorPreferences,
} from '@/lib/ai/donor-impact-engine'

// POST /api/donor-impact/recommendations
// Body: DonorPreferences
// Generates an AI allocation recommendation and optionally persists it.
export async function POST(req: NextRequest) {
try {
const body = await req.json()
const prefs: DonorPreferences = body

// Validate priorities exist
const requiredKeys = ['technology', 'data', 'transparency', 'education', 'advocacy']
for (const key of requiredKeys) {
if (typeof prefs.priorities?.[key as keyof typeof prefs.priorities] !== 'number') {
return NextResponse.json(
{ error: `Missing or invalid priority: ${key}` },
{ status: 400 }
)
}
}

// Optionally pull live program metrics from DB to override defaults
const supabase = await createClient()
const { data: dbPrograms } = await supabase
.from('tpa_programs')
.select('id, families_served_annual, avg_outcome_score, cost_per_family_usd, roi_multiplier, evidence_level')
.eq('status', 'active')

// Merge live DB metrics into static profiles
const programs = TPA_PROGRAMS.map(p => {
const live = dbPrograms?.find(d => d.id === p.id)
if (!live) return p
return {
...p,
familiesServedAnnual: live.families_served_annual ?? p.familiesServedAnnual,
avgOutcomeScore: live.avg_outcome_score ?? p.avgOutcomeScore,
costPerFamilyUsd: live.cost_per_family_usd ?? p.costPerFamilyUsd,
roiMultiplier: live.roi_multiplier ?? p.roiMultiplier,
evidenceLevel: (live.evidence_level ?? p.evidenceLevel) as typeof p.evidenceLevel,
}
})

const recommendation = await generateAllocationRecommendation(prefs, programs)

// Persist preferences + recommendation if user is authenticated
const {
data: { user },
} = await supabase.auth.getUser()

if (user) {
const { data: prefRow, error: prefErr } = await supabase
.from('donor_preferences')
.insert({
user_id: user.id,
priority_technology: prefs.priorities.technology,
priority_data: prefs.priorities.data,
priority_transparency: prefs.priorities.transparency,
priority_education: prefs.priorities.education,
priority_advocacy: prefs.priorities.advocacy,
annual_giving_usd: prefs.annualGivingUsd ?? null,
giving_goals: prefs.givingGoals ?? [],
risk_tolerance: prefs.riskTolerance ?? 'balanced',
raw_input: prefs,
})
.select('id')
.single()

if (!prefErr && prefRow) {
await supabase.from('donor_recommendations').insert({
preference_id: prefRow.id,
allocations: recommendation.allocations,
rationale: recommendation.rationale,
key_insights: recommendation.keyInsights,
total_impact_estimate: recommendation.totalImpactEstimate,
confidence_score: recommendation.confidenceScore,
model_version: recommendation.modelVersion,
prompt_version: recommendation.promptVersion,
})
}
}

return NextResponse.json(recommendation)
} catch (err) {
console.error('[donor-impact/recommendations] error:', err)
return NextResponse.json(
{ error: 'Failed to generate recommendation' },
{ status: 500 }
)
}
}

// GET /api/donor-impact/recommendations
// Returns the authenticated user's most recent recommendation.
export async function GET() {
try {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const { data, error } = await supabase
.from('donor_preferences')
.select(
`id, priority_technology, priority_data, priority_transparency, priority_education,
priority_advocacy, annual_giving_usd, giving_goals, risk_tolerance, created_at,
donor_recommendations(
id, allocations, rationale, key_insights, total_impact_estimate,
confidence_score, created_at
)`
)
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(1)
.single()

if (error) {
return NextResponse.json({ error: 'No recommendations found' }, { status: 404 })
}

return NextResponse.json(data)
} catch (err) {
console.error('[donor-impact/recommendations GET] error:', err)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

Loading
Loading