Files
adopt-a-street/backend/routes/rewards.js
William Valentin ae77e30ffb feat: implement API response caching with node-cache
- Add in-memory cache middleware with configurable TTL
- Cache GET endpoints: streets (5min), events (2min), posts (1min), rewards (10min)
- Automatic cache invalidation on POST/PUT/DELETE operations
- Add cache statistics endpoint (GET /api/cache/stats)
- Add cache management endpoint (DELETE /api/cache)
- Cache hit rate tracking and monitoring
- Pattern-based cache invalidation
- Optimized for Raspberry Pi deployment (lightweight in-memory)

🤖 Generated with Claude

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 13:25:50 -08:00

258 lines
6.2 KiB
JavaScript

const express = require("express");
const Reward = require("../models/Reward");
const auth = require("../middleware/auth");
const { asyncHandler } = require("../middleware/errorHandler");
const {
createRewardValidation,
rewardIdValidation,
} = require("../middleware/validators/rewardValidator");
const { paginate, buildPaginatedResponse } = require("../middleware/pagination");
const { getCacheMiddleware, invalidateCacheByPattern } = require("../middleware/cache");
const router = express.Router();
// Get all rewards (with pagination)
router.get(
"/",
paginate,
getCacheMiddleware(600), // Cache for 10 minutes
asyncHandler(async (req, res) => {
const { page, limit } = req.pagination;
const result = await Reward.getAllPaginated(page, limit);
res.json(buildPaginatedResponse(result.rewards, result.pagination.totalCount, page, limit));
}),
);
// Create a reward
router.post(
"/",
auth,
createRewardValidation,
asyncHandler(async (req, res) => {
const { name, description, cost, isPremium } = req.body;
const reward = await Reward.create({
name,
description,
cost,
isPremium,
});
// Invalidate rewards cache
invalidateCacheByPattern('/api/rewards');
res.json(reward);
}),
);
// Redeem a reward
router.post(
"/redeem/:id",
auth,
rewardIdValidation,
asyncHandler(async (req, res) => {
const rewardId = req.params.id;
const userId = req.user.id;
try {
const result = await Reward.redeemReward(userId, rewardId);
res.json({
msg: "Reward redeemed successfully",
pointsDeducted: result.pointsDeducted,
newBalance: result.newBalance,
redemption: result.redemption
});
} catch (error) {
if (error.message === "Reward not found") {
return res.status(404).json({ msg: "Reward not found" });
}
if (error.message === "Reward is not available") {
return res.status(400).json({ msg: "Reward is not available" });
}
if (error.message === "User not found") {
return res.status(404).json({ msg: "User not found" });
}
if (error.message === "Not enough points") {
return res.status(400).json({ msg: "Not enough points" });
}
if (error.message === "Premium reward not available") {
return res.status(403).json({ msg: "Premium reward not available" });
}
throw error;
}
}),
);
// Get reward by ID
router.get(
"/:id",
rewardIdValidation,
asyncHandler(async (req, res) => {
const reward = await Reward.findById(req.params.id);
if (!reward) {
return res.status(404).json({ msg: "Reward not found" });
}
res.json(reward);
})
);
// Update reward
router.put(
"/:id",
auth,
rewardIdValidation,
createRewardValidation,
asyncHandler(async (req, res) => {
const { name, description, cost, isPremium, isActive } = req.body;
const reward = await Reward.findById(req.params.id);
if (!reward) {
return res.status(404).json({ msg: "Reward not found" });
}
const updateData = { name, description, cost, isPremium };
if (isActive !== undefined) {
updateData.isActive = isActive;
}
const updatedReward = await Reward.update(req.params.id, updateData);
res.json(updatedReward);
})
);
// Delete reward
router.delete(
"/:id",
auth,
rewardIdValidation,
asyncHandler(async (req, res) => {
const reward = await Reward.findById(req.params.id);
if (!reward) {
return res.status(404).json({ msg: "Reward not found" });
}
await Reward.delete(req.params.id);
res.json({ msg: "Reward deleted successfully" });
})
);
// Get active rewards only
router.get(
"/active/list",
asyncHandler(async (req, res) => {
const rewards = await Reward.getActiveRewards();
res.json(rewards);
})
);
// Get premium rewards
router.get(
"/premium/list",
asyncHandler(async (req, res) => {
const rewards = await Reward.getPremiumRewards();
res.json(rewards);
})
);
// Get regular rewards
router.get(
"/regular/list",
asyncHandler(async (req, res) => {
const rewards = await Reward.getRegularRewards();
res.json(rewards);
})
);
// Get rewards by cost range
router.get(
"/cost-range/:min/:max",
asyncHandler(async (req, res) => {
const { min, max } = req.params;
const rewards = await Reward.findByCostRange(parseInt(min), parseInt(max));
res.json(rewards);
})
);
// Get user's redemption history
router.get(
"/redemptions/user/:userId",
auth,
asyncHandler(async (req, res) => {
const { userId } = req.params;
const { limit = 20 } = req.query;
// Only allow users to see their own redemption history
if (userId !== req.user.id) {
return res.status(403).json({ msg: "Access denied" });
}
const redemptions = await Reward.getUserRedemptions(userId, parseInt(limit));
res.json(redemptions);
})
);
// Get reward statistics
router.get(
"/stats/:id",
rewardIdValidation,
asyncHandler(async (req, res) => {
const stats = await Reward.getRewardStats(req.params.id);
res.json(stats);
})
);
// Get catalog statistics
router.get(
"/catalog/stats",
auth,
asyncHandler(async (req, res) => {
const stats = await Reward.getCatalogStats();
res.json(stats);
})
);
// Search rewards
router.get(
"/search/:term",
asyncHandler(async (req, res) => {
const { term } = req.params;
const { limit = 20 } = req.query;
const rewards = await Reward.searchRewards(term, { limit: parseInt(limit) });
res.json(rewards);
})
);
// Toggle reward active status
router.patch(
"/:id/toggle",
auth,
rewardIdValidation,
asyncHandler(async (req, res) => {
const updatedReward = await Reward.toggleActiveStatus(req.params.id);
res.json(updatedReward);
})
);
// Bulk create rewards (admin only)
router.post(
"/bulk",
auth,
asyncHandler(async (req, res) => {
const { rewards } = req.body;
if (!Array.isArray(rewards) || rewards.length === 0) {
return res.status(400).json({ msg: "Invalid rewards array" });
}
const result = await Reward.bulkCreate(rewards);
res.json({
msg: `Created ${result.length} rewards`,
rewards: result
});
})
);
module.exports = router;