@curless/hotel-storefront 0.27.2 → 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-audit.test.js +27 -3
- package/dist/__tests__/huazhu-audit.test.js.map +1 -1
- package/dist/__tests__/huazhu-backend.test.js +88 -1
- 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 +8 -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/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.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 +6 -0
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +76 -13
- 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/legal.d.ts.map +1 -1
- package/dist/legal.js +16 -8
- 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 +5 -4
- package/dist/server.js.map +1 -1
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +14 -3
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +75 -19
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +177 -26
- package/dist/tools.js.map +1 -1
- package/dist/trip.d.ts +12 -1
- package/dist/trip.d.ts.map +1 -1
- package/dist/trip.js +32 -0
- package/dist/trip.js.map +1 -1
- package/dist/types.d.ts +34 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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';
|
|
6
6
|
import { MAX_COMPARED, MAX_CONS, MAX_DAYS, MAX_ITEMS_PER_DAY, MAX_MOMENTS, MAX_PROS, buildItinerary, compareHotels, } from './present.js';
|
|
7
7
|
import { regionOf } from './regions.js';
|
|
8
|
-
import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, TRIP_TTL_MS, clampCount, tripKey, } from './trip.js';
|
|
8
|
+
import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, TRIP_TTL_MS, clampCount, shownOf, tripKey, } from './trip.js';
|
|
9
9
|
import { DEMO_NOTE } from './vocab.js';
|
|
10
10
|
// The eleven things a guest can do: find hotels, look at one on dates, see what
|
|
11
11
|
// else it offers, build a trip, ask for the rooms, and deal with a booking they
|
|
@@ -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
|
/**
|
|
@@ -150,10 +156,16 @@ const forCapabilities = (tools, n, v, caps) => {
|
|
|
150
156
|
};
|
|
151
157
|
const allTools = (n, v) => {
|
|
152
158
|
const open = !v.demo;
|
|
159
|
+
const places = v.hints.places ?? [];
|
|
153
160
|
const stayProps = {
|
|
154
161
|
checkIn: DATE('The arrival date'),
|
|
155
162
|
checkOut: DATE('The departure date - the night before it is the last night'),
|
|
156
|
-
guests: {
|
|
163
|
+
guests: {
|
|
164
|
+
type: 'integer',
|
|
165
|
+
minimum: 1,
|
|
166
|
+
maximum: MAX_GUESTS,
|
|
167
|
+
description: 'People in the room. Default 2',
|
|
168
|
+
},
|
|
157
169
|
};
|
|
158
170
|
return [
|
|
159
171
|
{
|
|
@@ -169,11 +181,13 @@ const allTools = (n, v) => {
|
|
|
169
181
|
},
|
|
170
182
|
city: {
|
|
171
183
|
type: 'string',
|
|
172
|
-
description:
|
|
184
|
+
description: `One city or one country${places.length ? `, e.g. ${places.map((p) => `"${p}"`).join(' or ')}` : ''}`,
|
|
173
185
|
},
|
|
174
186
|
...stayProps,
|
|
175
187
|
rooms: {
|
|
176
|
-
type: '
|
|
188
|
+
type: 'integer',
|
|
189
|
+
minimum: 1,
|
|
190
|
+
maximum: MAX_ROOMS_PER_LINE,
|
|
177
191
|
description: `With dates: how many rooms of one type the party needs, 1 to ${MAX_ROOMS_PER_LINE}. Default 1`,
|
|
178
192
|
},
|
|
179
193
|
sort: {
|
|
@@ -230,7 +244,9 @@ const allTools = (n, v) => {
|
|
|
230
244
|
},
|
|
231
245
|
...stayProps,
|
|
232
246
|
rooms: {
|
|
233
|
-
type: '
|
|
247
|
+
type: 'integer',
|
|
248
|
+
minimum: 1,
|
|
249
|
+
maximum: MAX_ROOMS_PER_LINE,
|
|
234
250
|
description: `How many rooms of one type, 1 to ${MAX_ROOMS_PER_LINE}. Default 1`,
|
|
235
251
|
},
|
|
236
252
|
},
|
|
@@ -261,7 +277,9 @@ const allTools = (n, v) => {
|
|
|
261
277
|
},
|
|
262
278
|
...stayProps,
|
|
263
279
|
rooms: {
|
|
264
|
-
type: '
|
|
280
|
+
type: 'integer',
|
|
281
|
+
minimum: 1,
|
|
282
|
+
maximum: MAX_ROOMS_PER_LINE,
|
|
265
283
|
description: `How many rooms of one type, 1 to ${MAX_ROOMS_PER_LINE}. Default 1`,
|
|
266
284
|
},
|
|
267
285
|
},
|
|
@@ -305,11 +323,15 @@ const allTools = (n, v) => {
|
|
|
305
323
|
},
|
|
306
324
|
...stayProps,
|
|
307
325
|
rooms: {
|
|
308
|
-
type: '
|
|
326
|
+
type: 'integer',
|
|
327
|
+
minimum: 0,
|
|
328
|
+
maximum: MAX_ROOMS_PER_LINE,
|
|
309
329
|
description: `How many of that room type, 1 to ${MAX_ROOMS_PER_LINE}. Default 1; 0 removes the line`,
|
|
310
330
|
},
|
|
311
331
|
quantity: {
|
|
312
|
-
type: '
|
|
332
|
+
type: 'integer',
|
|
333
|
+
minimum: 0,
|
|
334
|
+
maximum: MAX_EXTRA_QUANTITY,
|
|
313
335
|
description: `For an add-on: how many, 1 to ${MAX_EXTRA_QUANTITY}. Default 1; 0 removes it`,
|
|
314
336
|
},
|
|
315
337
|
},
|
|
@@ -330,7 +352,12 @@ const allTools = (n, v) => {
|
|
|
330
352
|
},
|
|
331
353
|
{
|
|
332
354
|
name: n.requestBooking,
|
|
333
|
-
|
|
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.`,
|
|
334
361
|
annotations: books(`Request a ${v.brand} booking`, open),
|
|
335
362
|
inputSchema: {
|
|
336
363
|
type: 'object',
|
|
@@ -436,7 +463,9 @@ const allTools = (n, v) => {
|
|
|
436
463
|
description: 'The new check-out, YYYY-MM-DD. Leave out to keep it',
|
|
437
464
|
},
|
|
438
465
|
rooms: {
|
|
439
|
-
type: '
|
|
466
|
+
type: 'integer',
|
|
467
|
+
minimum: 1,
|
|
468
|
+
maximum: MAX_ROOMS_PER_LINE,
|
|
440
469
|
description: `How many of that room type, 1 to ${MAX_ROOMS_PER_LINE}. Leave out to keep it`,
|
|
441
470
|
},
|
|
442
471
|
room: {
|
|
@@ -448,7 +477,9 @@ const allTools = (n, v) => {
|
|
|
448
477
|
description: `An add-on at this ${v.noun}, by id or name, to put on the booking, take off or change - with \`quantity\``,
|
|
449
478
|
},
|
|
450
479
|
quantity: {
|
|
451
|
-
type: '
|
|
480
|
+
type: 'integer',
|
|
481
|
+
minimum: 0,
|
|
482
|
+
maximum: MAX_EXTRA_QUANTITY,
|
|
452
483
|
description: `How many of that add-on the booking should hold, 0 to ${MAX_EXTRA_QUANTITY}; 0 takes it off`,
|
|
453
484
|
},
|
|
454
485
|
email: {
|
|
@@ -476,7 +507,9 @@ const allTools = (n, v) => {
|
|
|
476
507
|
description: 'The new check-out, YYYY-MM-DD. Leave out to keep it',
|
|
477
508
|
},
|
|
478
509
|
rooms: {
|
|
479
|
-
type: '
|
|
510
|
+
type: 'integer',
|
|
511
|
+
minimum: 1,
|
|
512
|
+
maximum: MAX_ROOMS_PER_LINE,
|
|
480
513
|
description: `How many of that room type, 1 to ${MAX_ROOMS_PER_LINE}. Leave out to keep it`,
|
|
481
514
|
},
|
|
482
515
|
room: {
|
|
@@ -488,7 +521,9 @@ const allTools = (n, v) => {
|
|
|
488
521
|
description: `An add-on at this ${v.noun}, by id or name, to put on the booking, take off or change - with \`quantity\``,
|
|
489
522
|
},
|
|
490
523
|
quantity: {
|
|
491
|
-
type: '
|
|
524
|
+
type: 'integer',
|
|
525
|
+
minimum: 0,
|
|
526
|
+
maximum: MAX_EXTRA_QUANTITY,
|
|
492
527
|
description: `How many of that add-on the booking should hold, 0 to ${MAX_EXTRA_QUANTITY}; 0 takes it off`,
|
|
493
528
|
},
|
|
494
529
|
email: {
|
|
@@ -555,7 +590,10 @@ const allTools = (n, v) => {
|
|
|
555
590
|
items: {
|
|
556
591
|
type: 'object',
|
|
557
592
|
properties: {
|
|
558
|
-
label: {
|
|
593
|
+
label: {
|
|
594
|
+
type: 'string',
|
|
595
|
+
description: `e.g. "Day 1 — Arrive${places[0] ? ` in ${places[0]}` : ''}"`,
|
|
596
|
+
},
|
|
559
597
|
note: {
|
|
560
598
|
type: 'string',
|
|
561
599
|
description: 'One or two lines on the day, in your own words',
|
|
@@ -722,6 +760,31 @@ const CHANGE_SCHEMA = {
|
|
|
722
760
|
},
|
|
723
761
|
required: ['view'],
|
|
724
762
|
};
|
|
763
|
+
/**
|
|
764
|
+
* The output schemas with what this backend never fills taken out.
|
|
765
|
+
*
|
|
766
|
+
* ⚠️ A SCHEMA PROMISES. A Huazhu hotel's still promised its add-ons and
|
|
767
|
+
* policies, and every trip a member badge and saving (2026-10-05 audit) -
|
|
768
|
+
* fields no answer there ever carries.
|
|
769
|
+
*/
|
|
770
|
+
export const outputSchemasFor = (schemas, caps) => {
|
|
771
|
+
const gone = [
|
|
772
|
+
...(caps.members ? [] : ['member', 'memberSaving']),
|
|
773
|
+
...(caps.extras ? [] : ['extras']),
|
|
774
|
+
...(caps.policies === false ? ['policies'] : []),
|
|
775
|
+
];
|
|
776
|
+
if (!gone.length)
|
|
777
|
+
return schemas;
|
|
778
|
+
return Object.fromEntries(Object.entries(schemas).map(([name, schema]) => {
|
|
779
|
+
const s = schema;
|
|
780
|
+
if (!s?.properties)
|
|
781
|
+
return [name, schema];
|
|
782
|
+
const properties = { ...s.properties };
|
|
783
|
+
for (const k of gone)
|
|
784
|
+
delete properties[k];
|
|
785
|
+
return [name, { ...s, properties }];
|
|
786
|
+
}));
|
|
787
|
+
};
|
|
725
788
|
export const buildOutputSchemas = (n) => ({
|
|
726
789
|
[n.listHotels]: {
|
|
727
790
|
type: 'object',
|
|
@@ -990,8 +1053,25 @@ const bookedLine = (deps, b) => {
|
|
|
990
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.`
|
|
991
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.`;
|
|
992
1055
|
}
|
|
993
|
-
|
|
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.`;
|
|
994
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
|
+
: '';
|
|
995
1075
|
const common = (deps, key) => ({
|
|
996
1076
|
brand: deps.vocab.brand,
|
|
997
1077
|
...(deps.logoUrl ? { logoUrl: deps.logoUrl } : {}),
|
|
@@ -1038,7 +1118,7 @@ const quoteForCard = (quote) => ({
|
|
|
1038
1118
|
day: l.day,
|
|
1039
1119
|
})),
|
|
1040
1120
|
subtotal: formatMoney(quote.subtotal),
|
|
1041
|
-
taxesAndFees: formatMoney(quote.taxesAndFees),
|
|
1121
|
+
taxesAndFees: quote.taxesIncluded ? 'Included' : formatMoney(quote.taxesAndFees),
|
|
1042
1122
|
total: formatMoney(quote.total),
|
|
1043
1123
|
// ⚠️ NOT `member`: that key means "who is signed in" on every payload, and
|
|
1044
1124
|
// the quote's block is a different shape (a tier NAME, a saving, points).
|
|
@@ -1057,7 +1137,7 @@ const tripText = (quote, demo) => {
|
|
|
1057
1137
|
: `- ${l.ordered ?? l.quantity}× ${l.title}${l.ordered !== undefined && l.quantity !== undefined && l.quantity !== l.ordered ? ` (${l.quantity} in all, over the stay)` : ''} — ${formatMoney(l.total)}`);
|
|
1058
1138
|
return [
|
|
1059
1139
|
...lines,
|
|
1060
|
-
`Taxes and fees ${formatMoney(quote.taxesAndFees)} · **Total ${formatMoney(quote.total)}**`,
|
|
1140
|
+
`Taxes and fees ${quote.taxesIncluded ? 'included in the prices' : formatMoney(quote.taxesAndFees)} · **Total ${formatMoney(quote.total)}**`,
|
|
1061
1141
|
...(quote.dropped ?? []).map((d) => `Left out: ${d.title} — ${d.why}`),
|
|
1062
1142
|
demo ? DEMO_NOTE : '',
|
|
1063
1143
|
]
|
|
@@ -1265,7 +1345,11 @@ const planGap = (plan) => {
|
|
|
1265
1345
|
const bookingLine = (b) => {
|
|
1266
1346
|
const kinds = b.requests?.kinds ?? [];
|
|
1267
1347
|
return [
|
|
1268
|
-
`${b.confirmationCode} · ${b.hotelName} · ${b.stay.checkIn} to ${b.stay.checkOut} · ${
|
|
1348
|
+
`${b.confirmationCode} · ${b.hotelName} · ${b.stay.checkIn} to ${b.stay.checkOut} · ${
|
|
1349
|
+
// ⚠️ SAID AS WHAT IT IS - see BookingStatus. Neither booked nor failed.
|
|
1350
|
+
b.status === 'unconfirmed'
|
|
1351
|
+
? 'unconfirmed: whether it was made is not known yet, and a person is confirming it'
|
|
1352
|
+
: b.status}`,
|
|
1269
1353
|
kinds.length ? ` · asked for ${kinds.map(bare).join(', ')}` : '',
|
|
1270
1354
|
].join('');
|
|
1271
1355
|
};
|
|
@@ -1394,6 +1478,42 @@ const remember = (deps, k, amount) => {
|
|
|
1394
1478
|
m.delete(oldest);
|
|
1395
1479
|
}
|
|
1396
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
|
+
};
|
|
1397
1517
|
/**
|
|
1398
1518
|
* What a backend's refusal says, for the guest - or, when the failure is the
|
|
1399
1519
|
* code's own, a sentence that says nothing about the code.
|
|
@@ -1499,6 +1619,11 @@ const needsRoomIn = (t, lines, parts) => {
|
|
|
1499
1619
|
const currentTrip = async (deps, key, who) => {
|
|
1500
1620
|
const lines = deps.trips.read(key);
|
|
1501
1621
|
const parts = await quoteParts(deps, lines, who);
|
|
1622
|
+
// ⚠️ WHAT THE GUEST IS SHOWN IS WHAT IS BOOKED - see `ShownQuote`. Every
|
|
1623
|
+
// priced trip answer goes through here: add_to_trip, view_trip, and the
|
|
1624
|
+
// card's own trip view.
|
|
1625
|
+
if (lines.length)
|
|
1626
|
+
deps.trips.show(key, shownOf(parts));
|
|
1502
1627
|
const [only] = parts;
|
|
1503
1628
|
const orphans = needsRoomIn(deps.tools, lines, parts);
|
|
1504
1629
|
const needs = orphans.needsRoom.length ? { needsRoom: orphans.needsRoom } : {};
|
|
@@ -1773,9 +1898,17 @@ export const handleTool = async (deps, name, rawArgs, ctx) => {
|
|
|
1773
1898
|
: answer;
|
|
1774
1899
|
};
|
|
1775
1900
|
const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
1776
|
-
const args = (rawArgs ?? {});
|
|
1901
|
+
const args = countsAsNumbers((rawArgs ?? {}));
|
|
1777
1902
|
const t = deps.tools;
|
|
1778
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
|
+
}
|
|
1779
1912
|
if (name === t.listHotels) {
|
|
1780
1913
|
const query = str(args.query);
|
|
1781
1914
|
const city = str(args.city);
|
|
@@ -2353,7 +2486,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2353
2486
|
].join('\n')
|
|
2354
2487
|
: (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
|
|
2355
2488
|
extras.length
|
|
2356
|
-
? `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('; ')}`
|
|
2357
2490
|
: '',
|
|
2358
2491
|
]
|
|
2359
2492
|
.filter(Boolean)
|
|
@@ -2451,7 +2584,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2451
2584
|
},
|
|
2452
2585
|
text: extras.length
|
|
2453
2586
|
? extras
|
|
2454
|
-
.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]})` : ''}`)
|
|
2455
2588
|
.concat(deps.vocab.demo ? [DEMO_NOTE] : [])
|
|
2456
2589
|
.join('\n')
|
|
2457
2590
|
: `${detail.hotel.name} has no add-ons listed here.`,
|
|
@@ -2599,7 +2732,8 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2599
2732
|
}
|
|
2600
2733
|
const { requests, dropped } = readRequests(args);
|
|
2601
2734
|
try {
|
|
2602
|
-
const
|
|
2735
|
+
const shown = deps.trips.shown(key);
|
|
2736
|
+
const made = await deps.backend.requestBooking({ lines, ...(shown ? { shown } : {}) }, {
|
|
2603
2737
|
...guest,
|
|
2604
2738
|
...(str(args.phone) ? { phone: str(args.phone) } : {}),
|
|
2605
2739
|
...(requests?.note ? { notes: requests.note } : {}),
|
|
@@ -2714,6 +2848,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2714
2848
|
}
|
|
2715
2849
|
catch (err) {
|
|
2716
2850
|
const words = guestWords(err, BOOKING_FAULT);
|
|
2851
|
+
// The price or terms moved since the guest saw them: nothing was booked,
|
|
2852
|
+
// and the trip is shown again at today's - which is then what the guest
|
|
2853
|
+
// has seen, and what a second ask books.
|
|
2854
|
+
if (err.code === 'price-changed') {
|
|
2855
|
+
const again = await currentTrip(deps, key, identity(deps, ctx, key));
|
|
2856
|
+
return { ...again, text: [words, again.text].join('\n\n') };
|
|
2857
|
+
}
|
|
2717
2858
|
return {
|
|
2718
2859
|
structured: {
|
|
2719
2860
|
view: 'error',
|
|
@@ -2830,6 +2971,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2830
2971
|
: 'The total is unchanged.',
|
|
2831
2972
|
preview.rule,
|
|
2832
2973
|
`Say the word and it moves (${t.changeBooking} with \`confirmed\`).`,
|
|
2974
|
+
demoBookingNote(deps),
|
|
2833
2975
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2834
2976
|
]
|
|
2835
2977
|
.filter(Boolean)
|
|
@@ -2892,6 +3034,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2892
3034
|
text: [
|
|
2893
3035
|
`${changed.confirmationCode} now holds ${changed.stay.checkIn} to ${changed.stay.checkOut}, ${formatMoney(changed.quote.total)} all in.`,
|
|
2894
3036
|
'The confirmation code has not changed. **Nothing has been charged.**',
|
|
3037
|
+
demoBookingNote(deps),
|
|
2895
3038
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2896
3039
|
]
|
|
2897
3040
|
.filter(Boolean)
|
|
@@ -2937,6 +3080,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2937
3080
|
preview.cancellable
|
|
2938
3081
|
? `Say the word and it is cancelled (${t.cancelBooking} with \`confirmed\`).`
|
|
2939
3082
|
: '',
|
|
3083
|
+
demoBookingNote(deps),
|
|
2940
3084
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2941
3085
|
]
|
|
2942
3086
|
.filter(Boolean)
|
|
@@ -2983,7 +3127,14 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2983
3127
|
cancellable: false,
|
|
2984
3128
|
},
|
|
2985
3129
|
text: [
|
|
2986
|
-
|
|
3130
|
+
deps.vocab.sandbox
|
|
3131
|
+
? // A test booking cancelled in the simulation: no hotel, no charge (2026-10-05 audit, P2).
|
|
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.`}`
|
|
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),
|
|
2987
3138
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2988
3139
|
]
|
|
2989
3140
|
.filter(Boolean)
|