@curless/hotel-storefront 0.46.0 → 0.47.1

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 (41) hide show
  1. package/dist/aggregate/hotels.d.ts.map +1 -1
  2. package/dist/aggregate/hotels.js +6 -1
  3. package/dist/aggregate/hotels.js.map +1 -1
  4. package/dist/card.generated.d.ts +1 -1
  5. package/dist/card.generated.js +2 -2
  6. package/dist/card.generated.js.map +1 -1
  7. package/dist/demo-hotels.d.ts.map +1 -1
  8. package/dist/demo-hotels.js +15 -6
  9. package/dist/demo-hotels.js.map +1 -1
  10. package/dist/huazhu/backend.d.ts.map +1 -1
  11. package/dist/huazhu/backend.js +54 -2
  12. package/dist/huazhu/backend.js.map +1 -1
  13. package/dist/huazhu/catalog.d.ts +9 -2
  14. package/dist/huazhu/catalog.d.ts.map +1 -1
  15. package/dist/huazhu/catalog.js +65 -5
  16. package/dist/huazhu/catalog.js.map +1 -1
  17. package/dist/huazhu/english.d.ts +8 -0
  18. package/dist/huazhu/english.d.ts.map +1 -1
  19. package/dist/huazhu/english.js +295 -2
  20. package/dist/huazhu/english.js.map +1 -1
  21. package/dist/huazhu/sellable.d.ts +12 -0
  22. package/dist/huazhu/sellable.d.ts.map +1 -1
  23. package/dist/huazhu/sellable.js +15 -0
  24. package/dist/huazhu/sellable.js.map +1 -1
  25. package/dist/names.d.ts +22 -0
  26. package/dist/names.d.ts.map +1 -1
  27. package/dist/names.js +43 -0
  28. package/dist/names.js.map +1 -1
  29. package/dist/room-filters.d.ts +37 -7
  30. package/dist/room-filters.d.ts.map +1 -1
  31. package/dist/room-filters.js +82 -21
  32. package/dist/room-filters.js.map +1 -1
  33. package/dist/server.d.ts +2 -2
  34. package/dist/server.js +2 -2
  35. package/dist/tools.d.ts +14 -1
  36. package/dist/tools.d.ts.map +1 -1
  37. package/dist/tools.js +111 -8
  38. package/dist/tools.js.map +1 -1
  39. package/dist/types.d.ts +16 -0
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +1 -1
package/dist/tools.js CHANGED
@@ -4,7 +4,7 @@ import { FACILITIES } from './facilities.js';
4
4
  import { DESCRIPTION_MAX, POLICY_MAX, guestText, hotelText } from './fence.js';
5
5
  import { clockOf, minutesOf, startProblem, stayWords, windowWords } from './hourly.js';
6
6
  import { amountsIn, formatMoney, keepingAmounts, money, multiplyMoney, toMinor } from './money.js';
7
- import { roomNamesOf, withTraditional } from './names.js';
7
+ import { namedExactly, roomNamesOf, withTraditional } from './names.js';
8
8
  import { MAX_COMPARED, MAX_CONS, MAX_DAYS, MAX_ITEMS_PER_DAY, MAX_MOMENTS, MAX_PROS, buildItinerary, candidatesFor, compareHotels, namesMoreThanOne, } from './present.js';
9
9
  import { regionOf } from './regions.js';
10
10
  import { activeKeysOf, byRoomType, facetsOf, keyWords, matchesTerms, optionsOf, roomTermsOf, termWords, } from './room-filters.js';
@@ -339,7 +339,7 @@ const allTools = (n, v) => {
339
339
  },
