@curless/hotel-storefront 0.27.7 → 0.27.9
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/README.md +1 -1
- package/dist/aggregate/hotels.d.ts +45 -0
- package/dist/aggregate/hotels.d.ts.map +1 -0
- package/dist/aggregate/hotels.js +633 -0
- package/dist/aggregate/hotels.js.map +1 -0
- package/dist/aggregate/ids.d.ts +36 -0
- package/dist/aggregate/ids.d.ts.map +1 -0
- package/dist/aggregate/ids.js +64 -0
- package/dist/aggregate/ids.js.map +1 -0
- package/dist/aggregate/make.d.ts +12 -0
- package/dist/aggregate/make.d.ts.map +1 -0
- package/dist/aggregate/make.js +96 -0
- package/dist/aggregate/make.js.map +1 -0
- package/dist/aggregate/paging.d.ts +35 -0
- package/dist/aggregate/paging.d.ts.map +1 -0
- package/dist/aggregate/paging.js +94 -0
- package/dist/aggregate/paging.js.map +1 -0
- package/dist/backend.d.ts +41 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js.map +1 -1
- package/dist/card.generated.d.ts +2 -2
- package/dist/card.generated.d.ts.map +1 -1
- package/dist/card.generated.js +3 -3
- package/dist/card.generated.js.map +1 -1
- package/dist/config.d.ts +31 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/demo-hotels.d.ts +6 -0
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +16 -6
- package/dist/demo-hotels.js.map +1 -1
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +2 -0
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/huazhu/make.d.ts +4 -0
- package/dist/huazhu/make.d.ts.map +1 -0
- package/dist/huazhu/make.js +105 -0
- package/dist/huazhu/make.js.map +1 -0
- package/dist/index.js +0 -0
- package/dist/legal.d.ts.map +1 -1
- package/dist/legal.js +136 -51
- package/dist/legal.js.map +1 -1
- package/dist/lib.d.ts +2 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +2 -0
- package/dist/lib.js.map +1 -1
- package/dist/listing.d.ts +10 -0
- package/dist/listing.d.ts.map +1 -0
- package/dist/listing.js +43 -0
- package/dist/listing.js.map +1 -0
- package/dist/server.d.ts +10 -22
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +40 -153
- package/dist/server.js.map +1 -1
- package/dist/state.d.ts +8 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +18 -1
- package/dist/state.js.map +1 -1
- package/dist/supplier.d.ts +50 -0
- package/dist/supplier.d.ts.map +1 -0
- package/dist/supplier.js +57 -0
- package/dist/supplier.js.map +1 -0
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +37 -21
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +29 -23
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +208 -69
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vocab.d.ts +20 -0
- package/dist/vocab.d.ts.map +1 -1
- package/dist/vocab.js +22 -1
- package/dist/vocab.js.map +1 -1
- package/package.json +12 -10
package/dist/tools.js
CHANGED
|
@@ -6,7 +6,7 @@ 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
8
|
import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, TRIP_TTL_MS, clampCount, shownOf, tripKey, } from './trip.js';
|
|
9
|
-
import { DEMO_NOTE } from './vocab.js';
|
|
9
|
+
import { DEMO_NOTE, branded, catalogue, upFirst } 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
|
|
12
12
|
// already have.
|
|
@@ -151,7 +151,8 @@ const forCapabilities = (tools, n, v, caps) => {
|
|
|
151
151
|
}
|
|
152
152
|
if (!caps.members)
|
|
153
153
|
out = { ...out, inputSchema: without(out.inputSchema, 'rememberRequests') };
|
|
154
|
-
|
|
154
|
+
// A market's suppliers book differently - its own description says so.
|
|
155
|
+
if (caps.booking === 'pay-at-hotel' && !v.market) {
|
|
155
156
|
out = {
|
|
156
157
|
...out,
|
|
157
158
|
// ⚠️ A SANDBOX BOOKS NOTHING REAL, and its description says so
|
|
@@ -182,8 +183,8 @@ const allTools = (n, v) => {
|
|
|
182
183
|
return [
|
|
183
184
|
{
|
|
184
185
|
name: n.listHotels,
|
|
185
|
-
description: `${v
|
|
186
|
-
annotations: reads(`Browse ${v
|
|
186
|
+
description: `${upFirst(catalogue(v))}, shown as a card. Asked with no place - or a country of many cities - it answers with the places to choose from: regions, countries and cities, each with how many ${v.nounPlural} it has; \`all\` lists the ${v.nounPlural} themselves. \`city\` (a city or a country) or \`query\` answers with the ${v.nounPlural}: a picture, the city and area, a rating where the ${v.noun} publishes one, and a from-price, and a city usually holds several. With \`checkIn\` and \`checkOut\` every ${v.noun} is priced for those nights - its cheapest room that takes the party, \`rooms\` of it, for the whole stay with taxes - or marked as having no room for them. \`sort\` and \`maxNightly\` order and cap by that price, or by the from-price without dates, where the matches share one currency. \`brand\`, \`facilities\`, \`freeCancellation\` and \`breakfast\` narrow the list, and the answer counts what each would leave. A long list comes a page at a time: the answer says how many more there are and the \`offset\` of the next page. Words or a place that match nothing are widened once, and the answer names what it widened to.${v.hints.search ? ` ${v.hints.search}` : ''}`,
|
|
187
|
+
annotations: reads(`Browse ${catalogue(v)}`, open),
|
|
187
188
|
inputSchema: {
|
|
188
189
|
type: 'object',
|
|
189
190
|
properties: {
|
|
@@ -223,6 +224,14 @@ const allTools = (n, v) => {
|
|
|
223
224
|
type: 'string',
|
|
224
225
|
description: "One of the group's brands, as the list's brand counts name it",
|
|
225
226
|
},
|
|
227
|
+
...(v.market
|
|
228
|
+
? {
|
|
229
|
+
group: {
|
|
230
|
+
type: 'string',
|
|
231
|
+
description: "One hotel group's hotels only, by the group's name",
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
: {}),
|
|
226
235
|
facilities: {
|
|
227
236
|
type: 'array',
|
|
228
237
|
items: { type: 'string', enum: FACILITIES.map((x) => x.key) },
|
|
@@ -246,7 +255,7 @@ const allTools = (n, v) => {
|
|
|
246
255
|
{
|
|
247
256
|
name: n.getHotel,
|
|
248
257
|
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, each with its nightly rate, the stay total including taxes and fees, whether it can be cancelled free and until when, and how many rooms are left. \`rooms\` leaves out the types that have fewer than that many. 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}` : ''}`,
|
|
249
|
-
annotations: reads(`View one ${v
|
|
258
|
+
annotations: reads(`View one ${catalogue(v, true)}`, open),
|
|
250
259
|
inputSchema: {
|
|
251
260
|
type: 'object',
|
|
252
261
|
properties: {
|
|
@@ -274,7 +283,7 @@ const allTools = (n, v) => {
|
|
|
274
283
|
// reference apps do the same split: search without a card, present with
|
|
275
284
|
// one.
|
|
276
285
|
description: `Reads up to ${LOOK_UP_MAX} ${v.nounPlural} - a \`city\`'s (or a country's), or the ones named in \`${v.nounPlural}\` - and returns them as text, with nothing drawn and nothing shown to the guest: each one's id, area, rating, description and amenities, its add-ons with their ids and prices, and, given \`checkIn\` and \`checkOut\`, its rooms priced for those nights with taxes in. The ids are the ones a comparison and a plan take.`,
|
|
277
|
-
annotations: reads(`Look up ${v
|
|
286
|
+
annotations: reads(`Look up ${catalogue(v)}`, open),
|
|
278
287
|
inputSchema: {
|
|
279
288
|
type: 'object',
|
|
280
289
|
properties: {
|
|
@@ -300,7 +309,7 @@ const allTools = (n, v) => {
|
|
|
300
309
|
{
|
|
301
310
|
name: n.listExtras,
|
|
302
311
|
description: `What one ${v.noun} offers beside the room: airport transfers, dinners, guided experiences, spa and family add-ons, each with its price and whether it is charged per stay, per person or per night, drawn as a card for the guest to look at.`,
|
|
303
|
-
annotations: reads(`${v.brand} add-ons`, open),
|
|
312
|
+
annotations: reads(v.market ? `Add-ons on ${v.brand}` : `${v.brand} add-ons`, open),
|
|
304
313
|
inputSchema: {
|
|
305
314
|
type: 'object',
|
|
306
315
|
properties: {
|
|
@@ -367,10 +376,15 @@ const allTools = (n, v) => {
|
|
|
367
376
|
// ⚠️ A DEMONSTRATION BOOKING IS SAID AS ONE (2026-10-05 audit, P2):
|
|
368
377
|
// "asks Shangri-La to hold the rooms" read as a real hold, and a guest
|
|
369
378
|
// could take the code to the real hotel.
|
|
370
|
-
description: v.
|
|
371
|
-
?
|
|
372
|
-
|
|
373
|
-
|
|
379
|
+
description: v.market
|
|
380
|
+
? // ⚠️ THE GROUP HOLDS THE BOOKING, NOT THE MARKET - and what a booking
|
|
381
|
+
// makes is the group's: a hold, a test booking, or a reservation paid
|
|
382
|
+
// at the hotel. The answer says which, stay by stay.
|
|
383
|
+
`Books the trip's rooms for a named guest with the hotel group each stay belongs to, one group at a time: a trip holding two groups' hotels is booked group by group. What a booking makes is that group's - a demonstration hold that lapses on its own, a test booking in a simulated system, or the hotel's own reservation paid at the hotel - and the answer says which for every stay. No payment is taken here and no card is collected. Preferences - a bed type, a floor, a late arrival - ride along on the booking as requests.`
|
|
384
|
+
: v.demo
|
|
385
|
+
? `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.`
|
|
386
|
+
: `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.`,
|
|
387
|
+
annotations: books(v.market ? `Request a booking on ${v.brand}` : `Request a ${v.brand} booking`, open),
|
|
374
388
|
inputSchema: {
|
|
375
389
|
type: 'object',
|
|
376
390
|
properties: {
|
|
@@ -397,6 +411,16 @@ const allTools = (n, v) => {
|
|
|
397
411
|
type: 'boolean',
|
|
398
412
|
description: 'True when the guest asked for these room preferences to be remembered for their next stays. Needs a signed-in member; what is kept is the lasting kind - a bed type, a floor, a quiet or connecting room; an accessible room is asked for on each stay and is not kept',
|
|
399
413
|
},
|
|
414
|
+
// A booking is made with one group at a time; a trip holding two
|
|
415
|
+
// groups' hotels is booked group by group (Marketplace, §6-1).
|
|
416
|
+
...(v.market
|
|
417
|
+
? {
|
|
418
|
+
group: {
|
|
419
|
+
type: 'string',
|
|
420
|
+
description: "The hotel group to book now, by name, when the trip holds more than one group's hotels. Its stays are booked and the others stay in the trip",
|
|
421
|
+
},
|
|
422
|
+
}
|
|
423
|
+
: {}),
|
|
400
424
|
},
|
|
401
425
|
required: ['name', 'email'],
|
|
402
426
|
},
|
|
@@ -557,7 +581,7 @@ const allTools = (n, v) => {
|
|
|
557
581
|
// model that could send a price could one day send the wrong one, onto a
|
|
558
582
|
// card that looks official.
|
|
559
583
|
description: `Puts two to four ${v.nounPlural} side by side as one card. Each column's price, rate, cancellation terms, breakfast and rooms left come from the ${v.noun}'s own record; the arguments carry only the ids, one line on who each suits, and its pros and cons. With \`checkIn\`/\`checkOut\` the columns carry each ${v.noun}'s cheapest room for those nights; without them, a from-price.`,
|
|
560
|
-
annotations: reads(`Compare ${v
|
|
584
|
+
annotations: reads(`Compare ${catalogue(v)}`, open),
|
|
561
585
|
inputSchema: {
|
|
562
586
|
type: 'object',
|
|
563
587
|
properties: {
|
|
@@ -590,7 +614,7 @@ const allTools = (n, v) => {
|
|
|
590
614
|
{
|
|
591
615
|
name: n.itinerary,
|
|
592
616
|
description: `A day-by-day plan as one card: each day carries its own label, an optional note, the moments of the day in order, and the ${v.nounPlural} and add-ons attached to it, with their details filled in from their own records - a ${v.noun} with its photograph and, given dates, the price of its nights. Each stay is marked with whether it is booked, in the trip or only in the plan, and the card says how many of each. Nothing is added to the trip.`,
|
|
593
|
-
annotations: reads(`Lay out a ${v.brand} itinerary`, open),
|
|
617
|
+
annotations: reads(v.market ? `Lay out an itinerary on ${v.brand}` : `Lay out a ${v.brand} itinerary`, open),
|
|
594
618
|
inputSchema: {
|
|
595
619
|
type: 'object',
|
|
596
620
|
properties: {
|
|
@@ -680,7 +704,7 @@ const allTools = (n, v) => {
|
|
|
680
704
|
{
|
|
681
705
|
name: n.policies,
|
|
682
706
|
description: `What one ${v.noun} publishes about cancellation, check-in and check-out, children and extra beds, pets, deposits and accessibility. With \`topic\` it returns that topic; without, all of them.`,
|
|
683
|
-
annotations: reads(`${v.brand} policies`, open),
|
|
707
|
+
annotations: reads(v.market ? `${upFirst(v.noun)} policies on ${v.brand}` : `${v.brand} policies`, open),
|
|
684
708
|
inputSchema: {
|
|
685
709
|
type: 'object',
|
|
686
710
|
properties: {
|
|
@@ -1046,6 +1070,40 @@ const memberBadge = (deps, key) => {
|
|
|
1046
1070
|
}
|
|
1047
1071
|
: { signedIn: false };
|
|
1048
1072
|
};
|
|
1073
|
+
/** A market's group by its id or its name, as a guest or a model would say it. */
|
|
1074
|
+
const groupNamed = (deps, said) => {
|
|
1075
|
+
const want = said.trim().toLowerCase();
|
|
1076
|
+
return deps.backend
|
|
1077
|
+
.suppliers?.()
|
|
1078
|
+
.find((x) => x.id.toLowerCase() === want || x.name.toLowerCase() === want);
|
|
1079
|
+
};
|
|
1080
|
+
/** The same trip line at a count of nothing - which takes it out (`TripStore.set`). */
|
|
1081
|
+
const unset = (l) => l.kind === 'extra' ? { ...l, quantity: 0 } : { ...l, rooms: 0 };
|
|
1082
|
+
/**
|
|
1083
|
+
* On a group's own connector, the connector itself. In a market, the supplier
|
|
1084
|
+
* named on the booking or the hotel.
|
|
1085
|
+
*
|
|
1086
|
+
* ⚠️ NEVER THE MARKET. "Asks Otaless to hold the rooms" or "Otaless's own
|
|
1087
|
+
* reservation" would send a guest to a desk that has never heard of Otaless,
|
|
1088
|
+
* and a market holding a demonstration beside a group's real system must not
|
|
1089
|
+
* call either by the other's terms (robin, 2026-10-07).
|
|
1090
|
+
*/
|
|
1091
|
+
const holderOf = (deps, supplier) => {
|
|
1092
|
+
const s = supplier ? deps.backend.suppliers?.().find((x) => x.id === supplier) : undefined;
|
|
1093
|
+
if (s)
|
|
1094
|
+
return {
|
|
1095
|
+
name: s.name,
|
|
1096
|
+
caps: s.capabilities,
|
|
1097
|
+
demo: s.mode !== 'live',
|
|
1098
|
+
sandbox: s.mode === 'sandbox',
|
|
1099
|
+
};
|
|
1100
|
+
return {
|
|
1101
|
+
name: deps.vocab.brand,
|
|
1102
|
+
caps: deps.backend.capabilities ?? FULL_CAPABILITIES,
|
|
1103
|
+
demo: deps.vocab.demo,
|
|
1104
|
+
sandbox: !!deps.vocab.sandbox,
|
|
1105
|
+
};
|
|
1106
|
+
};
|
|
1049
1107
|
/**
|
|
1050
1108
|
* What was made, in one sentence - which depends on what asking MAKES here.
|
|
1051
1109
|
*
|
|
@@ -1058,47 +1116,56 @@ const memberBadge = (deps, key) => {
|
|
|
1058
1116
|
*/
|
|
1059
1117
|
const bookedLine = (deps, b) => {
|
|
1060
1118
|
const total = formatMoney(b.quote.total);
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1119
|
+
const h = holderOf(deps, b.supplier);
|
|
1120
|
+
if (h.caps.booking === 'pay-at-hotel') {
|
|
1121
|
+
if (h.sandbox)
|
|
1122
|
+
return `${total} in the simulated system. **This is a test booking** - not a ${h.name} reservation: no room is held and nothing is paid, now or at any hotel.`;
|
|
1064
1123
|
return b.status === 'requested'
|
|
1065
|
-
? `${total} all in, to be paid at the hotel. ${
|
|
1066
|
-
: `${total} all in, paid at the hotel. **Nothing has been charged here** - this is ${
|
|
1124
|
+
? `${total} all in, to be paid at the hotel. ${h.name} has not confirmed this booking yet - that takes up to eight minutes, and ${deps.tools.listBookings} shows when it has. Nothing has been charged.`
|
|
1125
|
+
: `${total} all in, paid at the hotel. **Nothing has been charged here** - this is ${h.name}'s own reservation, kept until it is cancelled, and ${b.guest.name} pays at check-in.`;
|
|
1067
1126
|
}
|
|
1068
1127
|
const lapses = b.holdUntilLocal
|
|
1069
1128
|
? `${b.holdUntilLocal} hotel time`
|
|
1070
1129
|
: `${b.holdUntil?.slice(0, 16).replace('T', ' ')} UTC`;
|
|
1071
1130
|
// ⚠️ NOT "PRESENTS A CARD AT CHECK-IN" (2026-10-05 audit, P2): a demo
|
|
1072
1131
|
// booking has no check-in, and saying so sent a guest towards a real desk.
|
|
1073
|
-
if (
|
|
1074
|
-
return `${total} all in. **${demoBookingWords(
|
|
1132
|
+
if (h.demo)
|
|
1133
|
+
return `${total} all in. **${demoBookingWords(h)}** Nothing has been charged and no card is needed. The demonstration hold lapses on its own at ${lapses}.`;
|
|
1075
1134
|
return `${total} all in. **Nothing has been charged** - the rooms are held until ${lapses}, and ${b.guest.name} presents a card at check-in.`;
|
|
1076
1135
|
};
|
|
1077
1136
|
/** What a demonstration booking is, in one sentence - see `demoBookingNote`. */
|
|
1078
|
-
const demoBookingWords = (
|
|
1137
|
+
const demoBookingWords = (h) => `This is a demonstration booking: ${h.name} has not received it and is not holding any room.`;
|
|
1079
1138
|
/**
|
|
1080
1139
|
* Said on every answer about a demonstration booking - made, changed or
|
|
1081
1140
|
* cancelled - and on nothing else: a hold of the demo's that a guest took for
|
|
1082
1141
|
* the hotel's would send them to a desk with a code nobody there has.
|
|
1083
1142
|
*/
|
|
1084
|
-
const demoBookingNote = (deps) =>
|
|
1085
|
-
|
|
1086
|
-
: '';
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
: deps.
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
:
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1143
|
+
const demoBookingNote = (deps, supplier) => {
|
|
1144
|
+
const h = holderOf(deps, supplier);
|
|
1145
|
+
return h.demo && h.caps.booking === 'hold' ? demoBookingWords(h) : '';
|
|
1146
|
+
};
|
|
1147
|
+
const common = (deps, key) => {
|
|
1148
|
+
const caps = deps.backend.capabilities ?? FULL_CAPABILITIES;
|
|
1149
|
+
return {
|
|
1150
|
+
brand: deps.vocab.brand,
|
|
1151
|
+
...(deps.logoUrl ? { logoUrl: deps.logoUrl } : {}),
|
|
1152
|
+
toolNames: deps.offered
|
|
1153
|
+
? Object.fromEntries(Object.entries(deps.tools).filter(([, name]) => deps.offered?.has(name)))
|
|
1154
|
+
: deps.tools,
|
|
1155
|
+
bookingMode: caps.booking,
|
|
1156
|
+
...(caps.requiresPhone ? { phoneRequired: true } : {}),
|
|
1157
|
+
...(deps.vocab.sandbox ? { sandbox: true } : {}),
|
|
1158
|
+
...(caps.programme ? { programme: caps.programme } : {}),
|
|
1159
|
+
...(caps.codePrefix ? { codePrefix: caps.codePrefix } : {}),
|
|
1160
|
+
// A market: the card draws each hotel and booking as its group's.
|
|
1161
|
+
...(deps.backend.suppliers
|
|
1162
|
+
? { market: { name: deps.vocab.brand, suppliers: deps.backend.suppliers() } }
|
|
1163
|
+
: {}),
|
|
1164
|
+
vocab: { noun: deps.vocab.noun, nounPlural: deps.vocab.nounPlural },
|
|
1165
|
+
...(deps.vocab.demo ? { demo: true, demoNote: DEMO_NOTE } : { demo: false }),
|
|
1166
|
+
...(key ? memberBadge(deps, key) : {}),
|
|
1167
|
+
};
|
|
1168
|
+
};
|
|
1102
1169
|
/**
|
|
1103
1170
|
* When a room stops being free to cancel, the way a guest reads it: the
|
|
1104
1171
|
* hotel's own clock when the backend knows it, else the instant in UTC - never
|
|
@@ -1107,10 +1174,12 @@ const common = (deps, key) => ({
|
|
|
1107
1174
|
const freeUntilText = (r) => r.freeCancellationLocal ??
|
|
1108
1175
|
`${(r.freeCancellationUntil ?? '').slice(0, 16).replace('T', ' ')} UTC`;
|
|
1109
1176
|
/** One line of a trip, flattened for the card. Prices are strings: the card never does arithmetic. */
|
|
1110
|
-
const quoteForCard = (quote) => ({
|
|
1177
|
+
const quoteForCard = (quote, groupOf) => ({
|
|
1111
1178
|
lines: quote.lines.map((l) => ({
|
|
1112
1179
|
kind: l.kind,
|
|
1113
1180
|
hotelId: l.hotelId,
|
|
1181
|
+
// In a market: whose stay this is, so the card books it with that group.
|
|
1182
|
+
...(groupOf?.(l.hotelId) ? { supplier: groupOf(l.hotelId) } : {}),
|
|
1114
1183
|
hotelName: l.hotelName,
|
|
1115
1184
|
roomRef: l.roomRef,
|
|
1116
1185
|
extraRef: l.extraRef,
|
|
@@ -1145,7 +1214,7 @@ const tripText = (quote, demo) => {
|
|
|
1145
1214
|
if (quote.lines.length === 0)
|
|
1146
1215
|
return 'The trip is empty.';
|
|
1147
1216
|
const lines = quote.lines.map((l) => l.kind === 'room'
|
|
1148
|
-
? `- ${l.quantity ?? 1}× ${l.title} at ${l.hotelName}, ${l.stay?.checkIn} to ${l.stay?.checkOut} (${l.nights} nights) — ${formatMoney(l.total)}`
|
|
1217
|
+
? `- ${l.quantity ?? 1}× ${l.title} at ${l.hotelName}, ${l.stay?.checkIn} to ${l.stay?.checkOut} (${l.nights} ${l.nights === 1 ? 'night' : 'nights'}) — ${formatMoney(l.total)}`
|
|
1149
1218
|
: `- ${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)}`);
|
|
1150
1219
|
return [
|
|
1151
1220
|
...lines,
|
|
@@ -1490,6 +1559,8 @@ const remember = (deps, k, amount) => {
|
|
|
1490
1559
|
m.delete(oldest);
|
|
1491
1560
|
}
|
|
1492
1561
|
};
|
|
1562
|
+
/** "a, b and c" - "a and b" - "a". */
|
|
1563
|
+
const listAnd = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(', ')} and ${xs.at(-1)}` : (xs[0] ?? '');
|
|
1493
1564
|
/** The counts a call can carry, and what each may be. */
|
|
1494
1565
|
const COUNTS = ['guests', 'rooms', 'quantity'];
|
|
1495
1566
|
/** "2" is 2: a host that sends a count as a string has not asked for something else. */
|
|
@@ -1639,12 +1710,15 @@ const currentTrip = async (deps, key, who) => {
|
|
|
1639
1710
|
const [only] = parts;
|
|
1640
1711
|
const orphans = needsRoomIn(deps.tools, lines, parts);
|
|
1641
1712
|
const needs = orphans.needsRoom.length ? { needsRoom: orphans.needsRoom } : {};
|
|
1713
|
+
const groupOf = deps.backend.supplierOf
|
|
1714
|
+
? (id) => deps.backend.supplierOf?.(id)
|
|
1715
|
+
: undefined;
|
|
1642
1716
|
if (parts.length === 1 && only) {
|
|
1643
1717
|
return {
|
|
1644
1718
|
structured: {
|
|
1645
1719
|
view: 'trip',
|
|
1646
1720
|
...common(deps, key),
|
|
1647
|
-
...quoteForCard(only),
|
|
1721
|
+
...quoteForCard(only, groupOf),
|
|
1648
1722
|
...needs,
|
|
1649
1723
|
count: lines.length,
|
|
1650
1724
|
},
|
|
@@ -1653,7 +1727,7 @@ const currentTrip = async (deps, key, who) => {
|
|
|
1653
1727
|
.join('\n'),
|
|
1654
1728
|
};
|
|
1655
1729
|
}
|
|
1656
|
-
const shaped = parts.map(quoteForCard);
|
|
1730
|
+
const shaped = parts.map((p) => quoteForCard(p, groupOf));
|
|
1657
1731
|
const bookable = parts.map((p) => p.lines.some((l) => l.kind === 'room'));
|
|
1658
1732
|
return {
|
|
1659
1733
|
structured: {
|
|
@@ -1703,6 +1777,7 @@ const filtersOf = (args) => {
|
|
|
1703
1777
|
: [];
|
|
1704
1778
|
const f = {
|
|
1705
1779
|
...(str(args.brand) ? { brand: str(args.brand) } : {}),
|
|
1780
|
+
...(str(args.group) ? { supplier: str(args.group) } : {}),
|
|
1706
1781
|
...(facilities.length ? { facilities } : {}),
|
|
1707
1782
|
...(args.freeCancellation === true ? { freeCancellation: true } : {}),
|
|
1708
1783
|
...(args.breakfast === true ? { breakfast: true } : {}),
|
|
@@ -1746,7 +1821,16 @@ const refineFor = (r, on, total,
|
|
|
1746
1821
|
dated = false) => {
|
|
1747
1822
|
const narrows = (count, active) => active || (count > 0 && count < total);
|
|
1748
1823
|
const brands = r.brands.filter((b) => narrows(b.count, b.name === on?.brand));
|
|
1824
|
+
// In a market: the groups, by id - the card names them from `market`.
|
|
1825
|
+
const groups = (r.suppliers ?? []).filter((x) => narrows(x.count, x.id === on?.supplier));
|
|
1749
1826
|
return {
|
|
1827
|
+
...(r.suppliers
|
|
1828
|
+
? {
|
|
1829
|
+
groups: groups.length > 1 || on?.supplier
|
|
1830
|
+
? groups.map((x) => ({ ...x, on: x.id === on?.supplier }))
|
|
1831
|
+
: [],
|
|
1832
|
+
}
|
|
1833
|
+
: {}),
|
|
1750
1834
|
brands: brands.length > 1 || on?.brand ? brands.map((b) => ({ ...b, on: b.name === on?.brand })) : [],
|
|
1751
1835
|
facilities: r.facilities
|
|
1752
1836
|
.filter((x) => narrows(x.count, !!on?.facilities?.includes(x.key)))
|
|
@@ -1943,6 +2027,10 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
1943
2027
|
: undefined,
|
|
1944
2028
|
filters: filtersOf(args),
|
|
1945
2029
|
};
|
|
2030
|
+
// A group as a guest names it, to the id the market files it under.
|
|
2031
|
+
const named = ask.filters?.supplier ? groupNamed(deps, ask.filters.supplier) : undefined;
|
|
2032
|
+
if (ask.filters?.supplier && named)
|
|
2033
|
+
ask.filters = { ...ask.filters, supplier: named.id };
|
|
1946
2034
|
const who = identity(deps, ctx, key, args);
|
|
1947
2035
|
let answer = await deps.backend.searchHotels(ask, who);
|
|
1948
2036
|
// A brand as the data spells it, so its chip shows as ON and can be turned
|
|
@@ -2108,7 +2196,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2108
2196
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
2109
2197
|
...(today ? { today } : {}),
|
|
2110
2198
|
},
|
|
2111
|
-
text: `${total} ${deps.vocab
|
|
2199
|
+
text: `${total} ${branded(deps.vocab, n)} in ${only?.name}, in ${only?.cities.length} cities to choose from: ${every(only?.cities ?? [])}. A city lists its ${n}; \`all\` lists every one in ${only?.name}.`,
|
|
2112
2200
|
};
|
|
2113
2201
|
}
|
|
2114
2202
|
const regions = [
|
|
@@ -2132,7 +2220,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2132
2220
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
2133
2221
|
...(today ? { today } : {}),
|
|
2134
2222
|
},
|
|
2135
|
-
text: `${total} ${deps.vocab
|
|
2223
|
+
text: `${total} ${branded(deps.vocab, n)} in ${facets.cities.length} cities across ${countries.length} countries, to choose from by region, country and city - ${regions.map((r) => `${r.name}: ${every(r.countries)}`).join('; ')}. A city or a country lists its ${n}.`,
|
|
2136
2224
|
};
|
|
2137
2225
|
}
|
|
2138
2226
|
if (hotels.length === 0) {
|
|
@@ -2166,7 +2254,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2166
2254
|
: ceilingEmpty
|
|
2167
2255
|
? `None of the ${answer.beforeCeiling} that match has a room for those nights.`
|
|
2168
2256
|
: [
|
|
2169
|
-
`No ${deps.vocab
|
|
2257
|
+
`No ${branded(deps.vocab, deps.vocab.nounPlural)} match that.`,
|
|
2170
2258
|
// What was tried and also failed is worth a sentence: it tells a
|
|
2171
2259
|
// guest the word itself is the problem, not the narrowing.
|
|
2172
2260
|
widened ? `${sentence(widened.to)} found nothing either.` : '',
|
|
@@ -2278,7 +2366,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2278
2366
|
...(answer.currencies.length > 1 ? { currencies: answer.currencies } : {}),
|
|
2279
2367
|
},
|
|
2280
2368
|
text: [
|
|
2281
|
-
`${total} ${deps.vocab
|
|
2369
|
+
`${total} ${branded(deps.vocab, total === 1 ? deps.vocab.noun : deps.vocab.nounPlural)}${where}.`,
|
|
2282
2370
|
// ⚠️ NO "BELOW", NO "TAP". This sentence is read out by a voice
|
|
2283
2371
|
// assistant as often as it is drawn next to a card, and a guest whose
|
|
2284
2372
|
// host has no card at all cannot tap the one it tells them to tap. The
|
|
@@ -2301,7 +2389,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2301
2389
|
if (!detail) {
|
|
2302
2390
|
return {
|
|
2303
2391
|
structured: { view: 'hotel-not-found', ...common(deps, key), query: wanted },
|
|
2304
|
-
text: `No ${deps.vocab
|
|
2392
|
+
text: `No ${branded(deps.vocab, deps.vocab.noun)} by that name. ${t.listHotels} lists them.`,
|
|
2305
2393
|
};
|
|
2306
2394
|
}
|
|
2307
2395
|
const h = detail.hotel;
|
|
@@ -2458,9 +2546,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2458
2546
|
...(dropped.length ? { dropped } : {}),
|
|
2459
2547
|
},
|
|
2460
2548
|
text: city
|
|
2461
|
-
? `No ${deps.vocab
|
|
2549
|
+
? `No ${branded(deps.vocab, deps.vocab.nounPlural)} in "${city}". ${t.listHotels} with no place shows the places there are.`
|
|
2462
2550
|
: named.length
|
|
2463
|
-
? `No ${deps.vocab
|
|
2551
|
+
? `No ${branded(deps.vocab, deps.vocab.noun)} answers to ${dropped.map((d) => `"${d}"`).join(', ')}.`
|
|
2464
2552
|
: `Name a \`city\`, or up to ${LOOK_UP_MAX} ${deps.vocab.nounPlural} in \`${deps.vocab.nounPlural}\`.`,
|
|
2465
2553
|
};
|
|
2466
2554
|
}
|
|
@@ -2520,7 +2608,14 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2520
2608
|
total > read.length
|
|
2521
2609
|
? `${total - read.length} more ${total - read.length === 1 ? 'is' : 'are'} there; name ${total - read.length === 1 ? 'it' : 'them'} in \`${deps.vocab.nounPlural}\` to read ${total - read.length === 1 ? 'it' : 'them'}.`
|
|
2522
2610
|
: '',
|
|
2523
|
-
|
|
2611
|
+
// ⚠️ WHAT THE IDS ARE FOR, SAID AS A FACT (2026-10-05 audit, P3): this was
|
|
2612
|
+
// "Draw the answer now as ONE card", an instruction in a tool result.
|
|
2613
|
+
// The rule itself lives in the server instructions; what stays here is
|
|
2614
|
+
// what each next call would draw, which is what kept it to one card.
|
|
2615
|
+
`These ids are what ${t.itinerary} (a plan or days - 行程, recommended or arranged) and ${t.compare} (which ${deps.vocab.noun} to stay at) take; each of those draws one card. ${listAnd(
|
|
2616
|
+
// Only tools this deployment offers: a name it withholds is one the
|
|
2617
|
+
// model would try (2026-10-05 audit, P2 - Huazhu has no add-ons).
|
|
2618
|
+
[t.listHotels, t.getHotel, t.listExtras].filter((n) => !deps.offered || deps.offered.has(n)))} would each draw a card of their own.`,
|
|
2524
2619
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2525
2620
|
]
|
|
2526
2621
|
.filter(Boolean)
|
|
@@ -2569,7 +2664,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2569
2664
|
if (!detail) {
|
|
2570
2665
|
return {
|
|
2571
2666
|
structured: { view: 'hotel-not-found', ...common(deps, key), query: wanted },
|
|
2572
|
-
text: `No ${deps.vocab
|
|
2667
|
+
text: `No ${branded(deps.vocab, deps.vocab.noun)} by that name.`,
|
|
2573
2668
|
};
|
|
2574
2669
|
}
|
|
2575
2670
|
const extras = await deps.backend.extrasFor(detail.hotel.id, booked?.stay ?? asStay(args));
|
|
@@ -2613,7 +2708,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2613
2708
|
error: 'hotel-not-found',
|
|
2614
2709
|
query: wanted,
|
|
2615
2710
|
},
|
|
2616
|
-
text: `No ${deps.vocab
|
|
2711
|
+
text: `No ${branded(deps.vocab, deps.vocab.noun)} by that name.`,
|
|
2617
2712
|
};
|
|
2618
2713
|
}
|
|
2619
2714
|
const hotelId = detail.hotel.id;
|
|
@@ -2629,9 +2724,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2629
2724
|
error: 'no-such-extra',
|
|
2630
2725
|
query: extraRef,
|
|
2631
2726
|
},
|
|
2632
|
-
text: (deps.
|
|
2727
|
+
text: holderOf(deps, detail.hotel.supplier).caps.extras
|
|
2633
2728
|
? `${detail.hotel.name} does not offer that. ${t.listExtras} lists what it has.`
|
|
2634
|
-
: `${deps.
|
|
2729
|
+
: `${holderOf(deps, detail.hotel.supplier).name} ${deps.vocab.nounPlural} sell rooms only here - there are no add-ons.`,
|
|
2635
2730
|
};
|
|
2636
2731
|
}
|
|
2637
2732
|
const line = {
|
|
@@ -2714,17 +2809,39 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2714
2809
|
};
|
|
2715
2810
|
}
|
|
2716
2811
|
if (name === t.requestBooking) {
|
|
2717
|
-
const
|
|
2718
|
-
if (
|
|
2812
|
+
const inTrip = deps.trips.read(key);
|
|
2813
|
+
if (inTrip.length === 0) {
|
|
2719
2814
|
return {
|
|
2720
2815
|
structured: { view: 'empty-trip', ...common(deps, key) },
|
|
2721
2816
|
text: `There is nothing in the trip yet. ${t.addToTrip} puts room nights in it.`,
|
|
2722
2817
|
};
|
|
2723
2818
|
}
|
|
2819
|
+
// ⚠️ ONE GROUP AT A TIME, in a market: a booking cannot be all-or-nothing
|
|
2820
|
+
// across two groups' systems (Marketplace, §6-1). `group` books one
|
|
2821
|
+
// group's stays and leaves the rest in the trip.
|
|
2822
|
+
const asked = deps.backend.suppliers ? str(args.group) : '';
|
|
2823
|
+
const group = asked ? groupNamed(deps, asked) : undefined;
|
|
2824
|
+
const groupOf = (l) => deps.backend.supplierOf?.(l.hotelId);
|
|
2825
|
+
const lines = group ? inTrip.filter((l) => groupOf(l) === group.id) : inTrip;
|
|
2826
|
+
if (asked && (!group || lines.length === 0)) {
|
|
2827
|
+
const there = [...new Set(inTrip.map(groupOf))]
|
|
2828
|
+
.map((id) => holderOf(deps, id).name)
|
|
2829
|
+
.filter(Boolean);
|
|
2830
|
+
const words = group
|
|
2831
|
+
? `Nothing in the trip is with ${group.name}. It holds stays with ${listAnd(there)}.`
|
|
2832
|
+
: `No hotel group here is called "${asked}". The trip holds stays with ${listAnd(there)}.`;
|
|
2833
|
+
return {
|
|
2834
|
+
structured: { view: 'error', ...common(deps, key), error: 'no-such-group', detail: words },
|
|
2835
|
+
text: words,
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2724
2838
|
const guest = { name: str(args.name), email: str(args.email) };
|
|
2725
2839
|
// A phone where the backend needs one: a booking paid at the hotel and
|
|
2726
2840
|
// guaranteed by nobody, which the hotel has to be able to follow up.
|
|
2727
|
-
|
|
2841
|
+
// In a market, the trip's own groups decide - not whichever group needs most.
|
|
2842
|
+
const phoneNeeded = deps.backend.suppliers
|
|
2843
|
+
? lines.some((l) => !!holderOf(deps, deps.backend.supplierOf?.(l.hotelId)).caps.requiresPhone)
|
|
2844
|
+
: !!(deps.backend.capabilities ?? FULL_CAPABILITIES).requiresPhone;
|
|
2728
2845
|
const phone = str(args.phone);
|
|
2729
2846
|
const missing = [
|
|
2730
2847
|
!guest.name && 'a name',
|
|
@@ -2759,7 +2876,16 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2759
2876
|
// only place an Identity is built; the guest's email still overrides,
|
|
2760
2877
|
// because a booking is found by the address it was made with.
|
|
2761
2878
|
{ ...identity(deps, ctx, key), email: guest.email }, requests);
|
|
2762
|
-
|
|
2879
|
+
if (lines.length === inTrip.length)
|
|
2880
|
+
deps.trips.clear(key);
|
|
2881
|
+
else
|
|
2882
|
+
for (const l of lines)
|
|
2883
|
+
deps.trips.set(key, unset(l));
|
|
2884
|
+
// What is left is priced again, and that price is what the next
|
|
2885
|
+
// group's booking is held to (ShownQuote).
|
|
2886
|
+
const rest = lines.length === inTrip.length
|
|
2887
|
+
? undefined
|
|
2888
|
+
: await currentTrip(deps, key, identity(deps, ctx, key));
|
|
2763
2889
|
// ⚠️ REMEMBERED ONLY WHEN ASKED - see `rememberPreferences`. Said either
|
|
2764
2890
|
// way, so neither the guest nor the model assumes something was kept.
|
|
2765
2891
|
const signedIn = deps.members.read(key);
|
|
@@ -2844,7 +2970,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2844
2970
|
},
|
|
2845
2971
|
text: [
|
|
2846
2972
|
several
|
|
2847
|
-
? `${booked.length} bookings, one for each stay - each hotel keeps its own nights under its own code, and each is ${(deps.
|
|
2973
|
+
? `${booked.length} bookings, one for each stay - each hotel keeps its own nights under its own code, and each is ${holderOf(deps, first.booking.supplier).caps.changes ? 'changed or cancelled' : 'cancelled'} on its own.`
|
|
2848
2974
|
: '',
|
|
2849
2975
|
...booked.flatMap(({ booking, asked }) => [
|
|
2850
2976
|
`**${booking.confirmationCode}** · ${booking.hotelName} · ${booking.stay.checkIn} to ${booking.stay.checkOut}`,
|
|
@@ -2852,6 +2978,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2852
2978
|
...asked,
|
|
2853
2979
|
]),
|
|
2854
2980
|
remembered?.line ?? '',
|
|
2981
|
+
rest ? `The trip's other stays are still in it, not booked:\n${rest.text}` : '',
|
|
2855
2982
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2856
2983
|
]
|
|
2857
2984
|
.filter(Boolean)
|
|
@@ -2867,6 +2994,17 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2867
2994
|
const again = await currentTrip(deps, key, identity(deps, ctx, key));
|
|
2868
2995
|
return { ...again, text: [words, again.text].join('\n\n') };
|
|
2869
2996
|
}
|
|
2997
|
+
if (err.code === 'mixed-supplier') {
|
|
2998
|
+
const again = await currentTrip(deps, key, identity(deps, ctx, key));
|
|
2999
|
+
return {
|
|
3000
|
+
...again,
|
|
3001
|
+
text: [
|
|
3002
|
+
words,
|
|
3003
|
+
`${t.requestBooking} with \`group\` books one group's stays and leaves the others in the trip.`,
|
|
3004
|
+
again.text,
|
|
3005
|
+
].join('\n\n'),
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
2870
3008
|
return {
|
|
2871
3009
|
structured: {
|
|
2872
3010
|
view: 'error',
|
|
@@ -2983,7 +3121,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
2983
3121
|
: 'The total is unchanged.',
|
|
2984
3122
|
preview.rule,
|
|
2985
3123
|
`Say the word and it moves (${t.changeBooking} with \`confirmed\`).`,
|
|
2986
|
-
demoBookingNote(deps),
|
|
3124
|
+
demoBookingNote(deps, preview.supplier),
|
|
2987
3125
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
2988
3126
|
]
|
|
2989
3127
|
.filter(Boolean)
|
|
@@ -3046,7 +3184,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3046
3184
|
text: [
|
|
3047
3185
|
`${changed.confirmationCode} now holds ${changed.stay.checkIn} to ${changed.stay.checkOut}, ${formatMoney(changed.quote.total)} all in.`,
|
|
3048
3186
|
'The confirmation code has not changed. **Nothing has been charged.**',
|
|
3049
|
-
demoBookingNote(deps),
|
|
3187
|
+
demoBookingNote(deps, changed.supplier),
|
|
3050
3188
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
3051
3189
|
]
|
|
3052
3190
|
.filter(Boolean)
|
|
@@ -3094,7 +3232,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3094
3232
|
preview.cancellable
|
|
3095
3233
|
? `Say the word and it is cancelled (${t.cancelBooking} with \`confirmed\`).`
|
|
3096
3234
|
: '',
|
|
3097
|
-
demoBookingNote(deps),
|
|
3235
|
+
demoBookingNote(deps, preview.supplier),
|
|
3098
3236
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
3099
3237
|
]
|
|
3100
3238
|
.filter(Boolean)
|
|
@@ -3130,6 +3268,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3130
3268
|
// the rate's terms, and where the two differ the hotel's is the one that
|
|
3131
3269
|
// will be charged.
|
|
3132
3270
|
const charged = cancelled.cancellationFee ?? preview.fee;
|
|
3271
|
+
const h = holderOf(deps, cancelled.supplier);
|
|
3133
3272
|
return {
|
|
3134
3273
|
structured: {
|
|
3135
3274
|
view: 'cancelled',
|
|
@@ -3140,14 +3279,14 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3140
3279
|
cancellable: false,
|
|
3141
3280
|
},
|
|
3142
3281
|
text: [
|
|
3143
|
-
|
|
3282
|
+
h.sandbox
|
|
3144
3283
|
? // A test booking cancelled in the simulation: no hotel, no charge (2026-10-05 audit, P2).
|
|
3145
3284
|
`${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.`}`
|
|
3146
|
-
:
|
|
3285
|
+
: h.demo
|
|
3147
3286
|
? // Nobody charges a demonstration anything (2026-10-05 audit, P2).
|
|
3148
3287
|
`${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.`}`
|
|
3149
3288
|
: `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `The hotel charges ${formatMoney(charged)}.`}`,
|
|
3150
|
-
demoBookingNote(deps),
|
|
3289
|
+
demoBookingNote(deps, cancelled.supplier),
|
|
3151
3290
|
deps.vocab.demo ? DEMO_NOTE : '',
|
|
3152
3291
|
]
|
|
3153
3292
|
.filter(Boolean)
|
|
@@ -3176,7 +3315,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3176
3315
|
...common(deps, key),
|
|
3177
3316
|
dropped: comparison.dropped,
|
|
3178
3317
|
},
|
|
3179
|
-
text: `None of those are ${deps.vocab
|
|
3318
|
+
text: `None of those are ${catalogue(deps.vocab)}: ${comparison.dropped.map((d) => d.name).join(', ')}.`,
|
|
3180
3319
|
};
|
|
3181
3320
|
}
|
|
3182
3321
|
return {
|
|
@@ -3332,7 +3471,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3332
3471
|
if (!detail) {
|
|
3333
3472
|
return {
|
|
3334
3473
|
structured: { view: 'hotel-not-found', ...common(deps, key), query: wanted },
|
|
3335
|
-
text: `No ${deps.vocab
|
|
3474
|
+
text: `No ${branded(deps.vocab, deps.vocab.noun)} by that name.`,
|
|
3336
3475
|
};
|
|
3337
3476
|
}
|
|
3338
3477
|
const policies = await deps.backend.policies(detail.hotel.id);
|