const database = require('./database'); class Product { constructor(data = {}) { this.id = data.id || null; this.name = data.name || ''; this.description = data.description || ''; this.category = data.category || ''; this.quantity = data.quantity || 0; this.unit = data.unit || ''; this.barcode = data.barcode || null; this.qr_code = data.qr_code || null; this.location = data.location || ''; this.min_stock_level = data.min_stock_level || 0; this.created_at = data.created_at || null; this.updated_at = data.updated_at || null; } /** * Validate product data * @returns {Object} validation result with isValid boolean and errors array */ validate() { const errors = []; // Required field validation if (!this.name || this.name.trim().length === 0) { errors.push('Product name is required'); } if (this.name && this.name.length > 255) { errors.push('Product name must be less than 255 characters'); } // Quantity validation if (typeof this.quantity !== 'number' || this.quantity < 0) { errors.push('Quantity must be a non-negative number'); } // Min stock level validation if (typeof this.min_stock_level !== 'number' || this.min_stock_level < 0) { errors.push('Minimum stock level must be a non-negative number'); } // Barcode validation (if provided) if (this.barcode && (typeof this.barcode !== 'string' || this.barcode.trim().length === 0)) { errors.push('Barcode must be a non-empty string if provided'); } // Category validation if (this.category && this.category.length > 100) { errors.push('Category must be less than 100 characters'); } // Unit validation if (this.unit && this.unit.length > 20) { errors.push('Unit must be less than 20 characters'); } return { isValid: errors.length === 0, errors: errors }; } /** * Save product to database * @returns {Promise} saved product instance */ async save() { const validation = this.validate(); if (!validation.isValid) { throw new Error(`Validation failed: ${validation.errors.join(', ')}`); } const db = database.getDatabase(); try { if (this.id) { // Update existing product const stmt = db.prepare(` UPDATE items SET name = ?, description = ?, category = ?, quantity = ?, unit = ?, barcode = ?, qr_code = ?, location = ?, min_stock_level = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `); stmt.run( this.name, this.description, this.category, this.quantity, this.unit, this.barcode, this.qr_code, this.location, this.min_stock_level, this.id ); } else { // Insert new product const stmt = db.prepare(` INSERT INTO items (name, description, category, quantity, unit, barcode, qr_code, location, min_stock_level) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run( this.name, this.description, this.category, this.quantity, this.unit, this.barcode, this.qr_code, this.location, this.min_stock_level ); this.id = result.lastInsertRowid; } return this; } catch (error) { if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { throw new Error('A product with this barcode already exists'); } throw error; } } /** * Find product by ID * @param {number} id - Product ID * @returns {Promise} Product instance or null if not found */ static async findById(id) { const db = database.getDatabase(); const stmt = db.prepare('SELECT * FROM items WHERE id = ?'); const row = stmt.get(id); return row ? new Product(row) : null; } /** * Find product by barcode * @param {string} barcode - Product barcode * @returns {Promise} Product instance or null if not found */ static async findByBarcode(barcode) { const db = database.getDatabase(); const stmt = db.prepare('SELECT * FROM items WHERE barcode = ?'); const row = stmt.get(barcode); return row ? new Product(row) : null; } /** * Get all products with optional filtering * @param {Object} filters - Optional filters * @returns {Promise} Array of Product instances */ static async findAll(filters = {}) { const db = database.getDatabase(); let query = 'SELECT * FROM items'; const params = []; const conditions = []; if (filters.category) { conditions.push('category = ?'); params.push(filters.category); } if (filters.name) { conditions.push('name LIKE ?'); params.push(`%${filters.name}%`); } if (conditions.length > 0) { query += ' WHERE ' + conditions.join(' AND '); } query += ' ORDER BY name'; const stmt = db.prepare(query); const rows = stmt.all(...params); return rows.map(row => new Product(row)); } /** * Delete product by ID * @param {number} id - Product ID * @returns {Promise} true if deleted, false if not found */ static async deleteById(id) { const db = database.getDatabase(); const stmt = db.prepare('DELETE FROM items WHERE id = ?'); const result = stmt.run(id); return result.changes > 0; } /** * Convert product to plain object * @returns {Object} Plain object representation */ toJSON() { return { id: this.id, name: this.name, description: this.description, category: this.category, quantity: this.quantity, unit: this.unit, barcode: this.barcode, qr_code: this.qr_code, location: this.location, min_stock_level: this.min_stock_level, created_at: this.created_at, updated_at: this.updated_at }; } /** * Get all unique categories * @returns {Promise} Array of unique category names */ static async getCategories() { const db = database.getDatabase(); try { const stmt = db.prepare(` SELECT DISTINCT category FROM products WHERE category IS NOT NULL AND category != '' ORDER BY category `); const rows = stmt.all(); return rows.map(row => row.category); } catch (error) { console.error('Error getting categories:', error); throw error; } } } module.exports = Product;