lovegpt / backend /src /services /matchService.ts
Crownelius's picture
Add encrypted matchmaker agent table
910f36a verified
Raw
History Blame Contribute Delete
4.88 kB
import { supabaseAdmin } from "../config/supabase";
import { HttpError } from "../utils/httpError";
interface Candidate {
candidate_profile_id: string;
compatibility_score: number;
shared_traits: string[];
caution_notes: string[];
}
function orderedPair(a: string, b: string): [string, string] {
return a < b ? [a, b] : [b, a];
}
export async function listMyMatches(userId: string) {
const { data: profile, error: profileError } = await supabaseAdmin
.from("profiles")
.select("id")
.eq("user_id", userId)
.maybeSingle();
if (profileError) {
throw profileError;
}
if (!profile) {
return [];
}
const { data, error } = await supabaseAdmin
.from("matches")
.select(`
id,
compatibility_score,
shared_traits,
caution_notes,
profile_a_accepted,
profile_b_accepted,
status,
profile_a_id,
profile_b_id,
profile_a:profiles!matches_profile_a_id_fkey(id, display_name, age, location, narrative_summary),
profile_b:profiles!matches_profile_b_id_fkey(id, display_name, age, location, narrative_summary)
`)
.or(`profile_a_id.eq.${profile.id},profile_b_id.eq.${profile.id}`)
.order("compatibility_score", { ascending: false });
if (error) {
throw error;
}
return (data ?? []).map((match) => {
const isA = match.profile_a_id === profile.id;
return {
id: match.id,
compatibility_score: match.compatibility_score,
shared_traits: match.shared_traits,
caution_notes: match.caution_notes,
status: match.status,
my_accepted: isA ? match.profile_a_accepted : match.profile_b_accepted,
their_accepted: isA ? match.profile_b_accepted : match.profile_a_accepted,
profile: isA ? match.profile_b : match.profile_a
};
});
}
export async function curateMatches(userId: string) {
const { data: profile, error: profileError } = await supabaseAdmin
.from("profiles")
.select("id, profile_complete, paid_entry")
.eq("user_id", userId)
.maybeSingle();
if (profileError) {
throw profileError;
}
if (!profile) {
throw new HttpError(409, "profile_required", "Create and complete a profile before curating matches");
}
if (!profile.profile_complete) {
throw new HttpError(422, "profile_incomplete", "Complete the full questionnaire before curating matches");
}
if (!profile.paid_entry) {
throw new HttpError(402, "entry_fee_required", "The one-time entry fee is required before match curation");
}
const { data: candidates, error: rpcError } = await supabaseAdmin.rpc("match_candidates", {
source_profile_id: profile.id,
match_count: 10
});
if (rpcError) {
throw rpcError;
}
for (const candidate of (candidates ?? []) as Candidate[]) {
const [profileAId, profileBId] = orderedPair(profile.id, candidate.candidate_profile_id);
const { error } = await supabaseAdmin.from("matches").upsert(
{
profile_a_id: profileAId,
profile_b_id: profileBId,
compatibility_score: candidate.compatibility_score,
shared_traits: candidate.shared_traits,
caution_notes: candidate.caution_notes
},
{ onConflict: "profile_a_id,profile_b_id" }
);
if (error) {
throw error;
}
}
return listMyMatches(userId);
}
export async function acceptMatch(userId: string, matchId: string) {
const { data: profile, error: profileError } = await supabaseAdmin
.from("profiles")
.select("id")
.eq("user_id", userId)
.maybeSingle();
if (profileError) {
throw profileError;
}
if (!profile) {
throw new HttpError(404, "profile_not_found", "Profile not found");
}
const { data: match, error: matchError } = await supabaseAdmin
.from("matches")
.select("*")
.eq("id", matchId)
.or(`profile_a_id.eq.${profile.id},profile_b_id.eq.${profile.id}`)
.single();
if (matchError || !match) {
throw new HttpError(404, "match_not_found", "Match not found");
}
const isProfileA = match.profile_a_id === profile.id;
const profileAAccepted = isProfileA ? true : match.profile_a_accepted;
const profileBAccepted = isProfileA ? match.profile_b_accepted : true;
const nextStatus = profileAAccepted && profileBAccepted ? "active" : "accepted";
const { data, error } = await supabaseAdmin
.from("matches")
.update({
profile_a_accepted: profileAAccepted,
profile_b_accepted: profileBAccepted,
status: nextStatus
})
.eq("id", matchId)
.select("*")
.single();
if (error) {
throw error;
}
if (nextStatus === "active") {
const { error: conversationError } = await supabaseAdmin
.from("conversations")
.upsert({ match_id: matchId }, { onConflict: "match_id" });
if (conversationError) {
throw conversationError;
}
}
return data;
}