fix: calculate stay totals
This commit is contained in:
70
server.js
70
server.js
@@ -22,6 +22,8 @@ const DEFAULT_PRICE_HISTORY_FILE = path.join(__dirname, 'price-watch', 'history.
|
||||
const PRICE_HISTORY_FILE = process.env.PRICE_HISTORY_FILE
|
||||
? path.resolve(process.env.PRICE_HISTORY_FILE)
|
||||
: DEFAULT_PRICE_HISTORY_FILE;
|
||||
const TRIP_CHECK_IN = process.env.TRIP_CHECK_IN || '2027-02-03';
|
||||
const TRIP_CHECK_OUT = process.env.TRIP_CHECK_OUT || '2027-02-07';
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
@@ -67,6 +69,15 @@ function normalizeSourceLabel(value) {
|
||||
return String(value || 'Unknown source').trim() || 'Unknown source';
|
||||
}
|
||||
|
||||
function formatCurrencyValue(value, currency = 'USD') {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) return '';
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
maximumFractionDigits: value >= 100 ? 0 : 2,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function normalizeBookingType(value) {
|
||||
const normalized = normalizeKey(value || '');
|
||||
if (['package', 'standalone', 'calculated'].includes(normalized)) return normalized;
|
||||
@@ -274,6 +285,42 @@ function inferPriceBasis(point, defaults = {}) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getTripNightCount() {
|
||||
const checkInMs = Date.parse(`${TRIP_CHECK_IN}T00:00:00Z`);
|
||||
const checkOutMs = Date.parse(`${TRIP_CHECK_OUT}T00:00:00Z`);
|
||||
if (Number.isNaN(checkInMs) || Number.isNaN(checkOutMs) || checkOutMs <= checkInMs) return 1;
|
||||
return Math.max(1, Math.round((checkOutMs - checkInMs) / (24 * 60 * 60 * 1000)));
|
||||
}
|
||||
|
||||
function isStayUnitPrice(priceBasis) {
|
||||
const normalized = normalizeKey(priceBasis);
|
||||
return ['pernight', 'perday', 'nightly', 'daily'].includes(normalized);
|
||||
}
|
||||
|
||||
function normalizeTripPrice({ price, priceBasis, currency, displayPrice }) {
|
||||
if (!isStayUnitPrice(priceBasis)) {
|
||||
return {
|
||||
price,
|
||||
unitPrice: price,
|
||||
tripTotalPrice: price,
|
||||
displayPrice,
|
||||
tripNights: null,
|
||||
};
|
||||
}
|
||||
|
||||
const tripNights = getTripNightCount();
|
||||
const tripTotalPrice = Number((price * tripNights).toFixed(2));
|
||||
const unitLabel = displayPrice || `${formatCurrencyValue(price, currency)}/night`;
|
||||
|
||||
return {
|
||||
price: tripTotalPrice,
|
||||
unitPrice: price,
|
||||
tripTotalPrice,
|
||||
displayPrice: `${formatCurrencyValue(tripTotalPrice, currency)} stay total (${unitLabel} x ${tripNights} nights)`,
|
||||
tripNights,
|
||||
};
|
||||
}
|
||||
|
||||
function loadPriceHistoryState() {
|
||||
const runs = readJsonLines(PRICE_HISTORY_FILE)
|
||||
.map((entry, index) => {
|
||||
@@ -304,19 +351,34 @@ function loadPriceHistoryState() {
|
||||
});
|
||||
|
||||
if (!key || price === null) return;
|
||||
const currency = point.currency || defaults.currency || 'USD';
|
||||
const priceBasis = inferPriceBasis(point, defaults);
|
||||
const rawDisplayPrice = point.displayPrice || point.priceLabel || point.displayLabel || point.label || defaults.displayPrice || null;
|
||||
const tripPrice = normalizeTripPrice({
|
||||
price,
|
||||
priceBasis,
|
||||
currency,
|
||||
displayPrice: rawDisplayPrice,
|
||||
});
|
||||
|
||||
const nextPoint = {
|
||||
checkedAt: run.checkedAt,
|
||||
checkedAtMs: run.checkedAtMs,
|
||||
runIndex: run.runIndex,
|
||||
price,
|
||||
currency: point.currency || defaults.currency || 'USD',
|
||||
displayPrice: point.displayPrice || point.priceLabel || point.displayLabel || point.label || defaults.displayPrice || null,
|
||||
price: tripPrice.price,
|
||||
unitPrice: tripPrice.unitPrice,
|
||||
tripTotalPrice: tripPrice.tripTotalPrice,
|
||||
tripNights: tripPrice.tripNights,
|
||||
tripCheckIn: tripPrice.tripNights ? TRIP_CHECK_IN : null,
|
||||
tripCheckOut: tripPrice.tripNights ? TRIP_CHECK_OUT : null,
|
||||
currency,
|
||||
displayPrice: tripPrice.displayPrice,
|
||||
unitDisplayPrice: rawDisplayPrice,
|
||||
source: normalizeSourceLabel(point.source || point.sourceLabel || point.vendor || defaults.source || null),
|
||||
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: inferPriceBasis(point, defaults),
|
||||
priceBasis,
|
||||
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