const User = require("../models/User"); const PointTransaction = require("../models/PointTransaction"); const Badge = require("../models/Badge"); const UserBadge = require("../models/UserBadge"); /** * Point rewards for different actions */ const POINT_VALUES = { STREET_ADOPTION: 100, TASK_COMPLETION: 50, POST_CREATION: 10, EVENT_PARTICIPATION: 75, }; /** * Awards points to a user with transaction tracking * * @param {string} userId - User ID * @param {number} amount - Points to award (can be negative for deductions) * @param {string} type - Transaction type (street_adoption, task_completion, etc.) * @param {string} description - Human-readable description * @param {Object} relatedEntity - Related entity {entityType, entityId} * @returns {Promise} - Updated user and transaction */ async function awardPoints( userId, amount, type, description, relatedEntity = {} ) { try { // Get current user const user = await User.findById(userId); if (!user) { throw new Error("User not found"); } // Calculate new balance const currentBalance = user.points || 0; const newBalance = currentBalance + amount; // Update user points const updatedUser = await User.update(userId, { points: newBalance }); // Create transaction record const transaction = await PointTransaction.create({ user: userId, amount: amount, transactionType: type, description: description, relatedEntity: relatedEntity, balanceAfter: newBalance, }); // Check for new badges await checkAndAwardBadges(userId, newBalance); return { user: updatedUser, transaction: transaction, newBalance: newBalance, }; } catch (error) { console.error("Error awarding points:", error); throw error; } } /** * Get user's current point balance */ async function getUserPoints(userId) { try { const user = await User.findById(userId); return user ? user.points || 0 : 0; } catch (error) { console.error("Error getting user points:", error); throw error; } } /** * Get user's transaction history */ async function getUserTransactionHistory(userId, limit = 50, skip = 0) { try { return await PointTransaction.findByUser(userId, limit, skip); } catch (error) { console.error("Error getting transaction history:", error); throw error; } } /** * Check if user qualifies for new badges and award them */ async function checkAndAwardBadges(userId, userPoints = null) { try { // Get user's current points if not provided if (userPoints === null) { userPoints = await getUserPoints(userId); } // Get user's stats for badge checking const userStats = await getUserStats(userId); const userBadges = await UserBadge.findByUser(userId); // Get all available badges const allBadges = await Badge.findAll(); // Check each badge criteria for (const badge of allBadges) { // Skip if user already has this badge if (userBadges.some(ub => ub.badgeId === badge._id)) { continue; } let qualifies = false; // Check different badge criteria switch (badge.criteria.type) { case 'points_earned': qualifies = userPoints >= badge.criteria.threshold; break; case 'street_adoptions': qualifies = userStats.streetAdoptions >= badge.criteria.threshold; break; case 'task_completions': qualifies = userStats.taskCompletions >= badge.criteria.threshold; break; case 'post_creations': qualifies = userStats.postCreations >= badge.criteria.threshold; break; case 'event_participations': qualifies = userStats.eventParticipations >= badge.criteria.threshold; break; case 'consecutive_days': qualifies = userStats.consecutiveDays >= badge.criteria.threshold; break; case 'special': // Special badges are awarded manually qualifies = false; break; } if (qualifies) { await awardBadge(userId, badge._id); } } } catch (error) { console.error("Error checking badges:", error); throw error; } } /** * Award a specific badge to a user */ async function awardBadge(userId, badgeId) { try { // Get badge details const badge = await Badge.findById(badgeId); if (!badge) { throw new Error("Badge not found"); } // Create user badge record const userBadge = await UserBadge.create({ userId: userId, badgeId: badgeId, awardedAt: new Date().toISOString(), }); // Award points for earning badge (if it's a rare or higher badge) let pointsAwarded = 0; if (badge.rarity === 'rare') { pointsAwarded = 50; } else if (badge.rarity === 'epic') { pointsAwarded = 100; } else if (badge.rarity === 'legendary') { pointsAwarded = 200; } if (pointsAwarded > 0) { await awardPoints( userId, pointsAwarded, 'badge_earned', `Earned ${badge.name} badge`, { entityType: 'Badge', entityId: badgeId } ); } return userBadge; } catch (error) { console.error("Error awarding badge:", error); throw error; } } /** * Get user statistics for badge checking */ async function getUserStats(userId) { try { // This would typically involve querying various collections // For now, return basic stats - this should be enhanced const user = await User.findById(userId); return { streetAdoptions: 0, // Would query Street collection taskCompletions: 0, // Would query Task collection postCreations: 0, // Would query Post collection eventParticipations: 0, // Would query Event participation consecutiveDays: 0, // Would calculate from login history }; } catch (error) { console.error("Error getting user stats:", error); throw error; } } /** * Get user's badges */ async function getUserBadges(userId) { try { const userBadges = await UserBadge.findByUser(userId); const badges = []; for (const userBadge of userBadges) { const badge = await Badge.findById(userBadge.badgeId); if (badge) { badges.push({ ...badge, awardedAt: userBadge.awardedAt, }); } } return badges; } catch (error) { console.error("Error getting user badges:", error); throw error; } } /** * Redeem points for a reward */ async function redeemPoints(userId, rewardId, pointsCost) { try { const currentPoints = await getUserPoints(userId); if (currentPoints < pointsCost) { throw new Error("Insufficient points"); } // Deduct points const result = await awardPoints( userId, -pointsCost, 'reward_redemption', `Redeemed reward ${rewardId}`, { entityType: 'Reward', entityId: rewardId } ); return result; } catch (error) { console.error("Error redeeming points:", error); throw error; } } /** * Get leaderboard */ async function getLeaderboard(limit = 10) { try { // This would typically use a more efficient query const users = await User.findAll(); // Sort by points (this should be done at database level for efficiency) const sortedUsers = users .filter(user => user.points > 0) .sort((a, b) => b.points - a.points) .slice(0, limit); return sortedUsers.map((user, index) => ({ rank: index + 1, userId: user._id, username: user.username, points: user.points, badges: [], // Would populate if needed })); } catch (error) { console.error("Error getting leaderboard:", error); throw error; } } module.exports = { awardPoints, getUserPoints, getUserTransactionHistory, checkAndAwardBadges, awardBadge, getUserBadges, redeemPoints, getLeaderboard, POINT_VALUES, };