fix(core): handle partial listing data

This commit is contained in:
2026-04-28 21:34:45 -04:00
parent 7966073bf8
commit 3fe5fdb63f
10 changed files with 150 additions and 124 deletions

View File

@@ -274,7 +274,7 @@ function parseEbayListings(
);
// Filter to only elements that actually contain prices (not labels)
const actualPrices: HTMLElement[] = [];
const actualPrices: Element[] = [];
for (const el of allPriceElements) {
const text = el.textContent?.trim();
if (text && EBAY_PRICE_TEXT_RE.test(text) && text.length < 50) {
@@ -301,11 +301,10 @@ function parseEbayListings(
if (nonStrikethroughPrices.length > 0) {
// Use the first non-strikethrough price (sale price)
priceElement = nonStrikethroughPrices[0];
priceElement = nonStrikethroughPrices[0] ?? null;
} else {
// Fallback: use the last price (likely the most current)
const lastPrice = actualPrices[actualPrices.length - 1];
priceElement = lastPrice;
priceElement = actualPrices[actualPrices.length - 1] ?? null;
}
}
}

View File

@@ -86,7 +86,7 @@ interface FacebookMarketplaceItem {
__typename: "GroupCommerceProductItem";
// Listing content
marketplace_listing_title: string;
marketplace_listing_title?: string;
redacted_description?: {
text: string;
};
@@ -99,7 +99,7 @@ interface FacebookMarketplaceItem {
listing_price?: {
amount: string;
currency: string;
amount_with_offset: string;
amount_with_offset?: string;
};
// Location
@@ -127,9 +127,9 @@ interface FacebookMarketplaceItem {
// Seller information
marketplace_listing_seller?: {
__typename: "User";
id: string;
name: string;
__typename?: "User";
id?: string;
name?: string;
profile_picture?: {
uri: string;
};
@@ -1321,6 +1321,14 @@ export async function fetchFacebookItem(
return null;
}
if (itemResponseUrl.includes("unavailable_product=1")) {
logExtractionMetrics(false, itemId);
console.warn(
`Item ${itemId} appears to be sold or removed from marketplace.`,
);
return null;
}
const itemData = extractFacebookItemData(itemHtml);
if (classification.unavailable && !itemData) {

View File

@@ -1,56 +1,53 @@
/** Custom error class for HTTP-related failures */
export class HttpError extends Error {
override name = "HttpError";
constructor(
message: string,
public readonly statusCode: number,
public readonly url?: string,
) {
super(message);
this.name = "HttpError";
}
}
/** Error class for network failures (timeouts, connection issues) */
export class NetworkError extends Error {
override name = "NetworkError";
constructor(
message: string,
public readonly url: string,
public readonly cause?: Error,
public override readonly cause?: Error,
) {
super(message);
this.name = "NetworkError";
}
}
/** Error class for parsing failures */
export class ParseError extends Error {
override name = "ParseError";
constructor(
message: string,
public readonly data?: unknown,
) {
super(message);
this.name = "ParseError";
}
}
/** Error class for rate limiting */
export class RateLimitError extends Error {
override name = "RateLimitError";
constructor(
message: string,
public readonly url: string,
public readonly resetTime?: number,
) {
super(message);
this.name = "RateLimitError";
}
}
/** Error class for validation failures */
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
override name = "ValidationError";
}
/** Type guard to check if a value is a record (object) */

View File

@@ -1,21 +1,29 @@
import type { ListingDetails, UnstableListingBuckets } from "../types/common";
import type { UnstableListingBuckets } from "../types/common";
interface HasListingPrice {
listingPrice?: { cents?: number } | null;
}
function getMedian(values: number[]): number {
const middleIndex = Math.floor(values.length / 2);
if (values.length % 2 === 0) {
return (values[middleIndex - 1] + values[middleIndex]) / 2;
const left = values[middleIndex - 1] ?? 0;
const right = values[middleIndex] ?? 0;
return (left + right) / 2;
}
return values[middleIndex];
return values[middleIndex] ?? 0;
}
export function classifyUnstableListings<T extends ListingDetails>(
export function classifyUnstableListings<T extends HasListingPrice>(
listings: T[],
): UnstableListingBuckets<T> {
const validPrices = listings
.map((listing) => listing.listingPrice.cents)
.filter((price) => Number.isFinite(price) && price > 0)
.map((listing) => listing.listingPrice?.cents)
.filter(
(price): price is number => Number.isFinite(price) && (price ?? 0) > 0,
)
.sort((left, right) => left - right);
if (validPrices.length < 2) {
@@ -32,9 +40,13 @@ export function classifyUnstableListings<T extends ListingDetails>(
};
for (const listing of listings) {
const price = listing.listingPrice.cents;
const price = listing.listingPrice?.cents;
if (Number.isFinite(price) && price > 0 && price < threshold) {
if (
Number.isFinite(price) &&
(price ?? 0) > 0 &&
(price ?? 0) < threshold
) {
buckets.unstableResults.push(listing);
continue;
}