@curless/hotel-storefront 0.45.2 → 0.47.0
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/card.generated.d.ts +1 -1
- package/dist/card.generated.js +2 -2
- package/dist/card.generated.js.map +1 -1
- package/dist/data.d.ts +2 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +281 -19
- package/dist/data.js.map +1 -1
- package/dist/demo-hotels.d.ts +46 -1
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +50 -12
- package/dist/demo-hotels.js.map +1 -1
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +9 -5
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/huazhu/catalog.d.ts +9 -2
- package/dist/huazhu/catalog.d.ts.map +1 -1
- package/dist/huazhu/catalog.js +65 -5
- package/dist/huazhu/catalog.js.map +1 -1
- package/dist/huazhu/english.d.ts +8 -0
- package/dist/huazhu/english.d.ts.map +1 -1
- package/dist/huazhu/english.js +295 -2
- package/dist/huazhu/english.js.map +1 -1
- package/dist/room-filters.d.ts +106 -0
- package/dist/room-filters.d.ts.map +1 -0
- package/dist/room-filters.js +252 -0
- package/dist/room-filters.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/dist/supplier.d.ts +1 -0
- package/dist/supplier.d.ts.map +1 -1
- package/dist/supplier.js +4 -1
- package/dist/supplier.js.map +1 -1
- package/dist/tools.d.ts +33 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +243 -9
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +44 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -7,6 +7,7 @@ import { amountsIn, formatMoney, keepingAmounts, money, multiplyMoney, toMinor }
|
|
|
7
7
|
import { roomNamesOf, withTraditional } from './names.js';
|
|
8
8
|
import { MAX_COMPARED, MAX_CONS, MAX_DAYS, MAX_ITEMS_PER_DAY, MAX_MOMENTS, MAX_PROS, buildItinerary, candidatesFor, compareHotels, namesMoreThanOne, } from './present.js';
|
|
9
9
|
import { regionOf } from './regions.js';
|
|
10
|
+
import { activeKeysOf, byRoomType, facetsOf, keyWords, matchesTerms, optionsOf, roomTermsOf, termWords, } from './room-filters.js';
|
|
10
11
|
import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, clampCount, shownOf, tripKey, } from './trip.js';
|
|
11
12
|
import { DEMO_NOTE, branded, catalogue, noteFor, upFirst } from './vocab.js';
|
|
12
13
|
// The eleven things a guest can do: find hotels, look at one on dates, see what
|
|
@@ -259,7 +260,7 @@ const allTools = (n, v) => {
|
|
|
259
260
|
return [
|
|
260
261
|
{
|
|
261
262
|
name: n.listHotels,
|
|
262
|
-
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 \`
|
|
263
|
+
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\`, \`breakfast\` and \`bed\` 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}` : ''}`,
|
|
263
264
|
annotations: reads(`Browse ${catalogue(v)}`, open),
|
|
264
265
|
inputSchema: {
|
|
265
266
|
type: 'object',
|
|
@@ -324,6 +325,11 @@ const allTools = (n, v) => {
|
|
|
324
325
|
type: 'boolean',
|
|
325
326
|
description: `Only ${v.nounPlural} with a room that includes breakfast - and, with dates, that room is the one priced`,
|
|
326
327
|
},
|
|
328
|
+
bed: {
|
|
329
|
+
type: 'string',
|
|
330
|
+
enum: ['king', 'twin'],
|
|
331
|
+
description: `Only ${v.nounPlural} with a room that has this bed - \`king\` is one large bed (king, queen or double), \`twin\` is two beds - and, with dates, that room is the one priced. Given with \`freeCancellation\` or \`breakfast\`, one room must have all of them`,
|
|
332
|
+
},
|
|
327
333
|
all: {
|
|
328
334
|
type: 'boolean',
|
|
329
335
|
description: `Unnarrowed, or narrowed to a country of many cities, the answer is the places to choose from; true lists the ${v.nounPlural} themselves`,
|
|
@@ -333,7 +339,7 @@ const allTools = (n, v) => {
|
|
|
333
339
|
},
|
|
334
340
|
{
|
|
335
341
|
name: n.getHotel,
|
|
336
|
-
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,
|
|
342
|
+
description: `One ${v.noun} in full: description, amenities, address, and check-in and check-out times where the ${v.noun} publishes them. Given \`checkIn\` and \`checkOut\` it also returns the room types that can be booked for those nights, one line per type at its lowest rate: the nightly rate, the stay total including taxes and fees, whether it can be cancelled free and until when, and how many rates it has. \`rooms\` leaves out the types that have fewer than that many left. \`bed\`, \`breakfast\`, \`freeCancellation\`, \`window\`, \`payment\` and \`family\` keep only the rates that meet all of those given; the card still holds every room, with the given filters selected. Without dates it returns the ${v.noun} only, because a room has no price until the nights are known. The ${v.noun} is drawn as a card for the guest to look at.${v.hints.hotel ? ` ${v.hints.hotel}` : ''}`,
|
|
337
343
|
annotations: reads(`View one ${catalogue(v, true)}`, open),
|
|
338
344
|
inputSchema: {
|
|
339
345
|
type: 'object',
|
|
@@ -349,6 +355,32 @@ const allTools = (n, v) => {
|
|
|
349
355
|
maximum: MAX_ROOMS_PER_LINE,
|
|
350
356
|
description: `How many rooms of one type, 1 to ${MAX_ROOMS_PER_LINE}. Default 1`,
|
|
351
357
|
},
|
|
358
|
+
bed: {
|
|
359
|
+
type: 'string',
|
|
360
|
+
enum: ['king', 'twin'],
|
|
361
|
+
description: 'With dates: only rooms with this bed - `king` is one large bed (king, queen or double), `twin` is two beds. A room sold as king or twin has both',
|
|
362
|
+
},
|
|
363
|
+
breakfast: {
|
|
364
|
+
type: 'boolean',
|
|
365
|
+
description: 'With dates: only rooms whose rate includes breakfast',
|
|
366
|
+
},
|
|
367
|
+
freeCancellation: {
|
|
368
|
+
type: 'boolean',
|
|
369
|
+
description: 'With dates: only rooms that can be cancelled free',
|
|
370
|
+
},
|
|
371
|
+
window: {
|
|
372
|
+
type: 'boolean',
|
|
373
|
+
description: 'With dates: only rooms with a window (not a partial or an inner one)',
|
|
374
|
+
},
|
|
375
|
+
payment: {
|
|
376
|
+
type: 'string',
|
|
377
|
+
enum: ['at-hotel', 'online'],
|
|
378
|
+
description: 'With dates: only rooms paid at the hotel (`at-hotel`) or in advance online (`online`)',
|
|
379
|
+
},
|
|
380
|
+
family: {
|
|
381
|
+
type: 'boolean',
|
|
382
|
+
description: 'With dates: only rooms that sleep 3 or more',
|
|
383
|
+
},
|
|
352
384
|
},
|
|
353
385
|
required: ['hotel'],
|
|
354
386
|
},
|
|
@@ -1029,6 +1061,24 @@ const HOTEL_PAGE = {
|
|
|
1029
1061
|
policies: ARR,
|
|
1030
1062
|
/** Arrivals a few days either side, each re-quoted for the same nights - what the date picker prices its days from. */
|
|
1031
1063
|
dates: ARR,
|
|
1064
|
+
/**
|
|
1065
|
+
* The room filters this hotel's rooms offer, each with how many rooms (rates)
|
|
1066
|
+
* it keeps - `{ key, count }`, `key` one of `breakfast`, `king`, `twin`,
|
|
1067
|
+
* `freeCancellation` (always here, `count` 0 where no room has it) and
|
|
1068
|
+
* `window`, `payAtHotel`, `payOnline`, `family` (here only where some rooms
|
|
1069
|
+
* have it and some do not).
|
|
1070
|
+
*/
|
|
1071
|
+
roomFilters: ARR,
|
|
1072
|
+
/** The filters the call asked for, as the same keys - the card opens with these selected. */
|
|
1073
|
+
activeFilters: STRS,
|
|
1074
|
+
/**
|
|
1075
|
+
* Room types with no room to book for these nights (none left, or fewer
|
|
1076
|
+
* than the rooms asked for): `{ id, name, names?, sleeps, ..., left? }`, no
|
|
1077
|
+
* price. The card draws them muted at the end of the list, so a type the
|
|
1078
|
+
* guest saw is said to be sold out and not left out. Absent where there are
|
|
1079
|
+
* none - and never in what the assistant is told.
|
|
1080
|
+
*/
|
|
1081
|
+
soldOut: ARR,
|
|
1032
1082
|
};
|
|
1033
1083
|
export const buildOutputSchemas = (n) => ({
|
|
1034
1084
|
[n.listHotels]: {
|
|
@@ -1146,6 +1196,8 @@ export const buildOutputSchemas = (n) => ({
|
|
|
1146
1196
|
accessibility: BOOL,
|
|
1147
1197
|
/** One per stay, when the trip held several. */
|
|
1148
1198
|
bookings: ARR,
|
|
1199
|
+
/** What all of `bookings` come to, where they are in one currency (the card's first screen). */
|
|
1200
|
+
totalAll: STR,
|
|
1149
1201
|
/**
|
|
1150
1202
|
* The stays that could not be booked when the others were - each with
|
|
1151
1203
|
* why, still in the trip, and what could be booked instead (robin,
|
|
@@ -1965,6 +2017,107 @@ const labelWords = (r) => {
|
|
|
1965
2017
|
];
|
|
1966
2018
|
return words.length ? ` [${words.join(', ')}]` : '';
|
|
1967
2019
|
};
|
|
2020
|
+
/** What a rate says about cancelling, as the text puts it. */
|
|
2021
|
+
const cancelWords = (r) => r.freeCancellationUntil
|
|
2022
|
+
? `free to cancel until ${freeUntilText(r)}`
|
|
2023
|
+
: r.refundable
|
|
2024
|
+
? 'the free cancellation window has closed - cancelling now has a fee'
|
|
2025
|
+
: 'non-refundable';
|
|
2026
|
+
/**
|
|
2027
|
+
* What a room is, in a parenthesis: its beds, how many it sleeps and - for one
|
|
2028
|
+
* rate, not a type whose rates differ - breakfast, unless the rate's labels
|
|
2029
|
+
* already say it.
|
|
2030
|
+
*/
|
|
2031
|
+
const roomFactsWords = (r, rate = false) => {
|
|
2032
|
+
const facts = [
|
|
2033
|
+
r.beds,
|
|
2034
|
+
`sleeps ${r.sleeps}`,
|
|
2035
|
+
rate && facetsOf(r).breakfast && !(r.labels ?? []).some((l) => l.startsWith('breakfast'))
|
|
2036
|
+
? 'breakfast included'
|
|
2037
|
+
: '',
|
|
2038
|
+
]
|
|
2039
|
+
.filter(Boolean)
|
|
2040
|
+
.join(', ');
|
|
2041
|
+
return ` (${facts})`;
|
|
2042
|
+
};
|
|
2043
|
+
/** The cheapest price a rate quotes, as the text says it: "from" a night, or the hours of a block. */
|
|
2044
|
+
const fromWords = (r) => r.hourly
|
|
2045
|
+
? `${formatMoney(r.stayTotal)} for ${r.hourly.hours} hours incl. taxes and fees`
|
|
2046
|
+
: `${formatMoney(r.nightly)} a night, ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees`;
|
|
2047
|
+
/**
|
|
2048
|
+
* A hotel's rooms for the nights, one line per ROOM TYPE.
|
|
2049
|
+
*
|
|
2050
|
+
* ⚠️ A TYPE IS ONE LINE, NOT ONE LINE PER RATE CODE. A Huazhu hotel sells a
|
|
2051
|
+
* dozen rates on three types, and every code in one long run buried the types
|
|
2052
|
+
* (robin, 2026-10-23, after Qunar's page). A type with one rate is that rate,
|
|
2053
|
+
* in full; a type with several says how many, its lowest price, which rate that
|
|
2054
|
+
* is - the name `room` takes - and what the rest have, in a few words.
|
|
2055
|
+
* Another rate is reached by narrowing: `bed`, `breakfast`, `freeCancellation`.
|
|
2056
|
+
*/
|
|
2057
|
+
const roomTypeLines = (rooms, roomsWanted,
|
|
2058
|
+
/** The terms already applied: what they say is true of every rate here, so it is not counted again. */
|
|
2059
|
+
terms) => {
|
|
2060
|
+
const forRooms = (r) => roomsWanted > 1
|
|
2061
|
+
? ` (${formatMoney(multiplyMoney(r.stayTotal, roomsWanted))} for ${roomsWanted} rooms)`
|
|
2062
|
+
: '';
|
|
2063
|
+
return byRoomType(rooms).map((type) => {
|
|
2064
|
+
const cheapest = type.rooms.reduce((a, b) => (b.stayTotal.minor < a.stayTotal.minor ? b : a));
|
|
2065
|
+
const n = type.rooms.length;
|
|
2066
|
+
if (n === 1)
|
|
2067
|
+
return `- ${cheapest.name}${labelWords(cheapest)}${roomFactsWords(cheapest, true)} — ${priceWords(cheapest)}${forRooms(cheapest)}, ${cancelWords(cheapest)}`;
|
|
2068
|
+
const some = (k, all, part) => k === n ? all : k > 0 ? `${k} of ${n} ${part}` : '';
|
|
2069
|
+
const also = [
|
|
2070
|
+
terms?.breakfast
|
|
2071
|
+
? ''
|
|
2072
|
+
: some(type.rooms.filter((r) => facetsOf(r).breakfast).length, 'all include breakfast', 'include breakfast'),
|
|
2073
|
+
terms?.freeCancellation
|
|
2074
|
+
? ''
|
|
2075
|
+
: some(type.rooms.filter((r) => facetsOf(r).freeCancellation).length, 'all are free to cancel', 'are free to cancel'),
|
|
2076
|
+
].filter(Boolean);
|
|
2077
|
+
return `- ${type.name}${roomFactsWords(cheapest)} — ${n} rates from ${fromWords(cheapest)}${forRooms(cheapest)}. Cheapest: "${cheapest.name}"${labelWords(cheapest)}, ${cancelWords(cheapest)}.${also.length ? ` ${also.join('; ')}.` : ''}`;
|
|
2078
|
+
});
|
|
2079
|
+
};
|
|
2080
|
+
/**
|
|
2081
|
+
* What a hotel's rooms look like to the assistant for the nights: one line per
|
|
2082
|
+
* type, and with filters only the rates that meet ALL of them. None meeting
|
|
2083
|
+
* them is said plainly - which filters were asked, what each leaves on its own
|
|
2084
|
+
* - and that the full list is there without them.
|
|
2085
|
+
*/
|
|
2086
|
+
const roomsText = (hotelName,
|
|
2087
|
+
/** "3 room types for 2026-11-04 to 2026-11-06, 2 guests" - what the rooms are, counted. */
|
|
2088
|
+
counted,
|
|
2089
|
+
/** "2026-11-04 to 2026-11-06" - when, for the sentence that says none fit. */
|
|
2090
|
+
when, enough, roomsWanted, terms) => {
|
|
2091
|
+
// A type with several rates is priced from its cheapest: say how to reach another.
|
|
2092
|
+
const lines = (rs) => [
|
|
2093
|
+
...roomTypeLines(rs, roomsWanted, terms),
|
|
2094
|
+
...(byRoomType(rs).some((t) => t.rooms.length > 1)
|
|
2095
|
+
? [
|
|
2096
|
+
'A type with several rates is priced from its cheapest one; bed, breakfast, freeCancellation, window, payment and family narrow to the others (the card lists every rate).',
|
|
2097
|
+
]
|
|
2098
|
+
: []),
|
|
2099
|
+
];
|
|
2100
|
+
if (!terms)
|
|
2101
|
+
return [`**${hotelName}** · ${counted(enough)}.`, ...lines(enough)];
|
|
2102
|
+
const asked = termWords(terms);
|
|
2103
|
+
const several = activeKeysOf(terms).length > 1;
|
|
2104
|
+
const rates = (n) => `${n} ${n === 1 ? 'rate' : 'rates'}`;
|
|
2105
|
+
const matching = enough.filter((r) => matchesTerms(r, terms));
|
|
2106
|
+
if (matching.length === 0) {
|
|
2107
|
+
const alone = several
|
|
2108
|
+
? ` On its own, ${activeKeysOf(terms)
|
|
2109
|
+
.map((k) => `${keyWords(k)} fits ${enough.filter((r) => facetsOf(r)[k]).length}`)
|
|
2110
|
+
.join(', ')}.`
|
|
2111
|
+
: '';
|
|
2112
|
+
return [
|
|
2113
|
+
`**${hotelName}** · none of the ${rates(enough.length)} for ${when} has ${asked}${several ? ' together' : ''}.${alone} The full list is there without ${several ? 'those filters' : 'that filter'}.`,
|
|
2114
|
+
];
|
|
2115
|
+
}
|
|
2116
|
+
return [
|
|
2117
|
+
`**${hotelName}** · ${counted(matching)}, with ${asked} (${matching.length} of ${rates(enough.length)}).`,
|
|
2118
|
+
...lines(matching),
|
|
2119
|
+
];
|
|
2120
|
+
};
|
|
1968
2121
|
const PAYMENT_WORDS = {
|
|
1969
2122
|
awaiting: 'to be paid on the card page',
|
|
1970
2123
|
issued: 'confirmed on UnionPay - the hotel is being asked to book it',
|
|
@@ -2093,6 +2246,52 @@ const dateFlex = (dates, stay) => {
|
|
|
2093
2246
|
text: `Arriving ${beats.checkIn} instead is ${formatMoney(saved)} less for ${sameNights}.`,
|
|
2094
2247
|
};
|
|
2095
2248
|
};
|
|
2249
|
+
/**
|
|
2250
|
+
* The room types a guest can no longer book on these nights - for the card to
|
|
2251
|
+
* say so, and not leave them out (r10 #10: a type the guest saw earlier
|
|
2252
|
+
* vanished from the list, and the heading counted one fewer, without a word).
|
|
2253
|
+
*
|
|
2254
|
+
* ⚠️ AN ADDITIVE FIELD, NEVER THE TEXT. What the assistant is told is the rooms
|
|
2255
|
+
* that can be booked, as before; this is only what the card draws, muted, at
|
|
2256
|
+
* the end of the list. A type is listed when
|
|
2257
|
+
* - the backend sends it with none left (`HotelDetail.soldOut`, or a room with
|
|
2258
|
+
* `roomsLeft: 0`), or
|
|
2259
|
+
* - it has some left, but fewer than the rooms asked for (`left`): a guest who
|
|
2260
|
+
* asked for two rooms and was shown no type with one left was not told why.
|
|
2261
|
+
* Not a type that sleeps fewer than the party (the backend never sends it), and
|
|
2262
|
+
* not a type any of whose rates can be booked. One entry a type, with the facts
|
|
2263
|
+
* the card says of a room and no price.
|
|
2264
|
+
*/
|
|
2265
|
+
const soldOutTypesOf = (detail, enough, wanted) => {
|
|
2266
|
+
const key = (r) => r.roomType?.id ?? r.id;
|
|
2267
|
+
const open = new Set(enough.map(key));
|
|
2268
|
+
const types = new Map();
|
|
2269
|
+
for (const r of [...(detail.rooms ?? []), ...(detail.soldOut ?? [])]) {
|
|
2270
|
+
if (r.hourly || open.has(key(r)) || r.roomsLeft >= wanted)
|
|
2271
|
+
continue;
|
|
2272
|
+
const seen = types.get(key(r));
|
|
2273
|
+
if (seen)
|
|
2274
|
+
seen.left = Math.max(seen.left, r.roomsLeft);
|
|
2275
|
+
else
|
|
2276
|
+
types.set(key(r), { room: r, left: r.roomsLeft });
|
|
2277
|
+
}
|
|
2278
|
+
return [...types.entries()].map(([id, { room: r, left }]) => ({
|
|
2279
|
+
id,
|
|
2280
|
+
name: r.roomType?.name ?? r.name,
|
|
2281
|
+
...((r.roomType?.names ?? r.names) ? { names: r.roomType?.names ?? r.names } : {}),
|
|
2282
|
+
sleeps: r.sleeps,
|
|
2283
|
+
...(r.maxChildren ? { maxChildren: r.maxChildren } : {}),
|
|
2284
|
+
...(r.sizeSqm ? { sizeSqm: r.sizeSqm } : {}),
|
|
2285
|
+
...(r.beds ? { beds: r.beds } : {}),
|
|
2286
|
+
...(r.bedTypes ? { bedTypes: r.bedTypes } : {}),
|
|
2287
|
+
...(r.view ? { view: r.view } : {}),
|
|
2288
|
+
...(r.window ? { window: r.window } : {}),
|
|
2289
|
+
...(r.floor ? { floor: r.floor } : {}),
|
|
2290
|
+
...(r.imageUrl ? { imageUrl: r.imageUrl } : {}),
|
|
2291
|
+
// Some left, but not as many as were asked for.
|
|
2292
|
+
...(left > 0 ? { left } : {}),
|
|
2293
|
+
}));
|
|
2294
|
+
};
|
|
2096
2295
|
const identity = (deps, ctx, key, args = {}) => {
|
|
2097
2296
|
const email = str(args.email);
|
|
2098
2297
|
const member = deps.members.read(key);
|
|
@@ -2381,6 +2580,7 @@ const filtersOf = (args) => {
|
|
|
2381
2580
|
...(facilities.length ? { facilities } : {}),
|
|
2382
2581
|
...(args.freeCancellation === true ? { freeCancellation: true } : {}),
|
|
2383
2582
|
...(args.breakfast === true ? { breakfast: true } : {}),
|
|
2583
|
+
...(args.bed === 'king' || args.bed === 'twin' ? { bed: args.bed } : {}),
|
|
2384
2584
|
...(args.hourly === true ? { hourly: true } : {}),
|
|
2385
2585
|
};
|
|
2386
2586
|
return Object.keys(f).length ? f : undefined;
|
|
@@ -2391,6 +2591,7 @@ const filterWords = (f) => [
|
|
|
2391
2591
|
...(f.facilities ?? []).map((k) => FACILITIES.find((x) => x.key === k)?.label.toLowerCase()),
|
|
2392
2592
|
f.freeCancellation ? 'free cancellation' : '',
|
|
2393
2593
|
f.breakfast ? 'breakfast included' : '',
|
|
2594
|
+
f.bed === 'king' ? 'king beds' : f.bed === 'twin' ? 'twin beds' : '',
|
|
2394
2595
|
f.hourly ? 'rooms by the hour today' : '',
|
|
2395
2596
|
]
|
|
2396
2597
|
.filter(Boolean)
|
|
@@ -2401,6 +2602,7 @@ const emptiedBy = (f, before, nouns) => {
|
|
|
2401
2602
|
(f.facilities?.length ?? 0) +
|
|
2402
2603
|
(f.freeCancellation ? 1 : 0) +
|
|
2403
2604
|
(f.breakfast ? 1 : 0) +
|
|
2605
|
+
(f.bed ? 1 : 0) +
|
|
2404
2606
|
// By the hour is a term like the others: alone, it is said alone - not
|
|
2405
2607
|
// "has rooms by the hour today together" (huazhu, 2026-10-13).
|
|
2406
2608
|
(f.hourly ? 1 : 0);
|
|
@@ -3022,7 +3224,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3022
3224
|
const applied = (wantedPrice && !oneCurrency) || answer.ignored?.sort ? 'group' : sort;
|
|
3023
3225
|
// Room terms the backend could not apply are not "filtered to".
|
|
3024
3226
|
const filtersSaid = asked.filters && answer.ignored?.roomTerms
|
|
3025
|
-
? { ...asked.filters, freeCancellation: undefined, breakfast: undefined }
|
|
3227
|
+
? { ...asked.filters, freeCancellation: undefined, breakfast: undefined, bed: undefined }
|
|
3026
3228
|
: asked.filters;
|
|
3027
3229
|
const priceNotes = [
|
|
3028
3230
|
// What the backend was asked and did not do, in its own words.
|
|
@@ -3125,7 +3327,15 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3125
3327
|
};
|
|
3126
3328
|
if (!stay) {
|
|
3127
3329
|
return {
|
|
3128
|
-
|
|
3330
|
+
// ⚠️ THE FILTERS RIDE ALONG WITHOUT DATES: the card prices the dates
|
|
3331
|
+
// itself and draws its chips from this answer, so a model that asked
|
|
3332
|
+
// for breakfast before any night was named still has it asked.
|
|
3333
|
+
structured: {
|
|
3334
|
+
view: 'hotel',
|
|
3335
|
+
...base,
|
|
3336
|
+
needsDates: true,
|
|
3337
|
+
activeFilters: activeKeysOf(roomTermsOf(args)),
|
|
3338
|
+
},
|
|
3129
3339
|
text: [
|
|
3130
3340
|
`**${h.name}** · ${[h.area, h.city].filter(Boolean).join(', ')}`,
|
|
3131
3341
|
description.fenced,
|
|
@@ -3185,8 +3395,15 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3185
3395
|
.join(' '),
|
|
3186
3396
|
};
|
|
3187
3397
|
}
|
|
3398
|
+
// ⚠️ THE CARD KEEPS EVERY ROOM, THE TEXT KEEPS THE ONES THAT MATCH. The
|
|
3399
|
+
// filters narrow what the assistant is told; the card is sent all of them
|
|
3400
|
+
// with what is true of each (`facets`), so a chip switched on or off on the
|
|
3401
|
+
// card needs no call to the server.
|
|
3402
|
+
const terms = roomTermsOf(args);
|
|
3403
|
+
const soldOut = soldOutTypesOf(detail, enough, roomsWanted);
|
|
3188
3404
|
const rooms = enough.map((r) => ({
|
|
3189
3405
|
...r,
|
|
3406
|
+
facets: facetsOf(r),
|
|
3190
3407
|
nightly: formatMoney(r.nightly),
|
|
3191
3408
|
stayTotal: formatMoney(r.stayTotal),
|
|
3192
3409
|
taxesAndFees: formatMoney(r.taxesAndFees),
|
|
@@ -3219,14 +3436,20 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3219
3436
|
stay,
|
|
3220
3437
|
rooms,
|
|
3221
3438
|
rooms_wanted: roomsWanted,
|
|
3439
|
+
roomFilters: optionsOf(enough),
|
|
3440
|
+
activeFilters: activeKeysOf(terms),
|
|
3441
|
+
// Types with no room to book for these nights, said and not left out.
|
|
3442
|
+
...(byTheHour || !soldOut.length ? {} : { soldOut }),
|
|
3222
3443
|
...flex.structured,
|
|
3223
3444
|
},
|
|
3224
3445
|
text: [
|
|
3225
|
-
byTheHour
|
|
3226
|
-
?
|
|
3227
|
-
:
|
|
3228
|
-
|
|
3229
|
-
flex.text
|
|
3446
|
+
...roomsText(h.name, byTheHour
|
|
3447
|
+
? (rs) => `${rs.length} ${rs.length === 1 ? 'room' : 'rooms'} by the hour for today, ${stay.checkIn}, ${stay.guests} guests`
|
|
3448
|
+
: (rs) => `${offerCount(rs)} for ${stay.checkIn} to ${stay.checkOut}, ${stay.guests} guests${roomsWanted > 1 ? `, ${roomsWanted} rooms` : ''}`, byTheHour ? `today, ${stay.checkIn}` : `${stay.checkIn} to ${stay.checkOut}`, enough, roomsWanted, terms),
|
|
3449
|
+
// The other arrivals are priced on the cheapest room of any kind.
|
|
3450
|
+
terms && flex.text
|
|
3451
|
+
? `${flex.text} (That is the cheapest room of any kind, not only one with ${termWords(terms)}.)`
|
|
3452
|
+
: flex.text,
|
|
3230
3453
|
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
3231
3454
|
]
|
|
3232
3455
|
.filter(Boolean)
|
|
@@ -3869,6 +4092,17 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
3869
4092
|
: {}),
|
|
3870
4093
|
paid: false,
|
|
3871
4094
|
...(remembered ? { remembered: remembered.kept } : {}),
|
|
4095
|
+
// What they come to together, said by the server like every figure on
|
|
4096
|
+
// the card - never added up there (r10, #15). Only in one currency.
|
|
4097
|
+
...(several &&
|
|
4098
|
+
booked.every(({ booking }) => booking.quote.total.currency === first.booking.quote.total.currency)
|
|
4099
|
+
? {
|
|
4100
|
+
totalAll: formatMoney({
|
|
4101
|
+
minor: booked.reduce((n, { booking }) => n + booking.quote.total.minor, 0),
|
|
4102
|
+
currency: first.booking.quote.total.currency,
|
|
4103
|
+
}),
|
|
4104
|
+
}
|
|
4105
|
+
: {}),
|
|
3872
4106
|
holdUntil: first.booking.holdUntil,
|
|
3873
4107
|
total: formatMoney(first.booking.quote.total),
|
|
3874
4108
|
...(first.asked.length ? { requested: first.asked } : {}),
|