import { supabaseAdmin } from "../config/supabase"; import type { ProfileAnswer } from "../types/domain"; import { HttpError } from "../utils/httpError"; import { getQuestionsDocument, isKnownQuestionId } from "../utils/questions"; import { createEmbedding } from "./embeddingService"; import { summarizeProfile } from "./openClawService"; interface UpsertProfileInput { displayName: string; age: number; location?: string; gender?: string; interestedIn?: string[]; } function answerText(answer: ProfileAnswer): string { const raw = Array.isArray(answer.answer) ? answer.answer.join(", ") : answer.answer; return [raw, answer.followup].filter(Boolean).join(" "); } function normalizeSignal(value: string): string { return value .toLowerCase() .replace(/[^a-z0-9\s-]/g, " ") .replace(/\s+/g, " ") .trim() .slice(0, 80); } function collectSignals(answers: Record, tag: "disgust" | "captivating_traits"): string[] { const questions = getQuestionsDocument().questions.filter((question) => question.tags.includes(tag)); const signals = new Set(); for (const question of questions) { const answer = answers[question.id]; if (!answer) { continue; } const values = Array.isArray(answer.answer) ? answer.answer : answer.answer.split(/[.;,\n]/).map((item) => item.trim()); for (const value of values) { const normalized = normalizeSignal(value); if (normalized.length >= 3) { signals.add(normalized); } } } return [...signals].slice(0, 32); } function profileTextForEmbedding(answers: Record, narrativeSummary: string): string { const questions = getQuestionsDocument().questions; const lines = questions .map((question) => { const answer = answers[question.id]; return answer ? `${question.prompt}\n${answerText(answer)}` : ""; }) .filter(Boolean); return [narrativeSummary, ...lines].join("\n\n"); } export async function getMyProfile(userId: string) { const { data, error } = await supabaseAdmin .from("profiles") .select("*") .eq("user_id", userId) .maybeSingle(); if (error) { throw error; } return data; } export async function upsertMyProfile(userId: string, input: UpsertProfileInput) { const { data, error } = await supabaseAdmin .from("profiles") .upsert( { user_id: userId, display_name: input.displayName, age: input.age, location: input.location ?? null, gender: input.gender ?? null, interested_in: input.interestedIn ?? [] }, { onConflict: "user_id" } ) .select("*") .single(); if (error) { throw error; } return data; } export async function saveProfileAnswer(userId: string, questionId: string, input: Omit) { if (!isKnownQuestionId(questionId)) { throw new HttpError(400, "unknown_question", `Unknown question id: ${questionId}`); } const profile = await getMyProfile(userId); if (!profile) { throw new HttpError(409, "profile_required", "Create the basic profile before saving answers"); } const answers = (profile.answers ?? {}) as Record; answers[questionId] = { questionId, answer: input.answer, followup: input.followup, dealbreakerSeverity: input.dealbreakerSeverity, updatedAt: new Date().toISOString() }; const { data, error } = await supabaseAdmin .from("profiles") .update({ answers, profile_complete: false }) .eq("user_id", userId) .select("*") .single(); if (error) { throw error; } return data; } export async function completeMyProfile(userId: string) { const profile = await getMyProfile(userId); if (!profile) { throw new HttpError(409, "profile_required", "Create the basic profile before completing the profile"); } const answers = (profile.answers ?? {}) as Record; const requiredQuestionIds = getQuestionsDocument().questions.map((question) => question.id); const missing = requiredQuestionIds.filter((questionId) => !answers[questionId]); if (missing.length > 0) { throw new HttpError(422, "profile_incomplete", `Missing required answers: ${missing.join(", ")}`); } const narrativeSummary = await summarizeProfile(answers); const embedding = await createEmbedding(profileTextForEmbedding(answers, narrativeSummary)); const disgustTriggers = collectSignals(answers, "disgust"); const captivatingTraits = collectSignals(answers, "captivating_traits"); const { data, error } = await supabaseAdmin .from("profiles") .update({ narrative_summary: narrativeSummary, embedding, disgust_triggers: disgustTriggers, captivating_traits: captivatingTraits, profile_complete: true }) .eq("user_id", userId) .select("*") .single(); if (error) { throw error; } return data; } export async function markUserPaid(userId: string, stripeCustomerId?: string | null, sessionId?: string | null) { const paidAt = new Date().toISOString(); const { error: entitlementError } = await supabaseAdmin .from("user_entitlements") .upsert( { user_id: userId, entry_paid: true, stripe_customer_id: stripeCustomerId, stripe_checkout_session_id: sessionId, paid_at: paidAt }, { onConflict: "user_id" } ); if (entitlementError) { throw entitlementError; } const { error: profileError } = await supabaseAdmin .from("profiles") .update({ paid_entry: true }) .eq("user_id", userId); if (profileError) { throw profileError; } }