@curless/hotel-storefront 0.30.2 → 0.30.4
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/backend.d.ts +7 -1
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +9 -3
- package/dist/backend.js.map +1 -1
- 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/dates.d.ts +11 -0
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +30 -9
- package/dist/dates.js.map +1 -1
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +42 -10
- package/dist/demo-hotels.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 +114 -30
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/huazhu/sellable.d.ts +3 -1
- package/dist/huazhu/sellable.d.ts.map +1 -1
- package/dist/huazhu/sellable.js +22 -3
- package/dist/huazhu/sellable.js.map +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.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/tools.d.ts.map +1 -1
- package/dist/tools.js +13 -0
- package/dist/tools.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/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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, arrivalInstant, atHotelTime, nights as nightsBetween, stayProblem, todayAt, } from '../dates.js';
|
|
3
|
+
import { addDays, arrivalInstant, atHotelTime, nights as nightsBetween, stayProblem, stayProblemOf, 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) => {
|
|
@@ -579,6 +583,7 @@ export const createHuazhuHotels = (opts) => {
|
|
|
579
583
|
ok: false,
|
|
580
584
|
kind: 'too-far',
|
|
581
585
|
why: `rooms here are on sale at most ${sellDays} days ahead`,
|
|
586
|
+
args: { days: sellDays },
|
|
582
587
|
},
|
|
583
588
|
}))
|
|
584
589
|
.filter((x) => !(x.roomType && x.roomType.status === 0));
|
|
@@ -795,6 +800,18 @@ export const createHuazhuHotels = (opts) => {
|
|
|
795
800
|
...(maxChildren > 0 && maxChildren < 99 ? { maxChildren } : {}),
|
|
796
801
|
};
|
|
797
802
|
};
|
|
803
|
+
/** A party too big for any room here, said by partyNeeds with its numbers beside it. */
|
|
804
|
+
const partyWhy = (guests, largest) => ({
|
|
805
|
+
why: partyNeeds(guests, largest),
|
|
806
|
+
code: 'party',
|
|
807
|
+
...(largest > 0 ? { args: { largest, rooms: Math.ceil(guests / largest) } } : {}),
|
|
808
|
+
});
|
|
809
|
+
/** A hotel's "nothing to book", as HotelDetail carries it. */
|
|
810
|
+
const noRoomsOf = (w) => ({
|
|
811
|
+
noRooms: w.why,
|
|
812
|
+
noRoomsCode: w.code,
|
|
813
|
+
...(w.args ? { noRoomsArgs: w.args } : {}),
|
|
814
|
+
});
|
|
798
815
|
/** The most a room here sleeps, from rates refused for the party ("sleeps 2"). */
|
|
799
816
|
const largestOf = (fails) => Math.max(...fails.map((f) => Number(/\d+/.exec(f.why)?.[0] ?? 0)));
|
|
800
817
|
/**
|
|
@@ -807,49 +824,84 @@ export const createHuazhuHotels = (opts) => {
|
|
|
807
824
|
*/
|
|
808
825
|
const hourlyNoRoomsWhy = (priced, guests) => {
|
|
809
826
|
if (priced.length === 0)
|
|
810
|
-
return 'This hotel sells no rooms by the hour today.';
|
|
827
|
+
return { why: 'This hotel sells no rooms by the hour today.', code: 'hourly-none' };
|
|
811
828
|
const fails = priced.map((p) => p.res).filter((r) => !r.ok);
|
|
812
829
|
if (fails.length > 0 && fails.every((f) => f.kind === 'party'))
|
|
813
|
-
return
|
|
830
|
+
return partyWhy(guests, largestOf(fails));
|
|
814
831
|
if (fails.some((f) => f.kind === 'prepaid'))
|
|
815
|
-
return
|
|
832
|
+
return {
|
|
833
|
+
why: "This hotel's rooms by the hour are paid in advance, and this connector books rates paid at the hotel.",
|
|
834
|
+
code: 'prepaid-only',
|
|
835
|
+
};
|
|
816
836
|
const said = (re) => fails
|
|
817
837
|
.map((f) => (f.kind === 'closed' ? re.exec(f.why)?.[1] : undefined))
|
|
818
838
|
.filter((t) => !!t)
|
|
819
839
|
.sort();
|
|
820
840
|
const opens = said(/on sale today from (\d\d:\d\d)/)[0];
|
|
821
841
|
if (opens)
|
|
822
|
-
return
|
|
842
|
+
return {
|
|
843
|
+
why: `Rooms by the hour here go on sale today from ${opens}.`,
|
|
844
|
+
code: 'hourly-not-yet',
|
|
845
|
+
args: { opensAt: opens },
|
|
846
|
+
};
|
|
823
847
|
const last = said(/the last start today was (\d\d:\d\d)/).pop();
|
|
824
848
|
if (last)
|
|
825
|
-
return
|
|
826
|
-
|
|
849
|
+
return {
|
|
850
|
+
why: `No room by the hour is left here today: the last start today was ${last}.`,
|
|
851
|
+
code: 'hourly-over',
|
|
852
|
+
args: { lastStart: last },
|
|
853
|
+
};
|
|
854
|
+
return { why: 'No room by the hour is left here today.', code: 'sold-out' };
|
|
827
855
|
};
|
|
828
856
|
/** Why nothing here can be sold for these nights - the most useful reason first. */
|
|
829
857
|
const noRoomsWhy = (priced, stay, rooms) => {
|
|
830
858
|
if (priced.length === 0)
|
|
831
|
-
return
|
|
859
|
+
return {
|
|
860
|
+
why: 'This hotel has no rooms on sale through this connector.',
|
|
861
|
+
code: 'none-on-sale',
|
|
862
|
+
};
|
|
832
863
|
const fails = priced.map((p) => p.res).filter((r) => !r.ok);
|
|
833
864
|
const kinds = new Set(fails.map((f) => f.kind));
|
|
834
865
|
const only = (k) => kinds.size === 1 && kinds.has(k);
|
|
835
866
|
if (only('party'))
|
|
836
|
-
return
|
|
867
|
+
return partyWhy(stay.guests, largestOf(fails));
|
|
837
868
|
if (only('prepaid'))
|
|
838
|
-
return
|
|
869
|
+
return {
|
|
870
|
+
why: 'Every rate here for those nights is paid in advance, and this connector books rates paid at the hotel.',
|
|
871
|
+
code: 'prepaid-only',
|
|
872
|
+
};
|
|
839
873
|
if (only('sold-out'))
|
|
840
874
|
return rooms > 1
|
|
841
|
-
?
|
|
842
|
-
|
|
875
|
+
? {
|
|
876
|
+
why: `No room type here has ${rooms} left for those nights.`,
|
|
877
|
+
code: 'sold-out',
|
|
878
|
+
args: { rooms },
|
|
879
|
+
}
|
|
880
|
+
: { why: 'Every room type here is taken for those nights.', code: 'sold-out' };
|
|
843
881
|
if (only('too-many-rooms'))
|
|
844
|
-
return
|
|
882
|
+
return {
|
|
883
|
+
why: `No rate here books ${rooms} rooms at once; ask for fewer.`,
|
|
884
|
+
code: 'too-many-rooms',
|
|
885
|
+
args: { rooms },
|
|
886
|
+
};
|
|
845
887
|
if (only('too-far'))
|
|
846
|
-
return
|
|
888
|
+
return {
|
|
889
|
+
why: `Not on sale yet: ${fails[0]?.why ?? 'rooms here are not sold that far ahead'}.`,
|
|
890
|
+
code: 'too-far',
|
|
891
|
+
...(fails[0]?.args?.days ? { args: { days: fails[0].args.days } } : {}),
|
|
892
|
+
};
|
|
847
893
|
const rule = fails.find((f) => f.kind === 'stay-rule');
|
|
848
894
|
if (rule)
|
|
849
|
-
return
|
|
895
|
+
return {
|
|
896
|
+
why: `Not for those nights: ${rule.why}.`,
|
|
897
|
+
// Booked too far ahead for this rate is "too far", said with its days.
|
|
898
|
+
...(rule.args?.days !== undefined && !rule.args.rule
|
|
899
|
+
? { code: 'too-far', args: { days: rule.args.days } }
|
|
900
|
+
: { code: 'stay-rule', ...(rule.args ? { args: rule.args } : {}) }),
|
|
901
|
+
};
|
|
850
902
|
if (kinds.has('closed'))
|
|
851
|
-
return 'Not on sale for those nights.';
|
|
852
|
-
return 'No room here can be booked for those nights.';
|
|
903
|
+
return { why: 'Not on sale for those nights.', code: 'closed' };
|
|
904
|
+
return { why: 'No room here can be booked for those nights.', code: 'unavailable' };
|
|
853
905
|
};
|
|
854
906
|
// --------------------------------------------------------------- quote ----
|
|
855
907
|
const quoteLines = async (trip, fresh = false) => {
|
|
@@ -1976,6 +2028,19 @@ export const createHuazhuHotels = (opts) => {
|
|
|
1976
2028
|
[h.brand, raw?.brandName ?? ''].some((b) => b.trim().toLowerCase() === (over.brand ?? '').trim().toLowerCase())) &&
|
|
1977
2029
|
(over.facilities ?? []).every((k) => hasFacility(h.amenities, k));
|
|
1978
2030
|
let matches = placed.filter(({ hotel, raw }) => passesStatic(hotel, f, raw));
|
|
2031
|
+
// ⚠️ ONLY HOTELS THAT SELL ROOMS BY THE HOUR TODAY - read from their
|
|
2032
|
+
// product lists, which carry no price (one call a hotel, kept for
|
|
2033
|
+
// `productsTtl`). Past PRICE_ALL the list was paged unfiltered, and every
|
|
2034
|
+
// hotel came back, most selling no room by the hour at all (robin,
|
|
2035
|
+
// 2026-10-14: 35 listed, a dozen sold them). A hotel that sells them
|
|
2036
|
+
// today and has none left, or none yet, stays - with why (`priceHotel`).
|
|
2037
|
+
if (hourlyOnly) {
|
|
2038
|
+
const scanned = matches.slice(0, HOURLY_SCAN);
|
|
2039
|
+
const sells = await pool(scanned, PRICE_CONCURRENCY, ({ raw }) => sellsByTheHourToday(raw.hotelId).catch(() => true));
|
|
2040
|
+
if (matches.length > HOURLY_SCAN)
|
|
2041
|
+
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.`);
|
|
2042
|
+
matches = scanned.filter((_, i) => sells[i]);
|
|
2043
|
+
}
|
|
1979
2044
|
// ⚠️ PRICED WHEN THE NIGHTS ARE NAMED - but a city can hold hundreds of
|
|
1980
2045
|
// these hotels and each one is two calls to Huazhu. Everything is priced
|
|
1981
2046
|
// when there are few enough; past that, only the page, and the price
|
|
@@ -2020,11 +2085,14 @@ export const createHuazhuHotels = (opts) => {
|
|
|
2020
2085
|
}
|
|
2021
2086
|
catch {
|
|
2022
2087
|
pricedRooms.set(raw.hotelId, []);
|
|
2023
|
-
why.set(raw.hotelId,
|
|
2088
|
+
why.set(raw.hotelId, {
|
|
2089
|
+
why: 'Rooms by the hour could not be read from Huazhu just now; open the hotel for them.',
|
|
2090
|
+
code: 'unreachable',
|
|
2091
|
+
});
|
|
2024
2092
|
}
|
|
2025
2093
|
return;
|
|
2026
2094
|
}
|
|
2027
|
-
const problem =
|
|
2095
|
+
const problem = stayProblemOf(stay, today());
|
|
2028
2096
|
if (problem) {
|
|
2029
2097
|
pricedRooms.set(raw.hotelId, []);
|
|
2030
2098
|
why.set(raw.hotelId, problem);
|
|
@@ -2038,9 +2106,12 @@ export const createHuazhuHotels = (opts) => {
|
|
|
2038
2106
|
}
|
|
2039
2107
|
catch (err) {
|
|
2040
2108
|
pricedRooms.set(raw.hotelId, []);
|
|
2041
|
-
why.set(raw.hotelId,
|
|
2042
|
-
|
|
2043
|
-
|
|
2109
|
+
why.set(raw.hotelId, {
|
|
2110
|
+
why: err instanceof HuazhuRefusal && err.code === 2009
|
|
2111
|
+
? 'Huazhu is answering slowly just now; open the hotel for its rooms.'
|
|
2112
|
+
: 'Prices could not be read from Huazhu just now; open the hotel for its rooms.',
|
|
2113
|
+
code: 'unreachable',
|
|
2114
|
+
});
|
|
2044
2115
|
}
|
|
2045
2116
|
};
|
|
2046
2117
|
const bestFor = (raw, over) => {
|
|
@@ -2140,11 +2211,18 @@ export const createHuazhuHotels = (opts) => {
|
|
|
2140
2211
|
if (!stay)
|
|
2141
2212
|
return m.hotel;
|
|
2142
2213
|
const b = bestFor(m.raw, f);
|
|
2143
|
-
if (!b)
|
|
2214
|
+
if (!b) {
|
|
2215
|
+
const w = why.get(m.raw.hotelId) ?? {
|
|
2216
|
+
why: 'No room here can be booked for those nights.',
|
|
2217
|
+
code: 'unavailable',
|
|
2218
|
+
};
|
|
2144
2219
|
return {
|
|
2145
2220
|
...m.hotel,
|
|
2146
|
-
noRoomsForStay: why
|
|
2221
|
+
noRoomsForStay: w.why,
|
|
2222
|
+
noRoomsForStayCode: w.code,
|
|
2223
|
+
...(w.args ? { noRoomsForStayArgs: w.args } : {}),
|
|
2147
2224
|
};
|
|
2225
|
+
}
|
|
2148
2226
|
return {
|
|
2149
2227
|
...m.hotel,
|
|
2150
2228
|
forStay: {
|
|
@@ -2233,9 +2311,9 @@ export const createHuazhuHotels = (opts) => {
|
|
|
2233
2311
|
const detail = { hotel: toHotel(raw, view, now()) };
|
|
2234
2312
|
if (!stay)
|
|
2235
2313
|
return detail;
|
|
2236
|
-
const problem =
|
|
2314
|
+
const problem = stayProblemOf(stay, today());
|
|
2237
2315
|
if (problem)
|
|
2238
|
-
return { ...detail,
|
|
2316
|
+
return { ...detail, ...noRoomsOf(problem) };
|
|
2239
2317
|
let priced;
|
|
2240
2318
|
try {
|
|
2241
2319
|
priced = await priceStay(raw, stay, 1);
|
|
@@ -2244,7 +2322,10 @@ export const createHuazhuHotels = (opts) => {
|
|
|
2244
2322
|
return {
|
|
2245
2323
|
...detail,
|
|
2246
2324
|
stay,
|
|
2247
|
-
|
|
2325
|
+
...noRoomsOf({
|
|
2326
|
+
why: `Huazhu could not be asked for this hotel's rooms just now (${err instanceof HuazhuRefusal ? err.huazhuMessage : 'no answer'}). Try again in a moment.`,
|
|
2327
|
+
code: 'unreachable',
|
|
2328
|
+
}),
|
|
2248
2329
|
};
|
|
2249
2330
|
}
|
|
2250
2331
|
const rooms = priced
|
|
@@ -2255,7 +2336,7 @@ export const createHuazhuHotels = (opts) => {
|
|
|
2255
2336
|
...detail,
|
|
2256
2337
|
stay,
|
|
2257
2338
|
rooms,
|
|
2258
|
-
...(rooms.length === 0 ?
|
|
2339
|
+
...(rooms.length === 0 ? noRoomsOf(noRoomsWhy(priced, stay, 1)) : {}),
|
|
2259
2340
|
};
|
|
2260
2341
|
},
|
|
2261
2342
|
async hourlyRooms(id, guests) {
|
|
@@ -2273,7 +2354,10 @@ export const createHuazhuHotels = (opts) => {
|
|
|
2273
2354
|
return {
|
|
2274
2355
|
hotel,
|
|
2275
2356
|
stay,
|
|
2276
|
-
|
|
2357
|
+
...noRoomsOf({
|
|
2358
|
+
why: `Huazhu could not be asked for this hotel's rooms by the hour just now (${err instanceof HuazhuRefusal ? err.huazhuMessage : 'no answer'}). Try again in a moment.`,
|
|
2359
|
+
code: 'unreachable',
|
|
2360
|
+
}),
|
|
2277
2361
|
};
|
|
2278
2362
|
}
|
|
2279
2363
|
const rooms = priced
|
|
@@ -2284,7 +2368,7 @@ export const createHuazhuHotels = (opts) => {
|
|
|
2284
2368
|
hotel,
|
|
2285
2369
|
stay,
|
|
2286
2370
|
rooms,
|
|
2287
|
-
...(rooms.length === 0 ?
|
|
2371
|
+
...(rooms.length === 0 ? noRoomsOf(hourlyNoRoomsWhy(priced, guests)) : {}),
|
|
2288
2372
|
};
|
|
2289
2373
|
},
|
|
2290
2374
|
async datesAround(id, stay, o) {
|