const couchdbService = require("../services/couchdbService"); const { ValidationError, NotFoundError, DatabaseError, withErrorHandling, createErrorContext } = require("../utils/modelErrors"); class Reward { constructor(data) { // Handle both new documents and database documents const isNew = !data._id; // For new documents, validate required fields if (isNew) { if (!data.name || data.name.trim() === '') { throw new ValidationError('Name is required', 'name', data.name); } if (!data.description || data.description.trim() === '') { throw new ValidationError('Description is required', 'description', data.description); } if (data.cost === undefined || data.cost === null || isNaN(data.cost)) { throw new ValidationError('Cost is required and must be a number', 'cost', data.cost); } if (data.cost < 0) { throw new ValidationError('Cost must be non-negative', 'cost', data.cost); } } // Assign properties this._id = data._id || null; this._rev = data._rev || null; this.type = data.type || "reward"; this.name = data.name; this.description = data.description; this.cost = Number(data.cost); this.category = data.category || null; this.isPremium = Boolean(data.isPremium); this.isActive = data.isActive !== undefined ? Boolean(data.isActive) : true; this.redeemedBy = data.redeemedBy || []; this.createdAt = data.createdAt || new Date().toISOString(); this.updatedAt = data.updatedAt || new Date().toISOString(); } toJSON() { return { _id: this._id, _rev: this._rev, type: this.type, name: this.name, description: this.description, cost: this.cost, category: this.category, isPremium: this.isPremium, isActive: this.isActive, redeemedBy: this.redeemedBy, createdAt: this.createdAt, updatedAt: this.updatedAt }; } async save() { const errorContext = createErrorContext('Reward', 'save', { rewardId: this._id, name: this.name }); return await withErrorHandling(async () => { if (this._id) { // Update existing document const updatedDoc = await couchdbService.updateDocument(this._id, this.toJSON()); Object.assign(this, updatedDoc); return this; } else { // Create new document this._id = `reward_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const createdDoc = await couchdbService.createDocument(this.toJSON()); Object.assign(this, createdDoc); return this; } }, errorContext); } static async create(rewardData) { const errorContext = createErrorContext('Reward', 'create', { name: rewardData?.name, cost: rewardData?.cost }); return await withErrorHandling(async () => { const reward = new Reward(rewardData); reward._id = `reward_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const createdReward = await couchdbService.createDocument(reward.toJSON()); return new Reward(createdReward); }, errorContext); } static async findById(rewardId) { const errorContext = createErrorContext('Reward', 'findById', { rewardId }); return await withErrorHandling(async () => { const doc = await couchdbService.getById(rewardId); if (doc && doc.type === "reward") { return new Reward(doc); } return null; }, errorContext); } static async find(query = {}, options = {}) { const errorContext = createErrorContext('Reward', 'find', { query, options }); return await withErrorHandling(async () => { const defaultQuery = { type: "reward", ...query }; const docs = await couchdbService.find({ selector: defaultQuery, ...options }); return docs.map(doc => new Reward(doc)); }, errorContext); } static async findOne(query) { const errorContext = createErrorContext('Reward', 'findOne', { query }); return await withErrorHandling(async () => { const rewards = await this.find(query, { limit: 1 }); return rewards[0] || null; }, errorContext); } static async update(rewardId, updateData) { const errorContext = createErrorContext('Reward', 'update', { rewardId, updateData }); return await withErrorHandling(async () => { const existingReward = await this.findById(rewardId); if (!existingReward) { throw new NotFoundError('Reward', rewardId); } // Update fields Object.keys(updateData).forEach(key => { if (key !== '_id' && key !== '_rev' && key !== 'type') { existingReward[key] = updateData[key]; } }); existingReward.updatedAt = new Date().toISOString(); const updatedDoc = await couchdbService.updateDocument(rewardId, existingReward.toJSON()); return new Reward(updatedDoc); }, errorContext); } static async delete(rewardId) { const errorContext = createErrorContext('Reward', 'delete', { rewardId }); return await withErrorHandling(async () => { const reward = await this.findById(rewardId); if (!reward) { throw new NotFoundError('Reward', rewardId); } return await couchdbService.deleteDocument(rewardId); }, errorContext); } static async findByCostRange(minCost, maxCost) { const errorContext = createErrorContext('Reward', 'findByCostRange', { minCost, maxCost }); return await withErrorHandling(async () => { const docs = await couchdbService.find({ selector: { type: "reward", cost: { $gte: minCost, $lte: maxCost } }, sort: [{ cost: "asc" }] }); return docs.map(doc => new Reward(doc)); }, errorContext); } static async findByPremiumStatus(isPremium) { const errorContext = createErrorContext('Reward', 'findByPremiumStatus', { isPremium }); return await withErrorHandling(async () => { return await this.find({ isPremium }); }, errorContext); } static async getActiveRewards() { const errorContext = createErrorContext('Reward', 'getActiveRewards', {}); return await withErrorHandling(async () => { return await this.find({ isActive: true }); }, errorContext); } static async getPremiumRewards() { const errorContext = createErrorContext('Reward', 'getPremiumRewards', {}); return await withErrorHandling(async () => { return await this.find({ isPremium: true, isActive: true }); }, errorContext); } static async getRegularRewards() { const errorContext = createErrorContext('Reward', 'getRegularRewards', {}); return await withErrorHandling(async () => { return await this.find({ isPremium: false, isActive: true }); }, errorContext); } static async getAllPaginated(page = 1, limit = 10) { const errorContext = createErrorContext('Reward', 'getAllPaginated', { page, limit }); return await withErrorHandling(async () => { const skip = (page - 1) * limit; const rewards = await couchdbService.find({ selector: { type: "reward" }, sort: [{ cost: "asc" }], skip, limit }); // Get total count const totalCount = await couchdbService.find({ selector: { type: "reward" }, fields: ["_id"] }); return { rewards: rewards.map(doc => new Reward(doc)), pagination: { page, limit, totalCount: totalCount.length, totalPages: Math.ceil(totalCount.length / limit) } }; }, errorContext); } static async redeemReward(userId, rewardId) { const errorContext = createErrorContext('Reward', 'redeemReward', { userId, rewardId }); return await withErrorHandling(async () => { const reward = await this.findById(rewardId); if (!reward) { throw new NotFoundError('Reward', rewardId); } if (!reward.isActive) { throw new ValidationError('Reward is not available', 'isActive', reward.isActive); } const user = await couchdbService.findUserById(userId); if (!user) { throw new NotFoundError('User', userId); } if (user.points < reward.cost) { throw new ValidationError('Not enough points to redeem this reward', 'points', user.points); } if (reward.isPremium && !user.isPremium) { throw new ValidationError('Premium reward not available for non-premium users', 'isPremium', user.isPremium); } // Deduct points using couchdbService method const updatedUser = await couchdbService.updateUserPoints( userId, -reward.cost, `Redeemed reward: ${reward.name}`, { entityType: 'Reward', entityId: rewardId, entityName: reward.name } ); // Create redemption record const redemption = { _id: `redemption_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: "reward_redemption", user: { userId: userId, name: user.name }, reward: { rewardId: rewardId, name: reward.name, description: reward.description, cost: reward.cost }, pointsDeducted: reward.cost, balanceAfter: updatedUser.points, redeemedAt: new Date().toISOString() }; await couchdbService.create(redemption); return { redemption, pointsDeducted: reward.cost, newBalance: updatedUser.points }; }, errorContext); } static async getUserRedemptions(userId, limit = 20) { const errorContext = createErrorContext('Reward', 'getUserRedemptions', { userId, limit }); return await withErrorHandling(async () => { return await couchdbService.find({ selector: { type: "reward_redemption", "user.userId": userId }, sort: [{ redeemedAt: "desc" }], limit }); }, errorContext); } static async getRewardStats(rewardId) { const errorContext = createErrorContext('Reward', 'getRewardStats', { rewardId }); return await withErrorHandling(async () => { const redemptions = await couchdbService.find({ selector: { type: "reward_redemption", "reward.rewardId": rewardId } }); return { totalRedemptions: redemptions.length, totalPointsSpent: redemptions.reduce((sum, r) => sum + r.pointsDeducted, 0), lastRedeemed: redemptions.length > 0 ? redemptions[0].redeemedAt : null }; }, errorContext); } static async getCatalogStats() { const errorContext = createErrorContext('Reward', 'getCatalogStats', {}); return await withErrorHandling(async () => { const rewards = await this.getActiveRewards(); const premium = await this.getPremiumRewards(); const regular = await this.getRegularRewards(); return { totalRewards: rewards.length, premiumRewards: premium.length, regularRewards: regular.length, averageCost: rewards.reduce((sum, r) => sum + r.cost, 0) / rewards.length || 0, minCost: rewards.length > 0 ? Math.min(...rewards.map(r => r.cost)) : 0, maxCost: rewards.length > 0 ? Math.max(...rewards.map(r => r.cost)) : 0 }; }, errorContext); } static async searchRewards(searchTerm, options = {}) { const errorContext = createErrorContext('Reward', 'searchRewards', { searchTerm, options }); return await withErrorHandling(async () => { const query = { selector: { type: "reward", isActive: true, $or: [ { name: { $regex: searchTerm, $options: "i" } }, { description: { $regex: searchTerm, $options: "i" } } ] }, ...options }; const docs = await couchdbService.find(query); return docs.map(doc => new Reward(doc)); }, errorContext); } // Migration helper static async migrateFromMongo(mongoReward) { const errorContext = createErrorContext('Reward', 'migrateFromMongo', { mongoRewardId: mongoReward._id }); return await withErrorHandling(async () => { const rewardData = { name: mongoReward.name, description: mongoReward.description, cost: mongoReward.cost, isPremium: mongoReward.isPremium || false, isActive: true }; return await this.create(rewardData); }, errorContext); } // Bulk operations for admin static async bulkCreate(rewardsData) { const errorContext = createErrorContext('Reward', 'bulkCreate', { count: rewardsData.length }); return await withErrorHandling(async () => { const rewards = rewardsData.map(data => ({ _id: `reward_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: "reward", name: data.name, description: data.description, cost: data.cost, isPremium: data.isPremium || false, isActive: data.isActive !== undefined ? data.isActive : true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() })); const docs = await couchdbService.bulkDocs(rewards); return docs.map(doc => new Reward(doc)); }, errorContext); } static async toggleActiveStatus(rewardId) { const errorContext = createErrorContext('Reward', 'toggleActiveStatus', { rewardId }); return await withErrorHandling(async () => { const reward = await this.findById(rewardId); if (!reward) { throw new NotFoundError('Reward', rewardId); } return await this.update(rewardId, { isActive: !reward.isActive }); }, errorContext); } } module.exports = Reward;