Files
adopt-a-street/backend/services/gamificationService.js
William Valentin 5efee88655 feat: complete MongoDB to CouchDB migration and deployment
- Remove all mongoose dependencies from backend
- Convert Badge and PointTransaction models to CouchDB
- Fix gamificationService for CouchDB architecture
- Update Docker registry URLs to use HTTPS (port 443)
- Fix ingress configuration for HAProxy
- Successfully deploy multi-architecture images
- Application fully running on Kubernetes with CouchDB

🤖 Generated with [AI Assistant]

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-11-02 14:39:49 -08:00

314 lines
7.8 KiB
JavaScript

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<Object>} - 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,
};