mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-05-15 10:52:36 +02:00
Detect pre-order/coming soon products as out of stock
- AI verification now checks if product is purchasable RIGHT NOW - AI returns stockStatus field for pre-order/coming soon detection - Enhanced generic stock status detection with pre-order phrases: - Coming soon, available soon, releases on, pre-order, etc. - Notify me when available, join waitlist, etc. - Pre-order buttons no longer count as Add to Cart - Fixes issue where unreleased products showed as in stock Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
059336536f
commit
61ffafdd8c
2 changed files with 127 additions and 16 deletions
|
|
@ -19,28 +19,39 @@ export interface AIVerificationResult {
|
|||
confidence: number;
|
||||
suggestedPrice: ParsedPrice | null;
|
||||
reason: string;
|
||||
stockStatus?: StockStatus;
|
||||
}
|
||||
|
||||
const VERIFICATION_PROMPT = `You are a price verification assistant. I scraped a product page and found a price. Please verify if this price is correct.
|
||||
const VERIFICATION_PROMPT = `You are a price and availability verification assistant. I scraped a product page and found a price. Please verify if this price is correct AND if the product is currently available for purchase.
|
||||
|
||||
Scraped Price: $SCRAPED_PRICE$ $CURRENCY$
|
||||
|
||||
Analyze the HTML content below and determine:
|
||||
1. Is the scraped price the correct CURRENT/SALE price for the main product?
|
||||
2. If not, what is the correct price?
|
||||
3. Is this product currently available for purchase RIGHT NOW?
|
||||
|
||||
Common issues to watch for:
|
||||
Common price issues to watch for:
|
||||
- Scraped price might be a "savings" amount (e.g., "Save $189.99")
|
||||
- Scraped price might be from a bundle/combo deal section
|
||||
- Scraped price might be shipping cost or add-on price
|
||||
- Scraped price might be the original/crossed-out price instead of the sale price
|
||||
|
||||
Common availability issues to watch for:
|
||||
- Product shows "Coming Soon" or "Available [future date]" - NOT in stock
|
||||
- Product shows "Pre-order" or "Reserve now" - NOT in stock
|
||||
- Product shows "Notify me when available" or "Sign up for alerts" - NOT in stock
|
||||
- Product shows "Out of stock" or "Sold out" - NOT in stock
|
||||
- Product has no "Add to Cart" button but shows a future release date - NOT in stock
|
||||
- Product CAN be added to cart and purchased today - IN stock
|
||||
|
||||
Return a JSON object with:
|
||||
- isCorrect: boolean - true if the scraped price is correct
|
||||
- confidence: number from 0 to 1
|
||||
- suggestedPrice: the correct price as a number (or null if scraped price is correct)
|
||||
- suggestedCurrency: currency code if suggesting a different price
|
||||
- reason: brief explanation of your decision
|
||||
- stockStatus: "in_stock", "out_of_stock", or "unknown" - based on whether the product can be purchased RIGHT NOW
|
||||
- reason: brief explanation of your decision (mention both price and availability)
|
||||
|
||||
Only return valid JSON, no explanation text outside the JSON.
|
||||
|
||||
|
|
@ -314,6 +325,7 @@ function parseVerificationResponse(
|
|||
confidence: 0.5,
|
||||
suggestedPrice: null,
|
||||
reason: 'Could not parse AI response',
|
||||
stockStatus: 'unknown',
|
||||
};
|
||||
|
||||
let jsonStr = responseText.trim();
|
||||
|
|
@ -348,11 +360,23 @@ function parseVerificationResponse(
|
|||
}
|
||||
}
|
||||
|
||||
// Parse stock status from AI response
|
||||
let stockStatus: StockStatus = 'unknown';
|
||||
if (data.stockStatus) {
|
||||
const status = data.stockStatus.toLowerCase().replace(/[^a-z_]/g, '');
|
||||
if (status === 'in_stock' || status === 'instock') {
|
||||
stockStatus = 'in_stock';
|
||||
} else if (status === 'out_of_stock' || status === 'outofstock') {
|
||||
stockStatus = 'out_of_stock';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isCorrect: data.isCorrect ?? true,
|
||||
confidence: data.confidence ?? 0.5,
|
||||
suggestedPrice,
|
||||
reason: data.reason || 'No reason provided',
|
||||
stockStatus,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[AI Verify] Failed to parse response:', responseText);
|
||||
|
|
|
|||
|
|
@ -846,6 +846,15 @@ export async function scrapeProduct(url: string, userId?: number): Promise<Scrap
|
|||
console.log(`[AI Verify] Price might be incorrect but no confident suggestion: ${verifyResult.reason}`);
|
||||
// Don't set aiStatus if verification was inconclusive
|
||||
}
|
||||
|
||||
// Use AI-detected stock status if we don't have a definitive one yet
|
||||
// or if AI says it's out of stock (AI can catch pre-order/coming soon)
|
||||
if (verifyResult.stockStatus && verifyResult.stockStatus !== 'unknown') {
|
||||
if (result.stockStatus === 'unknown' || verifyResult.stockStatus === 'out_of_stock') {
|
||||
console.log(`[AI Verify] Stock status: ${verifyResult.stockStatus} (was: ${result.stockStatus})`);
|
||||
result.stockStatus = verifyResult.stockStatus;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (verifyError) {
|
||||
console.error(`[AI Verify] Verification failed for ${url}:`, verifyError);
|
||||
|
|
@ -1060,7 +1069,8 @@ function extractGenericStockStatus($: CheerioAPI): StockStatus {
|
|||
const availability = $('[itemprop="availability"]').attr('content') ||
|
||||
$('[itemprop="availability"]').attr('href') || '';
|
||||
if (availability.toLowerCase().includes('outofstock') ||
|
||||
availability.toLowerCase().includes('discontinued')) {
|
||||
availability.toLowerCase().includes('discontinued') ||
|
||||
availability.toLowerCase().includes('preorder')) {
|
||||
return 'out_of_stock';
|
||||
}
|
||||
if (availability.toLowerCase().includes('instock') ||
|
||||
|
|
@ -1068,14 +1078,96 @@ function extractGenericStockStatus($: CheerioAPI): StockStatus {
|
|||
return 'in_stock';
|
||||
}
|
||||
|
||||
// Check for add to cart button - strong indicator of in stock
|
||||
const hasAddToCart = $('button[class*="add-to-cart" i]').length > 0 ||
|
||||
$('button[id*="add-to-cart" i]').length > 0 ||
|
||||
$('[data-testid*="add-to-cart" i]').length > 0 ||
|
||||
$('button:contains("Add to Cart")').length > 0 ||
|
||||
$('input[value*="Add to Cart" i]').length > 0;
|
||||
// Be conservative - only check main product area text, not entire body
|
||||
// to avoid false positives from sidebar recommendations, etc.
|
||||
const mainContent = $('main, [role="main"], #main, .main-content, .product-detail, .pdp-main').text().toLowerCase();
|
||||
const textToCheck = mainContent || $('body').text().toLowerCase().slice(0, 5000);
|
||||
|
||||
if (hasAddToCart) {
|
||||
// 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
|
||||
const preOrderComingSoonPhrases = [
|
||||
'coming soon',
|
||||
'coming in',
|
||||
'available soon',
|
||||
'available in',
|
||||
'arriving soon',
|
||||
'arriving in',
|
||||
'releases on',
|
||||
'release date',
|
||||
'expected release',
|
||||
'launches on',
|
||||
'launching soon',
|
||||
'pre-order',
|
||||
'preorder',
|
||||
'pre order',
|
||||
'notify me when available',
|
||||
'notify when available',
|
||||
'sign up to be notified',
|
||||
'sign up for availability',
|
||||
'email me when available',
|
||||
'get notified when',
|
||||
'join the waitlist',
|
||||
'join waitlist',
|
||||
'not yet released',
|
||||
'not yet available',
|
||||
'coming this',
|
||||
'coming next',
|
||||
'available starting',
|
||||
];
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for explicit pre-order/coming soon elements
|
||||
const hasPreOrderBadge = $('[class*="pre-order" i]').length > 0 ||
|
||||
$('[class*="preorder" i]').length > 0 ||
|
||||
$('[class*="coming-soon" i]').length > 0 ||
|
||||
$('[class*="comingsoon" i]').length > 0 ||
|
||||
$('[data-testid*="pre-order" i]').length > 0 ||
|
||||
$('[data-testid*="coming-soon" i]').length > 0 ||
|
||||
$('button:contains("Pre-order")').length > 0 ||
|
||||
$('button:contains("Preorder")').length > 0 ||
|
||||
$('button:contains("Notify Me")').length > 0;
|
||||
|
||||
if (hasPreOrderBadge) {
|
||||
return 'out_of_stock';
|
||||
}
|
||||
|
||||
// Check for add to cart button - strong indicator of in stock
|
||||
// But make sure it's not a pre-order button
|
||||
const addToCartButtons = $('button[class*="add-to-cart" i], button[id*="add-to-cart" i], [data-testid*="add-to-cart" i], button:contains("Add to Cart"), input[value*="Add to Cart" i]');
|
||||
let hasRealAddToCart = false;
|
||||
|
||||
addToCartButtons.each((_, el) => {
|
||||
const buttonText = $(el).text().toLowerCase();
|
||||
const buttonClass = $(el).attr('class')?.toLowerCase() || '';
|
||||
// Make sure it's not a pre-order or notify button
|
||||
if (!buttonText.includes('pre-order') &&
|
||||
!buttonText.includes('preorder') &&
|
||||
!buttonText.includes('notify') &&
|
||||
!buttonText.includes('waitlist') &&
|
||||
!buttonClass.includes('pre-order') &&
|
||||
!buttonClass.includes('preorder')) {
|
||||
hasRealAddToCart = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasRealAddToCart) {
|
||||
return 'in_stock';
|
||||
}
|
||||
|
||||
|
|
@ -1088,11 +1180,6 @@ function extractGenericStockStatus($: CheerioAPI): StockStatus {
|
|||
return 'out_of_stock';
|
||||
}
|
||||
|
||||
// Be conservative - only check main product area text, not entire body
|
||||
// to avoid false positives from sidebar recommendations, etc.
|
||||
const mainContent = $('main, [role="main"], #main, .main-content, .product-detail, .pdp-main').text().toLowerCase();
|
||||
const textToCheck = mainContent || $('body').text().toLowerCase().slice(0, 5000);
|
||||
|
||||
// Strong out-of-stock phrases (must be exact matches to avoid false positives)
|
||||
const strongOutOfStockPhrases = [
|
||||
'this item is out of stock',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue