@curless/hotel-storefront 0.27.3 → 0.27.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.
- package/dist/__tests__/audit-p2.test.d.ts +2 -0
- package/dist/__tests__/audit-p2.test.d.ts.map +1 -0
- package/dist/__tests__/audit-p2.test.js +98 -0
- package/dist/__tests__/audit-p2.test.js.map +1 -0
- package/dist/__tests__/demo-hotels.test.js +3 -1
- package/dist/__tests__/demo-hotels.test.js.map +1 -1
- package/dist/__tests__/huazhu-backend.test.js +13 -0
- package/dist/__tests__/huazhu-backend.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +21 -5
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/backend.d.ts +2 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +2 -0
- package/dist/backend.js.map +1 -1
- package/dist/card.generated.d.ts +1 -1
- package/dist/card.generated.js +2 -2
- package/dist/card.generated.js.map +1 -1
- package/dist/data.js +2 -2
- package/dist/data.js.map +1 -1
- package/dist/demo-hotels.d.ts +1 -1
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +9 -3
- package/dist/demo-hotels.js.map +1 -1
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +32 -8
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/huazhu/sellable.d.ts +3 -1
- package/dist/huazhu/sellable.d.ts.map +1 -1
- package/dist/huazhu/sellable.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/legal.d.ts.map +1 -1
- package/dist/legal.js +6 -2
- package/dist/legal.js.map +1 -1
- package/dist/present.d.ts +1 -1
- package/dist/present.d.ts.map +1 -1
- package/dist/present.js +2 -1
- package/dist/present.js.map +1 -1
- package/dist/server.d.ts +8 -8
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +6 -1
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +65 -17
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +121 -19
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- 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
|
-
|
|
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: {
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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)}
|
|
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)}
|
|
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
|
-
:
|
|
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)
|