const couchdbService = require("../services/couchdbService"); const { ValidationError, NotFoundError, DatabaseError, withErrorHandling, createErrorContext } = require("../utils/modelErrors"); class PointTransaction { constructor(transactionData) { this.validate(transactionData); Object.assign(this, transactionData); } validate(transactionData) { // Validate required fields if (!transactionData.user || transactionData.user.trim() === '') { throw new ValidationError('User field is required', 'user', transactionData.user); } if (transactionData.amount === undefined || transactionData.amount === null) { throw new ValidationError('Amount field is required', 'amount', transactionData.amount); } if (!transactionData.transactionType || transactionData.transactionType.trim() === '') { throw new ValidationError('Transaction type field is required', 'transactionType', transactionData.transactionType); } if (!transactionData.description || transactionData.description.trim() === '') { throw new ValidationError('Description field is required', 'description', transactionData.description); } // Validate transaction type const validTypes = ['earned', 'spent', 'bonus', 'penalty', 'refund']; if (!validTypes.includes(transactionData.transactionType)) { throw new ValidationError(`Transaction type must be one of: ${validTypes.join(', ')}`, 'transactionType', transactionData.transactionType); } // Validate amount based on transaction type if (transactionData.transactionType === 'earned' && transactionData.amount <= 0) { throw new ValidationError('Earned transactions must have positive amount', 'amount', transactionData.amount); } if (transactionData.transactionType === 'spent' && transactionData.amount >= 0) { throw new ValidationError('Spent transactions must have negative amount', 'amount', transactionData.amount); } if (transactionData.transactionType === 'bonus' && transactionData.amount <= 0) { throw new ValidationError('Bonus transactions must have positive amount', 'amount', transactionData.amount); } if (transactionData.transactionType === 'penalty' && transactionData.amount >= 0) { throw new ValidationError('Penalty transactions must have negative amount', 'amount', transactionData.amount); } // Validate source type if provided if (transactionData.relatedEntity && transactionData.relatedEntity.type) { const validSourceTypes = ['task_completion', 'street_adoption', 'event_participation', 'reward_redemption', 'badge_earned', 'manual_adjustment', 'system_bonus']; if (!validSourceTypes.includes(transactionData.relatedEntity.type)) { throw new ValidationError(`Source type must be one of: ${validSourceTypes.join(', ')}`, 'relatedEntity.type', transactionData.relatedEntity.type); } } } static async create(transactionData) { const errorContext = createErrorContext('PointTransaction', 'create', { user: transactionData.user, amount: transactionData.amount, transactionType: transactionData.transactionType }); return await withErrorHandling(async () => { // Validate using constructor new PointTransaction(transactionData); const transaction = { _id: `point_transaction_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, type: 'point_transaction', user: transactionData.user, amount: transactionData.amount, transactionType: transactionData.transactionType, description: transactionData.description.trim(), relatedEntity: transactionData.relatedEntity || null, balanceAfter: transactionData.balanceAfter, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; const result = await couchdbService.createDocument(transaction); return { ...transaction, _rev: result.rev }; }, errorContext); } static async findByUser(userId, limit = 50, skip = 0) { const errorContext = createErrorContext('PointTransaction', 'findByUser', { userId, limit, skip }); return await withErrorHandling(async () => { const result = await couchdbService.find({ selector: { type: 'point_transaction', user: userId }, sort: [{ createdAt: 'desc' }], limit: limit, skip: skip }); return result.docs; }, errorContext); } static async findByType(transactionType, limit = 50, skip = 0) { const errorContext = createErrorContext('PointTransaction', 'findByType', { transactionType, limit, skip }); return await withErrorHandling(async () => { const result = await couchdbService.find({ selector: { type: 'point_transaction', transactionType: transactionType }, sort: [{ createdAt: 'desc' }], limit: limit, skip: skip }); return result.docs; }, errorContext); } static async findById(id) { const errorContext = createErrorContext('PointTransaction', 'findById', { transactionId: id }); return await withErrorHandling(async () => { try { const transaction = await couchdbService.getDocument(id); if (!transaction || transaction.type !== 'point_transaction') { return null; } return transaction; } catch (error) { if (error.statusCode === 404) { return null; } throw error; } }, errorContext); } static async getUserBalance(userId) { const errorContext = createErrorContext('PointTransaction', 'getUserBalance', { userId }); return await withErrorHandling(async () => { // Get the most recent transaction for the user to find current balance const result = await couchdbService.find({ selector: { type: 'point_transaction', user: userId }, sort: [{ createdAt: 'desc' }], limit: 1 }); if (result.docs.length === 0) { return 0; } return result.docs[0].balanceAfter; }, errorContext); } static async getUserTransactionHistory(userId, startDate, endDate) { const errorContext = createErrorContext('PointTransaction', 'getUserTransactionHistory', { userId, startDate, endDate }); return await withErrorHandling(async () => { const selector = { type: 'point_transaction', user: userId }; if (startDate || endDate) { selector.createdAt = {}; if (startDate) { selector.createdAt.$gte = startDate; } if (endDate) { selector.createdAt.$lte = endDate; } } const result = await couchdbService.find({ selector: selector, sort: [{ createdAt: 'desc' }] }); return result.docs; }, errorContext); } static async getTransactionStats(userId, startDate, endDate) { const errorContext = createErrorContext('PointTransaction', 'getTransactionStats', { userId, startDate, endDate }); return await withErrorHandling(async () => { const transactions = await this.getUserTransactionHistory(userId, startDate, endDate); const stats = { totalEarned: 0, totalSpent: 0, transactionCount: transactions.length, transactionBreakdown: {} }; transactions.forEach(transaction => { if (transaction.amount > 0) { stats.totalEarned += transaction.amount; } else { stats.totalSpent += Math.abs(transaction.amount); } const type = transaction.transactionType; if (!stats.transactionBreakdown[type]) { stats.transactionBreakdown[type] = { count: 0, total: 0 }; } stats.transactionBreakdown[type].count++; stats.transactionBreakdown[type].total += transaction.amount; }); return stats; }, errorContext); } } module.exports = PointTransaction;