@curless/hotel-storefront 0.54.0 → 0.55.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/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 +33 -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.map +1 -1
- package/dist/paid.js +9 -11
- 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 +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +6 -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 +86 -66
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +8 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +152 -109
- 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.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,mBAAmB,EAWzB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAa,YAAY,EAAgB,MAAM,cAAc,CAAC;AAC1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAM7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAkChD,OAAO,EAGL,KAAK,SAAS,EAMf,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAEV,cAAc,EAqBd,eAAe,EAMhB,MAAM,YAAY,CAAC;AACpB,OAAO,
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
|
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: {
|
|
@@ -1709,7 +1729,7 @@ const holderOf = (deps, supplier) => {
|
|
|
1709
1729
|
demo: s.mode !== 'live',
|
|
1710
1730
|
sandbox: s.mode === 'sandbox',
|
|
1711
1731
|
test: s.mode === 'test',
|
|
1712
|
-
notReal:
|
|
1732
|
+
notReal: s.mode !== 'live' && s.mode !== 'test',
|
|
1713
1733
|
};
|
|
1714
1734
|
return {
|
|
1715
1735
|
name: deps.vocab.brand,
|
|
@@ -1717,7 +1737,7 @@ const holderOf = (deps, supplier) => {
|
|
|
1717
1737
|
demo: deps.vocab.demo,
|
|
1718
1738
|
sandbox: !!deps.vocab.sandbox,
|
|
1719
1739
|
test: !!deps.vocab.test,
|
|
1720
|
-
notReal:
|
|
1740
|
+
notReal: deps.vocab.demo && !deps.vocab.test,
|
|
1721
1741
|
};
|
|
1722
1742
|
};
|
|
1723
1743
|
/**
|
|
@@ -1796,17 +1816,14 @@ const paidLine = (deps, b, fee) => {
|
|
|
1796
1816
|
const back = formatMoney(p.refundAmount ?? p.amount);
|
|
1797
1817
|
const whole = !p.refundAmount || p.refundAmount.minor === p.amount.minor;
|
|
1798
1818
|
const unionpay = p.method === 'unionpay';
|
|
1799
|
-
// ⚠️
|
|
1800
|
-
//
|
|
1801
|
-
//
|
|
1802
|
-
// channel's own environment, not the simulation's word.
|
|
1819
|
+
// ⚠️ THE FACT, NO ENVIRONMENT'S NAME (robin, 2026-11-03): no real money moves, and
|
|
1820
|
+
// the booking was not sent to the group - or, in its own test system, holds no
|
|
1821
|
+
// room for anyone.
|
|
1803
1822
|
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
|
-
: '';
|
|
1823
|
+
? ` No real money moves, and this booking was not sent to ${h.name}.`
|
|
1824
|
+
: h.test
|
|
1825
|
+
? ' No real money moves, and this booking holds no room for anyone.'
|
|
1826
|
+
: '';
|
|
1810
1827
|
const ended = b.status === 'cancelled' || b.status === 'expired' || b.status === 'failed';
|
|
1811
1828
|
const said = (() => {
|
|
1812
1829
|
// Its money as it stands once the stay has ended - never "if it does not take it".
|
|
@@ -1913,13 +1930,11 @@ const heldLine = (deps, b) => {
|
|
|
1913
1930
|
const total = formatMoney(b.quote.total);
|
|
1914
1931
|
const h = holderOf(deps, b.supplier);
|
|
1915
1932
|
if (h.caps.booking === 'pay-at-hotel') {
|
|
1916
|
-
// ⚠️
|
|
1933
|
+
// ⚠️ THE FACT, IN NO ENVIRONMENT'S WORD (robin, 2026-11-03).
|
|
1917
1934
|
if (h.notReal)
|
|
1918
1935
|
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
1936
|
if (h.test)
|
|
1922
|
-
return `${total} in
|
|
1937
|
+
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
1938
|
return b.status === 'requested'
|
|
1924
1939
|
? `${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
1940
|
: `${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 +1951,14 @@ const heldLine = (deps, b) => {
|
|
|
1936
1951
|
// nothing for a real stay; said as how long it lasts, and that it is only the
|
|
1937
1952
|
// demonstration.
|
|
1938
1953
|
if (h.demo)
|
|
1939
|
-
return `${total} all in. **${demoBookingWords(h)}** Nothing has been charged and no card is needed. ${demoHoldWords(b, deps.trips.now()
|
|
1954
|
+
return `${total} all in. **${demoBookingWords(h)}** Nothing has been charged and no card is needed. ${demoHoldWords(b, deps.trips.now())}`;
|
|
1940
1955
|
return `${total} all in. **Nothing has been charged** - the rooms are held until ${lapses}, and the guest presents a card at check-in.`;
|
|
1941
1956
|
};
|
|
1942
1957
|
/**
|
|
1943
1958
|
* How long a demonstration hold lasts, as "in N minutes" from now - the hold
|
|
1944
1959
|
* lapses by itself, and says nothing about the stay (T2, `heldLine`).
|
|
1945
1960
|
*/
|
|
1946
|
-
const demoHoldWords = (b, now
|
|
1961
|
+
const demoHoldWords = (b, now) => {
|
|
1947
1962
|
const minutes = b.holdUntil ? Math.ceil((Date.parse(b.holdUntil) - now) / 60_000) : Number.NaN;
|
|
1948
1963
|
if (Number.isNaN(minutes))
|
|
1949
1964
|
return '';
|
|
@@ -1954,15 +1969,11 @@ const demoHoldWords = (b, now, h) => {
|
|
|
1954
1969
|
: minutes < 48 * 60
|
|
1955
1970
|
? `in about ${Math.round(minutes / 60)} hours`
|
|
1956
1971
|
: `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).`;
|
|
1972
|
+
// No "demonstration" (robin, 2026-11-03): only that it lapses and holds no room.
|
|
1973
|
+
return `It lapses by itself ${lasting} (no real room is held; the stay's own dates are not affected).`;
|
|
1961
1974
|
};
|
|
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.`;
|
|
1975
|
+
/** What a booking that is not sent to the group is, in one sentence - see `demoBookingNote`. */
|
|
1976
|
+
const demoBookingWords = (h) => `This booking was not sent to ${h.name}, and ${h.name} is not holding any room.`;
|
|
1966
1977
|
/**
|
|
1967
1978
|
* Said on every answer about a demonstration booking - made, changed or
|
|
1968
1979
|
* cancelled - and on nothing else: a hold of the demo's that a guest took for
|
|
@@ -2011,10 +2022,10 @@ const common = (deps, key) => {
|
|
|
2011
2022
|
? { market: { name: deps.vocab.brand, suppliers: deps.backend.suppliers() } }
|
|
2012
2023
|
: {}),
|
|
2013
2024
|
vocab: { noun: deps.vocab.noun, nounPlural: deps.vocab.nounPlural },
|
|
2014
|
-
//
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2025
|
+
// ⚠️ NO NOTE ABOUT THE DATA (robin, 2026-11-03: 「不用特意写不是真的」): `demo`
|
|
2026
|
+
// still tells the card a booking here is not a real one, which it says as a
|
|
2027
|
+
// booking's facts; nothing labels the environment.
|
|
2028
|
+
demo: deps.vocab.demo,
|
|
2018
2029
|
...(key ? memberBadge(deps, key) : {}),
|
|
2019
2030
|
...(langOf(deps, key) ? { lang: langOf(deps, key) } : {}),
|
|
2020
2031
|
};
|
|
@@ -2034,6 +2045,8 @@ const langOf = (deps, _key) => deps.lang;
|
|
|
2034
2045
|
* hotel's own clock when the backend knows it, else the instant in UTC - never
|
|
2035
2046
|
* the date alone (see `atHotelTime`).
|
|
2036
2047
|
*/
|
|
2048
|
+
/** A rate whose own name already says it cannot be refunded - see T10 where a booking is said. */
|
|
2049
|
+
const SAYS_NON_REFUNDABLE = /non-?refundable|不可取消|不可退|不能取消/i;
|
|
2037
2050
|
const freeUntilText = (r) => r.freeCancellationLocal ??
|
|
2038
2051
|
`${(r.freeCancellationUntil ?? '').slice(0, 16).replace('T', ' ')} UTC`;
|
|
2039
2052
|
/** Any Han character: a name already in Chinese needs none beside it. */
|
|
@@ -2111,11 +2124,15 @@ const quoteForCard = (quote, groupOf) => ({
|
|
|
2111
2124
|
// stepper read 3 and sent 3 ± 1 as the new count (2026-09-28 audit).
|
|
2112
2125
|
...(l.ordered !== undefined ? { ordered: l.ordered } : {}),
|
|
2113
2126
|
unit: formatMoney(l.unit),
|
|
2127
|
+
...(l.publicUnit ? { publicUnit: formatMoney(l.publicUnit) } : {}),
|
|
2114
2128
|
total: formatMoney(l.total),
|
|
2115
2129
|
// The line with its taxes in, where the backend knows them per line: what the
|
|
2116
2130
|
// review and the trip show, as the hotel's page does (r10 #23).
|
|
2117
2131
|
...(l.totalWithTaxes ? { totalWithTaxes: formatMoney(l.totalWithTaxes) } : {}),
|
|
2118
2132
|
refundable: l.refundable,
|
|
2133
|
+
// The supplier's own sentence about cancelling, where it gives only that
|
|
2134
|
+
// (`QuoteLine.terms`, Otaless): the card says it as it came.
|
|
2135
|
+
...(l.terms !== undefined ? { terms: l.terms } : {}),
|
|
2119
2136
|
freeCancellationUntil: l.freeCancellationUntil,
|
|
2120
2137
|
// The deadline on the hotel's clock, where the backend knows it - the
|
|
2121
2138
|
// review restates the terms beside the total (round-2 jury).
|
|
@@ -2144,7 +2161,20 @@ const quoteForCard = (quote, groupOf) => ({
|
|
|
2144
2161
|
// the receipt every guest gets: room, taxes, less the tier, less the
|
|
2145
2162
|
// coupon, is the total (0.54.0, M1: a member's started from the
|
|
2146
2163
|
// public rate taxes in, a guest's from the room before them).
|
|
2147
|
-
|
|
2164
|
+
// ⚠️ THE PUBLIC RATE'S OWN ROOM CHARGE (0.55.0, review r3 N7) where the
|
|
2165
|
+
// backend gives it: the public total less the taxes CHARGED held the public
|
|
2166
|
+
// rate's larger tax as well, and the line's 「公开价每晚 ¥478」 for 2 nights and
|
|
2167
|
+
// 2 rooms (¥1,912) did not meet 「房费(公开价)¥1,922」. The tier's row is then
|
|
2168
|
+
// what it took off the rooms (`roomSaved`), and room, taxes, less the tier,
|
|
2169
|
+
// less the coupon is still the total.
|
|
2170
|
+
publicSubtotal: formatMoney(quote.member.publicSubtotal ??
|
|
2171
|
+
money(quote.member.publicTotal.minor -
|
|
2172
|
+
(quote.taxesIncluded ? 0 : quote.taxesAndFees.minor), quote.total.currency)),
|
|
2173
|
+
...(quote.member.publicSubtotal
|
|
2174
|
+
? {
|
|
2175
|
+
roomSaved: formatMoney(money(quote.member.publicSubtotal.minor - quote.subtotal.minor, quote.total.currency)),
|
|
2176
|
+
}
|
|
2177
|
+
: {}),
|
|
2148
2178
|
memberTotal: formatMoney(money(quote.member.publicTotal.minor - quote.member.saved.minor, quote.total.currency)),
|
|
2149
2179
|
},
|
|
2150
2180
|
}
|
|
@@ -2275,13 +2305,11 @@ const memberTripLines = (q) => {
|
|
|
2275
2305
|
const couponRefusedLine = (q) => q.couponRefused
|
|
2276
2306
|
? `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
2307
|
: '';
|
|
2278
|
-
const tripText = (quote,
|
|
2308
|
+
const tripText = (quote, noCards) => {
|
|
2279
2309
|
// ⚠️ NOT "EMPTY" WHEN SOMETHING WAS LEFT OUT: a trip whose only group did not
|
|
2280
2310
|
// answer is not an empty trip (2026-11-03 review, C5).
|
|
2281
2311
|
if (quote.lines.length === 0 && quote.dropped?.length)
|
|
2282
|
-
return
|
|
2283
|
-
.filter(Boolean)
|
|
2284
|
-
.join('\n');
|
|
2312
|
+
return quote.dropped.map((d) => `Left out: ${d.title} — ${d.why}`).join('\n');
|
|
2285
2313
|
if (quote.lines.length === 0)
|
|
2286
2314
|
return 'The trip is empty.';
|
|
2287
2315
|
const lines = quote.lines.map((l) => l.kind === 'room'
|
|
@@ -2293,7 +2321,6 @@ const tripText = (quote, demo, noCards) => {
|
|
|
2293
2321
|
couponRefusedLine(quote),
|
|
2294
2322
|
...memberTripLines(quote),
|
|
2295
2323
|
...(quote.dropped ?? []).map((d) => `Left out: ${d.title} — ${d.why}`),
|
|
2296
|
-
demo ? DEMO_NOTE : '',
|
|
2297
2324
|
]
|
|
2298
2325
|
.filter(Boolean)
|
|
2299
2326
|
.join('\n');
|
|
@@ -2565,12 +2592,20 @@ export const notSoldHereWords = (group) => `Rates marked paid in advance are lis
|
|
|
2565
2592
|
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
2593
|
/** The group a hotel is with, by name - the deployment's own brand outside a market. */
|
|
2567
2594
|
const groupNameOf = (deps, hotelId) => holderOf(deps, deps.backend.supplierOf?.(hotelId)).name;
|
|
2595
|
+
/** A rate's cancellation terms in the supplier's own words (`Room.terms`), quoted - or that none were given. */
|
|
2596
|
+
export const termsWords = (terms) => terms
|
|
2597
|
+
? `cancellation terms, in the hotel group's own words: "${terms}"`
|
|
2598
|
+
: 'no cancellation terms given';
|
|
2568
2599
|
/** What a rate says about cancelling, as the text puts it. */
|
|
2569
|
-
const cancelWords = (r) =>
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2600
|
+
const cancelWords = (r) =>
|
|
2601
|
+
// The supplier's own sentence, as it came - see `Room.terms`.
|
|
2602
|
+
r.terms !== undefined
|
|
2603
|
+
? termsWords(r.terms)
|
|
2604
|
+
: r.freeCancellationUntil
|
|
2605
|
+
? `free to cancel until ${freeUntilText(r)}`
|
|
2606
|
+
: r.refundable
|
|
2607
|
+
? 'the free cancellation window has closed - cancelling now has a fee'
|
|
2608
|
+
: 'non-refundable';
|
|
2574
2609
|
/**
|
|
2575
2610
|
* What a room is, in a parenthesis: its beds, how many it sleeps and - for one
|
|
2576
2611
|
* rate, not a type whose rates differ - breakfast, unless the rate's labels
|
|
@@ -2731,7 +2766,11 @@ const heldRoomsWords = (b) => b.quote.lines
|
|
|
2731
2766
|
: l.freeCancellationUntil
|
|
2732
2767
|
? `, free to cancel until ${freeUntilText(l)}`
|
|
2733
2768
|
: l.refundable === false
|
|
2734
|
-
?
|
|
2769
|
+
? // ⚠️ NOT TWICE (0.55.0, review r3 T10): 「… · Non-refundable(… · 不可取消),
|
|
2770
|
+
// non-refundable」 - a rate whose name says it is not told it again.
|
|
2771
|
+
SAYS_NON_REFUNDABLE.test(`${l.title} ${Object.values(l.titleNames ?? {}).join(' ')}`)
|
|
2772
|
+
? ''
|
|
2773
|
+
: ', non-refundable'
|
|
2735
2774
|
: l.refundable
|
|
2736
2775
|
? ', past its free cancellation - cancelling has a fee'
|
|
2737
2776
|
: ''}`)
|
|
@@ -2961,12 +3000,19 @@ const listAnd = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(', ')} and ${xs.
|
|
|
2961
3000
|
*/
|
|
2962
3001
|
const degradedOf = (deps, answer,
|
|
2963
3002
|
/** What is missing from the list: the hotels of a search, the bookings of "my bookings". */
|
|
2964
|
-
noun = deps.vocab.nounPlural
|
|
3003
|
+
noun = deps.vocab.nounPlural,
|
|
3004
|
+
/**
|
|
3005
|
+
* The client draws no card: each group's own Chinese name goes beside its
|
|
3006
|
+
* name, as a hotel's does (`withZh`) - told only "JI Hotel", a model with no
|
|
3007
|
+
* card called it 锦江之星, another group (11-03, text-only eval). A card host
|
|
3008
|
+
* is told the English name alone; the card says the group in its own words.
|
|
3009
|
+
*/
|
|
3010
|
+
noCards = false) => {
|
|
2965
3011
|
const down = answer.degraded ?? [];
|
|
2966
3012
|
if (down.length === 0)
|
|
2967
3013
|
return { structured: {}, said: '' };
|
|
2968
3014
|
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.`;
|
|
3015
|
+
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
3016
|
return {
|
|
2971
3017
|
structured: {
|
|
2972
3018
|
degraded: down.map((d) => ({ supplier: d.supplier, name: d.name })),
|
|
@@ -3274,7 +3320,9 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3274
3320
|
// came back "needs a phone number". Whose need it is is the trip's groups',
|
|
3275
3321
|
// as `requestBooking` judges it.
|
|
3276
3322
|
const nextStep = ctx?.noCards && parts.some((p) => p.lines.some((l) => l.kind === 'room'))
|
|
3277
|
-
?
|
|
3323
|
+
? (deps.backend.capabilities ?? FULL_CAPABILITIES).readOnly
|
|
3324
|
+
? readOnlyWords(deps.vocab)
|
|
3325
|
+
: `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
3326
|
: '';
|
|
3279
3327
|
if (parts.length === 1 && only) {
|
|
3280
3328
|
return {
|
|
@@ -3288,13 +3336,7 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3288
3336
|
count: lines.length,
|
|
3289
3337
|
...(nextStep ? { nextStep } : {}),
|
|
3290
3338
|
},
|
|
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
|
-
]
|
|
3339
|
+
text: [tripText(only, ctx?.noCards), ...overlaps.map((o) => o.said), orphans.line, nextStep]
|
|
3298
3340
|
.filter(Boolean)
|
|
3299
3341
|
.join('\n'),
|
|
3300
3342
|
};
|
|
@@ -3336,11 +3378,10 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3336
3378
|
},
|
|
3337
3379
|
text: [
|
|
3338
3380
|
`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,
|
|
3381
|
+
...parts.map((q) => tripText(q, ctx?.noCards)),
|
|
3340
3382
|
...overlaps.map((o) => o.said),
|
|
3341
3383
|
orphans.line,
|
|
3342
3384
|
nextStep,
|
|
3343
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
3344
3385
|
]
|
|
3345
3386
|
.filter(Boolean)
|
|
3346
3387
|
.join('\n\n'),
|
|
@@ -3749,11 +3790,13 @@ const comparedInWords = (c) => {
|
|
|
3749
3790
|
const priced = p
|
|
3750
3791
|
? [
|
|
3751
3792
|
`${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
|
-
:
|
|
3793
|
+
p.terms !== undefined
|
|
3794
|
+
? termsWords(p.terms)
|
|
3795
|
+
: p.freeCancellationUntil
|
|
3796
|
+
? `free to cancel until ${freeUntilText(p)}`
|
|
3797
|
+
: p.refundable
|
|
3798
|
+
? 'no free cancellation'
|
|
3799
|
+
: 'non-refundable',
|
|
3757
3800
|
p.payment === 'prepaid'
|
|
3758
3801
|
? p.notSoldHere
|
|
3759
3802
|
? 'paid in advance - not booked here'
|
|
@@ -4063,7 +4106,6 @@ const bookedFromThisForm = async (deps, ctx, key, args) => {
|
|
|
4063
4106
|
notUsed?.said ?? '',
|
|
4064
4107
|
`**${confirming(deps, b) ? 'Being confirmed' : b.confirmationCode}** · ${b.hotelName} · ${stayWords(b.stay, b.hourly)}`,
|
|
4065
4108
|
bookedLine(deps, b) ?? '',
|
|
4066
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
4067
4109
|
]
|
|
4068
4110
|
.filter(Boolean)
|
|
4069
4111
|
.join('\n'),
|
|
@@ -4644,7 +4686,6 @@ const bookTripLines = async (deps, t, ctx, key, store, args, nothingThere) => {
|
|
|
4644
4686
|
...failedText,
|
|
4645
4687
|
remembered?.line ?? '',
|
|
4646
4688
|
others ? `The trip's other stays are still in it, not booked:\n${rest?.text ?? ''}` : '',
|
|
4647
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
4648
4689
|
]
|
|
4649
4690
|
.filter(Boolean)
|
|
4650
4691
|
.join('\n'),
|
|
@@ -5028,7 +5069,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5028
5069
|
: `${c.name} (${c.count})`)
|
|
5029
5070
|
.join(', ');
|
|
5030
5071
|
const n = deps.vocab.nounPlural;
|
|
5031
|
-
const degraded = degradedOf(deps, answer);
|
|
5072
|
+
const degraded = degradedOf(deps, answer, undefined, ctx?.noCards);
|
|
5032
5073
|
if (namedCountry) {
|
|
5033
5074
|
const [only] = countries;
|
|
5034
5075
|
return {
|
|
@@ -5065,13 +5106,19 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5065
5106
|
ask: { ...stayArgs },
|
|
5066
5107
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
5067
5108
|
...(today ? { today } : {}),
|
|
5109
|
+
// ⚠️ IN A MARKET, HOW MANY HOTELS EACH GROUP HAS HERE (11-03): the card
|
|
5110
|
+
// says "from N groups" of the groups that have one - not of every group
|
|
5111
|
+
// on the market's list, six of which may have none. Only the counts.
|
|
5112
|
+
...(deps.backend.suppliers && answer.refine?.suppliers
|
|
5113
|
+
? { refine: { suppliers: answer.refine.suppliers } }
|
|
5114
|
+
: {}),
|
|
5068
5115
|
...degraded.structured,
|
|
5069
5116
|
},
|
|
5070
5117
|
text: `${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
5118
|
};
|
|
5072
5119
|
}
|
|
5073
5120
|
if (hotels.length === 0) {
|
|
5074
|
-
const degraded = degradedOf(deps, answer);
|
|
5121
|
+
const degraded = degradedOf(deps, answer, undefined, ctx?.noCards);
|
|
5075
5122
|
const nearest = (answer.nearest ?? []).map((h) => ({
|
|
5076
5123
|
...h,
|
|
5077
5124
|
summary: hotelText('description', h.name, h.summary, DESCRIPTION_MAX).text,
|
|
@@ -5218,7 +5265,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5218
5265
|
priceNotes.push(notSold);
|
|
5219
5266
|
// Every one by name and city, and that there are no others - or, told
|
|
5220
5267
|
// only "2 hotels", the model asked for the list again to learn which.
|
|
5221
|
-
const degraded = degradedOf(deps, answer);
|
|
5268
|
+
const degraded = degradedOf(deps, answer, undefined, ctx?.noCards);
|
|
5222
5269
|
const inAll = homeList
|
|
5223
5270
|
? `There are ${total} ${branded(deps.vocab, deps.vocab.nounPlural)} here in all, and this answer lists every one: ${hotels
|
|
5224
5271
|
.map((h) => `${hotelName(h.name)}${h.city ? ` in ${h.city}` : ''}`)
|
|
@@ -5424,12 +5471,21 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5424
5471
|
const notSold = enough.some((r) => r.notSoldHere)
|
|
5425
5472
|
? notSoldHereWords(groupNameOf(deps, h.id))
|
|
5426
5473
|
: '';
|
|
5427
|
-
const rooms = enough.map((r) => ({
|
|
5474
|
+
const rooms = enough.map(({ roomsLeft, refundable, ...r }) => ({
|
|
5428
5475
|
...r,
|
|
5429
|
-
|
|
5476
|
+
// ⚠️ NO COUNT WHERE THE SUPPLIER GIVES NONE, and no yes / no on
|
|
5477
|
+
// cancelling beside its own sentence (`Room.uncounted`, `Room.terms`).
|
|
5478
|
+
...(r.uncounted ? {} : { roomsLeft }),
|
|
5479
|
+
...(r.terms !== undefined ? {} : { refundable }),
|
|
5480
|
+
facets: facetsOf({ ...r, roomsLeft, refundable }),
|
|
5430
5481
|
nightly: formatMoney(r.nightly),
|
|
5431
5482
|
stayTotal: formatMoney(r.stayTotal),
|
|
5432
|
-
|
|
5483
|
+
// ⚠️ "INCLUDED", NOT "¥0", where the supplier's price has its taxes in and
|
|
5484
|
+
// gives no split (`Room.terms`, Otaless): told "taxesAndFees ¥0", the
|
|
5485
|
+
// model said "税费 ¥0" - no tax - of a price the group says includes it.
|
|
5486
|
+
taxesAndFees: r.terms !== undefined && r.taxesAndFees.minor === 0
|
|
5487
|
+
? 'Included'
|
|
5488
|
+
: formatMoney(r.taxesAndFees),
|
|
5433
5489
|
// ⚠️ THE NUMBER THE GUEST IS LOOKING AT HAS TO BE THE ONE THEY ASKED FOR.
|
|
5434
5490
|
// With two rooms picked, the card was still showing one room's total and
|
|
5435
5491
|
// nothing said so - robin changed the count and watched the price not
|
|
@@ -5478,7 +5534,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5478
5534
|
? `${flex.text} (That is the cheapest room of any kind, not only one with ${termWords(terms)}.)`
|
|
5479
5535
|
: flex.text,
|
|
5480
5536
|
notSold,
|
|
5481
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
5482
5537
|
]
|
|
5483
5538
|
.filter(Boolean)
|
|
5484
5539
|
.join('\n'),
|
|
@@ -5521,7 +5576,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5521
5576
|
total = found.total;
|
|
5522
5577
|
degradedFound = found;
|
|
5523
5578
|
}
|
|
5524
|
-
const degraded = degradedOf(deps, degradedFound);
|
|
5579
|
+
const degraded = degradedOf(deps, degradedFound, undefined, ctx?.noCards);
|
|
5525
5580
|
const unique = [...new Set(ids)];
|
|
5526
5581
|
// A name that fits more than one is said with the ones it fits; the rest are not found.
|
|
5527
5582
|
const notFound = dropped.filter((d) => !candidates.some((c) => c.for === d));
|
|
@@ -5586,7 +5641,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5586
5641
|
// answer says them.
|
|
5587
5642
|
...(ctx.noCards
|
|
5588
5643
|
? 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'}`)),
|
|
5644
|
+
: 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
5645
|
].join('\n')
|
|
5591
5646
|
: (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
|
|
5592
5647
|
extras.length
|
|
@@ -5628,7 +5683,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5628
5683
|
ctx.noCards
|
|
5629
5684
|
? 'answer with what the guest is told, for a guest who asked to see a list, a hotel or its add-ons.'
|
|
5630
5685
|
: 'would each draw a card of their own.'}`,
|
|
5631
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
5632
5686
|
]
|
|
5633
5687
|
.filter(Boolean)
|
|
5634
5688
|
.join('\n\n'),
|
|
@@ -5712,7 +5766,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5712
5766
|
text: extras.length
|
|
5713
5767
|
? extras
|
|
5714
5768
|
.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
5769
|
.join('\n')
|
|
5717
5770
|
: `${detail.hotel.name} has no add-ons listed here.`,
|
|
5718
5771
|
};
|
|
@@ -5799,7 +5852,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5799
5852
|
const here = await deps.backend.listBookings(who, typeof args.limit === 'number' ? args.limit : undefined);
|
|
5800
5853
|
// Read here, before a filter or a spread drops it: the groups whose
|
|
5801
5854
|
// bookings are not in `here` (2026-11-03 review, C5).
|
|
5802
|
-
const missing = degradedOf(deps, { degraded: here.degraded }, 'bookings');
|
|
5855
|
+
const missing = degradedOf(deps, { degraded: here.degraded }, 'bookings', ctx?.noCards);
|
|
5803
5856
|
// Elsewhere, only with the code AND the email - see `listBookings`.
|
|
5804
5857
|
const code = str(args.booking);
|
|
5805
5858
|
const { found: there, tooMany } = code && str(args.email)
|
|
@@ -6015,7 +6068,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6015
6068
|
preview.rule,
|
|
6016
6069
|
`Say the word and it moves (${t.changeBooking} with \`confirmed\`).`,
|
|
6017
6070
|
demoBookingNote(deps, preview.supplier),
|
|
6018
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6019
6071
|
]
|
|
6020
6072
|
.filter(Boolean)
|
|
6021
6073
|
.join(' ')
|
|
@@ -6078,7 +6130,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6078
6130
|
`${changed.confirmationCode} now holds ${changed.stay.checkIn} to ${changed.stay.checkOut}, ${formatMoney(changed.quote.total)} all in.`,
|
|
6079
6131
|
'The confirmation code has not changed. **Nothing has been charged.**',
|
|
6080
6132
|
demoBookingNote(deps, changed.supplier),
|
|
6081
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6082
6133
|
]
|
|
6083
6134
|
.filter(Boolean)
|
|
6084
6135
|
.join(' '),
|
|
@@ -6210,7 +6261,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6210
6261
|
? `Say the word and it is cancelled (${t.cancelBooking} with \`confirmed\`).`
|
|
6211
6262
|
: '',
|
|
6212
6263
|
demoBookingNote(deps, preview.supplier),
|
|
6213
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6214
6264
|
]
|
|
6215
6265
|
.filter(Boolean)
|
|
6216
6266
|
.join(' '),
|
|
@@ -6287,20 +6337,14 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6287
6337
|
? // Paid here in advance: what becomes of the money is the answer.
|
|
6288
6338
|
`${cancelled.confirmationCode} is ${cancelled.status === 'expired' ? 'called off before it was paid' : 'cancelled'}. ${paidLine(deps, cancelled, charged)}`
|
|
6289
6339
|
: h.notReal
|
|
6290
|
-
? //
|
|
6340
|
+
? // The fact, in no environment's word (robin, 2026-11-03); nobody charges
|
|
6341
|
+
// a booking that was never sent anything (2026-10-05 audit, P2).
|
|
6291
6342
|
`${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)}.`}`,
|
|
6343
|
+
: h.test
|
|
6344
|
+
? `${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.`}`
|
|
6345
|
+
: `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `The hotel charges ${formatMoney(charged)}.`}`,
|
|
6301
6346
|
couponNow?.said ?? '',
|
|
6302
6347
|
demoBookingNote(deps, cancelled.supplier),
|
|
6303
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6304
6348
|
]
|
|
6305
6349
|
.filter(Boolean)
|
|
6306
6350
|
.join(' '),
|
|
@@ -6384,7 +6428,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6384
6428
|
: '',
|
|
6385
6429
|
...comparison.dropped.map((d) => `Left out: ${d.name} — ${d.why}`),
|
|
6386
6430
|
...fitsManyOf(deps, comparison.dropped),
|
|
6387
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6388
6431
|
]
|
|
6389
6432
|
.filter(Boolean)
|
|
6390
6433
|
.join('\n'),
|
|
@@ -6450,11 +6493,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6450
6493
|
// With no card, until when it is free to cancel too: the reply is told to
|
|
6451
6494
|
// give each stay's terms (2026-11-02 audit, P2-3).
|
|
6452
6495
|
const termsOf = (x) => [
|
|
6453
|
-
x.
|
|
6454
|
-
?
|
|
6455
|
-
:
|
|
6456
|
-
?
|
|
6457
|
-
:
|
|
6496
|
+
x.terms
|
|
6497
|
+
? `cancellation: "${x.terms}"`
|
|
6498
|
+
: x.refundable === false
|
|
6499
|
+
? 'non-refundable'
|
|
6500
|
+
: ctx.noCards && x.freeCancellationUntil
|
|
6501
|
+
? `free to cancel until ${freeUntilText(x)}`
|
|
6502
|
+
: '',
|
|
6458
6503
|
x.payment === 'prepaid'
|
|
6459
6504
|
? x.notSoldHere
|
|
6460
6505
|
? 'paid in advance - not booked here'
|
|
@@ -6510,7 +6555,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6510
6555
|
...fitsManyOf(deps, plan.dropped),
|
|
6511
6556
|
uncheckedSaid,
|
|
6512
6557
|
ctx.noCards ? PLAN_NOT_DRAWN : PLAN_DRAWN_ONCE,
|
|
6513
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6514
6558
|
]
|
|
6515
6559
|
.filter(Boolean)
|
|
6516
6560
|
.join('\n'),
|
|
@@ -6566,7 +6610,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6566
6610
|
member.preferences?.length
|
|
6567
6611
|
? `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
6612
|
: '',
|
|
6569
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6570
6613
|
]
|
|
6571
6614
|
.filter(Boolean)
|
|
6572
6615
|
.join('\n'),
|