340
340
  {
341
341
  name: n.getHotel,
342
- description: `One ${v.noun} in full: description, amenities, address, and check-in and check-out times where the ${v.noun} publishes them. Given \`checkIn\` and \`checkOut\` it also returns the room types that can be booked for those nights, one line per type at its lowest rate: the nightly rate, the stay total including taxes and fees, whether it can be cancelled free and until when, and how many rates it has. \`rooms\` leaves out the types that have fewer than that many left. \`bed\`, \`breakfast\` and \`freeCancellation\` keep only the rates that meet all of those given; the card still holds every room, with the given filters selected. Without dates it returns the ${v.noun} only, because a room has no price until the nights are known. The ${v.noun} is drawn as a card for the guest to look at.${v.hints.hotel ? ` ${v.hints.hotel}` : ''}`,
342
+ description: `One ${v.noun} in full: description, amenities, address, and check-in and check-out times where the ${v.noun} publishes them. Given \`checkIn\` and \`checkOut\` it also returns the room types that can be booked for those nights, one line per type at its lowest rate: the nightly rate, the stay total including taxes and fees, whether it can be cancelled free and until when, and how many rates it has. \`rooms\` leaves out the types that have fewer than that many left. \`bed\`, \`breakfast\`, \`freeCancellation\`, \`window\`, \`payment\` and \`family\` keep only the rates that meet all of those given; the card still holds every room, with the given filters selected. Without dates it returns the ${v.noun} only, because a room has no price until the nights are known. The ${v.noun} is drawn as a card for the guest to look at.${v.hints.hotel ? ` ${v.hints.hotel}` : ''}`,
343
343
  annotations: reads(`View one ${catalogue(v, true)}`, open),
344
344
  inputSchema: {
345
345
  type: 'object',
@@ -368,6 +368,19 @@ const allTools = (n, v) => {
368
368
  type: 'boolean',
369
369
  description: 'With dates: only rooms that can be cancelled free',
370
370
  },
371
+ window: {
372
+ type: 'boolean',
373
+ description: 'With dates: only rooms with a window (not a partial or an inner one)',
374
+ },
375
+ payment: {
376
+ type: 'string',
377
+ enum: ['at-hotel', 'online'],
378
+ description: 'With dates: only rooms paid at the hotel (`at-hotel`) or in advance online (`online`)',
379
+ },
380
+ family: {
381
+ type: 'boolean',
382
+ description: 'With dates: only rooms that sleep 3 or more',
383
+ },
371
384
  },
372
385
  required: ['hotel'],
373
386
  },
