@curless/hotel-storefront 0.27.28 → 0.28.0

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.
Files changed (61) hide show
  1. package/dist/backend.d.ts +12 -0
  2. package/dist/backend.d.ts.map +1 -1
  3. package/dist/backend.js.map +1 -1
  4. package/dist/config.d.ts +7 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/hourly.d.ts +31 -0
  8. package/dist/hourly.d.ts.map +1 -0
  9. package/dist/hourly.js +61 -0
  10. package/dist/hourly.js.map +1 -0
  11. package/dist/huazhu/backend.d.ts +2 -0
  12. package/dist/huazhu/backend.d.ts.map +1 -1
  13. package/dist/huazhu/backend.js +209 -20
  14. package/dist/huazhu/backend.js.map +1 -1
  15. package/dist/huazhu/check.js +2 -2
  16. package/dist/huazhu/check.js.map +1 -1
  17. package/dist/huazhu/client.d.ts +3 -3
  18. package/dist/huazhu/client.d.ts.map +1 -1
  19. package/dist/huazhu/client.js +4 -4
  20. package/dist/huazhu/client.js.map +1 -1
  21. package/dist/huazhu/english.d.ts.map +1 -1
  22. package/dist/huazhu/english.js +15 -0
  23. package/dist/huazhu/english.js.map +1 -1
  24. package/dist/huazhu/fake.d.ts +3 -1
  25. package/dist/huazhu/fake.d.ts.map +1 -1
  26. package/dist/huazhu/fake.js +69 -7
  27. package/dist/huazhu/fake.js.map +1 -1
  28. package/dist/huazhu/labels.d.ts +9 -0
  29. package/dist/huazhu/labels.d.ts.map +1 -0
  30. package/dist/huazhu/labels.js +28 -0
  31. package/dist/huazhu/labels.js.map +1 -0
  32. package/dist/huazhu/make.d.ts.map +1 -1
  33. package/dist/huazhu/make.js +1 -0
  34. package/dist/huazhu/make.js.map +1 -1
  35. package/dist/huazhu/sandbox.d.ts.map +1 -1
  36. package/dist/huazhu/sandbox.js +111 -0
  37. package/dist/huazhu/sandbox.js.map +1 -1
  38. package/dist/huazhu/sellable.d.ts +29 -3
  39. package/dist/huazhu/sellable.d.ts.map +1 -1
  40. package/dist/huazhu/sellable.js +68 -4
  41. package/dist/huazhu/sellable.js.map +1 -1
  42. package/dist/huazhu/wire.d.ts +11 -0
  43. package/dist/huazhu/wire.d.ts.map +1 -1
  44. package/dist/huazhu/wire.js.map +1 -1
  45. package/dist/present.d.ts.map +1 -1
  46. package/dist/present.js +8 -4
  47. package/dist/present.js.map +1 -1
  48. package/dist/server.d.ts +1 -1
  49. package/dist/server.d.ts.map +1 -1
  50. package/dist/server.js +1 -1
  51. package/dist/server.js.map +1 -1
  52. package/dist/tools.d.ts.map +1 -1
  53. package/dist/tools.js +172 -15
  54. package/dist/tools.js.map +1 -1
  55. package/dist/trip.d.ts +6 -0
  56. package/dist/trip.d.ts.map +1 -1
  57. package/dist/trip.js +17 -3
  58. package/dist/trip.js.map +1 -1
  59. package/dist/types.d.ts +68 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { DEFAULT_PAGE, MAX_PAGE, REQUEST_KINDS, REQUEST_NOTE_MAX, guestTooLong,
3
3
  import { addDays, 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
+ import { endOf, startProblem } from '../hourly.js';
6
7
  import { BOOKING_LIMITS, DAY_MS, HOUR_MS, WindowCounter } from '../limits.js';
7
8
  import { formatMoney, minorFactor, money, multiplyMoney, toMinor } from '../money.js';
8
9
  import { MAX_ROOMS_PER_LINE, quoteLineKey } from '../trip.js';
@@ -10,8 +11,9 @@ import { IN_REQUEST_AT_MS, StoreWriteError, nextCheckAt, statusOf, } from './boo
10
11
  import { describePolicy, feeFor, freeUntilOf, nightAuditOf, parseCancelPolicy, penaltyAt, refundableFrom, } from './cancel-policy.js';
11
12
  import { haystackOf, inPlace, roomPhoto, toHotel } from './catalog.js';
12
13
  import { HuazhuRefusal, HuazhuUnreachable } from './client.js';
13
- import { bedNameEn, offerNameEn, roomNameEn } from './english.js';
14
- import { ariRanges, bookableNow, clockAt, offeredAtAll, sell, } from './sellable.js';
14
+ import { bedNameEn, offerNameEn, rateNameEn, roomNameEn } from './english.js';
15
+ import { rateLabels } from './labels.js';
16
+ import { ariRanges, bookableNow, clockAt, hourlyWindow, inStayWindow, offeredAtAll, sell, } from './sellable.js';
15
17
  import { BOOK_EXISTS, BOOK_REFUSALS, CODES, } from './wire.js';
16
18
  export const HUAZHU_CAPABILITIES = {
17
19
  members: false,
@@ -312,6 +314,8 @@ export const createHuazhuHotels = (opts) => {
312
314
  /** A room as offered - type and rate - in the connector's language. */
313
315
  const offerOf = (p) => (en ? offerNameEn(p.roomTypeName, p.rateCodeName) : undefined) ??
314
316
  [p.roomTypeName, p.rateCodeName].filter(Boolean).join(' · ');
317
+ /** A rate's own name in the connector's language, or Huazhu's words as they are. */
318
+ const rateNameOf = (p) => (en ? rateNameEn(p.rateCodeName) : undefined) ?? p.rateCodeName ?? p.rateCode;
315
319
  const today = () => todayAt(now(), opts.tz);
316
320
  const limits = { ...BOOKING_LIMITS, ...(opts.limits ?? {}) };
317
321
  const requestsByEmail = new WindowCounter(DAY_MS, now);
@@ -494,7 +498,9 @@ export const createHuazhuHotels = (opts) => {
494
498
  };
495
499
  /** Every sellable-or-not product of one hotel for one stay. */
496
500
  const priceStay = async (h, stay, rooms, fresh = false) => {
497
- const { value: products, sellDays } = await productsWithDays(h.hotelId);
501
+ const { value: all, sellDays } = await productsWithDays(h.hotelId);
502
+ // Nights only: rooms by the hour are priced for today apart - `priceHourly`.
503
+ const products = all.filter((p) => p.checkinType === 'DAY');
498
504
  if (products.length === 0)
499
505
  return [];
500
506
  const roomTypeOf = (p) => (h.hotelRoomTypeList ?? []).find((r) => r.roomTypeCode === p.roomTypeCode);
@@ -537,6 +543,84 @@ export const createHuazhuHotels = (opts) => {
537
543
  })
538
544
  .filter((x) => !(x.roomType && x.roomType.status === 0));
539
545
  };
546
+ const hourlyAriCache = new Map();
547
+ /**
548
+ * Today's prices and rooms for a hotel's rooms by the hour. Huazhu answers
549
+ * these for today only ("时租房 … 只能查 startDate 为当天的 ARI"), asked apart
550
+ * from the nights with `checkinType: HOUR`.
551
+ */
552
+ const hourlyAriFor = async (hotelId, date, fresh = false) => {
553
+ const key = `${hotelId}|${date}`;
554
+ const hit = hourlyAriCache.get(key);
555
+ if (!fresh && hit && now() - hit.at < ariTtl)
556
+ return hit.value;
557
+ const [offers, inventory] = await Promise.all([
558
+ client.offers(hotelId, date, date, undefined, 'HOUR').catch((err) => {
559
+ if (err instanceof HuazhuRefusal && [4206, 2008].includes(err.code))
560
+ return { dailyOffers: [] };
561
+ throw err;
562
+ }),
563
+ client.inventory(hotelId, date, date, undefined, 'HOUR').catch((err) => {
564
+ if (err instanceof HuazhuRefusal && [4306, 2008].includes(err.code))
565
+ return {
566
+ dailyInventories: [],
567
+ };
568
+ throw err;
569
+ }),
570
+ ]);
571
+ const out = new Map();
572
+ for (const offer of offers.dailyOffers ?? []) {
573
+ const row = (inventory.dailyInventories ?? []).find((i) => roomIdOf(i) === roomIdOf(offer));
574
+ out.set(roomIdOf(offer), { start: date, offer, inventory: row?.inventories ?? [] });
575
+ }
576
+ hourlyAriCache.set(key, { at: now(), value: out });
577
+ if (hourlyAriCache.size > 500)
578
+ hourlyAriCache.delete(hourlyAriCache.keys().next().value);
579
+ return out;
580
+ };
581
+ /**
582
+ * A hotel's rooms by the hour, for today on the hotel's own date and clock:
583
+ * each sellable one with the starts it takes, or why not.
584
+ *
585
+ * ⚠️ TODAY IS THE HOTEL'S, NOT THE SERVER'S (总线程, 2026-10-12): at 01:00 in
586
+ * Singapore it is still yesterday nowhere that matters, and a start that has
587
+ * already gone by on the hotel's clock is not offered.
588
+ */
589
+ const priceHourly = async (h, guests, rooms, fresh = false) => {
590
+ const products = (await productsOf(h.hotelId)).filter((p) => p.checkinType === 'HOUR');
591
+ if (products.length === 0)
592
+ return [];
593
+ const date = today();
594
+ const clock = clockAt(now(), opts.tz);
595
+ const live = products.filter((p) => bookableNow(p.bookWindow, date, clock) && inStayWindow(p.stayWindow, date));
596
+ if (live.length === 0)
597
+ return [];
598
+ const ari = await hourlyAriFor(h.hotelId, date, fresh);
599
+ // One day, priced once: `sell` reads it as a single night from today.
600
+ const context = {
601
+ checkIn: date,
602
+ checkOut: addDays(date, 1),
603
+ adults: guests,
604
+ rooms,
605
+ today: date,
606
+ clock,
607
+ paymentType: opts.paymentType,
608
+ allowTags,
609
+ };
610
+ return live
611
+ .map((product) => {
612
+ const roomType = (h.hotelRoomTypeList ?? []).find((r) => r.roomTypeCode === product.roomTypeCode);
613
+ const w = ari.get(roomIdOf(product));
614
+ const hourly = hourlyWindow(product, date, clock);
615
+ let res = w
616
+ ? sell(product, w, roomType, context)
617
+ : { ok: false, kind: 'no-price', why: 'no price for today' };
618
+ if (res.ok && !hourly)
619
+ res = { ok: false, kind: 'closed', why: 'no start time is left today' };
620
+ return { product, roomType, res, ...(hourly ? { hourly } : {}) };
621
+ })
622
+ .filter((x) => !(x.roomType && x.roomType.status === 0));
623
+ };
540
624
  const policyOf = (code) => parseCancelPolicy(code);
541
625
  const toRoom = (h, p, stay) => {
542
626
  const s = p.res;
@@ -553,6 +637,8 @@ export const createHuazhuHotels = (opts) => {
553
637
  .filter(Boolean)
554
638
  .join(' '))
555
639
  .filter(Boolean);
640
+ const hourly = p.hourly;
641
+ const maxChildren = Number(p.product.occupancy?.maxChild ?? rt?.maxChild);
556
642
  return {
557
643
  id: roomIdOf(p.product),
558
644
  name: offerOf(p.product),
@@ -565,11 +651,14 @@ export const createHuazhuHotels = (opts) => {
565
651
  ? { imageUrl: roomPhoto(h, p.product.roomTypeCode) }
566
652
  : {}),
567
653
  // A whole yuan, as a hotel quotes a nightly rate - not ¥343.50 (2026-10-05
568
- // audit, P3). The stay total stays exact.
569
- nightly: money(Math.round(pre / n / minorFactor(cur)) * minorFactor(cur), cur),
654
+ // audit, P3). The stay total stays exact. A room by the hour is the
655
+ // price of its hours, and no nights.
656
+ nightly: hourly
657
+ ? money(total, cur)
658
+ : money(Math.round(pre / n / minorFactor(cur)) * minorFactor(cur), cur),
570
659
  stayTotal: money(total, cur),
571
660
  taxesAndFees: money(total - pre, cur),
572
- nights: n,
661
+ nights: hourly ? 0 : n,
573
662
  refundable: policy ? refundableFrom(policy, stay.checkIn, opts.tz, now()) : false,
574
663
  ...(freeUntil
575
664
  ? {
@@ -581,6 +670,15 @@ export const createHuazhuHotels = (opts) => {
581
670
  }
582
671
  : {}),
583
672
  roomsLeft: s.roomsLeft,
673
+ roomType: { id: p.product.roomTypeCode, name: roomTypeOf(p.product) },
674
+ rate: { id: p.product.rateCode, name: rateNameOf(p.product) },
675
+ labels: rateLabels(p.product, { type: s.mealType, count: s.mealCount }),
676
+ // ⚠️ AT THE HOTEL, ALWAYS, HERE: prepaid rates are left out before
677
+ // this (`sell`, kind 'prepaid') until robin decides how that money is
678
+ // taken - a live deployment never offers one (总线程, 2026-10-12).
679
+ payment: 'at-hotel',
680
+ ...(hourly ? { hourly } : {}),
681
+ ...(maxChildren > 0 && maxChildren < 99 ? { maxChildren } : {}),
584
682
  };
585
683
  };
586
684
  /** Why nothing here can be sold for these nights - the most useful reason first. */
@@ -636,7 +734,13 @@ export const createHuazhuHotels = (opts) => {
636
734
  });
637
735
  continue;
638
736
  }
639
- const problem = stayProblem(line.stay, today());
737
+ const hourly = line.hourly;
738
+ // A room by the hour is today, on the hotel's date, in and out the same day.
739
+ const problem = hourly
740
+ ? line.stay.checkIn !== today() || line.stay.checkOut !== line.stay.checkIn
741
+ ? `Rooms by the hour are sold for today (${today()}) only, in and out the same day.`
742
+ : undefined
743
+ : stayProblem(line.stay, today());
640
744
  if (problem) {
641
745
  dropped.push({ title: hotelName, why: problem });
642
746
  continue;
@@ -644,7 +748,9 @@ export const createHuazhuHotels = (opts) => {
644
748
  const wanted = Math.max(1, Math.floor(line.rooms ?? 1));
645
749
  let priced;
646
750
  try {
647
- priced = await priceStay(h, line.stay, wanted, fresh);
751
+ priced = hourly
752
+ ? await priceHourly(h, line.stay.guests, wanted, fresh)
753
+ : await priceStay(h, line.stay, wanted, fresh);
648
754
  }
649
755
  catch (err) {
650
756
  dropped.push({
@@ -657,7 +763,7 @@ export const createHuazhuHotels = (opts) => {
657
763
  if (!p) {
658
764
  dropped.push({
659
765
  title: `${hotelName} - ${line.roomId}`,
660
- why: 'That rate is no longer sold.',
766
+ why: hourly ? 'That room by the hour is not sold today.' : 'That rate is no longer sold.',
661
767
  });
662
768
  continue;
663
769
  }
@@ -665,7 +771,20 @@ export const createHuazhuHotels = (opts) => {
665
771
  dropped.push({ title: `${hotelName} - ${roomTypeOf(p.product)}`, why: `${p.res.why}.` });
666
772
  continue;
667
773
  }
668
- const held = `${h.hotelId}|${line.roomId}|${line.stay.checkIn}|${line.stay.checkOut}`;
774
+ if (hourly) {
775
+ // ⚠️ CHECKED AGAIN AT EVERY PRICING: the starts a room takes shrink as
776
+ // the day goes on, and a start that was fine at noon is gone by four.
777
+ const bad = !p.hourly
778
+ ? 'No start time is left today.'
779
+ : hourly.hours !== p.hourly.hours
780
+ ? `This room is ${p.hourly.hours} hours, not ${hourly.hours}.`
781
+ : startProblem(p.hourly, hourly.start);
782
+ if (bad) {
783
+ dropped.push({ title: `${hotelName} - ${offerOf(p.product)}`, why: bad });
784
+ continue;
785
+ }
786
+ }
787
+ const held = `${h.hotelId}|${line.roomId}|${line.stay.checkIn}|${line.stay.checkOut}${hourly ? `|${hourly.start}` : ''}`;
669
788
  const already = taken.get(held) ?? 0;
670
789
  if (p.res.roomsLeft - already < wanted) {
671
790
  dropped.push({
@@ -703,11 +822,13 @@ export const createHuazhuHotels = (opts) => {
703
822
  p.res.meal
704
823
  ? `${p.res.meal}${p.res.mealCount > 1 ? ` × ${p.res.mealCount}` : ''}`
705
824
  : 'Room only',
825
+ hourly ? `${hourly.start}–${endOf(hourly.start, hourly.hours)}` : undefined,
706
826
  ]
707
827
  .filter(Boolean)
708
828
  .join(' · '),
709
829
  stay: line.stay,
710
830
  nights: room.nights,
831
+ ...(hourly ? { hourly } : {}),
711
832
  quantity: wanted,
712
833
  unit: room.nightly,
713
834
  total: money(preTax, room.stayTotal.currency),
@@ -756,6 +877,7 @@ export const createHuazhuHotels = (opts) => {
756
877
  hotelId: b.hotelId,
757
878
  hotelName: b.hotelName,
758
879
  stay: b.stay,
880
+ ...(b.hourly ? { hourly: b.hourly } : {}),
759
881
  guest: b.guest,
760
882
  quote: b.quote,
761
883
  ...(b.cancelledAt ? { cancelledAt: b.cancelledAt } : {}),
@@ -961,6 +1083,20 @@ export const createHuazhuHotels = (opts) => {
961
1083
  slots.delete(slot);
962
1084
  }
963
1085
  };
1086
+ /**
1087
+ * A stay's dates as Huazhu takes them: check-in and check-out, and for a room
1088
+ * by the hour the same day with its hours ("时租入离同一天").
1089
+ */
1090
+ const stayDatesOf = (line) => line.hourly
1091
+ ? {
1092
+ checkin: line.stay.checkIn,
1093
+ checkout: line.stay.checkIn,
1094
+ hourlyRoom: {
1095
+ hourlyRateStartTime: line.hourly.start,
1096
+ hourlyRateEndTime: endOf(line.hourly.start, line.hourly.hours),
1097
+ },
1098
+ }
1099
+ : { checkin: line.stay.checkIn, checkout: line.stay.checkOut };
964
1100
  const bookHeld = async (lines, shown, tripKey, email, guest, contact, who, requests, slot) => {
965
1101
  // Priced afresh, not from the cache: this is the price that will be sent.
966
1102
  const priced = await quoteLines({ lines }, true);
@@ -988,14 +1124,13 @@ export const createHuazhuHotels = (opts) => {
988
1124
  try {
989
1125
  const out = await client.prebook({
990
1126
  hotelId: plan.line.hotelId,
991
- checkinType: 'DAY',
1127
+ checkinType: plan.line.hourly ? 'HOUR' : 'DAY',
992
1128
  paymentType: opts.paymentType,
993
1129
  reservations: [
994
1130
  {
995
1131
  roomTypeCode: plan.sold.product.roomTypeCode,
996
1132
  rateCode: plan.sold.product.rateCode,
997
- checkin: plan.line.stay.checkIn,
998
- checkout: plan.line.stay.checkOut,
1133
+ ...stayDatesOf(plan.line),
999
1134
  amountAfterTax: s.after,
1000
1135
  ...(s.before ? { amountBeforeTax: s.before } : {}),
1001
1136
  currencyCode: s.currency,
@@ -1103,6 +1238,7 @@ export const createHuazhuHotels = (opts) => {
1103
1238
  hotelId: plan.line.hotelId,
1104
1239
  hotelName: plan.qline.hotelName,
1105
1240
  stay: plan.line.stay,
1241
+ ...(plan.line.hourly ? { hourly: plan.line.hourly } : {}),
1106
1242
  guest,
1107
1243
  quote: {
1108
1244
  lines: [plan.qline],
@@ -1147,8 +1283,7 @@ export const createHuazhuHotels = (opts) => {
1147
1283
  const reservation = {
1148
1284
  roomTypeCode: plan.sold.product.roomTypeCode,
1149
1285
  rateCode: plan.sold.product.rateCode,
1150
- checkin: plan.line.stay.checkIn,
1151
- checkout: plan.line.stay.checkOut,
1286
+ ...stayDatesOf(plan.line),
1152
1287
  amountAfterTax: s.after,
1153
1288
  ...(s.before ? { amountBeforeTax: s.before } : {}),
1154
1289
  ...(s.rack ? { rackRate: s.rack } : {}),
@@ -1161,7 +1296,7 @@ export const createHuazhuHotels = (opts) => {
1161
1296
  };
1162
1297
  const request = {
1163
1298
  hotelId: plan.line.hotelId,
1164
- checkinType: 'DAY',
1299
+ checkinType: plan.line.hourly ? 'HOUR' : 'DAY',
1165
1300
  contactPerson: contact,
1166
1301
  distributorResId: dres,
1167
1302
  paymentType: opts.paymentType,
@@ -1239,7 +1374,7 @@ export const createHuazhuHotels = (opts) => {
1239
1374
  };
1240
1375
  return {
1241
1376
  brand: opts.brand,
1242
- capabilities: HUAZHU_CAPABILITIES,
1377
+ capabilities: { ...HUAZHU_CAPABILITIES, ...(opts.hourly ? { hourly: true } : {}) },
1243
1378
  imageDomains() {
1244
1379
  const hosts = new Set();
1245
1380
  for (const h of catalog.all()) {
@@ -1332,10 +1467,17 @@ export const createHuazhuHotels = (opts) => {
1332
1467
  return terms.every((t) => hay.includes(t));
1333
1468
  });
1334
1469
  const f = q.filters ?? {};
1335
- const stay = q.stay;
1470
+ // Rooms by the hour are today's, on the hotel's date: the nights asked
1471
+ // for, if any, do not apply to them.
1472
+ const hourlyOnly = !!f.hourly;
1473
+ const stay = hourlyOnly
1474
+ ? { checkIn: today(), checkOut: today(), guests: q.stay?.guests ?? 2 }
1475
+ : q.stay;
1336
1476
  const wantRooms = Math.max(1, Math.min(MAX_ROOMS_PER_LINE, Math.floor(q.rooms ?? 1)));
1337
1477
  const unapplied = [];
1338
1478
  const ignored = {};
1479
+ if (hourlyOnly && q.stay)
1480
+ unapplied.push('Rooms by the hour are sold for today only, so the nights asked for were not used.');
1339
1481
  const roomTerms = !!(f.freeCancellation || f.breakfast);
1340
1482
  // A brand by the name shown or by Huazhu's own: "Hanting" and "汉庭" are one brand.
1341
1483
  const passesStatic = (h, over, raw) => (!over.brand ||
@@ -1357,6 +1499,8 @@ export const createHuazhuHotels = (opts) => {
1357
1499
  if (roomTerms)
1358
1500
  ignored.roomTerms = true;
1359
1501
  }
1502
+ if (hourlyOnly && !priceEverything)
1503
+ unapplied.push(`${matches.length} hotels matched - too many to ask each for rooms by the hour at once, so only this page was asked. A district, a brand or a landmark narrows it.`);
1360
1504
  if (stay && !priceEverything && (wantsPriceKnob || roomTerms)) {
1361
1505
  unapplied.push(`${matches.length} hotels matched - too many to price at once, so ${[
1362
1506
  wantsPriceKnob ? 'the price order and ceiling' : '',
@@ -1365,7 +1509,7 @@ export const createHuazhuHotels = (opts) => {
1365
1509
  .filter(Boolean)
1366
1510
  .join(' and ')} were not applied. A district, a brand or a landmark narrows it.`);
1367
1511
  }
1368
- if (!stay && (wantsPriceKnob || roomTerms)) {
1512
+ if (!stay && !hourlyOnly && (wantsPriceKnob || roomTerms)) {
1369
1513
  unapplied.push('Prices, free cancellation and breakfast belong to a room on given nights, and Huazhu publishes no price without them - with check-in and check-out they apply.');
1370
1514
  }
1371
1515
  const best = new Map();
@@ -1375,6 +1519,19 @@ export const createHuazhuHotels = (opts) => {
1375
1519
  const priceHotel = async (raw) => {
1376
1520
  if (!stay || pricedRooms.has(raw.hotelId))
1377
1521
  return;
1522
+ if (hourlyOnly) {
1523
+ try {
1524
+ const priced = await priceHourly(raw, stay.guests, wantRooms);
1525
+ pricedRooms.set(raw.hotelId, priced);
1526
+ if (!priced.some((p) => p.res.ok))
1527
+ why.set(raw.hotelId, 'No room by the hour here today.');
1528
+ }
1529
+ catch {
1530
+ pricedRooms.set(raw.hotelId, []);
1531
+ why.set(raw.hotelId, 'Rooms by the hour could not be read from Huazhu just now; open the hotel for them.');
1532
+ }
1533
+ return;
1534
+ }
1378
1535
  const problem = stayProblem(stay, today());
1379
1536
  if (problem) {
1380
1537
  pricedRooms.set(raw.hotelId, []);
@@ -1417,7 +1574,7 @@ export const createHuazhuHotels = (opts) => {
1417
1574
  if (priceEverything)
1418
1575
  await pool(matches.map((m) => m.raw), PRICE_CONCURRENCY, priceHotel);
1419
1576
  const canRoomFilter = priceEverything;
1420
- if (canRoomFilter && roomTerms)
1577
+ if (canRoomFilter && (roomTerms || hourlyOnly))
1421
1578
  matches = matches.filter(({ raw }) => bestFor(raw, f) !== null);
1422
1579
  const currencies = [...new Set(matches.map((m) => m.hotel.currency))].sort();
1423
1580
  const onePrice = currencies.length === 1;
@@ -1492,6 +1649,8 @@ export const createHuazhuHotels = (opts) => {
1492
1649
  nights: b.room.nights,
1493
1650
  roomsLeft: b.room.roomsLeft,
1494
1651
  freeCancellation: !!b.room.freeCancellationUntil,
1652
+ ...(b.room.hourly ? { hours: b.room.hourly.hours } : {}),
1653
+ ...(b.room.labels?.length ? { labels: b.room.labels } : {}),
1495
1654
  },
1496
1655
  };
1497
1656
  };
@@ -1593,6 +1752,36 @@ export const createHuazhuHotels = (opts) => {
1593
1752
  ...(rooms.length === 0 ? { noRooms: noRoomsWhy(priced, stay, 1) } : {}),
1594
1753
  };
1595
1754
  },
1755
+ async hourlyRooms(id, guests) {
1756
+ const raw = findHotel(id);
1757
+ if (!raw)
1758
+ return null;
1759
+ const hotel = toHotel(raw, view, now());
1760
+ const date = today();
1761
+ const stay = { checkIn: date, checkOut: date, guests };
1762
+ let priced;
1763
+ try {
1764
+ priced = await priceHourly(raw, guests, 1);
1765
+ }
1766
+ catch (err) {
1767
+ return {
1768
+ hotel,
1769
+ stay,
1770
+ noRooms: `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.`,
1771
+ };
1772
+ }
1773
+ const rooms = priced
1774
+ .filter((p) => p.res.ok)
1775
+ .map((p) => toRoom(raw, p, stay))
1776
+ .sort((a, b) => a.stayTotal.minor - b.stayTotal.minor);
1777
+ const why = priced.length === 0
1778
+ ? 'This hotel sells no rooms by the hour today.'
1779
+ : priced.some((p) => !p.res.ok && p.res.kind === 'prepaid') &&
1780
+ priced.every((p) => !p.res.ok)
1781
+ ? "This hotel's rooms by the hour are paid in advance, and this connector books rates paid at the hotel."
1782
+ : 'No room by the hour is left here today.';
1783
+ return { hotel, stay, rooms, ...(rooms.length === 0 ? { noRooms: why } : {}) };
1784
+ },
1596
1785
  async datesAround(id, stay, o) {
1597
1786
  const raw = findHotel(id);
1598
1787
  if (!raw)