Files
motor-de-encuestas/app/actions/submit-survey.ts

110 lines
3.9 KiB
TypeScript

'use server'
import { headers } from 'next/headers'
import { createHash, randomUUID } from 'crypto'
import { createClient as createSupabaseClient } from '@supabase/supabase-js'
export type SubmitResult =
| { success: true }
| { success: false; error: string }
export async function submitSurvey(payload: {
surveyId: string
answers: Record<string, unknown>
questionMap: Record<string, string> // código → question_id UUID
consentOperativo: boolean
consentMacroN1: boolean
}): Promise<SubmitResult> {
// 1. ip_hash: server-only, nunca en texto plano
const headersList = await headers()
const rawIp =
headersList.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
const salt = process.env.IP_HASH_SALT ?? ''
const ipHash = createHash('sha256').update(rawIp + salt).digest('hex')
// 2. Cliente Supabase con anon key (RLS policies permiten INSERT a anon)
// NUNCA usar service_role — el anon key respeta RLS
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ''
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? ''
const supabase = createSupabaseClient(supabaseUrl, supabaseAnonKey)
// 3. Leer survey para obtener org_id y consent_version_id
const { data: survey, error: sErr } = await supabase
.from('surveys')
.select('org_id, consent_version_id')
.eq('id', payload.surveyId)
.single()
if (sErr || !survey) return { success: false, error: 'survey_not_found' }
// 4. Pre-generar UUIDs server-side para evitar .select().single() post-INSERT.
//
// DIAGNÓSTICO: el Supabase JS client usa RETURNING * en .select().single(), lo que
// requiere SELECT policy para anon en consent_records/responses. Esa policy no existe
// (correcto — anon no debe leer consent_records de otros respondentes).
// Solución: pre-generar UUIDs con randomUUID() y NO hacer SELECT after INSERT.
const consentId = randomUUID()
const responseId = randomUUID()
// 5. INSERT consent_record (inmutable, ADR-003)
// Sin .select() para no activar RETURNING y evitar el error RLS 42501.
const { error: cErr } = await supabase
.from('consent_records')
.insert({
id: consentId,
consent_version_id: survey.consent_version_id,
granted_scopes: {
operativo: payload.consentOperativo,
macro_n1: payload.consentMacroN1,
},
granted_at: new Date().toISOString(),
})
if (cErr) return { success: false, error: `consent_insert_failed: ${cErr.message}` }
// 6. Extraer qi_* de las respuestas
const qi_age_bucket =
typeof payload.answers['A5'] === 'string' ? payload.answers['A5'] : null
const qi_sector =
typeof payload.answers['A4'] === 'string' ? payload.answers['A4'] : null
// 7. INSERT response (respondent_id=null — encuesta anónima, regla 2 CLAUDE.md)
const { error: rErr } = await supabase
.from('responses')
.insert({
id: responseId,
survey_id: payload.surveyId,
company_id: survey.org_id,
respondent_id: null,
ip_hash: ipHash,
consent_record_id: consentId,
qi_age_bucket: qi_age_bucket || null,
qi_sector: qi_sector || null,
qi_zone: null,
})
if (rErr) return { success: false, error: `response_insert_failed: ${rErr.message}` }
// 8. INSERT answers — una fila por pregunta respondida
const answerRows = Object.entries(payload.answers)
.map(([code, value]) => ({
response_id: responseId,
question_id: payload.questionMap[code],
value,
}))
.filter(
(row) =>
row.question_id &&
row.value !== null &&
row.value !== undefined &&
row.value !== '' &&
!(Array.isArray(row.value) && row.value.length === 0)
)
if (answerRows.length > 0) {
const { error: aErr } = await supabase.from('answers').insert(answerRows)
if (aErr) return { success: false, error: `answers_insert_failed: ${aErr.message}` }
}
return { success: true }
}