- 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>
314 lines
7.8 KiB
JavaScript
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,
|
|
}; |