Fix anchor price selection - prioritize anchor over method

The previous logic checked preferred method first, which could select
a wrong price even when anchor price was available. Now:

1. PRIORITY 1: Anchor price - if user confirmed a price, find closest
   match (within 10% tolerance) across ALL candidates
2. PRIORITY 2: Preferred method - only used if no anchor match found
3. PRIORITY 3: Consensus voting

Also added debug logging to trace anchor price saving and retrieval.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
clucraft 2026-01-24 19:01:15 -05:00
parent 389915a6ec
commit 8131017f3a
3 changed files with 31 additions and 29 deletions

View file

@ -59,6 +59,7 @@ router.post('/', async (req: AuthRequest, res: Response) => {
// Store the anchor price - used on refresh to select the correct variant
await productQueries.updateAnchorPrice(product.id, selectedPrice);
console.log(`[Products] Saved anchor price ${selectedPrice} for product ${product.id} (method: ${selectedMethod})`);
// Record the user-selected price
await priceHistoryQueries.create(

View file

@ -29,6 +29,8 @@ async function checkPrices(): Promise<void> {
// Get anchor price for variant products (the price the user confirmed)
const anchorPrice = await productQueries.getAnchorPrice(product.id);
console.log(`[Scheduler] Product ${product.id} - preferredMethod: ${preferredMethod}, anchorPrice: ${anchorPrice}`);
// Use voting scraper with preferred method and anchor price if available
const scrapedData = await scrapeProductWithVoting(
product.url,
@ -37,6 +39,8 @@ async function checkPrices(): Promise<void> {
anchorPrice || undefined
);
console.log(`[Scheduler] Product ${product.id} - scraped price: ${scrapedData.price?.price}, candidates: ${scrapedData.priceCandidates.map(c => `${c.price}(${c.method})`).join(', ')}`);
// Check for back-in-stock notification
const wasOutOfStock = product.stock_status === 'out_of_stock';
const nowInStock = scrapedData.stockStatus === 'in_stock';

View file

@ -1429,47 +1429,44 @@ export async function scrapeProductWithVoting(
// Store all candidates
result.priceCandidates = allCandidates;
// If user has a preferred method, try to use it
if (preferredMethod && allCandidates.length > 0) {
const preferredCandidates = allCandidates.filter(c => c.method === preferredMethod);
if (preferredCandidates.length > 0) {
let selectedCandidate = preferredCandidates[0];
// If we have an anchor price and multiple candidates from preferred method,
// select the one closest to the anchor price (handles variant products)
if (anchorPrice && preferredCandidates.length > 1) {
selectedCandidate = preferredCandidates.reduce((closest, candidate) => {
const closestDiff = Math.abs(closest.price - anchorPrice);
const candidateDiff = Math.abs(candidate.price - anchorPrice);
return candidateDiff < closestDiff ? candidate : closest;
});
console.log(`[Voting] Using anchor price ${anchorPrice} to select from ${preferredCandidates.length} candidates: ${selectedCandidate.price}`);
}
console.log(`[Voting] Using preferred method ${preferredMethod}: ${selectedCandidate.price}`);
result.price = { price: selectedCandidate.price, currency: selectedCandidate.currency };
result.selectedMethod = preferredMethod;
return result;
}
}
// If we have an anchor price but no preferred method, find closest candidate overall
// PRIORITY 1: If we have an anchor price, it takes precedence (user confirmed this price)
// This handles variant products where multiple prices exist on the page
if (anchorPrice && allCandidates.length > 0) {
console.log(`[Voting] Have anchor price ${anchorPrice}, searching ${allCandidates.length} candidates...`);
// Find the candidate closest to the anchor price
const closestCandidate = allCandidates.reduce((closest, candidate) => {
const closestDiff = Math.abs(closest.price - anchorPrice);
const candidateDiff = Math.abs(candidate.price - anchorPrice);
return candidateDiff < closestDiff ? candidate : closest;
});
// Only use anchor matching if the difference is reasonable (within 20%)
const priceDiff = Math.abs(closestCandidate.price - anchorPrice) / anchorPrice;
if (priceDiff < 0.20) {
console.log(`[Voting] Using anchor price ${anchorPrice} to select: ${closestCandidate.price} (${(priceDiff * 100).toFixed(1)}% diff)`);
// Use anchor matching if within 10% (tight tolerance for variants)
// or if it's an exact match
if (closestCandidate.price === anchorPrice || priceDiff < 0.10) {
console.log(`[Voting] Found match for anchor price ${anchorPrice}: ${closestCandidate.price} via ${closestCandidate.method} (${(priceDiff * 100).toFixed(1)}% diff)`);
result.price = { price: closestCandidate.price, currency: closestCandidate.currency };
result.selectedMethod = closestCandidate.method;
return result;
} else {
console.log(`[Voting] Anchor price ${anchorPrice} too different from candidates (closest: ${closestCandidate.price}, ${(priceDiff * 100).toFixed(1)}% diff), using consensus`);
// No close match - price may have legitimately changed
console.log(`[Voting] No candidate close to anchor price ${anchorPrice} (closest: ${closestCandidate.price}, ${(priceDiff * 100).toFixed(1)}% diff)`);
// Fall through to preferred method or consensus
}
}
// PRIORITY 2: If user has a preferred method and no anchor match, try that method
if (preferredMethod && allCandidates.length > 0) {
const preferredCandidates = allCandidates.filter(c => c.method === preferredMethod);
if (preferredCandidates.length > 0) {
// Use highest confidence candidate from preferred method
const selectedCandidate = preferredCandidates.sort((a, b) => b.confidence - a.confidence)[0];
console.log(`[Voting] Using preferred method ${preferredMethod}: ${selectedCandidate.price}`);
result.price = { price: selectedCandidate.price, currency: selectedCandidate.currency };
result.selectedMethod = preferredMethod;
return result;
}
}