Files
adopt-a-street/backend/routes/rewards.js
William Valentin e7396c10d6 feat(backend): implement complete gamification system
Implement comprehensive points and badges system with MongoDB transactions:

Point System:
- Create PointTransaction model for transaction history
- Award points atomically using MongoDB transactions
- Point values: street adoption (+100), task completion (+50), post creation (+10), event participation (+75)
- Track balance after each transaction
- Support point deduction for reward redemption

Badge System:
- Create Badge and UserBadge models
- Define badge criteria types: street_adoptions, task_completions, post_creations, event_participations, points_earned
- Auto-award badges based on user achievements
- Badge rarity levels: common, rare, epic, legendary
- Track badge progress for users
- Prevent duplicate badge awards

Gamification Service:
- Implement gamificationService.js with 390 lines of logic
- awardPoints() with transaction support
- checkAndAwardBadges() for auto-awarding
- getUserBadgeProgress() for progress tracking
- getUserStats() for achievement statistics
- Atomic operations prevent double-awarding

Integration:
- Streets route: Award points and badges on adoption
- Tasks route: Award points and badges on completion
- Posts route: Award points and badges on creation
- Events route: Award points and badges on RSVP
- Rewards route: Deduct points on redemption
- Badges API: List badges, track progress, view earned badges

Updated User Model:
- Add points field (default 0)
- Add earnedBadges virtual relationship
- Add indexes for performance (points for leaderboards)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 10:42:51 -07:00

115 lines
2.9 KiB
JavaScript

const express = require("express");
const mongoose = require("mongoose");
const Reward = require("../models/Reward");
const User = require("../models/User");
const auth = require("../middleware/auth");
const { asyncHandler } = require("../middleware/errorHandler");
const {
createRewardValidation,
rewardIdValidation,
} = require("../middleware/validators/rewardValidator");
const { paginate, buildPaginatedResponse } = require("../middleware/pagination");
const { deductRewardPoints } = require("../services/gamificationService");
const router = express.Router();
// Get all rewards (with pagination)
router.get(
"/",
paginate,
asyncHandler(async (req, res) => {
const { skip, limit, page } = req.pagination;
const rewards = await Reward.find()
.sort({ cost: 1 })
.skip(skip)
.limit(limit);
const totalCount = await Reward.countDocuments();
res.json(buildPaginatedResponse(rewards, totalCount, page, limit));
}),
);
// Create a reward
router.post(
"/",
auth,
createRewardValidation,
asyncHandler(async (req, res) => {
const { name, description, cost, isPremium } = req.body;
const newReward = new Reward({
name,
description,
cost,
isPremium,
});
const reward = await newReward.save();
res.json(reward);
}),
);
// Redeem a reward
router.post(
"/redeem/:id",
auth,
rewardIdValidation,
asyncHandler(async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
const reward = await Reward.findById(req.params.id).session(session);
if (!reward) {
await session.abortTransaction();
session.endSession();
return res.status(404).json({ msg: "Reward not found" });
}
const user = await User.findById(req.user.id).session(session);
if (!user) {
await session.abortTransaction();
session.endSession();
return res.status(404).json({ msg: "User not found" });
}
if (user.points < reward.cost) {
await session.abortTransaction();
session.endSession();
return res.status(400).json({ msg: "Not enough points" });
}
if (reward.isPremium && !user.isPremium) {
await session.abortTransaction();
session.endSession();
return res.status(403).json({ msg: "Premium reward not available" });
}
// Deduct points using gamification service
const { transaction } = await deductRewardPoints(
req.user.id,
reward._id,
reward.cost,
session
);
await session.commitTransaction();
session.endSession();
res.json({
msg: "Reward redeemed successfully",
pointsDeducted: Math.abs(transaction.amount),
newBalance: transaction.balanceAfter,
});
} catch (err) {
await session.abortTransaction();
session.endSession();
throw err;
}
}),
);
module.exports = router;