@@ -923,6 +936,8 @@ const CANCEL_SCHEMA = {
923
936
  demoHold: BOOL,
924
937
  addOnsFree: BOOL,
925
938
  fee: STR,
939
+ /** What the booking being cancelled came to, taxes in - the fee is read against it. */
940
+ bookingTotal: STR,
926
941
  refund: STR,
927
942
  rule: STR,
928
943
  freeUntil: STR,
@@ -1050,12 +1065,22 @@ const HOTEL_PAGE = {
1050
1065
  dates: ARR,
1051
1066
  /**
1052
1067
  * The room filters this hotel's rooms offer, each with how many rooms (rates)
1053
- * it keeps - `{ key: 'breakfast' | 'king' | 'twin' | 'freeCancellation', count }`.
1054
- * Only a kind at least one room has.
1068
+ * it keeps - `{ key, count }`, `key` one of `breakfast`, `king`, `twin`,
1069
+ * `freeCancellation` (always here, `count` 0 where no room has it) and
1070
+ * `window`, `payAtHotel`, `payOnline`, `family` (here only where some rooms
1071
+ * have it and some do not).
1055
1072
  */
1056
1073
  roomFilters: ARR,
1057
1074
  /** The filters the call asked for, as the same keys - the card opens with these selected. */
1058
1075
  activeFilters: STRS,
1076
+ /**
1077
+ * Room types with no room to book for these nights (none left, or fewer
1078
+ * than the rooms asked for): `{ id, name, names?, sleeps, ..., left? }`, no
1079
+ * price. The card draws them muted at the end of the list, so a type the
1080
+ * guest saw is said to be sold out and not left out. Absent where there are
1081
+ * none - and never in what the assistant is told.
1082
+ */
1083
+ soldOut: ARR,
1059
1084
  };
1060
1085
  export const buildOutputSchemas = (n) => ({
1061
1086
  [n.listHotels]: {
@@ -1173,6 +1198,8 @@ export const buildOutputSchemas = (n) => ({
1173
1198
  accessibility: BOOL,
1174
1199
  /** One per stay, when the trip held several. */
1175
1200
  bookings: ARR,
1201
+ /** What all of `bookings` come to, where they are in one currency (the card's first screen). */
1202
+ totalAll: STR,
1176
1203
  /**
1177
1204
  * The stays that could not be booked when the others were - each with
1178
1205
  * why, still in the trip, and what could be booked instead (robin,
@@ -1457,6 +1484,10 @@ const holderOf = (deps, supplier) => {
1457
1484
  const bookingForCard = (b) => ({
1458
1485
  ...b,
1459
1486
  quote: quoteForCard(b.quote),
1487
+ // What a cancelled booking was charged to cancel, as text like every amount the
1488
+ // card is given: "Cancelled" in My bookings says it beside the struck-out total
1489
+ // (r10 #21). `cancellationFee` stays the money it was.
1490
+ ...(b.cancellationFee ? { cancellationFeeText: formatMoney(b.cancellationFee) } : {}),
1460
1491
  ...(b.payment
1461
1492
  ? {
1462
1493
  payment: {
@@ -1720,6 +1751,9 @@ const quoteForCard = (quote, groupOf) => ({
1720
1751
  ...(l.ordered !== undefined ? { ordered: l.ordered } : {}),
1721
1752
  unit: formatMoney(l.unit),
1722
1753
  total: formatMoney(l.total),
1754
+ // The line with its taxes in, where the backend knows them per line: what the
1755
+ // review and the trip show, as the hotel's page does (r10 #23).
1756
+ ...(l.totalWithTaxes ? { totalWithTaxes: formatMoney(l.totalWithTaxes) } : {}),
1723
1757
  refundable: l.refundable,
1724
1758
  freeCancellationUntil: l.freeCancellationUntil,
1725
1759
  // The deadline on the hotel's clock, where the backend knows it - the
@@ -2068,7 +2102,7 @@ when, enough, roomsWanted, terms) => {
2068
2102
  ...roomTypeLines(rs, roomsWanted, terms),
2069
2103
  ...(byRoomType(rs).some((t) => t.rooms.length > 1)
2070
2104
  ? [
2071
- 'A type with several rates is priced from its cheapest one; bed, breakfast and freeCancellation narrow to the others (the card lists every rate).',
2105
+ 'A type with several rates is priced from its cheapest one; bed, breakfast, freeCancellation, window, payment and family narrow to the others (the card lists every rate).',
2072
2106
  ]
2073
2107
  : []),
2074
2108
  ];
@@ -2221,6 +2255,52 @@ const dateFlex = (dates, stay) => {
2221
2255
  text: `Arriving ${beats.checkIn} instead is ${formatMoney(saved)} less for ${sameNights}.`,
2222
2256
  };
2223
2257
  };
2258
+ /**
2259
+ * The room types a guest can no longer book on these nights - for the card to
2260
+ * say so, and not leave them out (r10 #10: a type the guest saw earlier
2261
+ * vanished from the list, and the heading counted one fewer, without a word).
2262
+ *
2263
+ * ⚠️ AN ADDITIVE FIELD, NEVER THE TEXT. What the assistant is told is the rooms
2264
+ * that can be booked, as before; this is only what the card draws, muted, at
2265
+ * the end of the list. A type is listed when
2266
+ * - the backend sends it with none left (`HotelDetail.soldOut`, or a room with
2267
+ * `roomsLeft: 0`), or
2268
+ * - it has some left, but fewer than the rooms asked for (`left`): a guest who
2269
+ * asked for two rooms and was shown no type with one left was not told why.
2270
+ * Not a type that sleeps fewer than the party (the backend never sends it), and
2271
+ * not a type any of whose rates can be booked. One entry a type, with the facts
2272
+ * the card says of a room and no price.
2273
+ */
2274
+ const soldOutTypesOf = (detail, enough, wanted) => {
2275
+ const key = (r) => r.roomType?.id ?? r.id;
2276
+ const open = new Set(enough.map(key));
2277
+ const types = new Map();
2278
+ for (const r of [...(detail.rooms ?? []), ...(detail.soldOut ?? [])]) {
2279
+ if (r.hourly || open.has(key(r)) || r.roomsLeft >= wanted)
2280
+ continue;
2281
+ const seen = types.get(key(r));
2282
+ if (seen)
2283
+ seen.left = Math.max(seen.left, r.roomsLeft);
2284
+ else
2285
+ types.set(key(r), { room: r, left: r.roomsLeft });
2286
+ }
2287
+ return [...types.entries()].map(([id, { room: r, left }]) => ({
2288
+ id,
2289
+ name: r.roomType?.name ?? r.name,
2290
+ ...((r.roomType?.names ?? r.names) ? { names: r.roomType?.names ?? r.names } : {}),
2291
+ sleeps: r.sleeps,
2292
+ ...(r.maxChildren ? { maxChildren: r.maxChildren } : {}),
2293
+ ...(r.sizeSqm ? { sizeSqm: r.sizeSqm } : {}),
2294
+ ...(r.beds ? { beds: r.beds } : {}),
2295
+ ...(r.bedTypes ? { bedTypes: r.bedTypes } : {}),
2296
+ ...(r.view ? { view: r.view } : {}),
2297
+ ...(r.window ? { window: r.window } : {}),
2298
+ ...(r.floor ? { floor: r.floor } : {}),
2299
+ ...(r.imageUrl ? { imageUrl: r.imageUrl } : {}),
2300
+ // Some left, but not as many as were asked for.
2301
+ ...(left > 0 ? { left } : {}),
2302
+ }));
2303
+ };
2224
2304
  const identity = (deps, ctx, key, args = {}) => {
2225
2305
  const email = str(args.email);
2226
2306
  const member = deps.members.read(key);
@@ -3329,6 +3409,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3329
3409
  // with what is true of each (`facets`), so a chip switched on or off on the
3330
3410
  // card needs no call to the server.
3331
3411
  const terms = roomTermsOf(args);
3412
+ const soldOut = soldOutTypesOf(detail, enough, roomsWanted);
3332
3413
  const rooms = enough.map((r) => ({
3333
3414
  ...r,
3334
3415
  facets: facetsOf(r),
@@ -3366,6 +3447,8 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3366
3447
  rooms_wanted: roomsWanted,
3367
3448
  roomFilters: optionsOf(enough),
3368
3449
  activeFilters: activeKeysOf(terms),
3450
+ // Types with no room to book for these nights, said and not left out.
3451
+ ...(byTheHour || !soldOut.length ? {} : { soldOut }),
3369
3452
  ...flex.structured,
3370
3453
  },
3371
3454
  text: [
@@ -3659,7 +3742,10 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3659
3742
  const today = (startTime || !asStay(args)
3660
3743
  ? deps.backend.hourlyRooms?.(hotelId, guestsOf(args), who)
3661
3744
  : undefined);
3662
- const byTheHour = async () => (await today)?.rooms?.find((r) => r.hourly && (r.id === roomRef || r.name.toLowerCase() === roomRef.toLowerCase()));
3745
+ const byTheHour = async () => {
3746
+ const sold = ((await today)?.rooms ?? []).filter((r) => r.hourly);
3747
+ return sold.find((r) => r.id === roomRef) ?? namedExactly(sold, roomRef);
3748
+ };
3663
3749
  if (startTime && deps.backend.hourlyRooms) {
3664
3750
  const hourlyRoom = await byTheHour();
3665
3751
  if (!hourlyRoom?.hourly) {
@@ -3766,7 +3852,11 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3766
3852
  text: dated?.noRooms ?? 'Those nights cannot be booked here.',
3767
3853
  };
3768
3854
  }
3769
- const room = dated.rooms.find((r) => r.id === roomRef || r.name.toLowerCase() === roomRef.toLowerCase());
3855
+ // ⚠️ BY THE OFFER'S WHOLE NAME, in any language it has: a guest who read
3856
+ // 「豪华大床房 · 双早 · 不可取消」 on the card says that, and it is the same offer
3857
+ // as "Deluxe King Room · Breakfast for 2 · Non-refundable". Never a part of
3858
+ // it, and never a room type's bare name when it has several rates.
3859
+ const room = dated.rooms.find((r) => r.id === roomRef) ?? namedExactly(dated.rooms, roomRef);
3770
3860
  if (!room) {
3771
3861
  return {
3772
3862
  structured: {
@@ -3784,7 +3874,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3784
3874
  hotel: { id: detail.hotel.id, name: detail.hotel.name },
3785
3875
  stay,
3786
3876
  },
3787
- text: `${detail.hotel.name} has no room type by that name for those nights. It has: ${dated.rooms.map((r) => r.name).join(', ')}.`,
3877
+ text: `${detail.hotel.name} has no room type by that name for those nights. It has: ${dated.rooms.map((r) => (/\p{Script=Han}/u.test(roomRef) && r.names?.zh ? `${r.name} (${r.names.zh})` : r.name)).join(', ')}.`,
3788
3878
  };
3789
3879
  }
3790
3880
  deps.trips.set(key, {
@@ -4018,6 +4108,17 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4018
4108
  : {}),
4019
4109
  paid: false,
4020
4110
  ...(remembered ? { remembered: remembered.kept } : {}),
4111
+ // What they come to together, said by the server like every figure on
4112
+ // the card - never added up there (r10, #15). Only in one currency.
4113
+ ...(several &&
4114
+ booked.every(({ booking }) => booking.quote.total.currency === first.booking.quote.total.currency)
4115
+ ? {
4116
+ totalAll: formatMoney({
4117
+ minor: booked.reduce((n, { booking }) => n + booking.quote.total.minor, 0),
4118
+ currency: first.booking.quote.total.currency,
4119
+ }),
4120
+ }
4121
+ : {}),
4021
4122
  holdUntil: first.booking.holdUntil,
4022
4123
  total: formatMoney(first.booking.quote.total),
4023
4124
  ...(first.asked.length ? { requested: first.asked } : {}),
@@ -4437,6 +4538,8 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4437
4538
  : {}),
4438
4539
  ...ownDemo,
4439
4540
  fee: formatMoney(preview.fee),
4541
+ // What the booking came to, so the fee is read against it (r10 #22).
4542
+ ...(booked ? { bookingTotal: formatMoney(booked.quote.total) } : {}),
4440
4543
  // ⚠️ NO "REFUND": nothing was ever paid here - a demonstration takes no
4441
4544
  // money and a hotel's own booking is paid at the hotel - so a figure
4442
4545
  // called a refund is money that is not coming back (2026-10-05 audit, P3).