@curless/hotel-storefront 0.27.3 → 0.27.5

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 (53) hide show
  1. package/dist/__tests__/audit-p2.test.d.ts +2 -0
  2. package/dist/__tests__/audit-p2.test.d.ts.map +1 -0
  3. package/dist/__tests__/audit-p2.test.js +98 -0
  4. package/dist/__tests__/audit-p2.test.js.map +1 -0
  5. package/dist/__tests__/card-contract.test.js +14 -0
  6. package/dist/__tests__/card-contract.test.js.map +1 -1
  7. package/dist/__tests__/demo-hotels.test.js +3 -1
  8. package/dist/__tests__/demo-hotels.test.js.map +1 -1
  9. package/dist/__tests__/huazhu-backend.test.js +13 -0
  10. package/dist/__tests__/huazhu-backend.test.js.map +1 -1
  11. package/dist/__tests__/tools.test.js +21 -5
  12. package/dist/__tests__/tools.test.js.map +1 -1
  13. package/dist/backend.d.ts +2 -0
  14. package/dist/backend.d.ts.map +1 -1
  15. package/dist/backend.js +2 -0
  16. package/dist/backend.js.map +1 -1
  17. package/dist/card.generated.d.ts +1 -1
  18. package/dist/card.generated.js +2 -2
  19. package/dist/card.generated.js.map +1 -1
  20. package/dist/data.js +2 -2
  21. package/dist/data.js.map +1 -1
  22. package/dist/demo-hotels.d.ts +1 -1
  23. package/dist/demo-hotels.d.ts.map +1 -1
  24. package/dist/demo-hotels.js +9 -3
  25. package/dist/demo-hotels.js.map +1 -1
  26. package/dist/huazhu/backend.d.ts.map +1 -1
  27. package/dist/huazhu/backend.js +32 -8
  28. package/dist/huazhu/backend.js.map +1 -1
  29. package/dist/huazhu/sellable.d.ts +3 -1
  30. package/dist/huazhu/sellable.d.ts.map +1 -1
  31. package/dist/huazhu/sellable.js.map +1 -1
  32. package/dist/index.js +0 -0
  33. package/dist/legal.d.ts.map +1 -1
  34. package/dist/legal.js +6 -2
  35. package/dist/legal.js.map +1 -1
  36. package/dist/present.d.ts +1 -1
  37. package/dist/present.d.ts.map +1 -1
  38. package/dist/present.js +2 -1
  39. package/dist/present.js.map +1 -1
  40. package/dist/server.d.ts +8 -8
  41. package/dist/server.d.ts.map +1 -1
  42. package/dist/server.js +3 -2
  43. package/dist/server.js.map +1 -1
  44. package/dist/surface.d.ts.map +1 -1
  45. package/dist/surface.js +6 -1
  46. package/dist/surface.js.map +1 -1
  47. package/dist/tools.d.ts +65 -17
  48. package/dist/tools.d.ts.map +1 -1
  49. package/dist/tools.js +121 -19
  50. package/dist/tools.js.map +1 -1
  51. package/dist/types.d.ts +8 -2
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +11 -9
package/dist/tools.js CHANGED
@@ -1,5 +1,5 @@
1
- import { DEFAULT_PAGE, FULL_CAPABILITIES, LASTING_REQUESTS, MAX_PAGE, REQUEST_KINDS, REQUEST_NOTE_MAX, guestTooLong, } from './backend.js';
2
- import { isDay, nights as nightsBetween } from './dates.js';
1
+ import { DEFAULT_PAGE, FULL_CAPABILITIES, LASTING_REQUESTS, MAX_PAGE, REQUEST_KINDS, REQUEST_NOTE_MAX, guestTooLong, unitWords, } from './backend.js';
2
+ import { MAX_GUESTS, isDay, nights as nightsBetween } from './dates.js';
3
3
  import { FACILITIES } from './facilities.js';
4
4
  import { DESCRIPTION_MAX, POLICY_MAX, hotelText } from './fence.js';
5
5
  import { formatMoney, money, multiplyMoney, toMinor } from './money.js';
@@ -60,7 +60,13 @@ const cancels = (title, openWorld) => ({
60
60
  idempotentHint: true,
61
61
  openWorldHint: openWorld,
62
62
  });
