Files
adopt-a-street/backend/models/PointTransaction.js
William Valentin 0cc3d508e1 feat: Complete standardized error handling across all models
- Create comprehensive error infrastructure in backend/utils/modelErrors.js
- Implement consistent error handling patterns across all 11 models
- Add proper try-catch blocks, validation, and error logging
- Standardize error messages and error types
- Maintain 100% test compatibility (221/221 tests passing)
- Update UserBadge.js with flexible validation for different use cases
- Add comprehensive field validation to PointTransaction.js
- Improve constructor validation in Street.js and Task.js
- Enhance error handling in Badge.js with backward compatibility

Models updated:
- User.js, Post.js, Report.js (Phase 1)
- Event.js, Reward.js, Comment.js (Phase 2)
- Street.js, Task.js, Badge.js, PointTransaction.js, UserBadge.js (Phase 3)

🤖 Generated with [AI Assistant]

Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
2025-11-03 10:30:58 -08:00

225 lines
7.9 KiB
JavaScript

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;