generate_test_products.js (12061B)
1 #!/usr/bin/env node 2 3 /** 4 * Generate realistic product test data for OtelMart 5 * 6 * This script generates product data that matches the UI expectations: 7 * - ProductCard interface (for list view) 8 * - Product interface (for detail view) 9 * 10 * Generated fields: 11 * - Basic info: name, brand, description 12 * - Pricing: price, initial_price, discount 13 * - Inventory: stock_quantity 14 * - Categories: with hierarchical support 15 * - Attributes: sizes, colors (for applicable products) 16 * - Ratings: sample ratings for some products 17 */ 18 19 const fs = require('fs'); 20 const {randomUUID} = require('crypto'); 21 22 // ===================================================================== 23 // Configuration 24 // ===================================================================== 25 26 const NUM_PRODUCTS = 2000; 27 const NUM_RATINGS = 500; // Number of sample ratings to generate 28 const OUTPUT_FILE = './test_data.sql'; 29 30 // ===================================================================== 31 // Data Templates 32 // ===================================================================== 33 34 const BRANDS = [ 35 'TechPro', 'CoreTech', 'SmartLife', 'HomeEssentials', 'StyleHub', 36 'SportMax', 'EcoGear', 'PremiumChoice', 'ValueBrand', 'EliteGoods', 37 'FastTrack', 'PureQuality', 'ModernHome', 'ActiveLife', 'ComfortZone' 38 ]; 39 40 const PRODUCT_TYPES = { 41 smartphones: ['iPhone Pro', 'Galaxy Ultra', 'Pixel Premium', 'OnePlus Flagship'], 42 laptops: ['MacBook Pro', 'ThinkPad Elite', 'XPS Premium', 'Surface Laptop'], 43 accessories: ['Wireless Mouse', 'Keyboard', 'USB Hub', 'Phone Case', 'Screen Protector', 'Charger', 'Cable', 'Earbuds'], 44 smartHome: ['Smart Speaker', 'Security Camera', 'Thermostat', 'Video Doorbell', 'Light Bulb', 'Smart Lock'], 45 mensClothing: ['T-Shirt', 'Jeans', 'Jacket', 'Hoodie', 'Shirt', 'Pants'], 46 womensClothing: ['Dress', 'Blouse', 'Skirt', 'Leggings', 'Sweater'], 47 shoes: ['Running Shoes', 'Sneakers', 'Boots', 'Sandals'], 48 homeGarden: ['Garden Tools', 'Furniture', 'Decor', 'Kitchen Set'], 49 sports: ['Yoga Mat', 'Dumbbells', 'Resistance Bands', 'Treadmill'], 50 books: ['Fiction Novel', 'Cookbook', 'Self-Help Book', 'Technical Manual'], 51 health: ['Vitamins', 'Face Cream', 'Shampoo', 'Supplements'], 52 toys: ['Board Game', 'Action Figure', 'Puzzle', 'LEGO Set'], 53 automotive: ['Car Cover', 'Floor Mats', 'Phone Mount', 'HDMI Cable'] 54 }; 55 56 const ADJECTIVES = [ 57 'Premium', 'Elite', 'Pro', 'Ultra', 'Smart', 'Eco-Friendly', 58 'Wireless', 'Portable', 'Compact', 'Heavy-Duty', 'Deluxe', 59 'Advanced', 'Professional', 'Classic', 'Modern', 'Innovative' 60 ]; 61 62 const SIZES = { 63 clothing: ['XS', 'S', 'M', 'L', 'XL', 'XXL'], 64 shoes: ['7', '8', '9', '10', '11', '12'], 65 default: [] 66 }; 67 68 const COLORS = { 69 electronics: ['Black', 'Silver', 'White', 'Space Gray'], 70 clothing: ['Black', 'White', 'Navy', 'Gray', 'Red', 'Blue', 'Green'], 71 shoes: ['Black', 'White', 'Brown', 'Gray'], 72 default: [] 73 }; 74 75 // Map product types to categories (will be replaced with DB category IDs) 76 const CATEGORY_MAP = { 77 smartphones: 'Smartphones', 78 laptops: 'Laptops', 79 accessories: 'Accessories', 80 smartHome: 'Smart Home', 81 mensClothing: "Men's Clothing", 82 womensClothing: "Women's Clothing", 83 shoes: 'Shoes', 84 homeGarden: 'Home & Garden', 85 sports: 'Sports & Outdoors', 86 books: 'Books & Media', 87 health: 'Health & Beauty', 88 toys: 'Toys & Games', 89 automotive: 'Automotive' 90 }; 91 92 // ===================================================================== 93 // Helper Functions 94 // ===================================================================== 95 96 function randomInt(min, max) { 97 return Math.floor(Math.random() * (max - min + 1)) + min; 98 } 99 100 function randomElement(array) { 101 return array[randomInt(0, array.length - 1)]; 102 } 103 104 function randomBool(probability = 0.5) { 105 return Math.random() < probability; 106 } 107 108 let skuCounter = 10000; 109 function generateSKU() { 110 const prefix = randomElement(['PRD', 'SKU', 'ITM']); 111 return `${prefix}-${skuCounter++}`; 112 } 113 114 let asinCounter = 100000; 115 function generateASIN() { 116 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 117 let asin = 'B'; 118 const counterStr = asinCounter.toString().padStart(8, '0'); 119 asin += counterStr; 120 const checkChar = chars[asinCounter % chars.length]; 121 asin += checkChar; 122 asinCounter++; 123 return asin; 124 } 125 126 function generateDescription(productName, brand) { 127 const templates = [ 128 `${productName} from ${brand}. High-quality product with excellent features and durability.`, 129 `Experience the best with this ${productName}. Perfect for everyday use and built to last.`, 130 `${brand}'s ${productName} combines style and functionality in one amazing package.`, 131 `Premium ${productName} designed for maximum performance and user satisfaction.`, 132 `Innovative ${productName} featuring cutting-edge technology and modern design.` 133 ]; 134 return randomElement(templates); 135 } 136 137 function generatePrice() { 138 const basePrice = randomInt(10, 500); 139 const cents = randomElement([99, 95, 50, 00]); 140 return `${basePrice}.${cents.toString().padStart(2, '0')}`; 141 } 142 143 function calculateDiscount(initialPrice, finalPrice) { 144 const initial = parseFloat(initialPrice); 145 const final = parseFloat(finalPrice); 146 147 if (initial <= final) return null; 148 149 const percentage = ((initial - final) / initial * 100).toFixed(0); 150 return `${percentage}%`; 151 } 152 153 function getSizesForCategory(category) { 154 if (category.includes('Clothing')) return SIZES.clothing; 155 if (category.includes('Shoes')) return SIZES.shoes; 156 return SIZES.default; 157 } 158 159 function getColorsForCategory(category) { 160 if (category.includes('Electronics') || category.includes('Smart')) return COLORS.electronics; 161 if (category.includes('Clothing')) return COLORS.clothing; 162 if (category.includes('Shoes')) return COLORS.shoes; 163 return COLORS.default; 164 } 165 166 // ===================================================================== 167 // Product Generation 168 // ===================================================================== 169 170 function generateProduct(id, typeKey, category) { 171 const productTypes = PRODUCT_TYPES[typeKey]; 172 const baseName = randomElement(productTypes); 173 const adjective = randomBool(0.6) ? randomElement(ADJECTIVES) + ' ' : ''; 174 const number = randomInt(1000, 9999); 175 const brand = randomElement(BRANDS); 176 177 const productName = `${adjective}${baseName} ${number}`; 178 const description = generateDescription(baseName, brand); 179 180 // Pricing logic: 30% of products have discounts 181 const hasDiscount = randomBool(0.3); 182 let finalPrice, initialPrice, discount; 183 184 if (hasDiscount) { 185 initialPrice = generatePrice(); 186 const discountPercent = randomInt(10, 50); 187 const final = parseFloat(initialPrice) * (1 - discountPercent / 100); 188 finalPrice = final.toFixed(2); 189 discount = calculateDiscount(initialPrice, finalPrice); 190 } else { 191 finalPrice = generatePrice(); 192 initialPrice = null; 193 discount = null; 194 } 195 196 // Stock 197 const stockQuantity = randomInt(0, 500); 198 199 // Sizes and colors 200 const sizes = getSizesForCategory(category); 201 const colors = getColorsForCategory(category); 202 203 const sizesArray = sizes.length > 0 && randomBool(0.7) ? 204 `'{${sizes.slice(0, randomInt(2, sizes.length)).join(',')}}'` : 205 'NULL'; 206 207 const colorsArray = colors.length > 0 && randomBool(0.8) ? 208 `'{${colors.slice(0, randomInt(2, colors.length)).join(',')}}'` : 209 'NULL'; 210 211 // Flags 212 const availableForDelivery = randomBool(0.95); 213 const availableForPickup = randomBool(0.3); 214 const freeReturns = randomBool(0.6); 215 const isActive = randomBool(0.98); 216 217 return { 218 id, 219 uuid: randomUUID(), 220 asin: generateASIN(), 221 sku: generateSKU(), 222 productName, 223 brand, 224 description, 225 category, 226 price: finalPrice, 227 initialPrice, 228 discount, 229 stockQuantity, 230 sizes: sizesArray, 231 colors: colorsArray, 232 availableForDelivery, 233 availableForPickup, 234 freeReturns, 235 isActive 236 }; 237 } 238 239 // ===================================================================== 240 // Generate SQL 241 // ===================================================================== 242 243 function escapeSQL(str) { 244 if (str === null || str === undefined) return 'NULL'; 245 return `'${str.replace(/'/g, "''")}'`; 246 } 247 248 function generateProductSQL(product) { 249 return `( 250 ${escapeSQL(product.uuid)}, 251 ${escapeSQL(product.asin)}, 252 ${escapeSQL(product.sku)}, 253 ${escapeSQL(product.productName)}, 254 ${escapeSQL(product.brand)}, 255 ${escapeSQL(product.description)}, 256 (SELECT id FROM categories WHERE name = ${escapeSQL(product.category)}), 257 ${product.price}, 258 ${product.initialPrice ? product.initialPrice : 'NULL'}, 259 ${escapeSQL(product.discount)}, 260 'USD', 261 ${product.stockQuantity}, 262 ${product.sizes}, 263 ${product.colors}, 264 ${product.availableForDelivery}, 265 ${product.availableForPickup}, 266 ${product.freeReturns}, 267 ${product.isActive} 268 )`; 269 } 270 271 function generateRatingSQL(productId, userId, rating, review) { 272 return `( 273 ${escapeSQL(randomUUID())}, 274 ${productId}, 275 ${escapeSQL(userId)}, 276 ${rating}, 277 ${escapeSQL(review)} 278 )`; 279 } 280 281 // ===================================================================== 282 // Main Generation 283 // ===================================================================== 284 285 console.log('Generating test product data...'); 286 287 const products = []; 288 let productId = 1; 289 290 // Distribute products across categories 291 const typeKeys = Object.keys(PRODUCT_TYPES); 292 const productsPerType = Math.floor(NUM_PRODUCTS / typeKeys.length); 293 294 typeKeys.forEach((typeKey, index) => { 295 const count = index === typeKeys.length - 1 ? 296 NUM_PRODUCTS - products.length : // Last type gets remainder 297 productsPerType; 298 299 const category = CATEGORY_MAP[typeKey]; 300 301 for (let i = 0; i < count; i++) { 302 products.push(generateProduct(productId++, typeKey, category)); 303 } 304 }); 305 306 console.log(`Generated ${products.length} products`); 307 308 // Generate SQL file 309 let sql = `-- ===================================================================== 310 -- OTELMART TEST DATA 311 -- Generated: ${new Date().toISOString()} 312 -- Products: ${products.length} 313 -- Ratings: ${NUM_RATINGS} 314 -- ===================================================================== 315 316 SET search_path TO products, public; 317 318 -- ===================================================================== 319 -- INSERT PRODUCTS 320 -- ===================================================================== 321 322 INSERT INTO products ( 323 uuid, asin, sku, product_name, brand, description, 324 category_id, price, initial_price, discount, currency, 325 stock_quantity, sizes, colors, 326 available_for_delivery, available_for_pickup, free_returns, is_active 327 ) VALUES\n`; 328 329 sql += products.map((p, i) => 330 generateProductSQL(p) + (i < products.length - 1 ? ',' : ';') 331 ).join('\n'); 332 333 // Generate sample ratings 334 sql += `\n\n-- ===================================================================== 335 -- INSERT SAMPLE RATINGS 336 -- ===================================================================== 337 338 INSERT INTO ratings (uuid, product_id, user_id, rating, review) VALUES\n`; 339 340 const ratingReviews = [ 341 'Great product! Highly recommended.', 342 'Excellent quality and fast shipping.', 343 'Good value for the price.', 344 'Works perfectly as described.', 345 'Very satisfied with this purchase.', 346 'Amazing! Better than expected.', 347 'Decent product, does the job.', 348 'Not bad, but could be better.', 349 'Fantastic quality!', 350 null // Some ratings without reviews 351 ]; 352 353 const ratings = []; 354 for (let i = 0; i < NUM_RATINGS; i++) { 355 const productId = randomInt(1, products.length); 356 const userId = randomUUID(); 357 const rating = randomInt(3, 5); // Mostly positive ratings 358 const review = randomElement(ratingReviews); 359 360 ratings.push(generateRatingSQL(productId, userId, rating, review)); 361 } 362 363 sql += ratings.map((r, i) => 364 r + (i < ratings.length - 1 ? ',' : ';') 365 ).join('\n'); 366 367 sql += '\n\n-- Data generation complete\n'; 368 369 // Write to file 370 fs.writeFileSync(OUTPUT_FILE, sql); 371 372 console.log(`\nTest data generated successfully!`); 373 console.log(`Output file: ${OUTPUT_FILE}`); 374 console.log(`\nTo import:`); 375 console.log(`psql -h localhost -p 5433 -U opentel_user -d opentel_db -f ${OUTPUT_FILE}`);