// Utility functions for matching products between orders and inventory export interface ProductMatch { orderItem: { title: string; quantity: number; price?: number; }; matchedProduct?: { _id: string; title: string; printingCost: number; costOfGoods: number; size?: string; }; confidence: number; // 0-1, how confident we are in the match } export interface MatchingResult { matches: ProductMatch[]; missingProducts: { title: string; quantity: number; price?: number; size?: string; }[]; } /** * Match order items against existing products in the database * Considers size as a factor but ignores color variations * Enhanced for Modern Minimalist Shelf Decor products with Large/Small variants */ export const matchOrderItemsToProducts = ( orderItems: any[], products: any[] ): MatchingResult => { const matches: ProductMatch[] = []; const missingProducts: any[] = []; for (const item of orderItems) { let bestMatch: any = null; let bestConfidence = 0; // Enhanced size extraction - look for "Large" or "Small" specifically const itemTitle = item.title.toLowerCase(); let itemSize = ''; // Check for Large/Small in various patterns if (itemTitle.includes('large') && !itemTitle.includes('small')) { itemSize = 'large'; } else if (itemTitle.includes('small') && !itemTitle.includes('large')) { itemSize = 'small'; } // Also check for "Size: Large" pattern const sizeMatch = item.title.match(/Size:\s*(Large|Small|Med|Medium)/i); if (sizeMatch) { itemSize = sizeMatch[1].toLowerCase(); if (itemSize === 'med' || itemSize === 'medium') itemSize = 'med'; } // Clean item title for comparison (remove color and size info for base matching) const cleanItemTitle = item.title .replace(/Colour:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '') // Remove color .replace(/Size:\s*[^,\s-]+/i, '') // Remove explicit size .replace(/3D-Printed/i, '') // Remove 3D-Printed as it's common .replace(/\s+/g, ' ') .trim() .toLowerCase(); console.log('🔍 Matching item:', item.title); console.log(' Clean title:', cleanItemTitle); console.log(' Extracted size:', itemSize); for (const product of products) { const productTitle = product.title.toLowerCase(); let productSize = ''; // Extract size from product title if (productTitle.includes('large') && !productTitle.includes('small')) { productSize = 'large'; } else if (productTitle.includes('small') && !productTitle.includes('large')) { productSize = 'small'; } else if (productTitle.includes('med')) { productSize = 'med'; } // Clean product title for comparison const cleanProductTitle = product.title .replace(/Colour:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '') .replace(/Size:\s*[^,\s-]+/i, '') .replace(/3D-Printed/i, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); // Calculate title similarity (basic word matching) const titleSimilarity = calculateTitleSimilarity(cleanItemTitle, cleanProductTitle); // Size matching - this is crucial for your shelf decor products let sizeSimilarity = 0; if (itemSize && productSize) { sizeSimilarity = itemSize === productSize ? 1 : 0; console.log(` Size match: ${itemSize} vs ${productSize} = ${sizeSimilarity}`); } else if (!itemSize && !productSize) { sizeSimilarity = 0.7; // Both have no size specified } else if (itemSize && !productSize) { // Item has size but product doesn't - penalize slightly sizeSimilarity = 0.3; } else { // Product has size but item doesn't - penalize more sizeSimilarity = 0.1; } // For shelf decor products, size is very important const isShelfDecor = cleanItemTitle.includes('shelf decor') || cleanProductTitle.includes('shelf decor'); let confidence; if (isShelfDecor && itemSize && productSize) { // For shelf decor with both sizes specified, size match is critical confidence = (titleSimilarity * 0.6) + (sizeSimilarity * 0.4); } else if (isShelfDecor) { // For shelf decor without clear size info, rely more on title confidence = (titleSimilarity * 0.8) + (sizeSimilarity * 0.2); } else { // For other products, standard weighting confidence = (titleSimilarity * 0.8) + (sizeSimilarity * 0.2); } console.log(` Product: ${product.title}`); console.log(` Clean: ${cleanProductTitle}`); console.log(` Product size: ${productSize}`); console.log(` Title similarity: ${titleSimilarity.toFixed(3)}`); console.log(` Size similarity: ${sizeSimilarity}`); console.log(` Final confidence: ${confidence.toFixed(3)}`); // Require higher confidence for shelf decor products to avoid wrong matches const minConfidence = isShelfDecor ? 0.7 : 0.5; if (confidence > bestConfidence && confidence >= minConfidence) { bestMatch = product; bestConfidence = confidence; } } if (bestMatch) { matches.push({ orderItem: { title: item.title, quantity: item.quantity, price: item.price }, matchedProduct: { _id: bestMatch._id, title: bestMatch.title, printingCost: bestMatch.printingCost || 0, costOfGoods: bestMatch.costOfGoods || 0, size: bestMatch.size }, confidence: bestConfidence }); console.log(`✅ Matched "${item.title}" to "${bestMatch.title}" (${(bestConfidence * 100).toFixed(1)}%)`); } else { missingProducts.push({ title: item.title, quantity: item.quantity, ...(item.price !== undefined && { price: item.price }), size: itemSize }); console.log(`❌ No match found for "${item.title}"`); } } return { matches, missingProducts }; }; /** * Calculate similarity between two product titles * Enhanced for Modern Minimalist Shelf Decor and similar products */ const calculateTitleSimilarity = (title1: string, title2: string): number => { // Split into words and filter out common stop words const stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', '-', '–', '3d', 'printed']; const words1 = title1.split(/\s+/).filter(word => word.length > 2 && !stopWords.includes(word.toLowerCase()) ); const words2 = title2.split(/\s+/).filter(word => word.length > 2 && !stopWords.includes(word.toLowerCase()) ); if (words1.length === 0 || words2.length === 0) return 0; // For shelf decor products, check for key identifying words const isShelfDecor1 = title1.includes('shelf') || title1.includes('decor'); const isShelfDecor2 = title2.includes('shelf') || title2.includes('decor'); if (isShelfDecor1 && isShelfDecor2) { // Special handling for shelf decor products const keyWords = ['modern', 'minimalist', 'shelf', 'decor', 'water', 'reservoir', 'bookend', 'planter']; let keyWordMatches = 0; for (const keyWord of keyWords) { const inTitle1 = title1.includes(keyWord); const inTitle2 = title2.includes(keyWord); if (inTitle1 && inTitle2) { keyWordMatches++; } } // If most key words match, it's likely the same product family if (keyWordMatches >= 4) { return 0.9; // High similarity for same product family } } // Count matching words with fuzzy matching let matchingWords = 0; for (const word1 of words1) { for (const word2 of words2) { if (word1.toLowerCase() === word2.toLowerCase() || word1.toLowerCase().includes(word2.toLowerCase()) || word2.toLowerCase().includes(word1.toLowerCase()) || levenshteinDistance(word1.toLowerCase(), word2.toLowerCase()) <= Math.max(1, Math.min(word1.length, word2.length) * 0.2)) { matchingWords++; break; } } } // Calculate similarity as ratio of matching words to total unique words const totalUniqueWords = Math.max(words1.length, words2.length); return matchingWords / totalUniqueWords; }; /** * Find potential matches for a missing product * This helps suggest existing products that might be similar */ export const findPotentialMatches = ( missingProductTitle: string, existingProducts: any[], maxSuggestions: number = 5 ): Array<{product: any; confidence: number; reason: string}> => { const suggestions: Array<{product: any; confidence: number; reason: string}> = []; const itemTitle = missingProductTitle.toLowerCase(); let itemSize = ''; // Extract size from missing product if (itemTitle.includes('large') && !itemTitle.includes('small')) { itemSize = 'large'; } else if (itemTitle.includes('small') && !itemTitle.includes('large')) { itemSize = 'small'; } const cleanItemTitle = missingProductTitle .replace(/Colour:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '') .replace(/Size:\s*[^,\s-]+/i, '') .replace(/3D-Printed/i, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); for (const product of existingProducts) { const productTitle = product.title.toLowerCase(); let productSize = ''; if (productTitle.includes('large') && !productTitle.includes('small')) { productSize = 'large'; } else if (productTitle.includes('small') && !productTitle.includes('large')) { productSize = 'small'; } const cleanProductTitle = product.title .replace(/Colour:\s*[^,\s-]+(?:\s+[^,\s-]+)*/i, '') .replace(/Size:\s*[^,\s-]+/i, '') .replace(/3D-Printed/i, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); const titleSimilarity = calculateTitleSimilarity(cleanItemTitle, cleanProductTitle); let reason = ''; let confidence = titleSimilarity; // Check if it's the same product family but different size if (titleSimilarity > 0.8) { if (itemSize && productSize && itemSize !== productSize) { reason = `Same product, different size (${productSize} vs ${itemSize})`; confidence = 0.95; // Very high confidence - likely meant to be this size variant } else if (itemSize && !productSize) { reason = `Same product, missing size specification`; confidence = 0.85; } else if (!itemSize && productSize) { reason = `Same product, has size: ${productSize}`; confidence = 0.8; } else { reason = `Very similar product`; confidence = titleSimilarity; } } else if (titleSimilarity > 0.6) { reason = `Similar product`; confidence = titleSimilarity; } if (confidence > 0.6) { suggestions.push({ product, confidence, reason }); } } // Sort by confidence and return top suggestions return suggestions .sort((a, b) => b.confidence - a.confidence) .slice(0, maxSuggestions); }; /** * Calculate Levenshtein distance between two strings * Used for fuzzy string matching */ const levenshteinDistance = (str1: string, str2: string): number => { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; };