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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user