@curless/hotel-storefront 0.54.0 → 0.55.1
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/backend.d.ts +9 -0
- package/dist/backend.d.ts.map +1 -1
- 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 +40 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +29 -13
- package/dist/demo-hotels.js.map +1 -1
- package/dist/fence.d.ts.map +1 -1
- package/dist/fence.js +4 -3
- package/dist/fence.js.map +1 -1
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +17 -22
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/huazhu/catalog.d.ts +6 -6
- package/dist/huazhu/catalog.d.ts.map +1 -1
- package/dist/huazhu/catalog.js +6 -9
- package/dist/huazhu/catalog.js.map +1 -1
- package/dist/legal.d.ts.map +1 -1
- package/dist/legal.js +104 -80
- package/dist/legal.js.map +1 -1
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/otaless/backend.d.ts +31 -0
- package/dist/otaless/backend.d.ts.map +1 -0
- package/dist/otaless/backend.js +502 -0
- package/dist/otaless/backend.js.map +1 -0
- package/dist/otaless/client.d.ts +103 -0
- package/dist/otaless/client.d.ts.map +1 -0
- package/dist/otaless/client.js +110 -0
- package/dist/otaless/client.js.map +1 -0
- package/dist/otaless/fake.d.ts +27 -0
- package/dist/otaless/fake.d.ts.map +1 -0
- package/dist/otaless/fake.js +239 -0
- package/dist/otaless/fake.js.map +1 -0
- package/dist/otaless/make.d.ts +4 -0
- package/dist/otaless/make.d.ts.map +1 -0
- package/dist/otaless/make.js +71 -0
- package/dist/otaless/make.js.map +1 -0
- package/dist/paid.d.ts +4 -1
- package/dist/paid.d.ts.map +1 -1
- package/dist/paid.js +14 -14
- package/dist/paid.js.map +1 -1
- package/dist/present.d.ts +6 -1
- package/dist/present.d.ts.map +1 -1
- package/dist/present.js +20 -5
- package/dist/present.js.map +1 -1
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js +10 -10
- package/dist/reference.js.map +1 -1
- package/dist/server.d.ts +8 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +26 -4
- package/dist/server.js.map +1 -1
- package/dist/supplier.d.ts.map +1 -1
- package/dist/supplier.js +3 -0
- package/dist/supplier.js.map +1 -1
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +87 -67
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +12 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +166 -111
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vocab.d.ts +0 -28
- package/dist/vocab.d.ts.map +1 -1
- package/dist/vocab.js +19 -46
- package/dist/vocab.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -14,7 +14,7 @@ import { regionOf } from './regions.js';
|
|
|
14
14
|
import { activeKeysOf, byRoomType, facetsOf, keyWords, matchesTerms, optionsOf, roomTermsOf, termWords, } from './room-filters.js';
|
|
15
15
|
import { SAME_BOOKING_MS } from './same-booking.js';
|
|
16
16
|
import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, clampCount, pickedKey, shownOf, stampOf, tripKey, } from './trip.js';
|
|
17
|
-
import {
|
|
17
|
+
import { branded, catalogue, upFirst } from './vocab.js';
|
|
18
18
|
// The eleven things a guest can do: find hotels, look at one on dates, see what
|
|
19
19
|
// else it offers, build a trip, ask for the rooms, and deal with a booking they
|
|
20
20
|
// already have.
|
|
@@ -145,7 +145,29 @@ export const withheldTools = (n, caps) => new Set([
|
|
|
145
145
|
...(caps.changes || caps.extras ? [] : [n.previewChange, n.changeBooking]),
|
|
146
146
|
...(caps.policies === false ? [n.policies] : []),
|
|
147
147
|
...(caps.unionpay ? [] : [n.payWithUnionPay]),
|
|
148
|
+
// Nothing can be booked here yet (`readOnly`, Otaless 2026-11-03): no tool
|
|
149
|
+
// that books, finds, changes or cancels a booking - the card's own two
|
|
150
|
+
// for Reserve included, so no button that books is drawn.
|
|
151
|
+
...(caps.readOnly
|
|
152
|
+
? [
|
|
153
|
+
n.requestBooking,
|
|
154
|
+
n.pickRoom,
|
|
155
|
+
n.bookPicked,
|
|
156
|
+
n.listBookings,
|
|
157
|
+
n.previewCancellation,
|
|
158
|
+
n.cancelBooking,
|
|
159
|
+
n.previewChange,
|
|
160
|
+
n.changeBooking,
|
|
161
|
+
n.payWithUnionPay,
|
|
162
|
+
]
|
|
163
|
+
: []),
|
|
148
164
|
]);
|
|
165
|
+
/**
|
|
166
|
+
* What to tell a guest who wants to book where nothing can be booked yet
|
|
167
|
+
* (`capabilities.readOnly`) - the same words in the instructions and in each
|
|
168
|
+
* answer that would otherwise say how to book.
|
|
169
|
+
*/
|
|
170
|
+
export const readOnlyWords = (v) => `Nothing can be booked through ${v.brand} yet: it shows ${v.noun}s, rooms and prices only. When the guest wants to book, say so plainly - nothing is booked or held - and suggest booking with the ${v.noun}'s own group directly, on its website or app, with the ${v.noun}, room and dates from here.`;
|
|
149
171
|
const forCapabilities = (tools, n, v, caps) => {
|
|
150
172
|
const withheld = withheldTools(n, caps);
|
|
151
173
|
const without = (schema, ...keys) => {
|
|
@@ -296,18 +318,16 @@ const forCapabilities = (tools, n, v, caps) => {
|
|
|
296
318
|
if (t.name === n.requestBooking && caps.booking === 'pay-at-hotel' && !v.market) {
|
|
297
319
|
out = {
|
|
298
320
|
...out,
|
|
299
|
-
// ⚠️ A
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
description: v.test
|
|
303
|
-
? `
|
|
304
|
-
? '
|
|
321
|
+
// ⚠️ A BOOKING THAT IS NOT A RESERVATION SAYS SO ITSELF (2026-09-30: the
|
|
322
|
+
// description promised "the hotel's own reservation" on the sandbox) -
|
|
323
|
+
// as the fact, never "test" or "simulated" (robin, 2026-11-03).
|
|
324
|
+
description: v.test || v.demo
|
|
325
|
+
? `Books the trip's rooms for a named guest: each stay gets its own confirmation code, and ${v.test
|
|
326
|
+
? 'none of it holds a room for anyone or is a stay anyone goes to'
|
|
327
|
+
: `none of it is sent to ${v.brand} or holds a real room`}. ${caps.prepaid
|
|
328
|
+
? 'A rate marked paid in advance is booked on its own, and not at once: the answer gives a card page where no real money moves and the time it is paid by, and the stay is booked once it is paid - no card is collected here. Every other rate is paid by nobody.'
|
|
305
329
|
: 'No payment is taken and no card is collected.'} All of the trip is booked or none of it. Preferences - a bed type, a floor, a late arrival - ride along on the booking as requests.`
|
|
306
|
-
: v.
|
|
307
|
-
? `Makes a simulated booking in a simulated copy of ${v.brand}'s booking system: each stay gets its own confirmation code there, and none of it is a ${v.brand} reservation or holds a real room. ${caps.prepaid
|
|
308
|
-
? 'A rate marked paid in advance is booked on its own, and not at once: the answer gives a card page where no real money moves and the time it is paid by, and the stay is booked once it is paid - no card is collected here. Every other rate is paid by nobody.'
|
|
309
|
-
: 'No payment is taken and no card is collected.'} All of the trip is booked or none of it. Preferences - a bed type, a floor, a late arrival - ride along on the booking as requests.`
|
|
310
|
-
: `Books the trip's rooms with ${v.brand} for a named guest: each stay becomes the hotel's own reservation, with its own confirmation code, paid at the hotel. No payment is taken here and no card is collected. All of the trip is booked or none of it. Preferences - a bed type, a floor, a late arrival - ride along on the booking as requests the desk tries to meet.`,
|
|
330
|
+
: `Books the trip's rooms with ${v.brand} for a named guest: each stay becomes the hotel's own reservation, with its own confirmation code, paid at the hotel. No payment is taken here and no card is collected. All of the trip is booked or none of it. Preferences - a bed type, a floor, a late arrival - ride along on the booking as requests the desk tries to meet.`,
|
|
311
331
|
};
|
|
312
332
|
}
|
|
313
333
|
}
|
|
@@ -596,10 +616,10 @@ const allTools = (n, v) => {
|
|
|
596
616
|
? // ⚠️ THE GROUP HOLDS THE BOOKING, NOT THE MARKET - and what a booking
|
|
597
617
|
// makes is the group's: a hold, a booking that holds no room, or a
|
|
598
618
|
// reservation paid at the hotel. The answer says which, stay by stay.
|
|
599
|
-
// ⚠️ NO "DEMONSTRATION" / "TEST" / "SIMULATED"
|
|
619
|
+
// ⚠️ NO "DEMONSTRATION" / "TEST" / "SIMULATED" (robin, 2026-11-03).
|
|
600
620
|
`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 hold that lapses on its own and is not sent to the group, a booking that holds no room for anyone, 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.`
|
|
601
621
|
: v.demo
|
|
602
|
-
? `Makes a
|
|
622
|
+
? `Makes a booking request for the trip's rooms, for a named guest: it returns a confirmation code and the moment the 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.`
|
|
603
623
|
: `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.`,
|
|
604
624
|
annotations: books(v.market ? `Request a booking on ${v.brand}` : `Request a ${v.brand} booking`, open),
|
|
605
625
|
inputSchema: {
|
|
@@ -1159,7 +1179,8 @@ export const outputSchemasFor = (schemas, caps, lang, also = {}) => {
|
|
|
1159
1179
|
!also.accent &&
|
|
1160
1180
|
!also.primary &&
|
|
1161
1181
|
!also.fonts &&
|
|
1162
|
-
!also.homeImage
|
|
1182
|
+
!also.homeImage &&
|
|
1183
|
+
!also.brandNames)
|
|
1163
1184
|
return schemas;
|
|
1164
1185
|
return Object.fromEntries(Object.entries(schemas).map(([name, schema]) => {
|
|
1165
1186
|
const s = schema;
|
|
@@ -1177,6 +1198,9 @@ export const outputSchemasFor = (schemas, caps, lang, also = {}) => {
|
|
|
1177
1198
|
// And the dark page's logo, where the group gave one.
|
|
1178
1199
|
if (also.logoDark)
|
|
1179
1200
|
properties.logoDarkUrl = STR;
|
|
1201
|
+
// And the group's own name in other languages, where its deployment gives them.
|
|
1202
|
+
if (also.brandNames)
|
|
1203
|
+
properties.brandNames = OBJ;
|
|
1180
1204
|
// And the group's own colour, where its deployment gives one.
|
|
1181
1205
|
if (also.accent)
|
|
1182
1206
|
properties.accent = { type: 'string', pattern: '^#[0-9a-fA-F]{6}$' };
|
|
@@ -1709,7 +1733,7 @@ const holderOf = (deps, supplier) => {
|
|
|
1709
1733
|
demo: s.mode !== 'live',
|
|
1710
1734
|
sandbox: s.mode === 'sandbox',
|
|
1711
1735
|
test: s.mode === 'test',
|
|
1712
|
-
notReal:
|
|
1736
|
+
notReal: s.mode !== 'live' && s.mode !== 'test',
|
|
1713
1737
|
};
|
|
1714
1738
|
return {
|
|
1715
1739
|
name: deps.vocab.brand,
|
|
@@ -1717,7 +1741,7 @@ const holderOf = (deps, supplier) => {
|
|
|
1717
1741
|
demo: deps.vocab.demo,
|
|
1718
1742
|
sandbox: !!deps.vocab.sandbox,
|
|
1719
1743
|
test: !!deps.vocab.test,
|
|
1720
|
-
notReal:
|
|
1744
|
+
notReal: deps.vocab.demo && !deps.vocab.test,
|
|
1721
1745
|
};
|
|
1722
1746
|
};
|
|
1723
1747
|
/**
|
|
@@ -1796,17 +1820,14 @@ const paidLine = (deps, b, fee) => {
|
|
|
1796
1820
|
const back = formatMoney(p.refundAmount ?? p.amount);
|
|
1797
1821
|
const whole = !p.refundAmount || p.refundAmount.minor === p.amount.minor;
|
|
1798
1822
|
const unionpay = p.method === 'unionpay';
|
|
1799
|
-
// ⚠️
|
|
1800
|
-
//
|
|
1801
|
-
//
|
|
1802
|
-
// channel's own environment, not the simulation's word.
|
|
1823
|
+
// ⚠️ THE FACT, NO ENVIRONMENT'S NAME (robin, 2026-11-03): no real money moves, and
|
|
1824
|
+
// the booking was not sent to the group - or, in its own test system, holds no
|
|
1825
|
+
// room for anyone.
|
|
1803
1826
|
const test = h.notReal
|
|
1804
|
-
? ` No real money moves, and this
|
|
1805
|
-
: h.
|
|
1806
|
-
? '
|
|
1807
|
-
:
|
|
1808
|
-
? ` This is a test: ${unionpay ? "UnionPay's test system" : 'the card page in test mode'}, no real money moves, and the booking is a test booking in ${h.name}'s test system.`
|
|
1809
|
-
: '';
|
|
1827
|
+
? ` No real money moves, and this booking was not sent to ${h.name}.`
|
|
1828
|
+
: h.test
|
|
1829
|
+
? ' No real money moves, and this booking holds no room for anyone.'
|
|
1830
|
+
: '';
|
|
1810
1831
|
const ended = b.status === 'cancelled' || b.status === 'expired' || b.status === 'failed';
|
|
1811
1832
|
const said = (() => {
|
|
1812
1833
|
// Its money as it stands once the stay has ended - never "if it does not take it".
|
|
@@ -1913,13 +1934,11 @@ const heldLine = (deps, b) => {
|
|
|
1913
1934
|
const total = formatMoney(b.quote.total);
|
|
1914
1935
|
const h = holderOf(deps, b.supplier);
|
|
1915
1936
|
if (h.caps.booking === 'pay-at-hotel') {
|
|
1916
|
-
// ⚠️
|
|
1937
|
+
// ⚠️ THE FACT, IN NO ENVIRONMENT'S WORD (robin, 2026-11-03).
|
|
1917
1938
|
if (h.notReal)
|
|
1918
1939
|
return `${total} all in. **This booking was not sent to ${h.name}** - no room is held for anyone and nothing is paid, now or at any hotel.`;
|
|
1919
|
-
if (h.sandbox)
|
|
1920
|
-
return `${total} in the simulated system. **This is a simulated booking** - not a ${h.name} reservation: no room is held and nothing is paid, now or at any hotel.`;
|
|
1921
1940
|
if (h.test)
|
|
1922
|
-
return `${total} in
|
|
1941
|
+
return `${total} all in. **This booking holds no room for anyone** - nothing is paid, now or at any hotel, and nobody needs to go there.`;
|
|
1923
1942
|
return b.status === 'requested'
|
|
1924
1943
|
? `${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.`
|
|
1925
1944
|
: `${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 the guest pays at check-in.`;
|
|
@@ -1936,14 +1955,14 @@ const heldLine = (deps, b) => {
|
|
|
1936
1955
|
// nothing for a real stay; said as how long it lasts, and that it is only the
|
|
1937
1956
|
// demonstration.
|
|
1938
1957
|
if (h.demo)
|
|
1939
|
-
return `${total} all in. **${demoBookingWords(h)}** Nothing has been charged and no card is needed. ${demoHoldWords(b, deps.trips.now()
|
|
1958
|
+
return `${total} all in. **${demoBookingWords(h)}** Nothing has been charged and no card is needed. ${demoHoldWords(b, deps.trips.now())}`;
|
|
1940
1959
|
return `${total} all in. **Nothing has been charged** - the rooms are held until ${lapses}, and the guest presents a card at check-in.`;
|
|
1941
1960
|
};
|
|
1942
1961
|
/**
|
|
1943
1962
|
* How long a demonstration hold lasts, as "in N minutes" from now - the hold
|
|
1944
1963
|
* lapses by itself, and says nothing about the stay (T2, `heldLine`).
|
|
1945
1964
|
*/
|
|
1946
|
-
const demoHoldWords = (b, now
|
|
1965
|
+
const demoHoldWords = (b, now) => {
|
|
1947
1966
|
const minutes = b.holdUntil ? Math.ceil((Date.parse(b.holdUntil) - now) / 60_000) : Number.NaN;
|
|
1948
1967
|
if (Number.isNaN(minutes))
|
|
1949
1968
|
return '';
|
|
@@ -1954,15 +1973,11 @@ const demoHoldWords = (b, now, h) => {
|
|
|
1954
1973
|
: minutes < 48 * 60
|
|
1955
1974
|
? `in about ${Math.round(minutes / 60)} hours`
|
|
1956
1975
|
: `in about ${Math.round(minutes / (24 * 60))} days`;
|
|
1957
|
-
//
|
|
1958
|
-
|
|
1959
|
-
return `It lapses by itself ${lasting} (no real room is held; the stay's own dates are not affected).`;
|
|
1960
|
-
return `This is a demonstration: it lapses by itself ${lasting} (demo only - no real room is held; the stay's own dates are not affected).`;
|
|
1976
|
+
// No "demonstration" (robin, 2026-11-03): only that it lapses and holds no room.
|
|
1977
|
+
return `It lapses by itself ${lasting} (no real room is held; the stay's own dates are not affected).`;
|
|
1961
1978
|
};
|
|
1962
|
-
/** What a
|
|
1963
|
-
const demoBookingWords = (h) => h.
|
|
1964
|
-
? `This booking was not sent to ${h.name}, and ${h.name} is not holding any room.`
|
|
1965
|
-
: `This is a demonstration booking: ${h.name} has not received it and is not holding any room.`;
|
|
1979
|
+
/** What a booking that is not sent to the group is, in one sentence - see `demoBookingNote`. */
|
|
1980
|
+
const demoBookingWords = (h) => `This booking was not sent to ${h.name}, and ${h.name} is not holding any room.`;
|
|
1966
1981
|
/**
|
|
1967
1982
|
* Said on every answer about a demonstration booking - made, changed or
|
|
1968
1983
|
* cancelled - and on nothing else: a hold of the demo's that a guest took for
|
|
@@ -1976,6 +1991,8 @@ const common = (deps, key) => {
|
|
|
1976
1991
|
const caps = deps.backend.capabilities ?? FULL_CAPABILITIES;
|
|
1977
1992
|
return {
|
|
1978
1993
|
brand: deps.vocab.brand,
|
|
1994
|
+
// The group in the card's language (「没有发到亚朵」) - see `HotelConfig.names`.
|
|
1995
|
+
...(deps.brandNames ? { brandNames: deps.brandNames } : {}),
|
|
1979
1996
|
...(deps.logoUrl ? { logoUrl: deps.logoUrl } : {}),
|
|
1980
1997
|
...(deps.logoDarkUrl ? { logoDarkUrl: deps.logoDarkUrl } : {}),
|
|
1981
1998
|
...(deps.accent ? { accent: deps.accent } : {}),
|
|
@@ -2011,10 +2028,10 @@ const common = (deps, key) => {
|
|
|
2011
2028
|
? { market: { name: deps.vocab.brand, suppliers: deps.backend.suppliers() } }
|
|
2012
2029
|
: {}),
|
|
2013
2030
|
vocab: { noun: deps.vocab.noun, nounPlural: deps.vocab.nounPlural },
|
|
2014
|
-
//
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2031
|
+
// ⚠️ NO NOTE ABOUT THE DATA (robin, 2026-11-03: 「不用特意写不是真的」): `demo`
|
|
2032
|
+
// still tells the card a booking here is not a real one, which it says as a
|
|
2033
|
+
// booking's facts; nothing labels the environment.
|
|
2034
|
+
demo: deps.vocab.demo,
|
|
2018
2035
|
...(key ? memberBadge(deps, key) : {}),
|
|
2019
2036
|
...(langOf(deps, key) ? { lang: langOf(deps, key) } : {}),
|
|
2020
2037
|
};
|
|
@@ -2034,6 +2051,8 @@ const langOf = (deps, _key) => deps.lang;
|
|
|
2034
2051
|
* hotel's own clock when the backend knows it, else the instant in UTC - never
|
|
2035
2052
|
* the date alone (see `atHotelTime`).
|
|
2036
2053
|
*/
|
|
2054
|
+
/** A rate whose own name already says it cannot be refunded - see T10 where a booking is said. */
|
|
2055
|
+
const SAYS_NON_REFUNDABLE = /non-?refundable|不可取消|不可退|不能取消/i;
|
|
2037
2056
|
const freeUntilText = (r) => r.freeCancellationLocal ??
|
|
2038
2057
|
`${(r.freeCancellationUntil ?? '').slice(0, 16).replace('T', ' ')} UTC`;
|
|
2039
2058
|
/** Any Han character: a name already in Chinese needs none beside it. */
|
|
@@ -2111,11 +2130,15 @@ const quoteForCard = (quote, groupOf) => ({
|
|
|
2111
2130
|
// stepper read 3 and sent 3 ± 1 as the new count (2026-09-28 audit).
|
|
2112
2131
|
...(l.ordered !== undefined ? { ordered: l.ordered } : {}),
|
|
2113
2132
|
unit: formatMoney(l.unit),
|
|
2133
|
+
...(l.publicUnit ? { publicUnit: formatMoney(l.publicUnit) } : {}),
|
|
2114
2134
|
total: formatMoney(l.total),
|
|
2115
2135
|
// The line with its taxes in, where the backend knows them per line: what the
|
|
2116
2136
|
// review and the trip show, as the hotel's page does (r10 #23).
|
|
2117
2137
|
...(l.totalWithTaxes ? { totalWithTaxes: formatMoney(l.totalWithTaxes) } : {}),
|
|
2118
2138
|
refundable: l.refundable,
|
|
2139
|
+
// The supplier's own sentence about cancelling, where it gives only that
|
|
2140
|
+
// (`QuoteLine.terms`, Otaless): the card says it as it came.
|
|
2141
|
+
...(l.terms !== undefined ? { terms: l.terms } : {}),
|
|
2119
2142
|
freeCancellationUntil: l.freeCancellationUntil,
|
|
2120
2143
|
// The deadline on the hotel's clock, where the backend knows it - the
|
|
2121
2144
|
// review restates the terms beside the total (round-2 jury).
|
|
@@ -2144,7 +2167,20 @@ const quoteForCard = (quote, groupOf) => ({
|
|
|
2144
2167
|
// the receipt every guest gets: room, taxes, less the tier, less the
|
|
2145
2168
|
// coupon, is the total (0.54.0, M1: a member's started from the
|
|
2146
2169
|
// public rate taxes in, a guest's from the room before them).
|
|
2147
|
-
|
|
2170
|
+
// ⚠️ THE PUBLIC RATE'S OWN ROOM CHARGE (0.55.0, review r3 N7) where the
|
|
2171
|
+
// backend gives it: the public total less the taxes CHARGED held the public
|
|
2172
|
+
// rate's larger tax as well, and the line's 「公开价每晚 ¥478」 for 2 nights and
|
|
2173
|
+
// 2 rooms (¥1,912) did not meet 「房费(公开价)¥1,922」. The tier's row is then
|
|
2174
|
+
// what it took off the rooms (`roomSaved`), and room, taxes, less the tier,
|
|
2175
|
+
// less the coupon is still the total.
|
|
2176
|
+
publicSubtotal: formatMoney(quote.member.publicSubtotal ??
|
|
2177
|
+
money(quote.member.publicTotal.minor -
|
|
2178
|
+
(quote.taxesIncluded ? 0 : quote.taxesAndFees.minor), quote.total.currency)),
|
|
2179
|
+
...(quote.member.publicSubtotal
|
|
2180
|
+
? {
|
|
2181
|
+
roomSaved: formatMoney(money(quote.member.publicSubtotal.minor - quote.subtotal.minor, quote.total.currency)),
|
|
2182
|
+
}
|
|
2183
|
+
: {}),
|
|
2148
2184
|
memberTotal: formatMoney(money(quote.member.publicTotal.minor - quote.member.saved.minor, quote.total.currency)),
|
|
2149
2185
|
},
|
|
2150
2186
|
}
|
|
@@ -2275,13 +2311,11 @@ const memberTripLines = (q) => {
|
|
|
2275
2311
|
const couponRefusedLine = (q) => q.couponRefused
|
|
2276
2312
|
? `The coupon ${q.couponRefused.name ? couponName({ name: q.couponRefused.name, names: q.couponRefused.names }) : q.couponRefused.id} was NOT taken off: ${q.couponRefused.why} The total is without it.`
|
|
2277
2313
|
: '';
|
|
2278
|
-
const tripText = (quote,
|
|
2314
|
+
const tripText = (quote, noCards) => {
|
|
2279
2315
|
// ⚠️ NOT "EMPTY" WHEN SOMETHING WAS LEFT OUT: a trip whose only group did not
|
|
2280
2316
|
// answer is not an empty trip (2026-11-03 review, C5).
|
|
2281
2317
|
if (quote.lines.length === 0 && quote.dropped?.length)
|
|
2282
|
-
return
|
|
2283
|
-
.filter(Boolean)
|
|
2284
|
-
.join('\n');
|
|
2318
|
+
return quote.dropped.map((d) => `Left out: ${d.title} — ${d.why}`).join('\n');
|
|
2285
2319
|
if (quote.lines.length === 0)
|
|
2286
2320
|
return 'The trip is empty.';
|
|
2287
2321
|
const lines = quote.lines.map((l) => l.kind === 'room'
|
|
@@ -2293,7 +2327,6 @@ const tripText = (quote, demo, noCards) => {
|
|
|
2293
2327
|
couponRefusedLine(quote),
|
|
2294
2328
|
...memberTripLines(quote),
|
|
2295
2329
|
...(quote.dropped ?? []).map((d) => `Left out: ${d.title} — ${d.why}`),
|
|
2296
|
-
demo ? DEMO_NOTE : '',
|
|
2297
2330
|
]
|
|
2298
2331
|
.filter(Boolean)
|
|
2299
2332
|
.join('\n');
|
|
@@ -2565,12 +2598,20 @@ export const notSoldHereWords = (group) => `Rates marked paid in advance are lis
|
|
|
2565
2598
|
export const notOnSaleWords = (group, why) => `${why.replace(/[.\s]+$/, '')} - ${NOT_SOLD_OUT}. To book it, the guest can use ${group}'s own channels: its app, its website or by phone.`;
|
|
2566
2599
|
/** The group a hotel is with, by name - the deployment's own brand outside a market. */
|
|
2567
2600
|
const groupNameOf = (deps, hotelId) => holderOf(deps, deps.backend.supplierOf?.(hotelId)).name;
|
|
2601
|
+
/** A rate's cancellation terms in the supplier's own words (`Room.terms`), quoted - or that none were given. */
|
|
2602
|
+
export const termsWords = (terms) => terms
|
|
2603
|
+
? `cancellation terms, in the hotel group's own words: "${terms}"`
|
|
2604
|
+
: 'no cancellation terms given';
|
|
2568
2605
|
/** What a rate says about cancelling, as the text puts it. */
|
|
2569
|
-
const cancelWords = (r) =>
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2606
|
+
const cancelWords = (r) =>
|
|
2607
|
+
// The supplier's own sentence, as it came - see `Room.terms`.
|
|
2608
|
+
r.terms !== undefined
|
|
2609
|
+
? termsWords(r.terms)
|
|
2610
|
+
: r.freeCancellationUntil
|
|
2611
|
+
? `free to cancel until ${freeUntilText(r)}`
|
|
2612
|
+
: r.refundable
|
|
2613
|
+
? 'the free cancellation window has closed - cancelling now has a fee'
|
|
2614
|
+
: 'non-refundable';
|
|
2574
2615
|
/**
|
|
2575
2616
|
* What a room is, in a parenthesis: its beds, how many it sleeps and - for one
|
|
2576
2617
|
* rate, not a type whose rates differ - breakfast, unless the rate's labels
|
|
@@ -2731,7 +2772,11 @@ const heldRoomsWords = (b) => b.quote.lines
|
|
|
2731
2772
|
: l.freeCancellationUntil
|
|
2732
2773
|
? `, free to cancel until ${freeUntilText(l)}`
|
|
2733
2774
|
: l.refundable === false
|
|
2734
|
-
?
|
|
2775
|
+
? // ⚠️ NOT TWICE (0.55.0, review r3 T10): 「… · Non-refundable(… · 不可取消),
|
|
2776
|
+
// non-refundable」 - a rate whose name says it is not told it again.
|
|
2777
|
+
SAYS_NON_REFUNDABLE.test(`${l.title} ${Object.values(l.titleNames ?? {}).join(' ')}`)
|
|
2778
|
+
? ''
|
|
2779
|
+
: ', non-refundable'
|
|
2735
2780
|
: l.refundable
|
|
2736
2781
|
? ', past its free cancellation - cancelling has a fee'
|
|
2737
2782
|
: ''}`)
|
|
@@ -2961,12 +3006,19 @@ const listAnd = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(', ')} and ${xs.
|
|
|
2961
3006
|
*/
|
|
2962
3007
|
const degradedOf = (deps, answer,
|
|
2963
3008
|
/** What is missing from the list: the hotels of a search, the bookings of "my bookings". */
|
|
2964
|
-
noun = deps.vocab.nounPlural
|
|
3009
|
+
noun = deps.vocab.nounPlural,
|
|
3010
|
+
/**
|
|
3011
|
+
* The client draws no card: each group's own Chinese name goes beside its
|
|
3012
|
+
* name, as a hotel's does (`withZh`) - told only "JI Hotel", a model with no
|
|
3013
|
+
* card called it 锦江之星, another group (11-03, text-only eval). A card host
|
|
3014
|
+
* is told the English name alone; the card says the group in its own words.
|
|
3015
|
+
*/
|
|
3016
|
+
noCards = false) => {
|
|
2965
3017
|
const down = answer.degraded ?? [];
|
|
2966
3018
|
if (down.length === 0)
|
|
2967
3019
|
return { structured: {}, said: '' };
|
|
2968
3020
|
const one = down.length === 1;
|
|
2969
|
-
const said = `${listAnd(down.map((d) => d.name))} did not answer in time, so none of ${one ? 'its' : 'their'} ${noun} is in this list - this does not mean ${one ? 'it has' : 'they have'} none here; asking again in a moment may bring them.`;
|
|
3021
|
+
const said = `${listAnd(down.map((d) => withZh(d.name, d.names, noCards)))} did not answer in time, so none of ${one ? 'its' : 'their'} ${noun} is in this list - this does not mean ${one ? 'it has' : 'they have'} none here; asking again in a moment may bring them.`;
|
|
2970
3022
|
return {
|
|
2971
3023
|
structured: {
|
|
2972
3024
|
degraded: down.map((d) => ({ supplier: d.supplier, name: d.name })),
|
|
@@ -3274,7 +3326,9 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3274
3326
|
// came back "needs a phone number". Whose need it is is the trip's groups',
|
|
3275
3327
|
// as `requestBooking` judges it.
|
|
3276
3328
|
const nextStep = ctx?.noCards && parts.some((p) => p.lines.some((l) => l.kind === 'room'))
|
|
3277
|
-
?
|
|
3329
|
+
? (deps.backend.capabilities ?? FULL_CAPABILITIES).readOnly
|
|
3330
|
+
? readOnlyWords(deps.vocab)
|
|
3331
|
+
: `Nothing in the trip is booked yet. To book it, ${deps.tools.requestBooking} takes the guest's own ${guestDetailsFor(deps, lines.filter((l) => l.kind === 'room'))} - ask for them if the guest has not typed them in this conversation.`
|
|
3278
3332
|
: '';
|
|
3279
3333
|
if (parts.length === 1 && only) {
|
|
3280
3334
|
return {
|
|
@@ -3288,13 +3342,7 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3288
3342
|
count: lines.length,
|
|
3289
3343
|
...(nextStep ? { nextStep } : {}),
|
|
3290
3344
|
},
|
|
3291
|
-
text: [
|
|
3292
|
-
tripText(only, false, ctx?.noCards),
|
|
3293
|
-
...overlaps.map((o) => o.said),
|
|
3294
|
-
orphans.line,
|
|
3295
|
-
nextStep,
|
|
3296
|
-
deps.vocab.demo && lines.length ? noteFor(deps.vocab) : '',
|
|
3297
|
-
]
|
|
3345
|
+
text: [tripText(only, ctx?.noCards), ...overlaps.map((o) => o.said), orphans.line, nextStep]
|
|
3298
3346
|
.filter(Boolean)
|
|
3299
3347
|
.join('\n'),
|
|
3300
3348
|
};
|
|
@@ -3336,11 +3384,10 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3336
3384
|
},
|
|
3337
3385
|
text: [
|
|
3338
3386
|
`This trip is in ${parts.length} currencies, priced separately and not converted - each hotel is booked and paid in its own.`,
|
|
3339
|
-
...parts.map((q) => tripText(q,
|
|
3387
|
+
...parts.map((q) => tripText(q, ctx?.noCards)),
|
|
3340
3388
|
...overlaps.map((o) => o.said),
|
|
3341
3389
|
orphans.line,
|
|
3342
3390
|
nextStep,
|
|
3343
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
3344
3391
|
]
|
|
3345
3392
|
.filter(Boolean)
|
|
3346
3393
|
.join('\n\n'),
|
|
@@ -3749,11 +3796,13 @@ const comparedInWords = (c) => {
|
|
|
3749
3796
|
const priced = p
|
|
3750
3797
|
? [
|
|
3751
3798
|
`${withZh(p.room, p.roomNames, true)} at ${p.nightly} ${nightWords(true)}, ${p.stayTotal} for the stay incl. taxes and fees`,
|
|
3752
|
-
p.
|
|
3753
|
-
?
|
|
3754
|
-
: p.
|
|
3755
|
-
?
|
|
3756
|
-
:
|
|
3799
|
+
p.terms !== undefined
|
|
3800
|
+
? termsWords(p.terms)
|
|
3801
|
+
: p.freeCancellationUntil
|
|
3802
|
+
? `free to cancel until ${freeUntilText(p)}`
|
|
3803
|
+
: p.refundable
|
|
3804
|
+
? 'no free cancellation'
|
|
3805
|
+
: 'non-refundable',
|
|
3757
3806
|
p.payment === 'prepaid'
|
|
3758
3807
|
? p.notSoldHere
|
|
3759
3808
|
? 'paid in advance - not booked here'
|
|
@@ -4063,7 +4112,6 @@ const bookedFromThisForm = async (deps, ctx, key, args) => {
|
|
|
4063
4112
|
notUsed?.said ?? '',
|
|
4064
4113
|
`**${confirming(deps, b) ? 'Being confirmed' : b.confirmationCode}** · ${b.hotelName} · ${stayWords(b.stay, b.hourly)}`,
|
|
4065
4114
|
bookedLine(deps, b) ?? '',
|
|
4066
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
4067
4115
|
]
|
|
4068
4116
|
.filter(Boolean)
|
|
4069
4117
|
.join('\n'),
|
|
@@ -4644,7 +4692,6 @@ const bookTripLines = async (deps, t, ctx, key, store, args, nothingThere) => {
|
|
|
4644
4692
|
...failedText,
|
|
4645
4693
|
remembered?.line ?? '',
|
|
4646
4694
|
others ? `The trip's other stays are still in it, not booked:\n${rest?.text ?? ''}` : '',
|
|
4647
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
4648
4695
|
]
|
|
4649
4696
|
.filter(Boolean)
|
|
4650
4697
|
.join('\n'),
|
|
@@ -5028,7 +5075,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5028
5075
|
: `${c.name} (${c.count})`)
|
|
5029
5076
|
.join(', ');
|
|
5030
5077
|
const n = deps.vocab.nounPlural;
|
|
5031
|
-
const degraded = degradedOf(deps, answer);
|
|
5078
|
+
const degraded = degradedOf(deps, answer, undefined, ctx?.noCards);
|
|
5032
5079
|
if (namedCountry) {
|
|
5033
5080
|
const [only] = countries;
|
|
5034
5081
|
return {
|
|
@@ -5065,13 +5112,25 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5065
5112
|
ask: { ...stayArgs },
|
|
5066
5113
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
5067
5114
|
...(today ? { today } : {}),
|
|
5115
|
+
// ⚠️ IN A MARKET, HOW MANY HOTELS EACH GROUP HAS HERE (11-03): the card
|
|
5116
|
+
// says "from N groups" of the groups that have one - not of every group
|
|
5117
|
+
// on the market's list, six of which may have none. Only the counts.
|
|
5118
|
+
...(deps.backend.suppliers && answer.refine?.suppliers
|
|
5119
|
+
? { refine: { suppliers: answer.refine.suppliers } }
|
|
5120
|
+
: {}),
|
|
5068
5121
|
...degraded.structured,
|
|
5069
5122
|
},
|
|
5070
|
-
|
|
5123
|
+
// ⚠️ NOTHING TO SHOW IS SAID AS THAT (0.55.1), as the card says it: 「0 hotels in 0
|
|
5124
|
+
// cities across 0 countries, to choose from by region, country and city - .」 was a
|
|
5125
|
+
// home of places over none - Otaless on a backend with no group answering. Then the
|
|
5126
|
+
// groups that did not answer, by name; no places, no plans.
|
|
5127
|
+
text: total === 0
|
|
5128
|
+
? `There are no ${n} to show right now.${degraded.said ? ` ${degraded.said}` : ''}`
|
|
5129
|
+
: `${total} ${branded(deps.vocab, n)} in ${facets.cities.length} cities across ${countries.length} ${countries.length === 1 ? 'country' : 'countries'}, to choose from by region, country and city - ${regions.map((r) => `${r.name}: ${ctx.noCards ? everyWithCities(r.countries) : every(r.countries)}`).join('; ')}. A city or a country lists its ${n}. ${degraded.said ? `${degraded.said} ` : ''}${forPlans}`,
|
|
5071
5130
|
};
|
|
5072
5131
|
}
|
|
5073
5132
|
if (hotels.length === 0) {
|
|
5074
|
-
const degraded = degradedOf(deps, answer);
|
|
5133
|
+
const degraded = degradedOf(deps, answer, undefined, ctx?.noCards);
|
|
5075
5134
|
const nearest = (answer.nearest ?? []).map((h) => ({
|
|
5076
5135
|
...h,
|
|
5077
5136
|
summary: hotelText('description', h.name, h.summary, DESCRIPTION_MAX).text,
|
|
@@ -5218,7 +5277,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5218
5277
|
priceNotes.push(notSold);
|
|
5219
5278
|
// Every one by name and city, and that there are no others - or, told
|
|
5220
5279
|
// only "2 hotels", the model asked for the list again to learn which.
|
|
5221
|
-
const degraded = degradedOf(deps, answer);
|
|
5280
|
+
const degraded = degradedOf(deps, answer, undefined, ctx?.noCards);
|
|
5222
5281
|
const inAll = homeList
|
|
5223
5282
|
? `There are ${total} ${branded(deps.vocab, deps.vocab.nounPlural)} here in all, and this answer lists every one: ${hotels
|
|
5224
5283
|
.map((h) => `${hotelName(h.name)}${h.city ? ` in ${h.city}` : ''}`)
|
|
@@ -5424,12 +5483,21 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5424
5483
|
const notSold = enough.some((r) => r.notSoldHere)
|
|
5425
5484
|
? notSoldHereWords(groupNameOf(deps, h.id))
|
|
5426
5485
|
: '';
|
|
5427
|
-
const rooms = enough.map((r) => ({
|
|
5486
|
+
const rooms = enough.map(({ roomsLeft, refundable, ...r }) => ({
|
|
5428
5487
|
...r,
|
|
5429
|
-
|
|
5488
|
+
// ⚠️ NO COUNT WHERE THE SUPPLIER GIVES NONE, and no yes / no on
|
|
5489
|
+
// cancelling beside its own sentence (`Room.uncounted`, `Room.terms`).
|
|
5490
|
+
...(r.uncounted ? {} : { roomsLeft }),
|
|
5491
|
+
...(r.terms !== undefined ? {} : { refundable }),
|
|
5492
|
+
facets: facetsOf({ ...r, roomsLeft, refundable }),
|
|
5430
5493
|
nightly: formatMoney(r.nightly),
|
|
5431
5494
|
stayTotal: formatMoney(r.stayTotal),
|
|
5432
|
-
|
|
5495
|
+
// ⚠️ "INCLUDED", NOT "¥0", where the supplier's price has its taxes in and
|
|
5496
|
+
// gives no split (`Room.terms`, Otaless): told "taxesAndFees ¥0", the
|
|
5497
|
+
// model said "税费 ¥0" - no tax - of a price the group says includes it.
|
|
5498
|
+
taxesAndFees: r.terms !== undefined && r.taxesAndFees.minor === 0
|
|
5499
|
+
? 'Included'
|
|
5500
|
+
: formatMoney(r.taxesAndFees),
|
|
5433
5501
|
// ⚠️ THE NUMBER THE GUEST IS LOOKING AT HAS TO BE THE ONE THEY ASKED FOR.
|
|
5434
5502
|
// With two rooms picked, the card was still showing one room's total and
|
|
5435
5503
|
// nothing said so - robin changed the count and watched the price not
|
|
@@ -5478,7 +5546,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5478
5546
|
? `${flex.text} (That is the cheapest room of any kind, not only one with ${termWords(terms)}.)`
|
|
5479
5547
|
: flex.text,
|
|
5480
5548
|
notSold,
|
|
5481
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
5482
5549
|
]
|
|
5483
5550
|
.filter(Boolean)
|
|
5484
5551
|
.join('\n'),
|
|
@@ -5521,7 +5588,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5521
5588
|
total = found.total;
|
|
5522
5589
|
degradedFound = found;
|
|
5523
5590
|
}
|
|
5524
|
-
const degraded = degradedOf(deps, degradedFound);
|
|
5591
|
+
const degraded = degradedOf(deps, degradedFound, undefined, ctx?.noCards);
|
|
5525
5592
|
const unique = [...new Set(ids)];
|
|
5526
5593
|
// A name that fits more than one is said with the ones it fits; the rest are not found.
|
|
5527
5594
|
const notFound = dropped.filter((d) => !candidates.some((c) => c.for === d));
|
|
@@ -5586,7 +5653,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5586
5653
|
// answer says them.
|
|
5587
5654
|
...(ctx.noCards
|
|
5588
5655
|
? roomTypeLines(rooms, roomsWanted, undefined, true)
|
|
5589
|
-
: rooms.map((r) => `- ${r.name} · sleeps ${r.sleeps}${r.breakfast ? ' · breakfast' : ''} · ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${roomsWanted > 1 ? ` (${formatMoney(multiplyMoney(r.stayTotal, roomsWanted))} for ${roomsWanted} rooms)` : ''}${r.freeCancellationUntil ? `, free to cancel until ${freeUntilText(r)}` : r.refundable ? '' : ', non-refundable'}`)),
|
|
5656
|
+
: rooms.map((r) => `- ${r.name} · sleeps ${r.sleeps}${r.breakfast ? ' · breakfast' : ''} · ${formatMoney(r.stayTotal)} for the stay incl. taxes and fees${roomsWanted > 1 ? ` (${formatMoney(multiplyMoney(r.stayTotal, roomsWanted))} for ${roomsWanted} rooms)` : ''}${r.terms !== undefined ? `, ${termsWords(r.terms)}` : r.freeCancellationUntil ? `, free to cancel until ${freeUntilText(r)}` : r.refundable ? '' : ', non-refundable'}`)),
|
|
5590
5657
|
].join('\n')
|
|
5591
5658
|
: (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
|
|
5592
5659
|
extras.length
|
|
@@ -5628,7 +5695,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5628
5695
|
ctx.noCards
|
|
5629
5696
|
? 'answer with what the guest is told, for a guest who asked to see a list, a hotel or its add-ons.'
|
|
5630
5697
|
: 'would each draw a card of their own.'}`,
|
|
5631
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
5632
5698
|
]
|
|
5633
5699
|
.filter(Boolean)
|
|
5634
5700
|
.join('\n\n'),
|
|
@@ -5712,7 +5778,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5712
5778
|
text: extras.length
|
|
5713
5779
|
? extras
|
|
5714
5780
|
.map((e) => `- ${e.name}${ctx.noCards ? ` (\`${e.id}\`)` : ''} — ${formatMoney(e.price)} ${unitWords(e.per)}${held[e.id] ? ` (${booked?.confirmationCode} holds ${held[e.id]})` : ''}`)
|
|
5715
|
-
.concat(deps.vocab.demo && noteFor(deps.vocab) ? [noteFor(deps.vocab)] : [])
|
|
5716
5781
|
.join('\n')
|
|
5717
5782
|
: `${detail.hotel.name} has no add-ons listed here.`,
|
|
5718
5783
|
};
|
|
@@ -5799,7 +5864,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5799
5864
|
const here = await deps.backend.listBookings(who, typeof args.limit === 'number' ? args.limit : undefined);
|
|
5800
5865
|
// Read here, before a filter or a spread drops it: the groups whose
|
|
5801
5866
|
// bookings are not in `here` (2026-11-03 review, C5).
|
|
5802
|
-
const missing = degradedOf(deps, { degraded: here.degraded }, 'bookings');
|
|
5867
|
+
const missing = degradedOf(deps, { degraded: here.degraded }, 'bookings', ctx?.noCards);
|
|
5803
5868
|
// Elsewhere, only with the code AND the email - see `listBookings`.
|
|
5804
5869
|
const code = str(args.booking);
|
|
5805
5870
|
const { found: there, tooMany } = code && str(args.email)
|
|
@@ -6015,7 +6080,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6015
6080
|
preview.rule,
|
|
6016
6081
|
`Say the word and it moves (${t.changeBooking} with \`confirmed\`).`,
|
|
6017
6082
|
demoBookingNote(deps, preview.supplier),
|
|
6018
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6019
6083
|
]
|
|
6020
6084
|
.filter(Boolean)
|
|
6021
6085
|
.join(' ')
|
|
@@ -6078,7 +6142,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6078
6142
|
`${changed.confirmationCode} now holds ${changed.stay.checkIn} to ${changed.stay.checkOut}, ${formatMoney(changed.quote.total)} all in.`,
|
|
6079
6143
|
'The confirmation code has not changed. **Nothing has been charged.**',
|
|
6080
6144
|
demoBookingNote(deps, changed.supplier),
|
|
6081
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6082
6145
|
]
|
|
6083
6146
|
.filter(Boolean)
|
|
6084
6147
|
.join(' '),
|
|
@@ -6210,7 +6273,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6210
6273
|
? `Say the word and it is cancelled (${t.cancelBooking} with \`confirmed\`).`
|
|
6211
6274
|
: '',
|
|
6212
6275
|
demoBookingNote(deps, preview.supplier),
|
|
6213
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6214
6276
|
]
|
|
6215
6277
|
.filter(Boolean)
|
|
6216
6278
|
.join(' '),
|
|
@@ -6287,20 +6349,14 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6287
6349
|
? // Paid here in advance: what becomes of the money is the answer.
|
|
6288
6350
|
`${cancelled.confirmationCode} is ${cancelled.status === 'expired' ? 'called off before it was paid' : 'cancelled'}. ${paidLine(deps, cancelled, charged)}`
|
|
6289
6351
|
: h.notReal
|
|
6290
|
-
? //
|
|
6352
|
+
? // The fact, in no environment's word (robin, 2026-11-03); nobody charges
|
|
6353
|
+
// a booking that was never sent anything (2026-10-05 audit, P2).
|
|
6291
6354
|
`${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `A real booking on this rate would be charged ${formatMoney(charged)}; this booking was never sent to ${h.name}, and nothing is charged.`}`
|
|
6292
|
-
: h.
|
|
6293
|
-
?
|
|
6294
|
-
|
|
6295
|
-
: h.test
|
|
6296
|
-
? `${cancelled.confirmationCode} is cancelled in ${h.name}'s test system. ${charged.minor === 0 ? 'No fee.' : `A live booking on this rate would be charged ${formatMoney(charged)}; this one is a test, and nothing is charged.`}`
|
|
6297
|
-
: h.demo
|
|
6298
|
-
? // Nobody charges a demonstration anything (2026-10-05 audit, P2).
|
|
6299
|
-
`${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.`}`
|
|
6300
|
-
: `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `The hotel charges ${formatMoney(charged)}.`}`,
|
|
6355
|
+
: h.test
|
|
6356
|
+
? `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `A real booking on this rate would be charged ${formatMoney(charged)}; this booking held no room for anyone, and nothing is charged.`}`
|
|
6357
|
+
: `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `The hotel charges ${formatMoney(charged)}.`}`,
|
|
6301
6358
|
couponNow?.said ?? '',
|
|
6302
6359
|
demoBookingNote(deps, cancelled.supplier),
|
|
6303
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6304
6360
|
]
|
|
6305
6361
|
.filter(Boolean)
|
|
6306
6362
|
.join(' '),
|
|
@@ -6384,7 +6440,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6384
6440
|
: '',
|
|
6385
6441
|
...comparison.dropped.map((d) => `Left out: ${d.name} — ${d.why}`),
|
|
6386
6442
|
...fitsManyOf(deps, comparison.dropped),
|
|
6387
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6388
6443
|
]
|
|
6389
6444
|
.filter(Boolean)
|
|
6390
6445
|
.join('\n'),
|
|
@@ -6450,11 +6505,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6450
6505
|
// With no card, until when it is free to cancel too: the reply is told to
|
|
6451
6506
|
// give each stay's terms (2026-11-02 audit, P2-3).
|
|
6452
6507
|
const termsOf = (x) => [
|
|
6453
|
-
x.
|
|
6454
|
-
?
|
|
6455
|
-
:
|
|
6456
|
-
?
|
|
6457
|
-
:
|
|
6508
|
+
x.terms
|
|
6509
|
+
? `cancellation: "${x.terms}"`
|
|
6510
|
+
: x.refundable === false
|
|
6511
|
+
? 'non-refundable'
|
|
6512
|
+
: ctx.noCards && x.freeCancellationUntil
|
|
6513
|
+
? `free to cancel until ${freeUntilText(x)}`
|
|
6514
|
+
: '',
|
|
6458
6515
|
x.payment === 'prepaid'
|
|
6459
6516
|
? x.notSoldHere
|
|
6460
6517
|
? 'paid in advance - not booked here'
|
|
@@ -6510,7 +6567,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6510
6567
|
...fitsManyOf(deps, plan.dropped),
|
|
6511
6568
|
uncheckedSaid,
|
|
6512
6569
|
ctx.noCards ? PLAN_NOT_DRAWN : PLAN_DRAWN_ONCE,
|
|
6513
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6514
6570
|
]
|
|
6515
6571
|
.filter(Boolean)
|
|
6516
6572
|
.join('\n'),
|
|
@@ -6566,7 +6622,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6566
6622
|
member.preferences?.length
|
|
6567
6623
|
? `Saved preferences on this membership: ${member.preferences.map((k) => REQUEST_WORDS[k]).join(', ')}. They are ticked on the booking form; mention them, and ask before booking with them.`
|
|
6568
6624
|
: '',
|
|
6569
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6570
6625
|
]
|
|
6571
6626
|
.filter(Boolean)
|
|
6572
6627
|
.join('\n'),
|