@curless/hotel-storefront 0.30.1 → 0.30.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/card.generated.d.ts +1 -1
- package/dist/card.generated.js +2 -2
- package/dist/card.generated.js.map +1 -1
- package/dist/huazhu/backend.d.ts +2 -0
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +72 -8
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +10 -2
- package/dist/server.js.map +1 -1
- package/dist/trip-ids.d.ts +16 -1
- package/dist/trip-ids.d.ts.map +1 -1
- package/dist/trip-ids.js +25 -7
- package/dist/trip-ids.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"card.generated.js","sourceRoot":"","sources":["../src/card.generated.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,wEAAwE;AACxE,gFAAgF;AAChF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,eAAe,GAAW,MAAM,CAAC,IAAI,CAChD,
|
|
1
|
+
{"version":3,"file":"card.generated.js","sourceRoot":"","sources":["../src/card.generated.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,wEAAwE;AACxE,gFAAgF;AAChF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,eAAe,GAAW,MAAM,CAAC,IAAI,CAChD,kjkiBAAkjkiB,EACljkiB,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnB,6GAA6G;AAC7G,MAAM,CAAC,MAAM,qBAAqB,GAChC,kEAAkE,CAAC"}
|
package/dist/huazhu/backend.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export declare const newPublicCode: (taken: (code: string) => boolean) => string
|
|
|
56
56
|
* rather than sorting the twenty it happened to price.
|
|
57
57
|
*/
|
|
58
58
|
export declare const PRICE_ALL = 20;
|
|
59
|
+
/** Hotels whose product lists are read to find which sell rooms by the hour today. */
|
|
60
|
+
export declare const HOURLY_SCAN = 60;
|
|
59
61
|
/**
|
|
60
62
|
* Whether a hotel names `place` - in its name, its district or the landmarks
|
|
61
63
|
* and stations around it - and not as the name of a road.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/huazhu/backend.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,mBAAmB,EAExB,KAAK,YAAY,EAMlB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/huazhu/backend.ts"],"names":[],"mappings":"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"}
|
package/dist/huazhu/backend.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
2
|
import { DEFAULT_PAGE, MAX_PAGE, REQUEST_KINDS, REQUEST_NOTE_MAX, guestTooLong, partyNeeds, } from '../backend.js';
|
|
3
|
-
import { addDays, atHotelTime, nights as nightsBetween, stayProblem, todayAt } from '../dates.js';
|
|
3
|
+
import { addDays, arrivalInstant, atHotelTime, nights as nightsBetween, stayProblem, todayAt, } from '../dates.js';
|
|
4
4
|
import { byWords, splitByStay } from '../demo-hotels.js';
|
|
5
5
|
import { FACILITIES, hasFacility } from '../facilities.js';
|
|
6
6
|
import { endOf, startProblem } from '../hourly.js';
|
|
@@ -64,6 +64,8 @@ const SAME_TRIP_MS = 10 * 60_000;
|
|
|
64
64
|
* rather than sorting the twenty it happened to price.
|
|
65
65
|
*/
|
|
66
66
|
export const PRICE_ALL = 20;
|
|
67
|
+
/** Hotels whose product lists are read to find which sell rooms by the hour today. */
|
|
68
|
+
export const HOURLY_SCAN = 60;
|
|
67
69
|
const PRICE_CONCURRENCY = 4;
|
|
68
70
|
/** What a room is called here: the room type, then the rate when there is more than one. */
|
|
69
71
|
const ROOM_SEP = '~';
|
|
@@ -509,6 +511,8 @@ export const createHuazhuHotels = (opts) => {
|
|
|
509
511
|
return got;
|
|
510
512
|
};
|
|
511
513
|
const productsOf = async (hotelId) => (await productsWithDays(hotelId)).value;
|
|
514
|
+
/** Whether a hotel sells any room by the hour on its today - from its products, no prices. */
|
|
515
|
+
const sellsByTheHourToday = async (hotelId) => (await productsOf(hotelId)).some((p) => p.checkinType === 'HOUR' && inStayWindow(p.stayWindow, today()));
|
|
512
516
|
const ariCache = new Map();
|
|
513
517
|
/** Every product's prices, rules and rooms left, one element per date from `start` to `end`. */
|
|
514
518
|
const ariFor = async (hotelId, start, end, fresh = false) => {
|
|
@@ -699,6 +703,39 @@ export const createHuazhuHotels = (opts) => {
|
|
|
699
703
|
].filter((x) => !(x.roomType && x.roomType.status === 0));
|
|
700
704
|
};
|
|
701
705
|
const policyOf = (code) => parseCancelPolicy(code);
|
|
706
|
+
/**
|
|
707
|
+
* A free-cancellation deadline, no later than a room by the hour's start.
|
|
708
|
+
*
|
|
709
|
+
* ⚠️ CANCELLING IS BEFORE THE STAY. AD0_0 reads "free until the end of the
|
|
710
|
+
* arrival day", and a 15:00-19:00 room was offered as free to cancel until
|
|
711
|
+
* 24:00 - five hours after the guest left (eval, 2026-10-14). Once it has
|
|
712
|
+
* begun it is a no-show, as a night is after its night audit.
|
|
713
|
+
*/
|
|
714
|
+
const beforeStart = (until, date, start) => {
|
|
715
|
+
if (until === undefined || !start)
|
|
716
|
+
return until;
|
|
717
|
+
const at = arrivalInstant(date, start, opts.tz);
|
|
718
|
+
const capped = Math.min(until, at);
|
|
719
|
+
return capped > now() ? capped : undefined;
|
|
720
|
+
};
|
|
721
|
+
/** A line's free-cancellation fields: the room's, held to the start chosen. */
|
|
722
|
+
const freeOf = (room, date, start) => {
|
|
723
|
+
const until = room.freeCancellationUntil ? Date.parse(room.freeCancellationUntil) : undefined;
|
|
724
|
+
const held = beforeStart(until, date, start);
|
|
725
|
+
if (held === undefined)
|
|
726
|
+
return { freeCancellationUntil: undefined };
|
|
727
|
+
if (held === until)
|
|
728
|
+
return {
|
|
729
|
+
freeCancellationUntil: room.freeCancellationUntil,
|
|
730
|
+
...(room.freeCancellationLocal
|
|
731
|
+
? { freeCancellationLocal: room.freeCancellationLocal }
|
|
732
|
+
: {}),
|
|
733
|
+
};
|
|
734
|
+
return {
|
|
735
|
+
freeCancellationUntil: new Date(held).toISOString(),
|
|
736
|
+
freeCancellationLocal: deadlineAt(held, opts.tz, opts.language === 'en' ? 'en' : 'zh'),
|
|
737
|
+
};
|
|
738
|
+
};
|
|
702
739
|
const toRoom = (h, p, stay) => {
|
|
703
740
|
const s = p.res;
|
|
704
741
|
const cur = s.currency;
|
|
@@ -707,7 +744,9 @@ export const createHuazhuHotels = (opts) => {
|
|
|
707
744
|
const total = after.reduce((a, b) => a + b, 0);
|
|
708
745
|
const pre = s.before ? s.before.map((x) => toMinor(x, cur)).reduce((a, b) => a + b, 0) : total;
|
|
709
746
|
const policy = policyOf(s.cancelCode);
|
|
710
|
-
|
|
747
|
+
// A room by the hour: free at most until the latest it can start - the
|
|
748
|
+
// line with its start chosen says the start itself (`quoteLines`).
|
|
749
|
+
const freeUntil = beforeStart(policy ? freeUntilOf(policy, stay.checkIn, opts.tz, now()) : undefined, stay.checkIn, p.hourly?.latestStart);
|
|
711
750
|
const rt = p.roomType;
|
|
712
751
|
const beds = (rt?.bedTypeList ?? [])
|
|
713
752
|
.map((b) => [(en ? bedNameEn(b.bedTypeName) : undefined) ?? b.bedTypeName, b.bedSpec]
|
|
@@ -939,10 +978,7 @@ export const createHuazhuHotels = (opts) => {
|
|
|
939
978
|
unit: room.nightly,
|
|
940
979
|
total: money(preTax, room.stayTotal.currency),
|
|
941
980
|
refundable: room.refundable,
|
|
942
|
-
|
|
943
|
-
...(room.freeCancellationLocal
|
|
944
|
-
? { freeCancellationLocal: room.freeCancellationLocal }
|
|
945
|
-
: {}),
|
|
981
|
+
...freeOf(room, line.stay.checkIn, hourly?.start),
|
|
946
982
|
day: line.stay.checkIn,
|
|
947
983
|
});
|
|
948
984
|
}
|
|
@@ -1110,13 +1146,28 @@ export const createHuazhuHotels = (opts) => {
|
|
|
1110
1146
|
freeUntil: undefined,
|
|
1111
1147
|
};
|
|
1112
1148
|
}
|
|
1149
|
+
// A room by the hour that has begun is a no-show, whatever the code says
|
|
1150
|
+
// about the rest of the day - see `beforeStart`.
|
|
1151
|
+
if (b.hourly && now() >= arrivalInstant(b.stay.checkIn, b.hourly.start, b.tz)) {
|
|
1152
|
+
return {
|
|
1153
|
+
fee: total,
|
|
1154
|
+
forbidden: false,
|
|
1155
|
+
rule: `This room by the hour began at ${b.hourly.start}, so cancelling now is a no-show; the fee shown is the most it could be, and Huazhu works out the actual fee when it cancels.`,
|
|
1156
|
+
freeUntil: undefined,
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1113
1159
|
const penalty = penaltyAt(policy, b.stay.checkIn, b.tz, now());
|
|
1114
1160
|
const fee = feeFor(penalty, amounts);
|
|
1161
|
+
const freeUntil = b.hourly
|
|
1162
|
+
? beforeStart(freeUntilOf(policy, b.stay.checkIn, b.tz, now()), b.stay.checkIn, b.hourly.start)
|
|
1163
|
+
: freeUntilOf(policy, b.stay.checkIn, b.tz, now());
|
|
1115
1164
|
return {
|
|
1116
1165
|
fee: fee ?? total,
|
|
1117
1166
|
forbidden: fee === null,
|
|
1118
|
-
rule:
|
|
1119
|
-
|
|
1167
|
+
rule: b.hourly && freeUntil !== undefined && fee === 0
|
|
1168
|
+
? `Free to cancel until ${b.hourly.start} on ${b.stay.checkIn}, when the room by the hour begins; after that it is a no-show.`
|
|
1169
|
+
: describePolicy(policy, b.stay.checkIn, b.tz, amounts),
|
|
1170
|
+
freeUntil,
|
|
1120
1171
|
};
|
|
1121
1172
|
};
|
|
1122
1173
|
/**
|
|
@@ -1929,6 +1980,19 @@ export const createHuazhuHotels = (opts) => {
|
|
|
1929
1980
|
[h.brand, raw?.brandName ?? ''].some((b) => b.trim().toLowerCase() === (over.brand ?? '').trim().toLowerCase())) &&
|
|
1930
1981
|
(over.facilities ?? []).every((k) => hasFacility(h.amenities, k));
|
|
1931
1982
|
let matches = placed.filter(({ hotel, raw }) => passesStatic(hotel, f, raw));
|
|
1983
|
+
// ⚠️ ONLY HOTELS THAT SELL ROOMS BY THE HOUR TODAY - read from their
|
|
1984
|
+
// product lists, which carry no price (one call a hotel, kept for
|
|
1985
|
+
// `productsTtl`). Past PRICE_ALL the list was paged unfiltered, and every
|
|
1986
|
+
// hotel came back, most selling no room by the hour at all (robin,
|
|
1987
|
+
// 2026-10-14: 35 listed, a dozen sold them). A hotel that sells them
|
|
1988
|
+
// today and has none left, or none yet, stays - with why (`priceHotel`).
|
|
1989
|
+
if (hourlyOnly) {
|
|
1990
|
+
const scanned = matches.slice(0, HOURLY_SCAN);
|
|
1991
|
+
const sells = await pool(scanned, PRICE_CONCURRENCY, ({ raw }) => sellsByTheHourToday(raw.hotelId).catch(() => true));
|
|
1992
|
+
if (matches.length > HOURLY_SCAN)
|
|
1993
|
+
unapplied.push(`${matches.length} hotels matched - only the first ${HOURLY_SCAN} were asked whether they sell rooms by the hour. A district, a brand or a landmark narrows it.`);
|
|
1994
|
+
matches = scanned.filter((_, i) => sells[i]);
|
|
1995
|
+
}
|
|
1932
1996
|
// ⚠️ PRICED WHEN THE NIGHTS ARE NAMED - but a city can hold hundreds of
|
|
1933
1997
|
// these hotels and each one is two calls to Huazhu. Everything is priced
|
|
1934
1998
|
// when there are few enough; past that, only the page, and the price
|