fix: parse gathered price labels
This commit is contained in:
78
server.js
78
server.js
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user