110 lines
3.9 KiB
TypeScript
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 }
|
|
}
|