diff --git a/client/lib/arm-affiliation-service.ts b/client/lib/arm-affiliation-service.ts new file mode 100644 index 00000000..c662534a --- /dev/null +++ b/client/lib/arm-affiliation-service.ts @@ -0,0 +1,197 @@ +import { supabase } from "./supabase"; + +export interface ArmAffiliation { + arm: "foundation" | "gameforge" | "labs" | "corp" | "devlink"; + detected: boolean; + reason: string; + activityCount: number; +} + +/** + * Detects which arms a user is affiliated with based on their activities + */ +export const armAffiliationService = { + /** + * Check if user has enrolled in any Foundation courses + */ + async hasFoundationActivity(userId: string): Promise<{ detected: boolean; count: number }> { + try { + const { data: enrollments, error } = await supabase + .from("course_enrollments") + .select("id", { count: "exact" }) + .eq("user_id", userId) + .limit(1); + + if (error) throw error; + return { detected: (enrollments?.length ?? 0) > 0, count: enrollments?.length ?? 0 }; + } catch (error) { + console.error("Error checking Foundation activity:", error); + return { detected: false, count: 0 }; + } + }, + + /** + * Check if user has GameForge projects/teams + */ + async hasGameForgeActivity(userId: string): Promise<{ detected: boolean; count: number }> { + try { + const { data: projects, error: projectError } = await supabase + .from("gameforge_projects") + .select("id", { count: "exact" }) + .eq("user_id", userId) + .limit(1); + + if (projectError) throw projectError; + + const { data: teams, error: teamError } = await supabase + .from("gameforge_team_members") + .select("id", { count: "exact" }) + .eq("user_id", userId) + .limit(1); + + if (teamError) throw teamError; + + const totalCount = (projects?.length ?? 0) + (teams?.length ?? 0); + return { detected: totalCount > 0, count: totalCount }; + } catch (error) { + console.error("Error checking GameForge activity:", error); + return { detected: false, count: 0 }; + } + }, + + /** + * Check if user has Labs research activities + */ + async hasLabsActivity(userId: string): Promise<{ detected: boolean; count: number }> { + try { + const { data: research, error } = await supabase + .from("labs_research_tracks") + .select("id", { count: "exact" }) + .eq("user_id", userId) + .limit(1); + + if (error) throw error; + return { detected: (research?.length ?? 0) > 0, count: research?.length ?? 0 }; + } catch (error) { + console.error("Error checking Labs activity:", error); + return { detected: false, count: 0 }; + } + }, + + /** + * Check if user has Corp-related activities + */ + async hasCorpActivity(userId: string): Promise<{ detected: boolean; count: number }> { + try { + // Corp activities could include partnerships, investments, or business accounts + const { data: accounts, error } = await supabase + .from("corp_accounts") + .select("id", { count: "exact" }) + .eq("user_id", userId) + .limit(1); + + if (error) throw error; + return { detected: (accounts?.length ?? 0) > 0, count: accounts?.length ?? 0 }; + } catch (error) { + // Corp table may not exist yet, return false + return { detected: false, count: 0 }; + } + }, + + /** + * Check if user has DevLink/Roblox development activity + */ + async hasDevLinkActivity(userId: string): Promise<{ detected: boolean; count: number }> { + try { + const { data: devLinkProfiles, error } = await supabase + .from("devlink_profiles") + .select("id", { count: "exact" }) + .eq("user_id", userId) + .limit(1); + + if (error && error.code !== "42P01") throw error; // Ignore table not found errors + return { detected: (devLinkProfiles?.length ?? 0) > 0, count: devLinkProfiles?.length ?? 0 }; + } catch (error) { + // Table may not exist yet + return { detected: false, count: 0 }; + } + }, + + /** + * Detect all arm affiliations for a user + */ + async detectAffiliations(userId: string): Promise { + const [foundation, gameforge, labs, corp, devlink] = await Promise.all([ + this.hasFoundationActivity(userId), + this.hasGameForgeActivity(userId), + this.hasLabsActivity(userId), + this.hasCorpActivity(userId), + this.hasDevLinkActivity(userId), + ]); + + return [ + { + arm: "foundation", + detected: foundation.detected, + reason: foundation.detected ? `${foundation.count} course(s) enrolled` : "No courses enrolled", + activityCount: foundation.count, + }, + { + arm: "gameforge", + detected: gameforge.detected, + reason: gameforge.detected ? `${gameforge.count} project(s) and team(s)` : "No projects or teams", + activityCount: gameforge.count, + }, + { + arm: "labs", + detected: labs.detected, + reason: labs.detected ? `${labs.count} research track(s)` : "No research tracks", + activityCount: labs.count, + }, + { + arm: "corp", + detected: corp.detected, + reason: corp.detected ? `${corp.count} corp account(s)` : "No corp accounts", + activityCount: corp.count, + }, + { + arm: "devlink", + detected: devlink.detected, + reason: devlink.detected ? "DevLink profile created" : "No DevLink profile", + activityCount: devlink.count, + }, + ]; + }, + + /** + * Sync detected affiliations to user_arm_affiliations table + */ + async syncDetectedAffiliations(userId: string, token: string): Promise { + try { + const detectedArms = await this.detectAffiliations(userId); + + for (const affiliation of detectedArms) { + if (affiliation.detected) { + await fetch(`${import.meta.env.VITE_API_BASE}/api/user/arm-affiliations`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + arm: affiliation.arm, + affiliation_type: "courses", // Generic type for auto-detected + affiliation_data: { + detected: true, + reason: affiliation.reason, + }, + confirmed: false, + }), + }); + } + } + } catch (error) { + console.error("Error syncing arm affiliations:", error); + } + }, +};