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>
This commit is contained in:
William Valentin
2025-11-03 10:30:58 -08:00
parent 742d1cac56
commit 0cc3d508e1
7 changed files with 776 additions and 574 deletions

View File

@@ -1,31 +1,95 @@
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) {
try {
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,
description: transactionData.description.trim(),
relatedEntity: transactionData.relatedEntity || null,
balanceAfter: transactionData.balanceAfter,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
const result = await couchdbService.insert(transaction);
const result = await couchdbService.createDocument(transaction);
return { ...transaction, _rev: result.rev };
} catch (error) {
console.error('Error creating point transaction:', error);
throw error;
}
}, errorContext);
}
static async findByUser(userId, limit = 50, skip = 0) {
try {
const errorContext = createErrorContext('PointTransaction', 'findByUser', { userId, limit, skip });
return await withErrorHandling(async () => {
const result = await couchdbService.find({
selector: {
type: 'point_transaction',
@@ -36,14 +100,13 @@ class PointTransaction {
skip: skip
});
return result.docs;
} catch (error) {
console.error('Error finding point transactions by user:', error);
throw error;
}
}, errorContext);
}
static async findByType(transactionType, limit = 50, skip = 0) {
try {
const errorContext = createErrorContext('PointTransaction', 'findByType', { transactionType, limit, skip });
return await withErrorHandling(async () => {
const result = await couchdbService.find({
selector: {
type: 'point_transaction',
@@ -54,30 +117,32 @@ class PointTransaction {
skip: skip
});
return result.docs;
} catch (error) {
console.error('Error finding point transactions by type:', error);
throw error;
}
}, errorContext);
}
static async findById(id) {
try {
const transaction = await couchdbService.get(id);
if (transaction.type !== 'point_transaction') {
return null;
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;
}
return transaction;
} catch (error) {
if (error.statusCode === 404) {
return null;
}
console.error('Error finding point transaction by ID:', error);
throw error;
}
}, errorContext);
}
static async getUserBalance(userId) {
try {
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: {
@@ -93,14 +158,13 @@ class PointTransaction {
}
return result.docs[0].balanceAfter;
} catch (error) {
console.error('Error getting user balance:', error);
throw error;
}
}, errorContext);
}
static async getUserTransactionHistory(userId, startDate, endDate) {
try {
const errorContext = createErrorContext('PointTransaction', 'getUserTransactionHistory', { userId, startDate, endDate });
return await withErrorHandling(async () => {
const selector = {
type: 'point_transaction',
user: userId
@@ -122,14 +186,13 @@ class PointTransaction {
});
return result.docs;
} catch (error) {
console.error('Error getting user transaction history:', error);
throw error;
}
}, errorContext);
}
static async getTransactionStats(userId, startDate, endDate) {
try {
const errorContext = createErrorContext('PointTransaction', 'getTransactionStats', { userId, startDate, endDate });
return await withErrorHandling(async () => {
const transactions = await this.getUserTransactionHistory(userId, startDate, endDate);
const stats = {
@@ -155,10 +218,7 @@ class PointTransaction {
});
return stats;
} catch (error) {
console.error('Error getting transaction stats:', error);
throw error;
}
}, errorContext);
}
}