@curless/hotel-storefront 0.53.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/about.d.ts +24 -0
- package/dist/about.d.ts.map +1 -0
- package/dist/about.js +28 -0
- package/dist/about.js.map +1 -0
- package/dist/aggregate/make.js +2 -2
- package/dist/aggregate/make.js.map +1 -1
- package/dist/backend.d.ts +15 -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/coupon-words.d.ts +12 -0
- package/dist/coupon-words.d.ts.map +1 -0
- package/dist/coupon-words.js +19 -0
- package/dist/coupon-words.js.map +1 -0
- package/dist/dates.js +1 -1
- package/dist/dates.js.map +1 -1
- package/dist/demo-hotels.d.ts +6 -0
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +57 -17
- 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 +6 -0
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +25 -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 +33 -15
- package/dist/huazhu/catalog.js.map +1 -1
- package/dist/huazhu/client.js +1 -1
- package/dist/huazhu/client.js.map +1 -1
- package/dist/huazhu/make.d.ts.map +1 -1
- package/dist/huazhu/make.js +1 -0
- package/dist/huazhu/make.js.map +1 -1
- package/dist/huazhu/sandbox.d.ts.map +1 -1
- package/dist/huazhu/sandbox.js +5 -3
- package/dist/huazhu/sandbox.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 -9
- 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 -3
- 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 +8 -6
- package/dist/server.js.map +1 -1
- package/dist/supplier.d.ts +6 -0
- package/dist/supplier.d.ts.map +1 -1
- package/dist/supplier.js +4 -0
- package/dist/supplier.js.map +1 -1
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +100 -71
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +19 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +204 -102
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vocab.d.ts +0 -14
- package/dist/vocab.d.ts.map +1 -1
- package/dist/vocab.js +19 -22
- package/dist/vocab.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { aboutLines } from './about.js';
|
|
1
2
|
import { DEFAULT_PAGE, FULL_CAPABILITIES, LASTING_REQUESTS, MAX_PAGE, REQUEST_KINDS, REQUEST_NOTE_MAX, guestTooLong, notPricedForNoAnswer, unansweredBy, unitWords, } from './backend.js';
|
|
3
|
+
import { usedOnWords } from './coupon-words.js';
|
|
2
4
|
import { MAX_GUESTS, addDays, isDay, nights as nightsBetween } from './dates.js';
|
|
3
5
|
import { FACILITIES } from './facilities.js';
|
|
4
6
|
import { DESCRIPTION_MAX, POLICY_MAX, hotelName, hotelText } from './fence.js';
|
|
@@ -12,7 +14,7 @@ import { regionOf } from './regions.js';
|
|
|
12
14
|
import { activeKeysOf, byRoomType, facetsOf, keyWords, matchesTerms, optionsOf, roomTermsOf, termWords, } from './room-filters.js';
|
|
13
15
|
import { SAME_BOOKING_MS } from './same-booking.js';
|
|
14
16
|
import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, clampCount, pickedKey, shownOf, stampOf, tripKey, } from './trip.js';
|
|
15
|
-
import {
|
|
17
|
+
import { branded, catalogue, upFirst } from './vocab.js';
|
|
16
18
|
// The eleven things a guest can do: find hotels, look at one on dates, see what
|
|
17
19
|
// else it offers, build a trip, ask for the rooms, and deal with a booking they
|
|
18
20
|
// already have.
|
|
@@ -143,7 +145,29 @@ export const withheldTools = (n, caps) => new Set([
|
|
|
143
145
|
...(caps.changes || caps.extras ? [] : [n.previewChange, n.changeBooking]),
|
|
144
146
|
...(caps.policies === false ? [n.policies] : []),
|
|
145
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
|
+
: []),
|
|
146
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.`;
|
|
147
171
|
const forCapabilities = (tools, n, v, caps) => {
|
|
148
172
|
const withheld = withheldTools(n, caps);
|
|
149
173
|
const without = (schema, ...keys) => {
|
|
@@ -294,18 +318,16 @@ const forCapabilities = (tools, n, v, caps) => {
|
|
|
294
318
|
if (t.name === n.requestBooking && caps.booking === 'pay-at-hotel' && !v.market) {
|
|
295
319
|
out = {
|
|
296
320
|
...out,
|
|
297
|
-
// ⚠️ A
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
description: v.test
|
|
301
|
-
? `
|
|
302
|
-
? '
|
|
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.'
|
|
303
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.`
|
|
304
|
-
: v.
|
|
305
|
-
? `Makes a booking in a simulated copy of ${v.brand}'s booking system, for testing: each stay gets its own confirmation code there, and none of it is a ${v.brand} reservation or holds a real room. ${caps.prepaid
|
|
306
|
-
? 'A rate marked paid in advance is booked on its own, and not at once: the answer gives a card page in test mode 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.'
|
|
307
|
-
: '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.`
|
|
308
|
-
: `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.`,
|
|
309
331
|
};
|
|
310
332
|
}
|
|
311
333
|
}
|
|
@@ -592,11 +614,12 @@ const allTools = (n, v) => {
|
|
|
592
614
|
// could take the code to the real hotel.
|
|
593
615
|
description: v.market
|
|
594
616
|
? // ⚠️ THE GROUP HOLDS THE BOOKING, NOT THE MARKET - and what a booking
|
|
595
|
-
// makes is the group's: a hold, a
|
|
596
|
-
// at the hotel. The answer says which, stay by stay.
|
|
597
|
-
|
|
617
|
+
// makes is the group's: a hold, a booking that holds no room, or a
|
|
618
|
+
// reservation paid at the hotel. The answer says which, stay by stay.
|
|
619
|
+
// ⚠️ NO "DEMONSTRATION" / "TEST" / "SIMULATED" (robin, 2026-11-03).
|
|
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.`
|
|
598
621
|
: v.demo
|
|
599
|
-
? `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.`
|
|
600
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.`,
|
|
601
624
|
annotations: books(v.market ? `Request a booking on ${v.brand}` : `Request a ${v.brand} booking`, open),
|
|
602
625
|
inputSchema: {
|
|
@@ -1706,6 +1729,7 @@ const holderOf = (deps, supplier) => {
|
|
|
1706
1729
|
demo: s.mode !== 'live',
|
|
1707
1730
|
sandbox: s.mode === 'sandbox',
|
|
1708
1731
|
test: s.mode === 'test',
|
|
1732
|
+
notReal: s.mode !== 'live' && s.mode !== 'test',
|
|
1709
1733
|
};
|
|
1710
1734
|
return {
|
|
1711
1735
|
name: deps.vocab.brand,
|
|
@@ -1713,6 +1737,7 @@ const holderOf = (deps, supplier) => {
|
|
|
1713
1737
|
demo: deps.vocab.demo,
|
|
1714
1738
|
sandbox: !!deps.vocab.sandbox,
|
|
1715
1739
|
test: !!deps.vocab.test,
|
|
1740
|
+
notReal: deps.vocab.demo && !deps.vocab.test,
|
|
1716
1741
|
};
|
|
1717
1742
|
};
|
|
1718
1743
|
/**
|
|
@@ -1791,10 +1816,13 @@ const paidLine = (deps, b, fee) => {
|
|
|
1791
1816
|
const back = formatMoney(p.refundAmount ?? p.amount);
|
|
1792
1817
|
const whole = !p.refundAmount || p.refundAmount.minor === p.amount.minor;
|
|
1793
1818
|
const unionpay = p.method === 'unionpay';
|
|
1794
|
-
|
|
1795
|
-
|
|
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.
|
|
1822
|
+
const test = h.notReal
|
|
1823
|
+
? ` No real money moves, and this booking was not sent to ${h.name}.`
|
|
1796
1824
|
: h.test
|
|
1797
|
-
?
|
|
1825
|
+
? ' No real money moves, and this booking holds no room for anyone.'
|
|
1798
1826
|
: '';
|
|
1799
1827
|
const ended = b.status === 'cancelled' || b.status === 'expired' || b.status === 'failed';
|
|
1800
1828
|
const said = (() => {
|
|
@@ -1826,7 +1854,7 @@ const paidLine = (deps, b, fee) => {
|
|
|
1826
1854
|
case 'refunded':
|
|
1827
1855
|
return `${whole ? `All ${back}` : `${back} of the ${amount} paid`} has gone back to the card it was paid with${p.refundReason ? ` - ${p.refundReason}` : ''}.`;
|
|
1828
1856
|
case 'hotel-refunds':
|
|
1829
|
-
return `The money is with ${h.name}, who charged the one-time card UnionPay issued: what its terms give back${fee && fee.minor > 0 ? ` after its fee of ${formatMoney(fee)}` : ''}, it returns to that card -
|
|
1857
|
+
return `The money is with ${h.name}, who charged the one-time card UnionPay issued: what its terms give back${fee && fee.minor > 0 ? ` after its fee of ${formatMoney(fee)}` : ''}, it returns to that card - nothing is refunded from here${p.refundReason ? ` (${p.refundReason})` : ''}.`;
|
|
1830
1858
|
}
|
|
1831
1859
|
}
|
|
1832
1860
|
switch (p.status) {
|
|
@@ -1860,7 +1888,7 @@ const paidLine = (deps, b, fee) => {
|
|
|
1860
1888
|
case 'refunded':
|
|
1861
1889
|
return `${whole ? `All ${back}` : `${back} of the ${amount} paid`} has gone back to the card it was paid with${p.refundReason ? ` - ${p.refundReason}` : ''}.`;
|
|
1862
1890
|
case 'hotel-refunds':
|
|
1863
|
-
return `The money is with ${h.name}, who returns what its terms give back to the card it charged -
|
|
1891
|
+
return `The money is with ${h.name}, who returns what its terms give back to the card it charged - nothing is refunded from here.`;
|
|
1864
1892
|
}
|
|
1865
1893
|
})();
|
|
1866
1894
|
return `${said}${test}`;
|
|
@@ -1902,10 +1930,11 @@ const heldLine = (deps, b) => {
|
|
|
1902
1930
|
const total = formatMoney(b.quote.total);
|
|
1903
1931
|
const h = holderOf(deps, b.supplier);
|
|
1904
1932
|
if (h.caps.booking === 'pay-at-hotel') {
|
|
1905
|
-
|
|
1906
|
-
|
|
1933
|
+
// ⚠️ THE FACT, IN NO ENVIRONMENT'S WORD (robin, 2026-11-03).
|
|
1934
|
+
if (h.notReal)
|
|
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.`;
|
|
1907
1936
|
if (h.test)
|
|
1908
|
-
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.`;
|
|
1909
1938
|
return b.status === 'requested'
|
|
1910
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.`
|
|
1911
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.`;
|
|
@@ -1940,10 +1969,11 @@ const demoHoldWords = (b, now) => {
|
|
|
1940
1969
|
: minutes < 48 * 60
|
|
1941
1970
|
? `in about ${Math.round(minutes / 60)} hours`
|
|
1942
1971
|
: `in about ${Math.round(minutes / (24 * 60))} days`;
|
|
1943
|
-
|
|
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).`;
|
|
1944
1974
|
};
|
|
1945
|
-
/** What a
|
|
1946
|
-
const demoBookingWords = (h) => `This
|
|
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.`;
|
|
1947
1977
|
/**
|
|
1948
1978
|
* Said on every answer about a demonstration booking - made, changed or
|
|
1949
1979
|
* cancelled - and on nothing else: a hold of the demo's that a guest took for
|
|
@@ -1992,7 +2022,10 @@ const common = (deps, key) => {
|
|
|
1992
2022
|
? { market: { name: deps.vocab.brand, suppliers: deps.backend.suppliers() } }
|
|
1993
2023
|
: {}),
|
|
1994
2024
|
vocab: { noun: deps.vocab.noun, nounPlural: deps.vocab.nounPlural },
|
|
1995
|
-
|
|
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,
|
|
1996
2029
|
...(key ? memberBadge(deps, key) : {}),
|
|
1997
2030
|
...(langOf(deps, key) ? { lang: langOf(deps, key) } : {}),
|
|
1998
2031
|
};
|
|
@@ -2012,6 +2045,8 @@ const langOf = (deps, _key) => deps.lang;
|
|
|
2012
2045
|
* hotel's own clock when the backend knows it, else the instant in UTC - never
|
|
2013
2046
|
* the date alone (see `atHotelTime`).
|
|
2014
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;
|
|
2015
2050
|
const freeUntilText = (r) => r.freeCancellationLocal ??
|
|
2016
2051
|
`${(r.freeCancellationUntil ?? '').slice(0, 16).replace('T', ' ')} UTC`;
|
|
2017
2052
|
/** Any Han character: a name already in Chinese needs none beside it. */
|
|
@@ -2089,11 +2124,15 @@ const quoteForCard = (quote, groupOf) => ({
|
|
|
2089
2124
|
// stepper read 3 and sent 3 ± 1 as the new count (2026-09-28 audit).
|
|
2090
2125
|
...(l.ordered !== undefined ? { ordered: l.ordered } : {}),
|
|
2091
2126
|
unit: formatMoney(l.unit),
|
|
2127
|
+
...(l.publicUnit ? { publicUnit: formatMoney(l.publicUnit) } : {}),
|
|
2092
2128
|
total: formatMoney(l.total),
|
|
2093
2129
|
// The line with its taxes in, where the backend knows them per line: what the
|
|
2094
2130
|
// review and the trip show, as the hotel's page does (r10 #23).
|
|
2095
2131
|
...(l.totalWithTaxes ? { totalWithTaxes: formatMoney(l.totalWithTaxes) } : {}),
|
|
2096
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 } : {}),
|
|
2097
2136
|
freeCancellationUntil: l.freeCancellationUntil,
|
|
2098
2137
|
// The deadline on the hotel's clock, where the backend knows it - the
|
|
2099
2138
|
// review restates the terms beside the total (round-2 jury).
|
|
@@ -2118,6 +2157,24 @@ const quoteForCard = (quote, groupOf) => ({
|
|
|
2118
2157
|
// The review's own order - public, member, coupon, total - each
|
|
2119
2158
|
// figure the server's, so the card adds nothing up (10-30, ①).
|
|
2120
2159
|
publicTotal: formatMoney(quote.member.publicTotal),
|
|
2160
|
+
// The room charges at the public rate, the taxes a row of their own -
|
|
2161
|
+
// the receipt every guest gets: room, taxes, less the tier, less the
|
|
2162
|
+
// coupon, is the total (0.54.0, M1: a member's started from the
|
|
2163
|
+
// public rate taxes in, a guest's from the room before them).
|
|
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
|
+
: {}),
|
|
2121
2178
|
memberTotal: formatMoney(money(quote.member.publicTotal.minor - quote.member.saved.minor, quote.total.currency)),
|
|
2122
2179
|
},
|
|
2123
2180
|
}
|
|
@@ -2158,7 +2215,7 @@ const couponTerms = (c, brand) => [
|
|
|
2158
2215
|
c.state === 'expired'
|
|
2159
2216
|
? `ran out on ${c.validUntil} - cannot be used`
|
|
2160
2217
|
: c.state === 'used'
|
|
2161
|
-
?
|
|
2218
|
+
? `already used ${usedOnWords(c.usedOn)}`
|
|
2162
2219
|
: c.state === 'not-yet'
|
|
2163
2220
|
? `usable from ${c.validFrom} to ${c.validUntil}`
|
|
2164
2221
|
: `until ${c.validUntil}`,
|
|
@@ -2248,13 +2305,11 @@ const memberTripLines = (q) => {
|
|
|
2248
2305
|
const couponRefusedLine = (q) => q.couponRefused
|
|
2249
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.`
|
|
2250
2307
|
: '';
|
|
2251
|
-
const tripText = (quote,
|
|
2308
|
+
const tripText = (quote, noCards) => {
|
|
2252
2309
|
// ⚠️ NOT "EMPTY" WHEN SOMETHING WAS LEFT OUT: a trip whose only group did not
|
|
2253
2310
|
// answer is not an empty trip (2026-11-03 review, C5).
|
|
2254
2311
|
if (quote.lines.length === 0 && quote.dropped?.length)
|
|
2255
|
-
return
|
|
2256
|
-
.filter(Boolean)
|
|
2257
|
-
.join('\n');
|
|
2312
|
+
return quote.dropped.map((d) => `Left out: ${d.title} — ${d.why}`).join('\n');
|
|
2258
2313
|
if (quote.lines.length === 0)
|
|
2259
2314
|
return 'The trip is empty.';
|
|
2260
2315
|
const lines = quote.lines.map((l) => l.kind === 'room'
|
|
@@ -2266,7 +2321,6 @@ const tripText = (quote, demo, noCards) => {
|
|
|
2266
2321
|
couponRefusedLine(quote),
|
|
2267
2322
|
...memberTripLines(quote),
|
|
2268
2323
|
...(quote.dropped ?? []).map((d) => `Left out: ${d.title} — ${d.why}`),
|
|
2269
|
-
demo ? DEMO_NOTE : '',
|
|
2270
2324
|
]
|
|
2271
2325
|
.filter(Boolean)
|
|
2272
2326
|
.join('\n');
|
|
@@ -2515,6 +2569,8 @@ const labelWords = (r) => {
|
|
|
2515
2569
|
];
|
|
2516
2570
|
return words.length ? ` [${words.join(', ')}]` : '';
|
|
2517
2571
|
};
|
|
2572
|
+
/** What a list and a page both say of a hotel with nothing on sale here: other nights would not help. */
|
|
2573
|
+
const NOT_SOLD_OUT = 'not sold out, and other nights make no difference';
|
|
2518
2574
|
/**
|
|
2519
2575
|
* What is said of rates listed but not booked here (`Room.notSoldHere`) - in
|
|
2520
2576
|
* the text and, word for word, in the structure (`notSoldHere`): the model reads
|
|
@@ -2522,15 +2578,34 @@ const labelWords = (r) => {
|
|
|
2522
2578
|
* card says no more than 预付 on them (robin, 2026-10-30), so the assistant is
|
|
2523
2579
|
* the one who knows - and says it when the guest wants one.
|
|
2524
2580
|
*/
|
|
2525
|
-
export const notSoldHereWords = (group) => `Rates marked paid in advance are listed as ${group} sells them, but a stay paid in advance cannot be booked here yet
|
|
2581
|
+
export const notSoldHereWords = (group) => `Rates marked paid in advance are listed as ${group} sells them, but a stay paid in advance cannot be booked here yet; only rates paid at the hotel can be. If the guest wants one, say so plainly - nothing is booked or charged - and offer a rate paid at the hotel, other dates, or booking it with ${group} directly.`;
|
|
2582
|
+
/**
|
|
2583
|
+
* A hotel none of whose rooms are on sale here (`none-on-sale`), said on its page
|
|
2584
|
+
* with the way out (2026-11-03 review, T5). The page's text had one sentence -
|
|
2585
|
+
* "This hotel has no rooms on sale here." - and the card said where to book it
|
|
2586
|
+
* instead, so a client that draws no card left the guest with a dead end. The
|
|
2587
|
+
* clause is the list's (see `NOT_SOLD_OUT`), so the list and the page say one
|
|
2588
|
+
* thing; the group is the deployment's own, or the hotel's in a market, never a
|
|
2589
|
+
* name written into the sentence. In the text and, word for word, in the
|
|
2590
|
+
* structure (`noRooms`): the model reads the structure.
|
|
2591
|
+
*/
|
|
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.`;
|
|
2526
2593
|
/** The group a hotel is with, by name - the deployment's own brand outside a market. */
|
|
2527
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';
|
|
2528
2599
|
/** What a rate says about cancelling, as the text puts it. */
|
|
2529
|
-
const cancelWords = (r) =>
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
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';
|
|
2534
2609
|
/**
|
|
2535
2610
|
* What a room is, in a parenthesis: its beds, how many it sleeps and - for one
|
|
2536
2611
|
* rate, not a type whose rates differ - breakfast, unless the rate's labels
|
|
@@ -2691,7 +2766,11 @@ const heldRoomsWords = (b) => b.quote.lines
|
|
|
2691
2766
|
: l.freeCancellationUntil
|
|
2692
2767
|
? `, free to cancel until ${freeUntilText(l)}`
|
|
2693
2768
|
: l.refundable === false
|
|
2694
|
-
?
|
|
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'
|
|
2695
2774
|
: l.refundable
|
|
2696
2775
|
? ', past its free cancellation - cancelling has a fee'
|
|
2697
2776
|
: ''}`)
|
|
@@ -2921,12 +3000,19 @@ const listAnd = (xs) => xs.length > 1 ? `${xs.slice(0, -1).join(', ')} and ${xs.
|
|
|
2921
3000
|
*/
|
|
2922
3001
|
const degradedOf = (deps, answer,
|
|
2923
3002
|
/** What is missing from the list: the hotels of a search, the bookings of "my bookings". */
|
|
2924
|
-
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) => {
|
|
2925
3011
|
const down = answer.degraded ?? [];
|
|
2926
3012
|
if (down.length === 0)
|
|
2927
3013
|
return { structured: {}, said: '' };
|
|
2928
3014
|
const one = down.length === 1;
|
|
2929
|
-
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.`;
|
|
2930
3016
|
return {
|
|
2931
3017
|
structured: {
|
|
2932
3018
|
degraded: down.map((d) => ({ supplier: d.supplier, name: d.name })),
|
|
@@ -3234,7 +3320,9 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3234
3320
|
// came back "needs a phone number". Whose need it is is the trip's groups',
|
|
3235
3321
|
// as `requestBooking` judges it.
|
|
3236
3322
|
const nextStep = ctx?.noCards && parts.some((p) => p.lines.some((l) => l.kind === 'room'))
|
|
3237
|
-
?
|
|
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.`
|
|
3238
3326
|
: '';
|
|
3239
3327
|
if (parts.length === 1 && only) {
|
|
3240
3328
|
return {
|
|
@@ -3248,13 +3336,7 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3248
3336
|
count: lines.length,
|
|
3249
3337
|
...(nextStep ? { nextStep } : {}),
|
|
3250
3338
|
},
|
|
3251
|
-
text: [
|
|
3252
|
-
tripText(only, false, ctx?.noCards),
|
|
3253
|
-
...overlaps.map((o) => o.said),
|
|
3254
|
-
orphans.line,
|
|
3255
|
-
nextStep,
|
|
3256
|
-
deps.vocab.demo && lines.length ? noteFor(deps.vocab) : '',
|
|
3257
|
-
]
|
|
3339
|
+
text: [tripText(only, ctx?.noCards), ...overlaps.map((o) => o.said), orphans.line, nextStep]
|
|
3258
3340
|
.filter(Boolean)
|
|
3259
3341
|
.join('\n'),
|
|
3260
3342
|
};
|
|
@@ -3296,11 +3378,10 @@ const currentTrip = async (deps, key, who, store = key, coupon, ctx) => {
|
|
|
3296
3378
|
},
|
|
3297
3379
|
text: [
|
|
3298
3380
|
`This trip is in ${parts.length} currencies, priced separately and not converted - each hotel is booked and paid in its own.`,
|
|
3299
|
-
...parts.map((q) => tripText(q,
|
|
3381
|
+
...parts.map((q) => tripText(q, ctx?.noCards)),
|
|
3300
3382
|
...overlaps.map((o) => o.said),
|
|
3301
3383
|
orphans.line,
|
|
3302
3384
|
nextStep,
|
|
3303
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
3304
3385
|
]
|
|
3305
3386
|
.filter(Boolean)
|
|
3306
3387
|
.join('\n\n'),
|
|
@@ -3709,11 +3790,13 @@ const comparedInWords = (c) => {
|
|
|
3709
3790
|
const priced = p
|
|
3710
3791
|
? [
|
|
3711
3792
|
`${withZh(p.room, p.roomNames, true)} at ${p.nightly} ${nightWords(true)}, ${p.stayTotal} for the stay incl. taxes and fees`,
|
|
3712
|
-
p.
|
|
3713
|
-
?
|
|
3714
|
-
: p.
|
|
3715
|
-
?
|
|
3716
|
-
:
|
|
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',
|
|
3717
3800
|
p.payment === 'prepaid'
|
|
3718
3801
|
? p.notSoldHere
|
|
3719
3802
|
? 'paid in advance - not booked here'
|
|
@@ -4023,7 +4106,6 @@ const bookedFromThisForm = async (deps, ctx, key, args) => {
|
|
|
4023
4106
|
notUsed?.said ?? '',
|
|
4024
4107
|
`**${confirming(deps, b) ? 'Being confirmed' : b.confirmationCode}** · ${b.hotelName} · ${stayWords(b.stay, b.hourly)}`,
|
|
4025
4108
|
bookedLine(deps, b) ?? '',
|
|
4026
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
4027
4109
|
]
|
|
4028
4110
|
.filter(Boolean)
|
|
4029
4111
|
.join('\n'),
|
|
@@ -4087,7 +4169,7 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
|
|
|
4087
4169
|
const prepaidNotHere = (room, open, stay, when) => {
|
|
4088
4170
|
const group = groupNameOf(deps, hotelId);
|
|
4089
4171
|
const said = [
|
|
4090
|
-
`Nothing was booked or charged: ${room.name} is paid in advance, and a stay paid in advance cannot be booked here yet -
|
|
4172
|
+
`Nothing was booked or charged: ${room.name} is paid in advance, and a stay paid in advance cannot be booked here yet - only rates paid at the hotel can be.`,
|
|
4091
4173
|
open.length
|
|
4092
4174
|
? `Paid at the hotel at ${detail.hotel.name} ${when}: ${open.map((r) => `${r.name} (\`${r.id}\`)`).join(', ')}.`
|
|
4093
4175
|
: `No rate at ${detail.hotel.name} ${when} is paid at the hotel.`,
|
|
@@ -4604,7 +4686,6 @@ const bookTripLines = async (deps, t, ctx, key, store, args, nothingThere) => {
|
|
|
4604
4686
|
...failedText,
|
|
4605
4687
|
remembered?.line ?? '',
|
|
4606
4688
|
others ? `The trip's other stays are still in it, not booked:\n${rest?.text ?? ''}` : '',
|
|
4607
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
4608
4689
|
]
|
|
4609
4690
|
.filter(Boolean)
|
|
4610
4691
|
.join('\n'),
|
|
@@ -4764,7 +4845,15 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4764
4845
|
// backend contract, but "the next backend honours it" is not something the
|
|
4765
4846
|
// card can be built on - and the cost of it being wrong is the whole group
|
|
4766
4847
|
// in the model's context.
|
|
4767
|
-
|
|
4848
|
+
// ⚠️ A HOTEL NOT ON SALE HERE SAYS WHERE IT IS (review r2, T5): its page said
|
|
4849
|
+
// the group's own channels, its tile on a list - the answer a model often
|
|
4850
|
+
// stops at - only that nothing was on sale. In the text and the structure.
|
|
4851
|
+
const hotels = answer.hotels.slice(0, MAX_PAGE).map((h) => h.noRoomsForStayCode === 'none-on-sale' && h.noRoomsForStay
|
|
4852
|
+
? {
|
|
4853
|
+
...h,
|
|
4854
|
+
noRoomsForStay: `${h.noRoomsForStay.replace(/[.\s]+$/, '')} - it can be booked through ${groupNameOf(deps, h.id)}'s own channels: its app, its website or by phone.`,
|
|
4855
|
+
}
|
|
4856
|
+
: h);
|
|
4768
4857
|
const offset = asked.offset ?? 0;
|
|
4769
4858
|
const total = Math.max(answer.total, offset + hotels.length);
|
|
4770
4859
|
const cities = [...new Set(hotels.map((h) => h.city))];
|
|
@@ -4936,7 +5025,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4936
5025
|
// assistant ask which city to list the rest in (2026-10-15 smoke run).
|
|
4937
5026
|
// And not "tell me another city": the second run said there was one
|
|
4938
5027
|
// hotel, then offered to look in another city (2026-10-15).
|
|
4939
|
-
const inAll = `
|
|
5028
|
+
const inAll = `There is one ${branded(deps.vocab, deps.vocab.noun)} here in all, ${sole.name}${sole.city ? ` in ${sole.city}` : ''}. There is no other hotel and no other city here - nothing else to search for or to offer.`;
|
|
4940
5029
|
return {
|
|
4941
5030
|
...page,
|
|
4942
5031
|
// In both, in the same words: a host may hand its model this rather
|
|
@@ -4980,7 +5069,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
4980
5069
|
: `${c.name} (${c.count})`)
|
|
4981
5070
|
.join(', ');
|
|
4982
5071
|
const n = deps.vocab.nounPlural;
|
|
4983
|
-
const degraded = degradedOf(deps, answer);
|
|
5072
|
+
const degraded = degradedOf(deps, answer, undefined, ctx?.noCards);
|
|
4984
5073
|
if (namedCountry) {
|
|
4985
5074
|
const [only] = countries;
|
|
4986
5075
|
return {
|
|
@@ -5017,13 +5106,19 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5017
5106
|
ask: { ...stayArgs },
|
|
5018
5107
|
...(ask.stay ? { stay: ask.stay } : {}),
|
|
5019
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
|
+
: {}),
|
|
5020
5115
|
...degraded.structured,
|
|
5021
5116
|
},
|
|
5022
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}`,
|
|
5023
5118
|
};
|
|
5024
5119
|
}
|
|
5025
5120
|
if (hotels.length === 0) {
|
|
5026
|
-
const degraded = degradedOf(deps, answer);
|
|
5121
|
+
const degraded = degradedOf(deps, answer, undefined, ctx?.noCards);
|
|
5027
5122
|
const nearest = (answer.nearest ?? []).map((h) => ({
|
|
5028
5123
|
...h,
|
|
5029
5124
|
summary: hotelText('description', h.name, h.summary, DESCRIPTION_MAX).text,
|
|
@@ -5158,7 +5253,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5158
5253
|
: ''}${
|
|
5159
5254
|
// The other kind, said as its own (U11): other nights will not help.
|
|
5160
5255
|
hotels.some((h) => h.noRoomsForStayCode === 'none-on-sale')
|
|
5161
|
-
? `; ${hotels.filter((h) => h.noRoomsForStayCode === 'none-on-sale').length} in this answer ${hotels.filter((h) => h.noRoomsForStayCode === 'none-on-sale').length === 1 ? 'has' : 'have'} no rooms on sale
|
|
5256
|
+
? `; ${hotels.filter((h) => h.noRoomsForStayCode === 'none-on-sale').length} in this answer ${hotels.filter((h) => h.noRoomsForStayCode === 'none-on-sale').length === 1 ? 'has' : 'have'} no rooms on sale here - ${NOT_SOLD_OUT}`
|
|
5162
5257
|
: ''}.`
|
|
5163
5258
|
: '',
|
|
5164
5259
|
].filter(Boolean);
|
|
@@ -5170,9 +5265,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5170
5265
|
priceNotes.push(notSold);
|
|
5171
5266
|
// Every one by name and city, and that there are no others - or, told
|
|
5172
5267
|
// only "2 hotels", the model asked for the list again to learn which.
|
|
5173
|
-
const degraded = degradedOf(deps, answer);
|
|
5268
|
+
const degraded = degradedOf(deps, answer, undefined, ctx?.noCards);
|
|
5174
5269
|
const inAll = homeList
|
|
5175
|
-
? `
|
|
5270
|
+
? `There are ${total} ${branded(deps.vocab, deps.vocab.nounPlural)} here in all, and this answer lists every one: ${hotels
|
|
5176
5271
|
.map((h) => `${hotelName(h.name)}${h.city ? ` in ${h.city}` : ''}`)
|
|
5177
5272
|
.join('; ')}. There is no other ${deps.vocab.noun} and no other city here - nothing else to list, search for or offer.`
|
|
5178
5273
|
: '';
|
|
@@ -5276,7 +5371,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5276
5371
|
};
|
|
5277
5372
|
}
|
|
5278
5373
|
const h = detail.hotel;
|
|
5279
|
-
const description = hotelText('description', h.name,
|
|
5374
|
+
const description = hotelText('description', h.name,
|
|
5375
|
+
// Its 【…】 parts a line each, the words as they came (`aboutLines`).
|
|
5376
|
+
aboutLines(h.summary), DESCRIPTION_MAX, deps.vocab.demo);
|
|
5280
5377
|
const policies = await deps.backend.policies(h.id);
|
|
5281
5378
|
const extras = await deps.backend.extrasFor(h.id, stay);
|
|
5282
5379
|
const base = {
|
|
@@ -5323,17 +5420,23 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5323
5420
|
: [];
|
|
5324
5421
|
const flex = dateFlex(dates, stay);
|
|
5325
5422
|
if (detail.noRooms || !detail.rooms) {
|
|
5423
|
+
// ⚠️ NOTHING ON SALE HERE SAYS WHERE TO BOOK IT (T5): the same sentence
|
|
5424
|
+
// in the text and in `noRooms`, which the card does not draw for this code
|
|
5425
|
+
// (it says it in the guest's language from `noRoomsCode`).
|
|
5426
|
+
const said = detail.noRooms && detail.noRoomsCode === 'none-on-sale'
|
|
5427
|
+
? notOnSaleWords(groupNameOf(deps, h.id), detail.noRooms)
|
|
5428
|
+
: detail.noRooms;
|
|
5326
5429
|
return {
|
|
5327
5430
|
structured: {
|
|
5328
5431
|
view: 'no-rooms',
|
|
5329
5432
|
...base,
|
|
5330
5433
|
stay,
|
|
5331
|
-
noRooms:
|
|
5434
|
+
noRooms: said,
|
|
5332
5435
|
...(detail.noRoomsCode ? { noRoomsCode: detail.noRoomsCode } : {}),
|
|
5333
5436
|
...(detail.noRoomsArgs ? { noRoomsArgs: detail.noRoomsArgs } : {}),
|
|
5334
5437
|
...flex.structured,
|
|
5335
5438
|
},
|
|
5336
|
-
text: [
|
|
5439
|
+
text: [said ?? 'Nothing is available for those nights.', flex.text]
|
|
5337
5440
|
.filter(Boolean)
|
|
5338
5441
|
.join(' '),
|
|
5339
5442
|
};
|
|
@@ -5368,12 +5471,21 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5368
5471
|
const notSold = enough.some((r) => r.notSoldHere)
|
|
5369
5472
|
? notSoldHereWords(groupNameOf(deps, h.id))
|
|
5370
5473
|
: '';
|
|
5371
|
-
const rooms = enough.map((r) => ({
|
|
5474
|
+
const rooms = enough.map(({ roomsLeft, refundable, ...r }) => ({
|
|
5372
5475
|
...r,
|
|
5373
|
-
|
|
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 }),
|
|
5374
5481
|
nightly: formatMoney(r.nightly),
|
|
5375
5482
|
stayTotal: formatMoney(r.stayTotal),
|
|
5376
|
-
|
|
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),
|
|
5377
5489
|
// ⚠️ THE NUMBER THE GUEST IS LOOKING AT HAS TO BE THE ONE THEY ASKED FOR.
|
|
5378
5490
|
// With two rooms picked, the card was still showing one room's total and
|
|
5379
5491
|
// nothing said so - robin changed the count and watched the price not
|
|
@@ -5422,7 +5534,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5422
5534
|
? `${flex.text} (That is the cheapest room of any kind, not only one with ${termWords(terms)}.)`
|
|
5423
5535
|
: flex.text,
|
|
5424
5536
|
notSold,
|
|
5425
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
5426
5537
|
]
|
|
5427
5538
|
.filter(Boolean)
|
|
5428
5539
|
.join('\n'),
|
|
@@ -5465,7 +5576,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5465
5576
|
total = found.total;
|
|
5466
5577
|
degradedFound = found;
|
|
5467
5578
|
}
|
|
5468
|
-
const degraded = degradedOf(deps, degradedFound);
|
|
5579
|
+
const degraded = degradedOf(deps, degradedFound, undefined, ctx?.noCards);
|
|
5469
5580
|
const unique = [...new Set(ids)];
|
|
5470
5581
|
// A name that fits more than one is said with the ones it fits; the rest are not found.
|
|
5471
5582
|
const notFound = dropped.filter((d) => !candidates.some((c) => c.for === d));
|
|
@@ -5501,7 +5612,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5501
5612
|
continue;
|
|
5502
5613
|
const h = detail.hotel;
|
|
5503
5614
|
read.push({ id: h.id, name: h.name });
|
|
5504
|
-
const about = hotelText('description', h.name, [h.summary, h.amenities.length ? `Has: ${h.amenities.join(', ')}.` : '']
|
|
5615
|
+
const about = hotelText('description', h.name, [aboutLines(h.summary), h.amenities.length ? `Has: ${h.amenities.join(', ')}.` : '']
|
|
5505
5616
|
.filter(Boolean)
|
|
5506
5617
|
.join('\n'), DESCRIPTION_MAX, deps.vocab.demo);
|
|
5507
5618
|
const extras = await deps.backend.extrasFor(h.id, stay);
|
|
@@ -5530,7 +5641,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5530
5641
|
// answer says them.
|
|
5531
5642
|
...(ctx.noCards
|
|
5532
5643
|
? roomTypeLines(rooms, roomsWanted, undefined, true)
|
|
5533
|
-
: 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'}`)),
|
|
5534
5645
|
].join('\n')
|
|
5535
5646
|
: (detail.noRooms ?? `No room type here has ${roomsWanted} left for those nights.`),
|
|
5536
5647
|
extras.length
|
|
@@ -5572,7 +5683,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5572
5683
|
ctx.noCards
|
|
5573
5684
|
? 'answer with what the guest is told, for a guest who asked to see a list, a hotel or its add-ons.'
|
|
5574
5685
|
: 'would each draw a card of their own.'}`,
|
|
5575
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
5576
5686
|
]
|
|
5577
5687
|
.filter(Boolean)
|
|
5578
5688
|
.join('\n\n'),
|
|
@@ -5656,7 +5766,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5656
5766
|
text: extras.length
|
|
5657
5767
|
? extras
|
|
5658
5768
|
.map((e) => `- ${e.name}${ctx.noCards ? ` (\`${e.id}\`)` : ''} — ${formatMoney(e.price)} ${unitWords(e.per)}${held[e.id] ? ` (${booked?.confirmationCode} holds ${held[e.id]})` : ''}`)
|
|
5659
|
-
.concat(deps.vocab.demo ? [noteFor(deps.vocab)] : [])
|
|
5660
5769
|
.join('\n')
|
|
5661
5770
|
: `${detail.hotel.name} has no add-ons listed here.`,
|
|
5662
5771
|
};
|
|
@@ -5743,7 +5852,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5743
5852
|
const here = await deps.backend.listBookings(who, typeof args.limit === 'number' ? args.limit : undefined);
|
|
5744
5853
|
// Read here, before a filter or a spread drops it: the groups whose
|
|
5745
5854
|
// bookings are not in `here` (2026-11-03 review, C5).
|
|
5746
|
-
const missing = degradedOf(deps, { degraded: here.degraded }, 'bookings');
|
|
5855
|
+
const missing = degradedOf(deps, { degraded: here.degraded }, 'bookings', ctx?.noCards);
|
|
5747
5856
|
// Elsewhere, only with the code AND the email - see `listBookings`.
|
|
5748
5857
|
const code = str(args.booking);
|
|
5749
5858
|
const { found: there, tooMany } = code && str(args.email)
|
|
@@ -5959,7 +6068,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
5959
6068
|
preview.rule,
|
|
5960
6069
|
`Say the word and it moves (${t.changeBooking} with \`confirmed\`).`,
|
|
5961
6070
|
demoBookingNote(deps, preview.supplier),
|
|
5962
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
5963
6071
|
]
|
|
5964
6072
|
.filter(Boolean)
|
|
5965
6073
|
.join(' ')
|
|
@@ -6022,7 +6130,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6022
6130
|
`${changed.confirmationCode} now holds ${changed.stay.checkIn} to ${changed.stay.checkOut}, ${formatMoney(changed.quote.total)} all in.`,
|
|
6023
6131
|
'The confirmation code has not changed. **Nothing has been charged.**',
|
|
6024
6132
|
demoBookingNote(deps, changed.supplier),
|
|
6025
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6026
6133
|
]
|
|
6027
6134
|
.filter(Boolean)
|
|
6028
6135
|
.join(' '),
|
|
@@ -6154,7 +6261,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6154
6261
|
? `Say the word and it is cancelled (${t.cancelBooking} with \`confirmed\`).`
|
|
6155
6262
|
: '',
|
|
6156
6263
|
demoBookingNote(deps, preview.supplier),
|
|
6157
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6158
6264
|
]
|
|
6159
6265
|
.filter(Boolean)
|
|
6160
6266
|
.join(' '),
|
|
@@ -6230,18 +6336,15 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6230
6336
|
cancelled.payment
|
|
6231
6337
|
? // Paid here in advance: what becomes of the money is the answer.
|
|
6232
6338
|
`${cancelled.confirmationCode} is ${cancelled.status === 'expired' ? 'called off before it was paid' : 'cancelled'}. ${paidLine(deps, cancelled, charged)}`
|
|
6233
|
-
: h.
|
|
6234
|
-
? //
|
|
6235
|
-
|
|
6339
|
+
: h.notReal
|
|
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).
|
|
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.`}`
|
|
6236
6343
|
: h.test
|
|
6237
|
-
? `${cancelled.confirmationCode} is cancelled
|
|
6238
|
-
:
|
|
6239
|
-
? // Nobody charges a demonstration anything (2026-10-05 audit, P2).
|
|
6240
|
-
`${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.`}`
|
|
6241
|
-
: `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `The hotel charges ${formatMoney(charged)}.`}`,
|
|
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)}.`}`,
|
|
6242
6346
|
couponNow?.said ?? '',
|
|
6243
6347
|
demoBookingNote(deps, cancelled.supplier),
|
|
6244
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6245
6348
|
]
|
|
6246
6349
|
.filter(Boolean)
|
|
6247
6350
|
.join(' '),
|
|
@@ -6325,7 +6428,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6325
6428
|
: '',
|
|
6326
6429
|
...comparison.dropped.map((d) => `Left out: ${d.name} — ${d.why}`),
|
|
6327
6430
|
...fitsManyOf(deps, comparison.dropped),
|
|
6328
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6329
6431
|
]
|
|
6330
6432
|
.filter(Boolean)
|
|
6331
6433
|
.join('\n'),
|
|
@@ -6391,11 +6493,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6391
6493
|
// With no card, until when it is free to cancel too: the reply is told to
|
|
6392
6494
|
// give each stay's terms (2026-11-02 audit, P2-3).
|
|
6393
6495
|
const termsOf = (x) => [
|
|
6394
|
-
x.
|
|
6395
|
-
?
|
|
6396
|
-
:
|
|
6397
|
-
?
|
|
6398
|
-
:
|
|
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
|
+
: '',
|
|
6399
6503
|
x.payment === 'prepaid'
|
|
6400
6504
|
? x.notSoldHere
|
|
6401
6505
|
? 'paid in advance - not booked here'
|
|
@@ -6451,7 +6555,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6451
6555
|
...fitsManyOf(deps, plan.dropped),
|
|
6452
6556
|
uncheckedSaid,
|
|
6453
6557
|
ctx.noCards ? PLAN_NOT_DRAWN : PLAN_DRAWN_ONCE,
|
|
6454
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6455
6558
|
]
|
|
6456
6559
|
.filter(Boolean)
|
|
6457
6560
|
.join('\n'),
|
|
@@ -6488,7 +6591,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6488
6591
|
// would find out at the desk.
|
|
6489
6592
|
return {
|
|
6490
6593
|
structured: { view: 'member-not-found', ...common(deps, key), query: number },
|
|
6491
|
-
text: `No ${deps.vocab.brand} membership matches that number and surname. Check both as they are printed; nothing else is needed -
|
|
6594
|
+
text: `No ${deps.vocab.brand} membership matches that number and surname. Check both as they are printed; nothing else is needed - a password is never asked for here.`,
|
|
6492
6595
|
};
|
|
6493
6596
|
}
|
|
6494
6597
|
deps.members.set(key, member);
|
|
@@ -6507,7 +6610,6 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
|
|
|
6507
6610
|
member.preferences?.length
|
|
6508
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.`
|
|
6509
6612
|
: '',
|
|
6510
|
-
deps.vocab.demo ? noteFor(deps.vocab) : '',
|
|
6511
6613
|
]
|
|
6512
6614
|
.filter(Boolean)
|
|
6513
6615
|
.join('\n'),
|