fix: parse gathered price labels

This commit is contained in:
TopherMayor
2026-04-30 11:59:21 -07:00
parent a3b8e9a4b0
commit 106270117d
3 changed files with 87 additions and 4 deletions

View File

@@ -107,6 +107,9 @@ const HISTORY_KEY_ALIASES = {
'costco-secrets': 'hotel-secrets',
'costco-corazon': 'hotel-corazon',
'costco-pacifica': 'hotel-pacifica',
'costco-dreams': 'hotel-dreams-los-cabos',
'costco-zoetry': 'hotel-zoetry-casa-del-mar',
'costco-hard-rock': 'hotel-hard-rock-los-cabos',
};
function toTextList(value) {
@@ -192,11 +195,82 @@ function extractNumericPrice(point) {
for (const candidate of candidates) {
if (typeof candidate === 'number' && Number.isFinite(candidate)) return candidate;
if (typeof candidate === 'string') {
const parsed = Number(candidate.replace(/[^0-9.-]/g, ''));
const parsed = parseNumericPriceFromText(candidate);
if (Number.isFinite(parsed)) return parsed;
}
}
const textCandidates = [
point.displayPrice,
point.displayLabel,
point.priceLabel,
point.label,
point.note,
point.description,
].filter(Boolean);
for (const candidate of textCandidates) {
const parsed = parseNumericPriceFromText(candidate, point.priceBasis || point.price_basis || point.unit);
if (Number.isFinite(parsed)) return parsed;
}
return null;
}
function parseNumericPriceFromText(value, priceBasis = '') {
if (typeof value !== 'string') return null;
const normalized = value.replace(/\s+/g, ' ').trim();
if (!normalized || /\b(no|not)\s+(fresh\s+)?(price|rates?|available|counted|visible|captured)\b/i.test(normalized)) {
return null;
}
const matches = [...normalized.matchAll(/\$?\s*([0-9]{1,3}(?:,[0-9]{3})*(?:\.[0-9]{1,2})?|[0-9]+(?:\.[0-9]{1,2})?)/g)]
.map((match) => ({
value: Number(match[1].replace(/,/g, '')),
index: match.index || 0,
}))
.filter((match) => Number.isFinite(match.value));
if (!matches.length) return null;
const basis = normalizeKey(priceBasis);
if (basis === 'totalpackage' || basis === 'pergroup') {
return matches.at(-1).value;
}
const travelerMatch = matches.find((match) => (
/per\s+(traveler|person|guest|adult|round|table|night)|pp|\/night/i.test(normalized.slice(match.index, match.index + 80))
));
if (travelerMatch) return travelerMatch.value;
return matches[0].value;
}
function inferPriceBasis(point, defaults = {}) {
if (point.priceBasis || point.price_basis || point.unit || defaults.priceBasis) {
return point.priceBasis || point.price_basis || point.unit || defaults.priceBasis;
}
const haystack = [
point.displayPrice,
point.displayLabel,
point.priceLabel,
point.label,
point.note,
point.description,
].filter(Boolean).join(' ').toLowerCase();
if (haystack.includes('/night') || haystack.includes('per night')) return 'perNight';
if (haystack.includes('per traveler')) return 'perTraveler';
if (haystack.includes('per person') || /\bpp\b/.test(haystack)) return 'perPerson';
if (haystack.includes('per round')) return 'perRound';
if (haystack.includes('per table')) return 'perTable';
if (haystack.includes('total') || haystack.includes('package')) return 'totalPackage';
const bookingType = normalizeBookingType(inferBookingType(point, defaults));
if (bookingType === 'package') return 'perTraveler';
if (bookingType === 'calculated') return 'perPerson';
return null;
}
@@ -242,7 +316,7 @@ function loadPriceHistoryState() {
sourceKey: normalizeKey(point.sourceKey || point.sourceId || point.source || point.sourceLabel || point.vendor || defaults.sourceKey || 'unknown-source'),
sourceUrl: point.sourceUrl || point.url || defaults.sourceUrl || null,
bookingType: normalizeBookingType(inferBookingType(point, defaults)),
priceBasis: point.priceBasis || point.price_basis || point.unit || defaults.priceBasis || null,
priceBasis: inferPriceBasis(point, defaults),
includedComponents: toTextList(point.includedComponents || point.includesComponents || point.componentsIncluded || defaults.includedComponents),
excludedComponents: toTextList(point.excludedComponents || point.componentsExcluded || defaults.excludedComponents),
origin: point.origin || point.originAirport || defaults.origin || null,