import { eq, sql, and, gt, isNotNull, count, sum } from "drizzle-orm"; import type { DB } from "../db/connection"; import { user_points, user_stats, recordings, recording_plays, comments, user_achievements, achievements, users, } from "../db/schema/index"; export const POINT_VALUES = { RECORDING_CREATE: 50, RECORDING_PLAY: 10, RECORDING_COMPLETE: 5, COMMENT_CREATE: 5, RECORDING_FEATURED: 100, } as const; const DECAY_LAMBDA = 0.005; export async function awardPoints( db: DB, userId: string, action: keyof typeof POINT_VALUES, recordingId?: string, ): Promise { const points = POINT_VALUES[action]; await db.insert(user_points).values({ user_id: userId, action, points, recording_id: recordingId || null, date_created: new Date(), }); await updateUserStats(db, userId); } export async function calculateWeightedScore(db: DB, userId: string): Promise { const now = new Date(); const result = await db.execute(sql` SELECT SUM( points * EXP(-${DECAY_LAMBDA} * EXTRACT(EPOCH FROM (${now}::timestamptz - date_created)) / 86400) ) as weighted_score FROM user_points WHERE user_id = ${userId} `); return parseFloat((result.rows[0] as any)?.weighted_score || "0"); } export async function updateUserStats(db: DB, userId: string): Promise { const now = new Date(); const rawPointsResult = await db .select({ total: sum(user_points.points) }) .from(user_points) .where(eq(user_points.user_id, userId)); const totalRawPoints = parseInt(String(rawPointsResult[0]?.total || "0")); const totalWeightedPoints = await calculateWeightedScore(db, userId); const recordingsResult = await db .select({ count: count() }) .from(recordings) .where(and(eq(recordings.user_id, userId), eq(recordings.status, "published"))); const recordingsCount = recordingsResult[0]?.count || 0; // Get playbacks count (excluding own recordings) const ownRecordingIds = await db .select({ id: recordings.id }) .from(recordings) .where(eq(recordings.user_id, userId)); const ownIds = ownRecordingIds.map((r) => r.id); let playbacksCount: number; if (ownIds.length > 0) { const playbacksResult = await db.execute(sql` SELECT COUNT(*) as count FROM recording_plays WHERE user_id = ${userId} AND recording_id NOT IN (${sql.join(ownIds.map(id => sql`${id}`), sql`, `)}) `); playbacksCount = parseInt((playbacksResult.rows[0] as any)?.count || "0"); } else { const playbacksResult = await db .select({ count: count() }) .from(recording_plays) .where(eq(recording_plays.user_id, userId)); playbacksCount = playbacksResult[0]?.count || 0; } const commentsResult = await db .select({ count: count() }) .from(comments) .where(and(eq(comments.user_id, userId), eq(comments.collection, "recordings"))); const commentsCount = commentsResult[0]?.count || 0; const achievementsResult = await db .select({ count: count() }) .from(user_achievements) .where(and(eq(user_achievements.user_id, userId), isNotNull(user_achievements.date_unlocked))); const achievementsCount = achievementsResult[0]?.count || 0; const existing = await db .select() .from(user_stats) .where(eq(user_stats.user_id, userId)) .limit(1); if (existing.length > 0) { await db .update(user_stats) .set({ total_raw_points: totalRawPoints, total_weighted_points: totalWeightedPoints, recordings_count: recordingsCount, playbacks_count: playbacksCount, comments_count: commentsCount, achievements_count: achievementsCount, last_updated: now, }) .where(eq(user_stats.user_id, userId)); } else { await db.insert(user_stats).values({ user_id: userId, total_raw_points: totalRawPoints, total_weighted_points: totalWeightedPoints, recordings_count: recordingsCount, playbacks_count: playbacksCount, comments_count: commentsCount, achievements_count: achievementsCount, last_updated: now, }); } } export async function checkAchievements( db: DB, userId: string, category?: string, ): Promise { let achievementsQuery = db .select() .from(achievements) .where(eq(achievements.status, "published")); if (category) { achievementsQuery = db .select() .from(achievements) .where(and(eq(achievements.status, "published"), eq(achievements.category, category))); } const achievementsList = await achievementsQuery; for (const achievement of achievementsList) { const progress = await getAchievementProgress(db, userId, achievement); const existing = await db .select() .from(user_achievements) .where( and( eq(user_achievements.user_id, userId), eq(user_achievements.achievement_id, achievement.id), ), ) .limit(1); const isUnlocked = progress >= achievement.required_count; const wasUnlocked = existing[0]?.date_unlocked !== null; if (existing.length > 0) { await db .update(user_achievements) .set({ progress, date_unlocked: isUnlocked ? (existing[0].date_unlocked || new Date()) : null, }) .where( and( eq(user_achievements.user_id, userId), eq(user_achievements.achievement_id, achievement.id), ), ); } else { await db.insert(user_achievements).values({ user_id: userId, achievement_id: achievement.id, progress, date_unlocked: isUnlocked ? new Date() : null, }); } if (isUnlocked && !wasUnlocked && achievement.points_reward > 0) { await db.insert(user_points).values({ user_id: userId, action: `ACHIEVEMENT_${achievement.code}`, points: achievement.points_reward, recording_id: null, date_created: new Date(), }); await updateUserStats(db, userId); } } } async function getAchievementProgress( db: DB, userId: string, achievement: typeof achievements.$inferSelect, ): Promise { const { code } = achievement; if (["first_recording", "recording_10", "recording_50", "recording_100"].includes(code)) { const result = await db .select({ count: count() }) .from(recordings) .where(and(eq(recordings.user_id, userId), eq(recordings.status, "published"))); return result[0]?.count || 0; } if (code === "featured_recording") { const result = await db .select({ count: count() }) .from(recordings) .where( and( eq(recordings.user_id, userId), eq(recordings.status, "published"), eq(recordings.featured, true), ), ); return result[0]?.count || 0; } if (["first_play", "play_100", "play_500"].includes(code)) { const result = await db.execute(sql` SELECT COUNT(*) as count FROM recording_plays rp LEFT JOIN recordings r ON rp.recording_id = r.id WHERE rp.user_id = ${userId} AND r.user_id != ${userId} `); return parseInt((result.rows[0] as any)?.count || "0"); } if (["completionist_10", "completionist_100"].includes(code)) { const result = await db .select({ count: count() }) .from(recording_plays) .where(and(eq(recording_plays.user_id, userId), eq(recording_plays.completed, true))); return result[0]?.count || 0; } if (["first_comment", "comment_50", "comment_250"].includes(code)) { const result = await db .select({ count: count() }) .from(comments) .where(and(eq(comments.user_id, userId), eq(comments.collection, "recordings"))); return result[0]?.count || 0; } if (code === "early_adopter") { const user = await db.select().from(users).where(eq(users.id, userId)).limit(1); if (user[0]) { const joinDate = new Date(user[0].date_created); const platformLaunch = new Date("2025-01-01"); const oneMonthAfterLaunch = new Date(platformLaunch); oneMonthAfterLaunch.setMonth(oneMonthAfterLaunch.getMonth() + 1); return joinDate <= oneMonthAfterLaunch ? 1 : 0; } } if (code === "one_year") { const user = await db.select().from(users).where(eq(users.id, userId)).limit(1); if (user[0]) { const joinDate = new Date(user[0].date_created); const oneYearAgo = new Date(); oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); return joinDate <= oneYearAgo ? 1 : 0; } } if (code === "balanced_creator") { const recordingsResult = await db .select({ count: count() }) .from(recordings) .where(and(eq(recordings.user_id, userId), eq(recordings.status, "published"))); const playsResult = await db.execute(sql` SELECT COUNT(*) as count FROM recording_plays rp LEFT JOIN recordings r ON rp.recording_id = r.id WHERE rp.user_id = ${userId} AND r.user_id != ${userId} `); const rc = recordingsResult[0]?.count || 0; const pc = parseInt((playsResult.rows[0] as any)?.count || "0"); return rc >= 50 && pc >= 100 ? 1 : 0; } if (code === "top_10_rank") { const userStat = await db .select() .from(user_stats) .where(eq(user_stats.user_id, userId)) .limit(1); if (!userStat[0]) return 0; const rankResult = await db .select({ count: count() }) .from(user_stats) .where(gt(user_stats.total_weighted_points, userStat[0].total_weighted_points || 0)); const userRank = (rankResult[0]?.count || 0) + 1; return userRank <= 10 ? 1 : 0; } return 0; } export async function recalculateAllWeightedScores(db: DB): Promise { const allUsers = await db.select({ user_id: user_stats.user_id }).from(user_stats); for (const u of allUsers) { await updateUserStats(db, u.user_id); } }