fix: cover scraper pricing edge cases

This commit is contained in:
2026-04-22 23:54:07 -04:00
parent b5e14e686a
commit 55faee7dd5
6 changed files with 233 additions and 29 deletions

View File

@@ -38,6 +38,8 @@ export interface EbayListingDetails {
address?: string | null;
}
const EBAY_PRICE_TEXT_RE = /^(?:\s*(?:CA|C)\s*\$|\s*[$£¥])/u;
// ----------------------------- Utilities -----------------------------
/**
@@ -253,7 +255,7 @@ function parseEbayListings(
const text = el.textContent?.trim();
if (
text &&
/^\s*[$£¥]/u.test(text) &&
EBAY_PRICE_TEXT_RE.test(text) &&
text.length < 50 &&
!/\d{4}/.test(text)
) {

View File

@@ -890,7 +890,7 @@ export function parseFacebookAds(
if (priceObj.formatted_amount) {
const match = priceObj.formatted_amount.match(/[\d,]+\.?\d*/);
if (match) {
const dollars = Number.parseFloat(match[0].replace(",", ""));
const dollars = Number.parseFloat(match[0].replace(/,/g, ""));
if (!Number.isNaN(dollars)) {
cents = Math.round(dollars * 100);
} else {

View File

@@ -214,14 +214,21 @@ const CATEGORY_SLUGS = Object.fromEntries(
const SEPS = new Set([" ", "", "—", "/", ":", ";", ",", ".", "-"]);
function normalizeLookupKey(value: string): string {
return value.toLowerCase().replace(/[\s-]+/g, "-");
}
/**
* Resolve location ID from name or return numeric ID
*/
export function resolveLocationId(location?: number | string): number {
if (typeof location === "number") return location;
if (typeof location === "string") {
const normalized = location.toLowerCase().replace(/\s+/g, "-");
return LOCATION_MAPPINGS[normalized] ?? 0; // Default to Canada (0)
const normalized = normalizeLookupKey(location);
const mapping = Object.entries(LOCATION_MAPPINGS).find(
([key]) => normalizeLookupKey(key) === normalized,
);
return mapping?.[1] ?? 0; // Default to Canada (0)
}
return 0; // Default to Canada
}
@@ -232,12 +239,38 @@ export function resolveLocationId(location?: number | string): number {
export function resolveCategoryId(category?: number | string): number {
if (typeof category === "number") return category;
if (typeof category === "string") {
const normalized = category.toLowerCase().replace(/\s+/g, "-");
return CATEGORY_MAPPINGS[normalized] ?? 0; // Default to all categories
const normalized = normalizeLookupKey(category);
const mapping = Object.entries(CATEGORY_MAPPINGS).find(
([key]) => normalizeLookupKey(key) === normalized,
);
return mapping?.[1] ?? 0; // Default to all categories
}
return 0; // Default to all categories
}
function matchesPriceFilters(
listing: DetailedListing,
searchOptions: Required<SearchOptions>,
): boolean {
const cents = listing.listingPrice?.cents;
if (typeof cents !== "number") return false;
if (
typeof searchOptions.priceMin === "number" &&
cents < searchOptions.priceMin
) {
return false;
}
if (
typeof searchOptions.priceMax === "number" &&
cents > searchOptions.priceMax
) {
return false;
}
return true;
}
/**
* Build search URL with enhanced parameters
*/
@@ -917,32 +950,30 @@ export default async function fetchKijijiItems(
}
}
const filteredListings = allListings.filter((listing) => {
const cents = listing.listingPrice?.cents;
const filteredListings = allListings.filter((listing) =>
matchesPriceFilters(listing, finalSearchOptions),
);
if (typeof cents !== "number") return false;
if (
typeof finalSearchOptions.priceMin === "number" &&
cents < finalSearchOptions.priceMin
) {
return false;
}
if (
typeof finalSearchOptions.priceMax === "number" &&
cents > finalSearchOptions.priceMax
) {
return false;
}
return true;
});
const finalListings = unstableMode.hideUnstableResults
? (() => {
const classified = classifyUnstableListings(allListings);
return {
results: classified.results.filter((listing) =>
matchesPriceFilters(listing, finalSearchOptions),
),
unstableResults: classified.unstableResults.filter((listing) =>
matchesPriceFilters(listing, finalSearchOptions),
),
};
})()
: filteredListings;
console.log(
`\nParsed ${unstableMode.hideUnstableResults ? allListings.length : filteredListings.length} detailed listings.`,
);
return finalizeResults(
unstableMode.hideUnstableResults ? allListings : filteredListings,
);
return unstableMode.hideUnstableResults
? finalListings
: finalizeResults(finalListings);
}
// Re-export error classes for convenience