@curless/hotel-storefront 0.50.3 → 0.50.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.
@@ -191,6 +191,12 @@ const localIsoAt = (instant, tz) => {
191
191
  .map((x) => [x.type, x.value]));
192
192
  return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}`;
193
193
  };
194
+ /**
195
+ * How a rate paid in advance is priced to be SHOWN where none is sold: as the
196
+ * test environment sells it with prepaid open (`OPN`), so the list is the same
197
+ * the day production opens it (robin, 2026-10-30: 「按照生产的标准来」).
198
+ */
199
+ const SHOWN_AS = 'OPN';
194
200
  /** A stay paid for - on the card page, or confirmed on UnionPay (whose one-time card Huazhu then charges). */
195
201
  const paidInAdvance = (p) => p.method === 'unionpay' ? p.issuedAt !== undefined : p.paidAt !== undefined;
196
202
  const pool = async (items, n, fn) => {
@@ -499,6 +505,30 @@ export const createHuazhuHotels = (opts) => {
499
505
  /** Unpaid this long, the page is closed and the booking lapses - inside the card page's own ~31 minutes. */
500
506
  const payWithin = opts.prepaid?.payWithinMs ?? 30 * 60_000;
501
507
  const prepaidType = checkout ? 'OPN' : undefined;
508
+ /**
509
+ * One rate for one stay: sold here, or refused - and, refused only for being
510
+ * paid in advance where no payment is taken, asked again as `SHOWN_AS`.
511
+ *
512
+ * ⚠️ THE REFUSAL STAYS THE ANSWER (`res`). What a deployment that takes the
513
+ * money would sell goes in `shown`, to be listed; nothing here books it.
514
+ * What even that deployment would refuse (cancelling keeps part of the
515
+ * stay) is not shown, and keeps this refusal: "paid in advance" is what
516
+ * stops it here - "fewer nights may be bookable" would not be true here.
517
+ */
518
+ const priceOne = (product, w, roomType, context) => {
519
+ const res = sell(product, w, roomType, context);
520
+ if (res.ok || res.kind !== 'prepaid' || context.prepaidType)
521
+ return { res };
522
+ const as = sell(product, w, roomType, { ...context, prepaidType: SHOWN_AS });
523
+ return as.ok ? { res, shown: as } : { res };
524
+ };
525
+ /** A rate found sold out, priced as if it had a room - as a deployment taking prepaid would show it. */
526
+ const openOne = (product, w, roomType, context) => sellIfOpen(product, w, roomType, context) ??
527
+ (context.prepaidType
528
+ ? undefined
529
+ : sellIfOpen(product, w, roomType, { ...context, prepaidType: SHOWN_AS }));
530
+ /** What a priced rate is listed as: sold here, or shown (`Priced.shown`) - or nothing. */
531
+ const listed = (p) => p.res.ok ? p : p.shown ? { ...p, res: p.shown } : undefined;
502
532
  const now = opts.now ?? (() => Date.now());
503
533
  const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
504
534
  const ariTtl = opts.ariTtlMs ?? 60_000;
@@ -807,13 +837,22 @@ export const createHuazhuHotels = (opts) => {
807
837
  .map((product) => {
808
838
  const roomType = (h.hotelRoomTypeList ?? []).find((r) => r.roomTypeCode === product.roomTypeCode);
809
839
  const w = ari.get(roomIdOf(product));
810
- const res = w
811
- ? sell(product, w, roomType, context)
812
- : { ok: false, kind: 'no-price', why: 'no price for those nights' };
840
+ const { res, shown } = w
841
+ ? priceOne(product, w, roomType, context)
842
+ : {
843
+ res: { ok: false, kind: 'no-price', why: 'no price for those nights' },
844
+ shown: undefined,
845
+ };
813
846
  const ifOpen = w && !res.ok && res.kind === 'sold-out'
814
- ? sellIfOpen(product, w, roomType, context)
847
+ ? openOne(product, w, roomType, context)
815
848
  : undefined;
816
- return { product, roomType, res, ...(ifOpen ? { ifOpen } : {}) };
849
+ return {
850
+ product,
851
+ roomType,
852
+ res,
853
+ ...(shown ? { shown } : {}),
854
+ ...(ifOpen ? { ifOpen } : {}),
855
+ };
817
856
  })
818
857
  .filter((x) => !(x.roomType && x.roomType.status === 0));
819
858
  };
@@ -898,12 +937,22 @@ export const createHuazhuHotels = (opts) => {
898
937
  const roomType = roomTypeOf(product);
899
938
  const w = ari.get(roomIdOf(product));
900
939
  const hourly = hourlyWindow(product, date, clock);
901
- let res = w
902
- ? sell(product, w, roomType, context)
903
- : { ok: false, kind: 'no-price', why: 'no price for today' };
904
- if (res.ok && !hourly)
940
+ const priced = w
941
+ ? priceOne(product, w, roomType, context)
942
+ : { res: { ok: false, kind: 'no-price', why: 'no price for today' } };
943
+ let { res } = priced;
944
+ let shown = priced.shown;
945
+ if ((res.ok || shown) && !hourly) {
905
946
  res = { ok: false, kind: 'closed', why: goneWhy(product, date, clock) };
906
- return { product, roomType, res, ...(hourly ? { hourly } : {}) };
947
+ shown = undefined;
948
+ }
949
+ return {
950
+ product,
951
+ roomType,
952
+ res,
953
+ ...(shown ? { shown } : {}),
954
+ ...(hourly ? { hourly } : {}),
955
+ };
907
956
  }),
908
957
  ...todays.filter((p) => !live.includes(p)).map(gone),
909
958
  ].filter((x) => !(x.roomType && x.roomType.status === 0));
@@ -1011,10 +1060,12 @@ export const createHuazhuHotels = (opts) => {
1011
1060
  ...(rateKindOf(p.product) ? { kind: rateKindOf(p.product) } : {}),
1012
1061
  },
1013
1062
  labels: rateLabels(p.product, { type: s.mealType, count: s.mealCount }),
1014
- // ⚠️ PREPAID ONLY WHERE THIS DEPLOYMENT TAKES PAYMENT - a sandbox's test
1015
- // mode (`opts.prepaid`); everywhere else `sell` leaves prepaid rates out
1016
- // and every room is paid at the hotel (总线程, 2026-10-12).
1063
+ // ⚠️ PREPAID IS SOLD ONLY WHERE THIS DEPLOYMENT TAKES PAYMENT - a sandbox's
1064
+ // test mode (`opts.prepaid`, 总线程 2026-10-12). Everywhere else a prepaid
1065
+ // rate is one `priceOne` SHOWED: listed and marked as it is sold, and
1066
+ // not booked here (robin, 2026-10-30).
1017
1067
  payment: s.prepaid ? 'prepaid' : 'at-hotel',
1068
+ ...(s.prepaid && !prepaidType ? { notSoldHere: 'prepaid' } : {}),
1018
1069
  ...(hourly ? { hourly } : {}),
1019
1070
  ...(maxChildren > 0 && maxChildren < 99 ? { maxChildren } : {}),
1020
1071
  };
@@ -1244,8 +1295,14 @@ export const createHuazhuHotels = (opts) => {
1244
1295
  });
1245
1296
  continue;
1246
1297
  }
1298
+ // ⚠️ A RATE ONLY SHOWN IS NEVER BOOKED: `res` is its refusal (`priceOne`).
1247
1299
  if (!p.res.ok) {
1248
- dropped.push({ title: `${hotelName} - ${roomTypeOf(p.product)}`, why: `${p.res.why}.` });
1300
+ dropped.push({
1301
+ title: `${hotelName} - ${roomTypeOf(p.product)}`,
1302
+ why: p.shown
1303
+ ? 'This rate is paid in advance, and a stay paid in advance cannot be booked here yet - only rates paid at the hotel can.'
1304
+ : `${p.res.why}.`,
1305
+ });
1249
1306
  continue;
1250
1307
  }
1251
1308
  if (hourly) {
@@ -3629,7 +3686,7 @@ export const createHuazhuHotels = (opts) => {
3629
3686
  try {
3630
3687
  const priced = await priceHourly(raw, stay.guests, wantRooms);
3631
3688
  pricedRooms.set(raw.hotelId, priced);
3632
- if (!priced.some((p) => p.res.ok))
3689
+ if (!priced.some((p) => listed(p)))
3633
3690
  why.set(raw.hotelId, hourlyNoRoomsWhy(priced, stay.guests));
3634
3691
  }
3635
3692
  catch {
@@ -3650,7 +3707,7 @@ export const createHuazhuHotels = (opts) => {
3650
3707
  try {
3651
3708
  const priced = await priceStay(raw, stay, wantRooms);
3652
3709
  pricedRooms.set(raw.hotelId, priced);
3653
- if (!priced.some((p) => p.res.ok))
3710
+ if (!priced.some((p) => listed(p)))
3654
3711
  why.set(raw.hotelId, noRoomsWhy(priced, stay, wantRooms));
3655
3712
  }
3656
3713
  catch (err) {
@@ -3669,8 +3726,9 @@ export const createHuazhuHotels = (opts) => {
3669
3726
  if (known !== undefined)
3670
3727
  return known;
3671
3728
  let found = null;
3672
- for (const p of pricedRooms.get(raw.hotelId) ?? []) {
3673
- if (!p.res.ok || !stay)
3729
+ for (const q of pricedRooms.get(raw.hotelId) ?? []) {
3730
+ const p = listed(q);
3731
+ if (!p || !stay)
3674
3732
  continue;
3675
3733
  const room = toRoom(raw, p, stay);
3676
3734
  if (over.freeCancellation && !room.freeCancellationUntil)
@@ -3795,6 +3853,8 @@ export const createHuazhuHotels = (opts) => {
3795
3853
  ...(b.room.hourly ? { hours: b.room.hourly.hours } : {}),
3796
3854
  ...(b.room.labels?.length ? { labels: b.room.labels } : {}),
3797
3855
  ...(b.room.meal ? { meal: b.room.meal } : {}),
3856
+ ...(b.room.payment ? { payment: b.room.payment } : {}),
3857
+ ...(b.room.notSoldHere ? { notSoldHere: b.room.notSoldHere } : {}),
3798
3858
  },
3799
3859
  };
3800
3860
  };
@@ -3904,7 +3964,7 @@ export const createHuazhuHotels = (opts) => {
3904
3964
  };
3905
3965
  }
3906
3966
  const rooms = priced
3907
- .filter((p) => p.res.ok)
3967
+ .flatMap((p) => listed(p) ?? [])
3908
3968
  .map((p) => toRoom(raw, p, stay))
3909
3969
  .sort((a, b) => a.stayTotal.minor - b.stayTotal.minor);
3910
3970
  const soldOut = soldOutOf(raw, priced, stay);
@@ -3938,7 +3998,7 @@ export const createHuazhuHotels = (opts) => {
3938
3998
  };
3939
3999
  }
3940
4000
  const rooms = priced
3941
- .filter((p) => p.res.ok)
4001
+ .flatMap((p) => listed(p) ?? [])
3942
4002
  .map((p) => toRoom(raw, p, stay))
3943
4003
  .sort((a, b) => a.stayTotal.minor - b.stayTotal.minor);
3944
4004
  return {
@@ -3990,6 +4050,7 @@ export const createHuazhuHotels = (opts) => {
3990
4050
  allowTags,
3991
4051
  };
3992
4052
  let cheapest;
4053
+ const day = [];
3993
4054
  for (const product of products) {
3994
4055
  if (!bookableNow(product.bookWindow, checkIn, context.clock))
3995
4056
  continue;
@@ -3997,8 +4058,14 @@ export const createHuazhuHotels = (opts) => {
3997
4058
  if (!w)
3998
4059
  continue;
3999
4060
  const roomType = (raw.hotelRoomTypeList ?? []).find((r) => r.roomTypeCode === product.roomTypeCode);
4000
- const res = sell(product, w, roomType, context);
4001
- if (!res.ok)
4061
+ // Priced as the hotel page lists it: a rate shown paid in advance
4062
+ // where none is sold has the price it is listed at (`priceOne`).
4063
+ const one = priceOne(product, w, roomType, context);
4064
+ // Why a day has none is read from its nights' rates, as the hotel page's.
4065
+ if (product.checkinType === 'DAY' && !(roomType && roomType.status === 0))
4066
+ day.push({ product, roomType, ...one });
4067
+ const res = one.res.ok ? one.res : one.shown;
4068
+ if (!res)
4002
4069
  continue;
4003
4070
  const total = money(res.after.reduce((a, b) => a + toMinor(b, res.currency), 0) * rooms, res.currency);
4004
4071
  if (!cheapest || total.minor < cheapest.minor)
@@ -4007,7 +4074,7 @@ export const createHuazhuHotels = (opts) => {
4007
4074
  out.push({
4008
4075
  checkIn,
4009
4076
  checkOut,
4010
- ...(cheapest ? { from: cheapest } : {}),
4077
+ ...(cheapest ? { from: cheapest } : { why: noRoomsWhy(day, candidate, rooms).code }),
4011
4078
  ...(shift === 0 ? { chosen: true } : {}),
4012
4079
  });
4013
4080
  }