@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.
- package/dist/backend.d.ts +12 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/hourly.d.ts +31 -0
- package/dist/hourly.d.ts.map +1 -0
- package/dist/hourly.js +61 -0
- package/dist/hourly.js.map +1 -0
- package/dist/huazhu/backend.d.ts +2 -0
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +209 -20
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/huazhu/check.js +2 -2
- package/dist/huazhu/check.js.map +1 -1
- package/dist/huazhu/client.d.ts +3 -3
- package/dist/huazhu/client.d.ts.map +1 -1
- package/dist/huazhu/client.js +4 -4
- package/dist/huazhu/client.js.map +1 -1
- package/dist/huazhu/english.d.ts.map +1 -1
- package/dist/huazhu/english.js +15 -0
- package/dist/huazhu/english.js.map +1 -1
- package/dist/huazhu/fake.d.ts +3 -1
- package/dist/huazhu/fake.d.ts.map +1 -1
- package/dist/huazhu/fake.js +69 -7
- package/dist/huazhu/fake.js.map +1 -1
- package/dist/huazhu/labels.d.ts +9 -0
- package/dist/huazhu/labels.d.ts.map +1 -0
- package/dist/huazhu/labels.js +28 -0
- package/dist/huazhu/labels.js.map +1 -0
- package/dist/huazhu/make.d.ts.map +1 -1
- package/dist/huazhu/make.js +1 -0
- package/dist/huazhu/make.js.map +1 -1
- package/dist/huazhu/sandbox.d.ts.map +1 -1
- package/dist/huazhu/sandbox.js +111 -0
- package/dist/huazhu/sandbox.js.map +1 -1
- package/dist/huazhu/sellable.d.ts +29 -3
- package/dist/huazhu/sellable.d.ts.map +1 -1
- package/dist/huazhu/sellable.js +68 -4
- package/dist/huazhu/sellable.js.map +1 -1
- package/dist/huazhu/wire.d.ts +11 -0
- package/dist/huazhu/wire.d.ts.map +1 -1
- package/dist/huazhu/wire.js.map +1 -1
- package/dist/present.d.ts.map +1 -1
- package/dist/present.js +8 -4
- package/dist/present.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +172 -15
- package/dist/tools.js.map +1 -1
- package/dist/trip.d.ts +6 -0
- package/dist/trip.d.ts.map +1 -1
- package/dist/trip.js +17 -3
- package/dist/trip.js.map +1 -1
- package/dist/types.d.ts +68 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/huazhu/backend.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|