63
- const DATE = (what) => ({ type: 'string', description: `${what}, as YYYY-MM-DD` });
63
+ // ⚠️ THE SHAPE IS IN THE SCHEMA, not only in the words (2026-10-05 audit, P2):
64
+ // a date, a count of rooms and a number of guests each say what they may be.
65
+ const DATE = (what) => ({
66
+ type: 'string',
67
+ pattern: '^\\d{4}-\\d{2}-\\d{2}$',
68
+ description: `${what}, as YYYY-MM-DD`,
69
+ });
64
70
  /** How many ${nounPlural} one look-up reads - a city's worth, and what a comparison can hold and then some. */
65
71
  export const LOOK_UP_MAX = 6;
66
72
  /**
@@ -154,7 +160,12 @@ const allTools = (n, v) => {
154
160
  const stayProps = {
155
161
  checkIn: DATE('The arrival date'),
156
162
  checkOut: DATE('The departure date - the night before it is the last night'),
157
- guests: { type: 'number', description: 'People in the room. Default 2' },
163
+ guests: {
164
+ type: 'integer',
165
+ minimum: 1,
166
+ maximum: MAX_GUESTS,
167
+ description: 'People in the room. Default 2',
168
+ },
158
169
  };
159
170
  return [
160
171
  {
@@ -174,7 +185,9 @@ const allTools = (n, v) => {
174
185
  },
175
186
  ...stayProps,
176
187
  rooms: {
177
- type: 'number',
188
+ type: 'integer',
189
+ minimum: 1,
190
+ maximum: MAX_ROOMS_PER_LINE,
178
191
  description: `With dates: how many rooms of one type the party needs, 1 to ${MAX_ROOMS_PER_LINE}. Default 1`,
179
192
  },
180
193
  sort: {
@@ -231,7 +244,9 @@ const allTools = (n, v) => {
231
244
  },
232
245
  ...stayProps,
233
246
  rooms: {
234
- type: 'number',
247
+ type: 'integer',
248
+ minimum: 1,
249
+ maximum: MAX_ROOMS_PER_LINE,
235
250
  description: `How many rooms of one type, 1 to ${MAX_ROOMS_PER_LINE}. Default 1`,
236
251
  },
237
252
  },
@@ -262,7 +277,9 @@ const allTools = (n, v) => {
262
277
  },
263
278
  ...stayProps,
264
279
  rooms: {
265
- type: 'number',
280
+ type: 'integer',
281
+ minimum: 1,
282
+ maximum: MAX_ROOMS_PER_LINE,
266
283
  description: `How many rooms of one type, 1 to ${MAX_ROOMS_PER_LINE}. Default 1`,
267
284
  },
268
285
  },
@@ -306,11 +323,15 @@ const allTools = (n, v) => {
306
323
  },
307
324
  ...stayProps,
308
325
  rooms: {
309
- type: 'number',
326
+ type: 'integer',
327
+ minimum: 0,
328
+ maximum: MAX_ROOMS_PER_LINE,
310
329
  description: `How many of that room type, 1 to ${MAX_ROOMS_PER_LINE}. Default 1; 0 removes the line`,
311
330
  },
312
331
  quantity: {
313
- type: 'number',
332
+ type: 'integer',
333
+ minimum: 0,
334
+ maximum: MAX_EXTRA_QUANTITY,
314
335
  description: `For an add-on: how many, 1 to ${MAX_EXTRA_QUANTITY}. Default 1; 0 removes it`,
315
336
  },
316
337
  },
@@ -331,7 +352,12 @@ const allTools = (n, v) => {
331
352
  },
332
353
  {
333
354
  name: n.requestBooking,
334
- description: `Asks ${v.brand} to hold the trip's rooms for a named guest and returns a confirmation code and the moment the hold lapses. No payment is taken and no card is collected; the rooms are held, not paid for. Preferences - a bed type, a floor, a late arrival - ride along on the booking as requests the desk tries to meet.`,
355
+ // ⚠️ A DEMONSTRATION BOOKING IS SAID AS ONE (2026-10-05 audit, P2):
356
+ // "asks Shangri-La to hold the rooms" read as a real hold, and a guest
357
+ // could take the code to the real hotel.
358
+ description: v.demo
359
+ ? `Makes a demonstration booking request for the trip's rooms, for a named guest: it returns a confirmation code and the moment the demonstration hold lapses on its own. ${v.brand} does not receive it and no real room is held. No payment is taken and no card is collected. Preferences - a bed type, a floor, a late arrival - ride along on the request.`
360
+ : `Asks ${v.brand} to hold the trip's rooms for a named guest and returns a confirmation code and the moment the hold lapses. No payment is taken and no card is collected; the rooms are held, not paid for. Preferences - a bed type, a floor, a late arrival - ride along on the booking as requests the desk tries to meet.`,
335
361
  annotations: books(`Request a ${v.brand} booking`, open),
336
362
  inputSchema: {
337
363
  type: 'object',
@@ -437,7 +463,9 @@ const allTools = (n, v) => {
437
463
  description: 'The new check-out, YYYY-MM-DD. Leave out to keep it',
438
464
  },
439
465
  rooms: {
440
- type: 'number',
466
+ type: 'integer',
467
+ minimum: 1,
468
+ maximum: MAX_ROOMS_PER_LINE,
441
469
  description: `How many of that room type, 1 to ${MAX_ROOMS_PER_LINE}. Leave out to keep it`,
442
470
  },
443
471
  room: {
@@ -449,7 +477,9 @@ const allTools = (n, v) => {
449
477
  description: `An add-on at this ${v.noun}, by id or name, to put on the booking, take off or change - with \`quantity\``,
450
478
  },
451
479
  quantity: {
452
- type: 'number',
480
+ type: 'integer',
481
+ minimum: 0,
482
+ maximum: MAX_EXTRA_QUANTITY,
453
483
  description: `How many of that add-on the booking should hold, 0 to ${MAX_EXTRA_QUANTITY}; 0 takes it off`,
454
484
  },
455
485
  email: {
@@ -477,7 +507,9 @@ const allTools = (n, v) => {
477
507
  description: 'The new check-out, YYYY-MM-DD. Leave out to keep it',
478
508
  },
479
509
  rooms: {
480
- type: 'number',
510
+ type: 'integer',
511
+ minimum: 1,
512
+ maximum: MAX_ROOMS_PER_LINE,
481
513
  description: `How many of that room type, 1 to ${MAX_ROOMS_PER_LINE}. Leave out to keep it`,
482
514
  },
483
515
  room: {
@@ -489,7 +521,9 @@ const allTools = (n, v) => {
489
521
  description: `An add-on at this ${v.noun}, by id or name, to put on the booking, take off or change - with \`quantity\``,
490
522
  },
491
523
  quantity: {
492
- type: 'number',
524
+ type: 'integer',
525
+ minimum: 0,
526
+ maximum: MAX_EXTRA_QUANTITY,
493
527
  description: `How many of that add-on the booking should hold, 0 to ${MAX_EXTRA_QUANTITY}; 0 takes it off`,
494
528
  },
495
529
  email: {
@@ -1019,8 +1053,25 @@ const bookedLine = (deps, b) => {
1019
1053
  ? `${total} all in, to be paid at the hotel. ${deps.vocab.brand} has not confirmed this booking yet - that takes up to eight minutes, and ${deps.tools.listBookings} shows when it has. Nothing has been charged.`
1020
1054
  : `${total} all in, paid at the hotel. **Nothing has been charged here** - this is ${deps.vocab.brand}'s own reservation, kept until it is cancelled, and ${b.guest.name} pays at check-in.`;
1021
1055
  }
1022
- return `${total} all in. **Nothing has been charged** - the rooms are held until ${b.holdUntil?.slice(0, 16).replace('T', ' ')} UTC, and ${b.guest.name} presents a card at check-in.`;
1056
+ const lapses = b.holdUntilLocal
1057
+ ? `${b.holdUntilLocal} hotel time`
1058
+ : `${b.holdUntil?.slice(0, 16).replace('T', ' ')} UTC`;
1059
+ // ⚠️ NOT "PRESENTS A CARD AT CHECK-IN" (2026-10-05 audit, P2): a demo
1060
+ // booking has no check-in, and saying so sent a guest towards a real desk.
1061
+ if (deps.vocab.demo)
1062
+ return `${total} all in. **${demoBookingWords(deps)}** Nothing has been charged and no card is needed. The demonstration hold lapses on its own at ${lapses}.`;
1063
+ return `${total} all in. **Nothing has been charged** - the rooms are held until ${lapses}, and ${b.guest.name} presents a card at check-in.`;
1023
1064
  };
1065
+ /** What a demonstration booking is, in one sentence - see `demoBookingNote`. */
1066
+ const demoBookingWords = (deps) => `This is a demonstration booking: ${deps.vocab.brand} has not received it and is not holding any room.`;
1067
+ /**
1068
+ * Said on every answer about a demonstration booking - made, changed or
1069
+ * cancelled - and on nothing else: a hold of the demo's that a guest took for
1070
+ * the hotel's would send them to a desk with a code nobody there has.
1071
+ */
1072
+ const demoBookingNote = (deps) => deps.vocab.demo && (deps.backend.capabilities ?? FULL_CAPABILITIES).booking === 'hold'
1073
+ ? demoBookingWords(deps)
1074
+ : '';
1024
1075
  const common = (deps, key) => ({
1025
1076
  brand: deps.vocab.brand,
1026
1077
  ...(deps.logoUrl ? { logoUrl: deps.logoUrl } : {}),
@@ -1427,6 +1478,42 @@ const remember = (deps, k, amount) => {
1427
1478
  m.delete(oldest);
1428
1479
  }
1429
1480
  };
1481
+ /** The counts a call can carry, and what each may be. */
1482
+ const COUNTS = ['guests', 'rooms', 'quantity'];
1483
+ /** "2" is 2: a host that sends a count as a string has not asked for something else. */
1484
+ const countsAsNumbers = (args) => {
1485
+ const out = { ...args };
1486
+ for (const f of COUNTS) {
1487
+ const v = out[f];
1488
+ if (typeof v === 'string' && /^\s*-?\d+\s*$/.test(v))
1489
+ out[f] = Number(v);
1490
+ }
1491
+ return out;
1492
+ };
1493
+ /**
1494
+ * Why a count in this call is not one this connector takes - or nothing.
1495
+ *
1496
+ * ⚠️ REFUSED, NOT QUIETLY MADE INTO SOMETHING ELSE (2026-10-05 audit, P2):
1497
+ * six rooms became five, -1 became 1 and a billion became five, and the answer
1498
+ * spoke as if that was what had been asked. A guest who asked for six rooms
1499
+ * has to hear that five is the most, not be booked five.
1500
+ */
1501
+ const countProblem = (t, name, args) => {
1502
+ const check = (field, min, max) => {
1503
+ const v = args[field];
1504
+ if (v === undefined || v === null)
1505
+ return undefined;
1506
+ if (typeof v === 'number' && Number.isInteger(v) && v >= min && v <= max)
1507
+ return undefined;
1508
+ return `\`${field}\` has to be a whole number from ${min} to ${max}, and ${JSON.stringify(v)} is not - nothing was changed. Ask the guest, or use a number in that range.`;
1509
+ };
1510
+ // 0 means something here: take the line out of the trip, or - on a change -
1511
+ // the backend's own "a booking keeps at least one room; cancel it instead".
1512
+ const removes = name === t.addToTrip || name === t.previewChange || name === t.changeBooking;
1513
+ return (check('guests', 1, MAX_GUESTS) ??
1514
+ check('rooms', removes ? 0 : 1, MAX_ROOMS_PER_LINE) ??
1515
+ check('quantity', 0, MAX_EXTRA_QUANTITY));
1516
+ };
1430
1517
  /**
1431
1518
  * What a backend's refusal says, for the guest - or, when the failure is the
1432
1519
  * code's own, a sentence that says nothing about the code.
@@ -1811,9 +1898,17 @@ export const handleTool = async (deps, name, rawArgs, ctx) => {
1811
1898
  : answer;
1812
1899
  };
1813
1900
  const answerTool = async (deps, name, rawArgs, ctx) => {
1814
- const args = (rawArgs ?? {});
1901
+ const args = countsAsNumbers((rawArgs ?? {}));
1815
1902
  const t = deps.tools;
1816
1903
  const key = ctx.tripKey ?? tripKey(ctx.session, deps.vocab.brand);
1904
+ const badCount = countProblem(t, name, args);
1905
+ if (badCount) {
1906
+ return {
1907
+ structured: { view: 'error', ...common(deps, key), error: 'bad-count', detail: badCount },
1908
+ text: badCount,
1909
+ isError: true,
1910
+ };
1911
+ }
1817
1912
  if (name === t.listHotels) {
1818
1913
  const query = str(args.query);
1819
1914
  const city = str(args.city);
@@ -2391,7 +2486,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
2391
2486
  ].join('\n')
2392
2487
  : (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
2393
2488
  extras.length
2394
- ? `Add-ons: ${extras.map((e) => `${e.name} (id \`${e.id}\`, ${formatMoney(e.price)} per ${e.per}${e.when && e.when !== 'any' ? `, on ${e.when}` : ''})`).join('; ')}`
2489
+ ? `Add-ons: ${extras.map((e) => `${e.name} (id \`${e.id}\`, ${formatMoney(e.price)} ${unitWords(e.per)}${e.when && e.when !== 'any' ? `, on ${e.when}` : ''})`).join('; ')}`
2395
2490
  : '',
2396
2491
  ]
2397
2492
  .filter(Boolean)
@@ -2489,7 +2584,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
2489
2584
  },
2490
2585
  text: extras.length
2491
2586
  ? extras
2492
- .map((e) => `- ${e.name} — ${formatMoney(e.price)} per ${e.per}${held[e.id] ? ` (${booked?.confirmationCode} holds ${held[e.id]})` : ''}`)
2587
+ .map((e) => `- ${e.name} — ${formatMoney(e.price)} ${unitWords(e.per)}${held[e.id] ? ` (${booked?.confirmationCode} holds ${held[e.id]})` : ''}`)
2493
2588
  .concat(deps.vocab.demo ? [DEMO_NOTE] : [])
2494
2589
  .join('\n')
2495
2590
  : `${detail.hotel.name} has no add-ons listed here.`,
@@ -2876,6 +2971,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
2876
2971
  : 'The total is unchanged.',
2877
2972
  preview.rule,
2878
2973
  `Say the word and it moves (${t.changeBooking} with \`confirmed\`).`,
2974
+ demoBookingNote(deps),
2879
2975
  deps.vocab.demo ? DEMO_NOTE : '',
2880
2976
  ]
2881
2977
  .filter(Boolean)
@@ -2938,6 +3034,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
2938
3034
  text: [
2939
3035
  `${changed.confirmationCode} now holds ${changed.stay.checkIn} to ${changed.stay.checkOut}, ${formatMoney(changed.quote.total)} all in.`,
2940
3036
  'The confirmation code has not changed. **Nothing has been charged.**',
3037
+ demoBookingNote(deps),
2941
3038
  deps.vocab.demo ? DEMO_NOTE : '',
2942
3039
  ]
2943
3040
  .filter(Boolean)
@@ -2983,6 +3080,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
2983
3080
  preview.cancellable
2984
3081
  ? `Say the word and it is cancelled (${t.cancelBooking} with \`confirmed\`).`
2985
3082
  : '',
3083
+ demoBookingNote(deps),
2986
3084
  deps.vocab.demo ? DEMO_NOTE : '',
2987
3085
  ]
2988
3086
  .filter(Boolean)
@@ -3032,7 +3130,11 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3032
3130
  deps.vocab.sandbox
3033
3131
  ? // A test booking cancelled in the simulation: no hotel, no charge (2026-10-05 audit, P2).
3034
3132
  `${cancelled.confirmationCode} is cancelled in the simulated system. ${charged.minor === 0 ? 'No fee.' : `A real booking on this rate would be charged ${formatMoney(charged)}; this one is a test, and nothing is charged.`}`
3035
- : `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `The hotel charges ${formatMoney(charged)}.`}`,
3133
+ : deps.vocab.demo
3134
+ ? // Nobody charges a demonstration anything (2026-10-05 audit, P2).
3135
+ `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `A real booking on this rate would be charged ${formatMoney(charged)}; this is a demonstration, and nothing is charged.`}`
3136
+ : `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `The hotel charges ${formatMoney(charged)}.`}`,
3137
+ demoBookingNote(deps),
3036
3138
  deps.vocab.demo ? DEMO_NOTE : '',
3037
3139
  ]
3038
3140
  .filter(Boolean)