@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.
@@ -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,87jiBAA87jiB,EAC97jiB,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnB,6GAA6G;AAC7G,MAAM,CAAC,MAAM,qBAAqB,GAChC,kEAAkE,CAAC"}
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"}
@@ -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":"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"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/huazhu/backend.ts"],"names":[],"mappings":"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"}
@@ -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
- const freeUntil = policy ? freeUntilOf(policy, stay.checkIn, opts.tz, now()) : undefined;
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
- freeCancellationUntil: room.freeCancellationUntil,
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: describePolicy(policy, b.stay.checkIn, b.tz, amounts),
1119
- freeUntil: freeUntilOf(policy, b.stay.checkIn, b.tz, now()),
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