fix(core): handle partial listing data
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user