@curless/hotel-storefront 0.55.6 → 0.55.8

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/tools.js CHANGED
@@ -589,7 +589,7 @@ const allTools = (n, v) => {
589
589
  type: 'integer',
590
590
  minimum: 0,
591
591
  maximum: MAX_EXTRA_QUANTITY,
592
- description: `For an add-on: how many, 1 to ${MAX_EXTRA_QUANTITY}. Default 1; 0 removes it`,
592
+ description: `For an add-on: how many, 1 to ${MAX_EXTRA_QUANTITY}. Default 1 - or, for an add-on priced per person, everyone in the rooms the trip holds at that hotel (guests per room x rooms; the guests asked for when there is no room yet); 0 removes it`,
593
593
  },
594
594
  },
595
595
  required: ['hotel'],
@@ -1252,6 +1252,8 @@ const HOTEL_PAGE = {
1252
1252
  roomFilters: ARR,
1253
1253
  /** The filters the call asked for, as the same keys - the card opens with these selected. */
1254
1254
  activeFilters: STRS,
1255
+ /** Only where the client draws cards: the hotels, or the rooms, are on the card - `ON_CARD`. */
1256
+ cardShows: STR,
1255
1257
  /**
1256
1258
  * Room types with no room to book for these nights (none left, or fewer
1257
1259
  * than the rooms asked for): `{ id, name, names?, sleeps, ..., left? }`, no
@@ -1611,6 +1613,24 @@ const asStay = (args) => {
1611
1613
  const guests = typeof args.guests === 'number' ? Math.floor(args.guests) : 2;
1612
1614
  return { checkIn, checkOut, guests };
1613
1615
  };
1616
+ /** The add-ons priced by the person: "per person", "per person per night". */
1617
+ const PRICED_BY_PERSON = new Set(['person', 'person-night']);
1618
+ /**
1619
+ * ⚠️ AN ADD-ON PRICED PER PERSON STARTS AT EVERYONE (0.55.7, robin 11-05: Shangri-La's
1620
+ * Bund night cruise, "per person", for two guests was added for ONE). Asked for with no
1621
+ * `quantity`, it is the party of the rooms the trip holds at that hotel - the guests in
1622
+ * each room times the rooms - else the guests the call names, else one. The guest can
1623
+ * change it as before; an add-on priced per stay, night or item stays at 1.
1624
+ */
1625
+ const partyOnTrip = (deps, key, hotelId, args) => {
1626
+ const room = deps.trips.read(key).find((l) => l.kind === 'room' && l.hotelId === hotelId);
1627
+ const people = room?.kind === 'room'
1628
+ ? room.stay.guests * (room.rooms ?? 1)
1629
+ : typeof args.guests === 'number'
1630
+ ? Math.floor(args.guests) * (typeof args.rooms === 'number' ? Math.floor(args.rooms) : 1)
1631
+ : 1;
1632
+ return Math.min(Math.max(1, people), MAX_EXTRA_QUANTITY);
1633
+ };
1614
1634
  /**
1615
1635
  * What every payload carries, so the card can draw a header and say whose data
1616
1636
  * this is.
@@ -1633,6 +1653,11 @@ const memberBadge = (deps, key) => {
1633
1653
  member: {
1634
1654
  programme: m.programme,
1635
1655
  tier: { name: m.tier.name, discount: m.tier.discount },
1656
+ // The member's own name, for the booking form's name (robin, 2026-11-05:
1657
+ // 「名字可以自动填」) - a card drawn after the sign-in, in another card or
1658
+ // from the chat, had only the tier to go by.
1659
+ ...(m.name ? { name: m.name } : {}),
1660
+ ...(m.names ? { names: m.names } : {}),
1636
1661
  ...(m.preferences?.length ? { preferences: m.preferences } : {}),
1637
1662
  // As they stand now - spent or handed back since signing in - so
1638
1663
  // the member page the badge opens is never a booking behind.
@@ -1873,9 +1898,15 @@ const paidLine = (deps, b, fee) => {
1873
1898
  // ⚠️ NOT "nothing paid": the card page may have been paid a moment ago
1874
1899
  // and not read yet (audit, P0-D) - what is said is what is to be done.
1875
1900
  return unionpay
1876
- ? `**Not booked yet.** ${amount} is to be confirmed on UnionPay's page by ${p.payByLocal ?? p.payBy} hotel time${p.url ? `: ${p.url}` : ''}. Once UnionPay issues its one-time card, the stay is booked with ${h.name}, who charges that card; not confirmed by then, nothing is booked or charged.`
1901
+ ? `**Not booked yet.** ${amount} is to be confirmed on UnionPay's page by ${p.payByLocal ?? p.payBy} hotel time${p.url ? `: ${p.url}` : ''}. ${h.notReal
1902
+ ? `Once UnionPay issues its one-time card, the booking is made here - it is not sent to ${h.name}`
1903
+ : `Once UnionPay issues its one-time card, the stay is booked with ${h.name}, who charges that card`}; not confirmed by then, nothing is booked or charged.`
1877
1904
  : p.url
1878
- ? `**Not booked yet.** ${amount} is to be paid on the card page by ${p.payByLocal ?? p.payBy} hotel time: ${p.url}. Once it is paid the stay is booked with ${h.name}, and ${deps.tools.listBookings} shows it; unpaid by then, the page closes and nothing is booked or charged.`
1905
+ ? `**Not booked yet.** ${amount} is to be paid on the card page by ${p.payByLocal ?? p.payBy} hotel time: ${p.url}. ${
1906
+ // ⚠️ NOT "BOOKED WITH" A GROUP IT IS NEVER SENT TO (0.55.7, 总线程 11-05).
1907
+ h.notReal
1908
+ ? `Once it is paid the booking is made here - it is not sent to ${h.name} - and ${deps.tools.listBookings} shows it`
1909
+ : `Once it is paid the stay is booked with ${h.name}, and ${deps.tools.listBookings} shows it`}; unpaid by then, the page closes and nothing is booked or charged.`
1879
1910
  : // ⚠️ NO LINK IS NOT "undefined" (2026-10-21 re-audit, T2-6): a card
1880
1911
  // page that was closed and could not be opened again leaves a stay
1881
1912
  // waiting with nowhere to pay it. Say so, and that it lapses.
@@ -1883,6 +1914,10 @@ const paidLine = (deps, b, fee) => {
1883
1914
  case 'issued':
1884
1915
  return `Confirmed on UnionPay: ${h.name} is being asked to book it now, and ${deps.tools.listBookings} shows when it has. If ${h.name} does not take it, the one-time card is not charged.`;
1885
1916
  case 'paid':
1917
+ // Paid on a page that moved no money, for a booking never sent (0.55.7): not
1918
+ // "booked with" the group - the sentence after it says it was not sent.
1919
+ if (h.notReal)
1920
+ return `${amount} paid ${unionpay ? 'on UnionPay' : 'on the card page'}: the booking is made here.`;
1886
1921
  if (unionpay)
1887
1922
  return b.status === 'confirmed'
1888
1923
  ? `${amount} paid with UnionPay, and booked with ${h.name}, who charges the one-time card UnionPay issued: nothing more is paid at the hotel.`
@@ -1913,23 +1948,26 @@ const confirming = (deps, b) => holderOf(deps, b.supplier).caps.booking === 'pay
1913
1948
  (b.status === 'requested' || b.status === 'unconfirmed');
1914
1949
  /** A booking for the card - with no code while it is being confirmed. */
1915
1950
  const madeForCard = (deps, b) => {
1916
- const card = bookingForCard(b);
1951
+ const card = listedForCard(deps, b);
1917
1952
  if (!confirming(deps, b))
1918
1953
  return card;
1919
1954
  const { confirmationCode: _unconfirmed, ...rest } = card;
1920
1955
  return { ...rest, pending: true };
1921
1956
  };
1922
- const bookedLine = (deps, b) => [heldLine(deps, b), couponPointsLine(b)].filter(Boolean).join(' ');
1957
+ const bookedLine = (deps, b) => [heldLine(deps, b), couponPointsLine(b, holderOf(deps, b.supplier))].filter(Boolean).join(' ');
1923
1958
  /**
1924
1959
  * The coupon a booking used and the points it should earn - on the booking
1925
1960
  * itself, so the confirmation says what the review said (§4, §5).
1926
1961
  */
1927
- const couponPointsLine = (b) => [
1962
+ const couponPointsLine = (b, h) => [
1928
1963
  b.quote.coupon
1929
1964
  ? `Coupon ${couponName(b.quote.coupon)} used: −${formatMoney(b.quote.coupon.off)}, and ${formatMoney(b.quote.total)} is the booking's total after it.`
1930
1965
  : '',
1931
1966
  b.quote.member
1932
- ? `About ${b.quote.member.points.toLocaleString('en-GB')} ${b.quote.member.programme} points after the stay - not added to the balance here.`
1967
+ ? h?.notReal
1968
+ ? // The card's one sentence (`earnsNote`, 0.55.7): nothing sent, nothing earned.
1969
+ `A stay at this rate earns about ${b.quote.member.points.toLocaleString('en-GB')} ${b.quote.member.programme} points; this booking was not sent to ${h.name}, so none are added to the balance.`
1970
+ : `About ${b.quote.member.points.toLocaleString('en-GB')} ${b.quote.member.programme} points after the stay - not added to the balance here.`
1933
1971
  : '',
1934
1972
  ]
1935
1973
  .filter(Boolean)
@@ -1943,7 +1981,7 @@ const heldLine = (deps, b) => {
1943
1981
  if (h.caps.booking === 'pay-at-hotel') {
1944
1982
  // ⚠️ THE FACT, IN NO ENVIRONMENT'S WORD (robin, 2026-11-03).
1945
1983
  if (h.notReal)
1946
- 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.`;
1984
+ 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. ${notSentSaid(h.name)}`;
1947
1985
  if (h.test)
1948
1986
  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.`;
1949
1987
  return b.status === 'requested'
@@ -1962,7 +2000,7 @@ const heldLine = (deps, b) => {
1962
2000
  // nothing for a real stay; said as how long it lasts, and that it is only the
1963
2001
  // demonstration.
1964
2002
  if (h.demo)
1965
- return `${total} all in. **${demoBookingWords(h)}** Nothing has been charged and no card is needed. ${demoHoldWords(b, deps.trips.now())}`;
2003
+ return `${total} all in. **${demoBookingWords(h)}** Nothing has been charged and no card is needed. ${demoHoldWords(b, deps.trips.now())} ${notSentSaid(h.name)}`;
1966
2004
  return `${total} all in. **Nothing has been charged** - the rooms are held until ${lapses}, and the guest presents a card at check-in.`;
1967
2005
  };
1968
2006
  /**
@@ -1985,6 +2023,18 @@ const demoHoldWords = (b, now) => {
1985
2023
  };
1986
2024
  /** What a booking that is not sent to the group is, in one sentence - see `demoBookingNote`. */
1987
2025
  const demoBookingWords = (h) => `This booking was not sent to ${h.name}, and ${h.name} is not holding any room.`;
2026
+ /**
2027
+ * ⚠️ THE FACT, AND NO WORD FOR AN ENVIRONMENT (0.55.8; robin 2026-11-03 「不用特意写不是
2028
+ * 真的」, 总线程 11-05). Told "a booking made here is not sent to Atour", the model on
2029
+ * claude.ai said 「这个亚朵连接器是演示环境」 - a word nobody gave it. Said after each
2030
+ * such fact, in the service instructions and in the answers about a booking, only
2031
+ * where nothing is sent to the group (`notReal`); a group's own test system, which a
2032
+ * booking does reach, is not told this.
2033
+ */
2034
+ export const notSentSaid = (name) =>
2035
+ // ⚠️ WITHOUT THE WORDS IT FORBIDS: a sentence naming them would be the label in the
2036
+ // model's text (env-one-word.test.ts holds every answer and instruction to none).
2037
+ `Say only that fact - not sent to ${name}, no room held, nothing charged - and nothing about what kind of connector or environment this is: do not call it, ${name} or its hotels a trial, a mock-up or not real.`;
1988
2038
  /**
1989
2039
  * Said on every answer about a demonstration booking - made, changed or
1990
2040
  * cancelled - and on nothing else: a hold of the demo's that a guest took for
@@ -1992,7 +2042,7 @@ const demoBookingWords = (h) => `This booking was not sent to ${h.name}, and ${h
1992
2042
  */
1993
2043
  const demoBookingNote = (deps, supplier) => {
1994
2044
  const h = holderOf(deps, supplier);
1995
- return h.demo && h.caps.booking === 'hold' ? demoBookingWords(h) : '';
2045
+ return h.demo && h.caps.booking === 'hold' ? `${demoBookingWords(h)} ${notSentSaid(h.name)}` : '';
1996
2046
  };
1997
2047
  const common = (deps, key) => {
1998
2048
  const caps = deps.backend.capabilities ?? FULL_CAPABILITIES;
@@ -2245,14 +2295,24 @@ const couponTerms = (c, brand) => [
2245
2295
  */
2246
2296
  const couponBackWords = (c, fee,
2247
2297
  /** Said after the cancellation, not before it. */
2248
- done = false) => c.returned
2298
+ done = false,
2299
+ /**
2300
+ * The booking was never sent to its group: nothing is charged, and the fee is the
2301
+ * terms' - "this cancellation was charged a fee" beside "nothing is charged" said
2302
+ * money was taken (0.55.7, 总线程 11-05; the card says it the same way).
2303
+ */
2304
+ notSent = false) => c.returned
2249
2305
  ? done
2250
2306
  ? `The coupon ${couponName(c)} is back with the member, usable until ${c.validUntil}: the cancellation was free.`
2251
2307
  : `The coupon ${couponName(c)} it used comes back to the member on cancelling, because the cancellation is free and the coupon is usable until ${c.validUntil}.`
2252
2308
  : c.notReturnedBecause === 'fee'
2253
- ? done
2254
- ? `The coupon ${couponName(c)} is NOT given back: this cancellation was charged a fee${fee ? ` (${fee})` : ''}, and a cancellation that is charged counts the coupon as used.`
2255
- : `The coupon ${couponName(c)} it used does NOT come back: cancelling costs a fee${fee ? ` (${fee})` : ''}, and a cancellation that is charged counts the coupon as used.`
2309
+ ? notSent
2310
+ ? done
2311
+ ? `The coupon ${couponName(c)} is NOT given back: under the terms this cancellation costs a fee${fee ? ` (${fee})` : ''}, and a cancellation that costs a fee counts the coupon as used.`
2312
+ : `The coupon ${couponName(c)} it used does NOT come back: under the terms cancelling costs a fee${fee ? ` (${fee})` : ''}, and a cancellation that costs a fee counts the coupon as used.`
2313
+ : done
2314
+ ? `The coupon ${couponName(c)} is NOT given back: this cancellation was charged a fee${fee ? ` (${fee})` : ''}, and a cancellation that is charged counts the coupon as used.`
2315
+ : `The coupon ${couponName(c)} it used does NOT come back: cancelling costs a fee${fee ? ` (${fee})` : ''}, and a cancellation that is charged counts the coupon as used.`
2256
2316
  : done
2257
2317
  ? `The coupon ${couponName(c)} ran out on ${c.validUntil}, so it is not given back.`
2258
2318
  : `The coupon ${couponName(c)} it used ran out on ${c.validUntil}, so cancelling does not give it back.`;
@@ -2798,15 +2858,33 @@ const heldRoomsWords = (b) => b.quote.lines
2798
2858
  ? ', past its free cancellation - cancelling has a fee'
2799
2859
  : ''}`)
2800
2860
  .join('; ');
2801
- const bookingLine = (b, noCards) => {
2861
+ /**
2862
+ * A booking's status said for the model where the booking was never sent to its
2863
+ * group (0.55.7, 总线程 11-05): "requested" - or "confirmed", paid on a page that moved
2864
+ * no money - promised an answer from a group that never heard of it. The card says
2865
+ * the same (`shownStatus`); `undefined` where the status says what it is.
2866
+ */
2867
+ const notSentStatus = (deps, b) => {
2868
+ const h = holderOf(deps, b.supplier);
2869
+ return h.notReal && ['requested', 'unconfirmed', 'confirmed'].includes(b.status)
2870
+ ? `not sent to ${h.name} - nobody confirms it, and no room is held`
2871
+ : undefined;
2872
+ };
2873
+ /** A booking for the card and the model's structure, with `notSentStatus` where there is one. */
2874
+ const listedForCard = (deps, b) => {
2875
+ const said = notSentStatus(deps, b);
2876
+ return { ...bookingForCard(b), ...(said ? { statusSaid: said } : {}) };
2877
+ };
2878
+ const bookingLine = (deps, b, noCards) => {
2802
2879
  const kinds = b.requests?.kinds ?? [];
2803
2880
  const held = noCards ? heldRoomsWords(b) : '';
2881
+ const notSent = holderOf(deps, b.supplier).notReal;
2804
2882
  return [
2805
- `${b.confirmationCode} · ${withZh(b.hotelName, b.hotelNames, noCards)} · ${stayWords(b.stay, b.hourly)} · ${
2806
- // ⚠️ SAID AS WHAT IT IS - see BookingStatus. Neither booked nor failed.
2807
- b.status === 'unconfirmed'
2808
- ? 'unconfirmed: whether it was made is not known yet, and a person is confirming it'
2809
- : b.status}`,
2883
+ `${b.confirmationCode} · ${withZh(b.hotelName, b.hotelNames, noCards)} · ${stayWords(b.stay, b.hourly)} · ${notSentStatus(deps, b) ??
2884
+ // ⚠️ SAID AS WHAT IT IS - see BookingStatus. Neither booked nor failed.
2885
+ (b.status === 'unconfirmed'
2886
+ ? 'unconfirmed: whether it was made is not known yet, and a person is confirming it'
2887
+ : b.status)}`,
2810
2888
  held ? ` · ${held}` : '',
2811
2889
  kinds.length ? ` · asked for ${kinds.map(bare).join(', ')}` : '',
2812
2890
  b.quote.coupon
@@ -2814,7 +2892,9 @@ const bookingLine = (b, noCards) => {
2814
2892
  ? ' (given back)'
2815
2893
  : b.couponReturned === false
2816
2894
  ? b.couponNotReturnedBecause === 'fee'
2817
- ? ' (not given back - the cancellation was charged a fee)'
2895
+ ? notSent
2896
+ ? ' (not given back - under the terms the cancellation costs a fee)'
2897
+ : ' (not given back - the cancellation was charged a fee)'
2818
2898
  : ' (ran out - not given back)'
2819
2899
  : ''}`
2820
2900
  : '',
@@ -3783,6 +3863,18 @@ export const PLAN_DRAWN_ONCE = 'The plan on the card is this one. Something to c
3783
3863
  export const PLAN_NOT_DRAWN = 'Nothing was drawn: this app shows no cards, so the guest has seen none of this plan. Tell it to them in your reply, day by day - where each day is spent, and each stay with its hotel, dates, price and terms as above. Something to change that you notice now - a sight closed on its day, a day too far from its hotel - is said in that reply, not planned again: the plan is made again only when the guest asks for the change.';
3784
3864
  /** A comparison's answer, for a client that shows no cards - see `PLAN_NOT_DRAWN`. */
3785
3865
  export const COMPARISON_NOT_DRAWN = "Nothing was drawn: this app shows no cards, so the guest has seen none of this comparison. Compare the hotels in your reply, with each one's price and terms as given here - each is its cheapest room that sleeps the party.";
3866
+ /**
3867
+ * What the model is told where the client DRAWS the card (0.55.7, 总线程 11-05,
3868
+ * robin 「3.ok」): on claude.ai the hotel's card listed its four room types, and the
3869
+ * reply listed them again in a table - breakfast, nightly rate, the stay, the terms.
3870
+ * Said only to a client that draws cards (`ToolContext.drawsCards`), in the text and
3871
+ * in the structure (`cardShows`); a client that draws none is given every room in
3872
+ * words as before (规矩 9).
3873
+ */
3874
+ export const ON_CARD = {
3875
+ hotels: 'The card shows these hotels, each with its price: reply in a sentence or two on what stands out for the guest, and do not list them again.',
3876
+ rooms: 'The card shows these rooms, each with its price and terms: reply in a sentence or two on what stands out for the guest, and do not list them again.',
3877
+ };
3786
3878
  /**
3787
3879
  * A list's answer, for a client that shows no cards - see `PLAN_NOT_DRAWN`.
3788
3880
  *
@@ -4986,6 +5078,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4986
5078
  // never a market, whose home is its groups.
4987
5079
  const unnarrowed = !ask.query && !ask.city && !ask.filters && !ask.maxNightly;
4988
5080
  const deployed = deps.hotelCount?.() ?? (unnarrowed ? answer.total : 0);
5081
+ // ⚠️ A BACKEND THAT HAS NO PRICE FOR A STAY (11-05; Otaless, whose /rooms is empty
5082
+ // and whose from-price says nothing of taxes - `Hotel.fromNightlyTaxes`) lists a
5083
+ // from-price a night and no stay total, with dates or without. The list must not
5084
+ // promise one ("priced for the nights you choose", "each price is for the whole
5085
+ // stay"); it says what the price is. The card reads the same flag.
5086
+ const fromPricesOnly = hotels.length > 0 &&
5087
+ hotels.every((h) => h.fromNightly && h.fromNightlyTaxes === 'unknown' && !h.forStay && !h.noRoomsForStay);
4989
5088
  const fitsOnePage = !deps.backend.suppliers && !deps.vocab.market && deployed >= 1 && deployed <= HOME_LIST_MAX;
4990
5089
  // ⚠️ THE SEARCH AS ASKED, `all` INCLUDED. The echo dropped `all`, so every
4991
5090
  // re-query the card made from a "Browse all" list - new dates, the default
@@ -5171,6 +5270,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5171
5270
  ask: { ...stayArgs },
5172
5271
  ...(ask.stay ? { stay: ask.stay } : {}),
5173
5272
  ...(today ? { today } : {}),
5273
+ ...(fromPricesOnly ? { fromPricesOnly: true } : {}),
5174
5274
  // ⚠️ IN A MARKET, HOW MANY HOTELS EACH GROUP HAS HERE (11-03): the card
5175
5275
  // says "from N groups" of the groups that have one - not of every group
5176
5276
  // on the market's list, six of which may have none. Only the counts.
@@ -5251,7 +5351,11 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5251
5351
  ...(answer.refine ? { refine: refineFor(answer.refine, asked.filters, 0) } : {}),
5252
5352
  beforeFilters: answer.beforeFilters,
5253
5353
  }
5254
- : {}),
5354
+ : // A market's group asked for and with no hotel here (11-05): the card says
5355
+ // which group, and does not send the guest off to "try a city".
5356
+ asked.filters?.supplier
5357
+ ? { ask: echo(sort) }
5358
+ : {}),
5255
5359
  },
5256
5360
  text: [
5257
5361
  widened?.found ? `Nothing matched ${widened.from}, so this looked in ${widened.to}.` : '',
@@ -5314,19 +5418,34 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5314
5418
  ? `${answer.overBudget} more matched and came in over the ceiling.`
5315
5419
  : '',
5316
5420
  filtersSaid && filterWords(filtersSaid) ? `Filtered to ${filterWords(filtersSaid)}.` : '',
5421
+ // ⚠️ THE SAME ROOMS ON THE HOTEL OPENED FROM IT (0.55.7, 总线程 11-05): a list with
5422
+ // free cancellation on priced each hotel at its cheapest free-to-cancel room, and
5423
+ // the hotel asked without it opened at a cheaper one that cannot be cancelled. The
5424
+ // card's own press carries them; the model is told to, as one line.
5425
+ filtersSaid && (filtersSaid.freeCancellation || filtersSaid.breakfast || filtersSaid.bed)
5426
+ ? `To open one of these, pass ${[
5427
+ filtersSaid.freeCancellation ? '`freeCancellation`' : '',
5428
+ filtersSaid.breakfast ? '`breakfast`' : '',
5429
+ filtersSaid.bed ? `\`bed: "${filtersSaid.bed}"\`` : '',
5430
+ ]
5431
+ .filter(Boolean)
5432
+ .join(' and ')} to ${deps.tools.getHotel} too, so its rooms are priced as here.`
5433
+ : '',
5317
5434
  // What the prices on a dated list are, in one line - and how many cannot
5318
5435
  // be had at all, so a sold-out hotel is not described as a choice.
5319
5436
  ask.filters?.hourly && hotels.some((h) => h.forStay?.hours)
5320
5437
  ? `By the hour, today: each price is that ${deps.vocab.noun}'s cheapest room by the hour, for its hours, taxes included.`
5321
- : ask.stay
5322
- ? `Priced for ${ask.stay.checkIn} to ${ask.stay.checkOut}${ask.rooms && ask.rooms > 1 ? `, ${ask.rooms} rooms` : ''}: each price is that ${deps.vocab.noun}'s cheapest room for the whole stay, taxes included${hotels.some((h) => h.noRoomsForStay && h.noRoomsForStayCode !== 'none-on-sale')
5323
- ? `; ${hotels.filter((h) => h.noRoomsForStay && h.noRoomsForStayCode !== 'none-on-sale').length} in this answer have no room for those nights`
5324
- : ''}${
5325
- // The other kind, said as its own (U11): other nights will not help.
5326
- hotels.some((h) => h.noRoomsForStayCode === 'none-on-sale')
5327
- ? `; ${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}`
5328
- : ''}.`
5329
- : '',
5438
+ : ask.stay && fromPricesOnly
5439
+ ? `No price for ${ask.stay.checkIn} to ${ask.stay.checkOut} is available here: each price is the ${deps.vocab.noun}'s from-price a night, not a total for the stay. Say so; do not quote a stay total.`
5440
+ : ask.stay
5441
+ ? `Priced for ${ask.stay.checkIn} to ${ask.stay.checkOut}${ask.rooms && ask.rooms > 1 ? `, ${ask.rooms} rooms` : ''}: each price is that ${deps.vocab.noun}'s cheapest room for the whole stay, taxes included${hotels.some((h) => h.noRoomsForStay && h.noRoomsForStayCode !== 'none-on-sale')
5442
+ ? `; ${hotels.filter((h) => h.noRoomsForStay && h.noRoomsForStayCode !== 'none-on-sale').length} in this answer have no room for those nights`
5443
+ : ''}${
5444
+ // The other kind, said as its own (U11): other nights will not help.
5445
+ hotels.some((h) => h.noRoomsForStayCode === 'none-on-sale')
5446
+ ? `; ${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}`
5447
+ : ''}.`
5448
+ : '',
5330
5449
  ].filter(Boolean);
5331
5450
  // A price on the list that is not booked here (`notSoldHere`): said as on
5332
5451
  // the hotel's page, in both - see `notSoldHereWords`.
@@ -5383,6 +5502,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5383
5502
  // not a new one - and a place chip keeps the order the guest chose.
5384
5503
  ask: echo(applied),
5385
5504
  ...(ask.stay ? { stay: ask.stay } : {}),
5505
+ ...(fromPricesOnly ? { fromPricesOnly: true } : {}),
5386
5506
  ...(answer.refine
5387
5507
  ? { refine: refineFor(answer.refine, asked.filters, total, !!ask.stay) }
5388
5508
  : {}),
@@ -5397,6 +5517,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5397
5517
  // In both, like a plan's: a host with no card may hand its model this
5398
5518
  // rather than the text.
5399
5519
  ...(ctx.noCards ? { notDrawn: LIST_NOT_DRAWN } : {}),
5520
+ ...(ctx.drawsCards && hotels.length ? { cardShows: ON_CARD.hotels } : {}),
5400
5521
  ...(notSold ? { notSoldHere: notSold } : {}),
5401
5522
  },
5402
5523
  text: [
@@ -5416,6 +5537,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5416
5537
  ctx.noCards && hotels.length
5417
5538
  ? `\n${LIST_NOT_DRAWN}\n${hotels.map(listedInWords).join('\n')}\n`
5418
5539
  : '',
5540
+ ctx.drawsCards && hotels.length ? ON_CARD.hotels : '',
5419
5541
  forPlans,
5420
5542
  ]
5421
5543
  .filter(Boolean)
@@ -5609,6 +5731,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5609
5731
  ...(notSold ? { notSoldHere: notSold } : {}),
5610
5732
  ...(memberRates ? { memberRates } : {}),
5611
5733
  ...flex.structured,
5734
+ ...(ctx.drawsCards && enough.length ? { cardShows: ON_CARD.rooms } : {}),
5612
5735
  },
5613
5736
  text: [
5614
5737
  memberRates?.said ?? '',
@@ -5620,6 +5743,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5620
5743
  ? `${flex.text} (That is the cheapest room of any kind, not only one with ${termWords(terms)}.)`
5621
5744
  : flex.text,
5622
5745
  notSold,
5746
+ ctx.drawsCards && enough.length ? ON_CARD.rooms : '',
5623
5747
  ]
5624
5748
  .filter(Boolean)
5625
5749
  .join('\n'),
@@ -5888,7 +6012,8 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5888
6012
  kind: 'extra',
5889
6013
  hotelId,
5890
6014
  extraId: extra.id,
5891
- quantity: clampCount(args.quantity ?? 1, MAX_EXTRA_QUANTITY),
6015
+ quantity: clampCount(args.quantity ??
6016
+ (PRICED_BY_PERSON.has(extra.per) ? partyOnTrip(deps, key, hotelId, args) : 1), MAX_EXTRA_QUANTITY),
5892
6017
  ...(isDay(str(args.day)) ? { day: str(args.day) } : {}),
5893
6018
  };
5894
6019
  deps.trips.set(key, line);
@@ -5963,7 +6088,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5963
6088
  structured: {
5964
6089
  view: 'bookings',
5965
6090
  ...common(deps, key),
5966
- bookings: bookings.map(bookingForCard),
6091
+ bookings: bookings.map((b) => listedForCard(deps, b)),
5967
6092
  count: bookings.length,
5968
6093
  // What was looked for, so a card can say THAT did not match rather
5969
6094
  // than "nothing in this conversation".
@@ -5974,7 +6099,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5974
6099
  text: [
5975
6100
  missed ? missLine : '',
5976
6101
  bookings.length
5977
- ? bookings.map((b) => bookingLine(b, ctx.noCards)).join('\n')
6102
+ ? bookings.map((b) => bookingLine(deps, b, ctx.noCards)).join('\n')
5978
6103
  : code
5979
6104
  ? ''
5980
6105
  : // ⚠️ NOT "NO BOOKINGS" when a group did not answer: it may hold them.
@@ -6072,7 +6197,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
6072
6197
  booking: bookingForCard(step.booking),
6073
6198
  total: formatMoney(step.booking.quote.total),
6074
6199
  },
6075
- text: `${step.booking.confirmationCode} was paid on the card page already, so it stays paid there: ${bookingLine(step.booking)}`,
6200
+ text: `${step.booking.confirmationCode} was paid on the card page already, so it stays paid there: ${bookingLine(deps, step.booking)}`,
6076
6201
  };
6077
6202
  }
6078
6203
  return {
@@ -6339,7 +6464,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
6339
6464
  ? {
6340
6465
  coupon: {
6341
6466
  ...preview.coupon,
6342
- said: couponBackWords(preview.coupon, formatMoney(preview.fee)),
6467
+ said: couponBackWords(preview.coupon, formatMoney(preview.fee), false, holderOf(deps, preview.supplier).notReal),
6343
6468
  },
6344
6469
  }
6345
6470
  : {}),
@@ -6347,7 +6472,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
6347
6472
  text: [
6348
6473
  `Cancelling ${preview.confirmationCode} would cost ${formatMoney(preview.fee)}${preview.fee.minor === 0 ? ' - nothing' : ''}.`,
6349
6474
  preview.rule,
6350
- preview.coupon ? couponBackWords(preview.coupon, formatMoney(preview.fee)) : '',
6475
+ preview.coupon
6476
+ ? couponBackWords(preview.coupon, formatMoney(preview.fee), false, holderOf(deps, preview.supplier).notReal)
6477
+ : '',
6351
6478
  preview.cancellable
6352
6479
  ? `Say the word and it is cancelled (${t.cancelBooking} with \`confirmed\`).`
6353
6480
  : '',
@@ -6408,7 +6535,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
6408
6535
  notReturnedBecause: cancelled.couponNotReturnedBecause ?? 'expired',
6409
6536
  }),
6410
6537
  };
6411
- return { ...c, said: couponBackWords(c, formatMoney(charged), true) };
6538
+ return { ...c, said: couponBackWords(c, formatMoney(charged), true, h.notReal) };
6412
6539
  })()
6413
6540
  : undefined;
6414
6541
  return {