@curless/hotel-storefront 0.30.0 → 0.30.2

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,8qhiBAA8qhiB,EAC9qhiB,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,87jiBAA87jiB,EAC97jiB,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnB,6GAA6G;AAC7G,MAAM,CAAC,MAAM,qBAAqB,GAChC,kEAAkE,CAAC"}
@@ -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';
@@ -699,6 +699,39 @@ export const createHuazhuHotels = (opts) => {
699
699
  ].filter((x) => !(x.roomType && x.roomType.status === 0));
700
700
  };
701
701
  const policyOf = (code) => parseCancelPolicy(code);
702
+ /**
703
+ * A free-cancellation deadline, no later than a room by the hour's start.
704
+ *
705
+ * ⚠️ CANCELLING IS BEFORE THE STAY. AD0_0 reads "free until the end of the
706
+ * arrival day", and a 15:00-19:00 room was offered as free to cancel until
707
+ * 24:00 - five hours after the guest left (eval, 2026-10-14). Once it has
708
+ * begun it is a no-show, as a night is after its night audit.
709
+ */
710
+ const beforeStart = (until, date, start) => {
711
+ if (until === undefined || !start)
712
+ return until;
713
+ const at = arrivalInstant(date, start, opts.tz);
714
+ const capped = Math.min(until, at);
715
+ return capped > now() ? capped : undefined;
716
+ };
717
+ /** A line's free-cancellation fields: the room's, held to the start chosen. */
718
+ const freeOf = (room, date, start) => {
719
+ const until = room.freeCancellationUntil ? Date.parse(room.freeCancellationUntil) : undefined;
720
+ const held = beforeStart(until, date, start);
721
+ if (held === undefined)
722
+ return { freeCancellationUntil: undefined };
723
+ if (held === until)
724
+ return {
725
+ freeCancellationUntil: room.freeCancellationUntil,
726
+ ...(room.freeCancellationLocal
727
+ ? { freeCancellationLocal: room.freeCancellationLocal }
728
+ : {}),
729
+ };
730
+ return {
731
+ freeCancellationUntil: new Date(held).toISOString(),
732
+ freeCancellationLocal: deadlineAt(held, opts.tz, opts.language === 'en' ? 'en' : 'zh'),
733
+ };
734
+ };
702
735
  const toRoom = (h, p, stay) => {
703
736
  const s = p.res;
704
737
  const cur = s.currency;
@@ -707,7 +740,9 @@ export const createHuazhuHotels = (opts) => {
707
740
  const total = after.reduce((a, b) => a + b, 0);
708
741
  const pre = s.before ? s.before.map((x) => toMinor(x, cur)).reduce((a, b) => a + b, 0) : total;
709
742
  const policy = policyOf(s.cancelCode);
710
- const freeUntil = policy ? freeUntilOf(policy, stay.checkIn, opts.tz, now()) : undefined;
743
+ // A room by the hour: free at most until the latest it can start - the
744
+ // line with its start chosen says the start itself (`quoteLines`).
745
+ const freeUntil = beforeStart(policy ? freeUntilOf(policy, stay.checkIn, opts.tz, now()) : undefined, stay.checkIn, p.hourly?.latestStart);
711
746
  const rt = p.roomType;
712
747
  const beds = (rt?.bedTypeList ?? [])
713
748
  .map((b) => [(en ? bedNameEn(b.bedTypeName) : undefined) ?? b.bedTypeName, b.bedSpec]
@@ -939,10 +974,7 @@ export const createHuazhuHotels = (opts) => {
939
974
  unit: room.nightly,
940
975
  total: money(preTax, room.stayTotal.currency),
941
976
  refundable: room.refundable,
942
- freeCancellationUntil: room.freeCancellationUntil,
943
- ...(room.freeCancellationLocal
944
- ? { freeCancellationLocal: room.freeCancellationLocal }
945
- : {}),
977
+ ...freeOf(room, line.stay.checkIn, hourly?.start),
946
978
  day: line.stay.checkIn,
947
979
  });
948
980
  }
@@ -1110,13 +1142,28 @@ export const createHuazhuHotels = (opts) => {
1110
1142
  freeUntil: undefined,
1111
1143
  };
1112
1144
  }
