diff --git a/CHANGELOG.md b/CHANGELOG.md index 839a94a..6c27dca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to PriceGhost will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.2] - 2026-01-23 + +### Fixed + +- **Stock status false positives** - Fixed overly aggressive pre-order detection that incorrectly marked in-stock items as out of stock. Pages with "in stock", "add to cart", or "add to basket" text now correctly prioritize these indicators +- **Magento 2 stock detection** - Added proper stock status detection for Magento 2 sites, checking for stock classes and add-to-cart buttons + +--- + ## [1.0.1] - 2026-01-23 ### Added @@ -105,5 +114,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | Version | Date | Description | |---------|------|-------------| +| 1.0.2 | 2026-01-23 | Fixed stock status false positives for in-stock items | | 1.0.1 | 2026-01-23 | Bug fixes, JS-rendered price support, pre-order detection | | 1.0.0 | 2026-01-23 | Initial public release | diff --git a/backend/package-lock.json b/backend/package-lock.json index 418a4a0..a3555fb 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,12 +1,12 @@ { "name": "priceghost-backend", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "priceghost-backend", - "version": "1.0.0", + "version": "1.0.2", "dependencies": { "@anthropic-ai/sdk": "^0.24.0", "axios": "^1.6.0", diff --git a/backend/package.json b/backend/package.json index 0144198..f71252f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "priceghost-backend", - "version": "1.0.0", + "version": "1.0.2", "description": "PriceGhost price tracking API", "main": "dist/index.js", "scripts": { diff --git a/backend/src/services/scraper.ts b/backend/src/services/scraper.ts index 091b348..1099b2a 100644 --- a/backend/src/services/scraper.ts +++ b/backend/src/services/scraper.ts @@ -742,9 +742,36 @@ const siteScrapers: SiteScraper[] = [ $('.fotorama__stage img').first().attr('src') || null; + // Stock status detection for Magento 2 + let stockStatus: StockStatus = 'unknown'; + + // Check for Magento's stock status elements + const stockElement = $('.product-info-stock-sku .stock').first(); + const stockText = stockElement.text().toLowerCase(); + const stockClass = stockElement.attr('class')?.toLowerCase() || ''; + + // Magento uses "available" class for in-stock items + if (stockClass.includes('available') || stockText.includes('in stock')) { + stockStatus = 'in_stock'; + } else if (stockClass.includes('unavailable') || stockText.includes('out of stock')) { + stockStatus = 'out_of_stock'; + } + + // Also check for add to cart button as backup + if (stockStatus === 'unknown') { + const addToCartBtn = $('#product-addtocart-button, button.tocart, button[title="Add to Cart"], button[title="Add to Basket"]').length > 0; + const outOfStockMsg = $('.out-of-stock, .unavailable, [class*="outofstock"]').length > 0; + + if (addToCartBtn && !outOfStockMsg) { + stockStatus = 'in_stock'; + } else if (outOfStockMsg) { + stockStatus = 'out_of_stock'; + } + } + // Only return if we found a price (indicates it's likely a Magento site) if (price) { - return { name, price, imageUrl }; + return { name, price, imageUrl, stockStatus }; } return {}; }, @@ -1259,13 +1286,11 @@ function extractGenericStockStatus($: CheerioAPI): StockStatus { // Check for pre-order / coming soon indicators BEFORE checking add to cart // Some sites show a "Pre-order" button that looks like add to cart + // NOTE: Be careful with generic phrases - "available in" matches "available in stock"! const preOrderComingSoonPhrases = [ 'coming soon', - 'coming in', 'available soon', - 'available in', 'arriving soon', - 'arriving in', 'releases on', 'release date', 'expected release', @@ -1284,25 +1309,53 @@ function extractGenericStockStatus($: CheerioAPI): StockStatus { 'join waitlist', 'not yet released', 'not yet available', - 'coming this', - 'coming next', + // Specific future availability phrases (avoid generic "available in" which matches "available in stock") 'available starting', + 'available from', // Usually followed by a date + 'ships in', // Usually indicates future shipping + 'expected to ship', + 'estimated arrival', ]; - for (const phrase of preOrderComingSoonPhrases) { - if (textToCheck.includes(phrase)) { - // Double check it's not just a section about pre-orders in general - // by looking for the phrase near price/product context - const phraseIndex = textToCheck.indexOf(phrase); - const contextStart = Math.max(0, phraseIndex - 200); - const contextEnd = Math.min(textToCheck.length, phraseIndex + 200); - const context = textToCheck.substring(contextStart, contextEnd); + // Phrases that indicate the product is NOT coming soon (should not trigger out of stock) + const inStockPhrases = [ + 'in stock', + 'add to cart', + 'add to basket', + 'buy now', + 'available now', + 'ships today', + 'ships immediately', + 'ready to ship', + ]; - // If the context mentions price, buy, cart, or product, it's likely about this product - if (context.includes('$') || context.includes('price') || - context.includes('buy') || context.includes('cart') || - context.includes('order') || context.includes('purchase')) { - return 'out_of_stock'; + // First, check if the page has strong in-stock indicators + // If so, don't let pre-order phrase matching override it + let hasInStockIndicator = false; + for (const phrase of inStockPhrases) { + if (textToCheck.includes(phrase)) { + hasInStockIndicator = true; + break; + } + } + + // Only check for pre-order/coming soon if we don't have a clear in-stock indicator + if (!hasInStockIndicator) { + for (const phrase of preOrderComingSoonPhrases) { + if (textToCheck.includes(phrase)) { + // Double check it's not just a section about pre-orders in general + // by looking for the phrase near price/product context + const phraseIndex = textToCheck.indexOf(phrase); + const contextStart = Math.max(0, phraseIndex - 200); + const contextEnd = Math.min(textToCheck.length, phraseIndex + 200); + const context = textToCheck.substring(contextStart, contextEnd); + + // If the context mentions price, buy, cart, or product, it's likely about this product + if (context.includes('$') || context.includes('price') || + context.includes('buy') || context.includes('cart') || + context.includes('order') || context.includes('purchase')) { + return 'out_of_stock'; + } } } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 46a1e9f..a518d95 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "priceghost-frontend", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "priceghost-frontend", - "version": "1.0.0", + "version": "1.0.2", "dependencies": { "axios": "^1.6.0", "react": "^18.2.0", diff --git a/frontend/package.json b/frontend/package.json index b1c9aad..58b1ac0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "priceghost-frontend", "private": true, - "version": "1.0.0", + "version": "1.0.2", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/public/version.json b/frontend/public/version.json index 398de8c..76cf47c 100644 --- a/frontend/public/version.json +++ b/frontend/public/version.json @@ -1,4 +1,4 @@ { - "version": "1.0.1", + "version": "1.0.2", "releaseDate": "2026-01-23" }