1145
+ // A room by the hour that has begun is a no-show, whatever the code says
1146
+ // about the rest of the day - see `beforeStart`.
1147
+ if (b.hourly && now() >= arrivalInstant(b.stay.checkIn, b.hourly.start, b.tz)) {
1148
+ return {
1149
+ fee: total,
1150
+ forbidden: false,
1151
+ 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.`,
1152
+ freeUntil: undefined,
1153
+ };
1154
+ }
1113
1155
  const penalty = penaltyAt(policy, b.stay.checkIn, b.tz, now());
1114
1156
  const fee = feeFor(penalty, amounts);
1157
+ const freeUntil = b.hourly
1158
+ ? beforeStart(freeUntilOf(policy, b.stay.checkIn, b.tz, now()), b.stay.checkIn, b.hourly.start)
1159
+ : freeUntilOf(policy, b.stay.checkIn, b.tz, now());
1115
1160
  return {
1116
1161
  fee: fee ?? total,
1117
1162
  forbidden: fee === null,
1118
- rule: describePolicy(policy, b.stay.checkIn, b.tz, amounts),
1119
- freeUntil: freeUntilOf(policy, b.stay.checkIn, b.tz, now()),
1163
+ rule: b.hourly && freeUntil !== undefined && fee === 0
1164
+ ? `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.`
1165
+ : describePolicy(policy, b.stay.checkIn, b.tz, amounts),
1166
+ freeUntil,
1120
1167
  };
1121
1168
  };
1122
1169
  /**
@@ -2019,16 +2066,22 @@ export const createHuazhuHotels = (opts) => {
2019
2066
  if (priceEverything)
2020
2067
  await pool(matches.map((m) => m.raw), PRICE_CONCURRENCY, priceHotel);
2021
2068
  const canRoomFilter = priceEverything;
2022
- // ⚠️ A HOTEL WHOSE ROOMS BY THE HOUR ARE ONLY TOO SMALL STILL SELLS
2023
- // THEM: it stays, with partyNeeds as its reason, so the card can offer
2024
- // more rooms with fewer in each (香格里拉, 2026-10-12) - three guests
2025
- // were shown no hotel at all, as if none sold rooms by the hour.
2026
- const tooSmallOnly = (raw) => {
2069
+ // ⚠️ A HOTEL THAT SELLS ROOMS BY THE HOUR TODAY STAYS, even when none
2070
+ // can be had now - with why, so the guest hears it:
2071
+ // - too small for the party: partyNeeds, so the card can offer more
2072
+ // rooms with fewer in each (香格里拉, 2026-10-12) - three guests were
2073
+ // shown no hotel at all, as if none sold rooms by the hour;
2074
+ // - the day's last start gone by, or not on sale yet: at 22:00 the
2075
+ // hotel the guest named was dropped and the model offered to look for
2076
+ // "others with rooms by the hour today" - there were none (eval,
2077
+ // 2026-10-13).
2078
+ const sellsTodayOnly = (raw) => {
2027
2079
  const priced = pricedRooms.get(raw.hotelId) ?? [];
2028
- return priced.length > 0 && priced.every((p) => !p.res.ok && p.res.kind === 'party');
2080
+ return (priced.length > 0 &&
2081
+ priced.every((p) => !p.res.ok && (p.res.kind === 'party' || p.res.kind === 'closed')));
2029
2082
  };
2030
2083
  if (canRoomFilter && (roomTerms || hourlyOnly))
2031
- matches = matches.filter(({ raw }) => bestFor(raw, f) !== null || (hourlyOnly && !roomTerms && tooSmallOnly(raw)));
2084
+ matches = matches.filter(({ raw }) => bestFor(raw, f) !== null || (hourlyOnly && !roomTerms && sellsTodayOnly(raw)));
2032
2085
  const currencies = [...new Set(matches.map((m) => m.hotel.currency))].sort();
2033
2086
  const onePrice = currencies.length === 1;
2034
2087
  const ceiling = priceEverything && onePrice && Number(q.maxNightly) > 0 ? Number(q.maxNightly) : undefined;