@curless/hotel-storefront 0.50.3 → 0.50.4

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
@@ -1166,6 +1166,12 @@ const HOTEL_PAGE = {
1166
1166
  * none - and never in what the assistant is told.
1167
1167
  */
1168
1168
  soldOut: ARR,
1169
+ /**
1170
+ * Where some rates are listed but not booked here (`Room.notSoldHere`, paid
1171
+ * in advance on a deployment that takes no payment): what that means and
1172
+ * what to offer - the same sentence as the text.
1173
+ */
1174
+ notSoldHere: STR,
1169
1175
  };
1170
1176
  /**
1171
1177
  * What request_booking answers - and book_picked_room, which books the same way
@@ -2173,10 +2179,22 @@ const LABEL_WORDS = {
2173
2179
  const labelWords = (r) => {
2174
2180
  const words = [
2175
2181
  ...(r.labels ?? []).map((l) => LABEL_WORDS[l]),
2176
- ...(r.payment === 'prepaid' ? ['paid in advance'] : []),
2182
+ ...(r.payment === 'prepaid'
2183
+ ? [r.notSoldHere ? 'paid in advance - not booked here' : 'paid in advance']
2184
+ : []),
2177
2185
  ];
2178
2186
  return words.length ? ` [${words.join(', ')}]` : '';
2179
2187
  };
2188
+ /**
2189
+ * What is said of rates listed but not booked here (`Room.notSoldHere`) - in
2190
+ * the text and, word for word, in the structure (`notSoldHere`): the model reads
2191
+ * the structure (2026-10-28). Listed and marked as the group sells them; the
2192
+ * card says no more than 预付 on them (robin, 2026-10-30), so the assistant is
2193
+ * the one who knows - and says it when the guest wants one.
2194
+ */
2195
+ 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: this connector books rates paid at the hotel. 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.`;
2196
+ /** The group a hotel is with, by name - the deployment's own brand outside a market. */
2197
+ const groupNameOf = (deps, hotelId) => holderOf(deps, deps.backend.supplierOf?.(hotelId)).name;
2180
2198
  /** What a rate says about cancelling, as the text puts it. */
2181
2199
  const cancelWords = (r) => r.freeCancellationUntil
2182
2200
  ? `free to cancel until ${freeUntilText(r)}`
@@ -2383,7 +2401,13 @@ const dateFlex = (dates, stay) => {
2383
2401
  dates: dates.map((d) => ({
2384
2402
  checkIn: d.checkIn,
2385
2403
  checkOut: d.checkOut,
2386
- ...(d.from ? { from: formatMoney(d.from) } : { soldOut: true }),
2404
+ // "Sold out" only when it is (D2, robin 2026-10-30): a day with rooms
2405
+ // that cannot be booked here says why instead.
2406
+ ...(d.from
2407
+ ? { from: formatMoney(d.from) }
2408
+ : !d.why || d.why === 'sold-out'
2409
+ ? { soldOut: true }
2410
+ : { noRoomsCode: d.why }),
2387
2411
  ...(d.chosen ? { chosen: true } : {}),
2388
2412
  // The chosen date cannot match: `beatsBy` is a price strictly below it.
2389
2413
  ...(beatsBy !== undefined && d.from?.minor === beatsBy ? { best: true } : {}),
@@ -3119,7 +3143,11 @@ const comparedInWords = (c) => {
3119
3143
  : p.refundable
3120
3144
  ? 'no free cancellation'
3121
3145
  : 'non-refundable',
3122
- p.payment === 'prepaid' ? 'paid in advance' : '',
3146
+ p.payment === 'prepaid'
3147
+ ? p.notSoldHere
3148
+ ? 'paid in advance - not booked here'
3149
+ : 'paid in advance'
3150
+ : '',
3123
3151
  p.breakfast ? 'breakfast included' : 'no breakfast',
3124
3152
  ]
3125
3153
  .filter(Boolean)
@@ -3534,7 +3562,10 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
3534
3562
  let took = '';
3535
3563
  const type = room ? undefined : roomTypeNamed(dated.rooms, roomRef);
3536
3564
  if (type) {
3537
- const fits = type.rooms.filter((r) => r.sleeps >= stay.guests && r.roomsLeft >= wanted);
3565
+ const fitting = type.rooms.filter((r) => r.sleeps >= stay.guests && r.roomsLeft >= wanted);
3566
+ // A rate booked here before one only shown (`notSoldHere`), where the type has both.
3567
+ const sold = fitting.filter((r) => !r.notSoldHere);
3568
+ const fits = sold.length ? sold : fitting;
3538
3569
  const [first, ...rest] = fits;
3539
3570
  const cheapest = first
3540
3571
  ? rest.reduce((a, b) => (b.stayTotal.minor < a.stayTotal.minor ? b : a), first)
@@ -3576,6 +3607,45 @@ const putRoomLine = async (deps, t, ctx, key, store, args, detail) => {
3576
3607
  text: `${type ? `${detail.hotel.name} has no ${type.name} that sleeps ${stay.guests} and has ${wanted === 1 ? 'a room' : `${wanted} rooms`} left for those nights.` : `${detail.hotel.name} has no room type by that name for those nights.`} It has: ${dated.rooms.map((r) => `${r.name} (${han && r.names?.zh ? `${r.names.zh}, ` : ''}\`${r.id}\`)`).join(', ')}. Call again with the id.`,
3577
3608
  };
3578
3609
  }
3610
+ // ⚠️ LISTED, NOT BOOKED HERE (robin, 2026-10-30): a rate paid in advance
3611
+ // where this deployment takes no payment goes in no trip and no pick. The
3612
+ // press on its Reserve - or the assistant asking for it - is answered with
3613
+ // what is true and what is left: nothing booked, nothing charged; the rates
3614
+ // here paid at the hotel, a press each; other dates; or the group itself.
3615
+ // Never a review that fails at Book, after the guest typed their name.
3616
+ if (room.notSoldHere) {
3617
+ const open = dated.rooms.filter((r) => !r.notSoldHere && r.sleeps >= stay.guests && r.roomsLeft >= wanted);
3618
+ const group = groupNameOf(deps, hotelId);
3619
+ const said = [
3620
+ `Nothing was booked or charged: ${room.name} is paid in advance, and a stay paid in advance cannot be booked here yet - this connector books rates paid at the hotel.`,
3621
+ open.length
3622
+ ? `Paid at the hotel at ${detail.hotel.name} for those nights: ${open.map((r) => `${r.name} (\`${r.id}\`)`).join(', ')}.`
3623
+ : `No rate at ${detail.hotel.name} for those nights is paid at the hotel.`,
3624
+ `Other dates, or booking this rate with ${group} directly, are the other ways on.`,
3625
+ ].join(' ');
3626
+ return {
3627
+ structured: {
3628
+ view: 'error',
3629
+ ...common(deps, key),
3630
+ error: 'prepaid-not-here',
3631
+ room: { id: room.id, name: room.name, ...(room.names ? { names: room.names } : {}) },
3632
+ rooms: open.map((r) => ({
3633
+ id: r.id,
3634
+ name: r.name,
3635
+ ...(r.names ? { names: r.names } : {}),
3636
+ })),
3637
+ hotel: {
3638
+ id: detail.hotel.id,
3639
+ name: detail.hotel.name,
3640
+ ...(detail.hotel.names ? { names: detail.hotel.names } : {}),
3641
+ },
3642
+ stay,
3643
+ group,
3644
+ said,
3645
+ },
3646
+ text: said,
3647
+ };
3648
+ }
3579
3649
  deps.trips.set(store, {
3580
3650
  kind: 'room',
3581
3651
  hotelId,
@@ -4343,6 +4413,12 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4343
4413
  : ''}.`
4344
4414
  : '',
4345
4415
  ].filter(Boolean);
4416
+ // A price on the list that is not booked here (`notSoldHere`): said as on
4417
+ // the hotel's page, in both - see `notSoldHereWords`.
4418
+ const notSoldOn = hotels.find((h) => h.forStay?.notSoldHere);
4419
+ const notSold = notSoldOn ? notSoldHereWords(groupNameOf(deps, notSoldOn.id)) : '';
4420
+ if (notSold)
4421
+ priceNotes.push(notSold);
4346
4422
  // Every one by name and city, and that there are no others - or, told
4347
4423
  // only "2 hotels", the model asked for the list again to learn which.
4348
4424
  const inAll = homeList
@@ -4377,6 +4453,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4377
4453
  // In both, like a plan's: a host with no card may hand its model this
4378
4454
  // rather than the text.
4379
4455
  ...(ctx.noCards ? { notDrawn: LIST_NOT_DRAWN } : {}),
4456
+ ...(notSold ? { notSoldHere: notSold } : {}),
4380
4457
  },
4381
4458
  text: [
4382
4459
  inAll ||
@@ -4512,6 +4589,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4512
4589
  // card needs no call to the server.
4513
4590
  const terms = roomTermsOf(args);
4514
4591
  const soldOut = soldOutTypesOf(detail, enough, roomsWanted);
4592
+ const notSold = enough.some((r) => r.notSoldHere)
4593
+ ? notSoldHereWords(groupNameOf(deps, h.id))
4594
+ : '';
4515
4595
  const rooms = enough.map((r) => ({
4516
4596
  ...r,
4517
4597
  facets: facetsOf(r),
@@ -4551,6 +4631,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4551
4631
  activeFilters: activeKeysOf(terms),
4552
4632
  // Types with no room to book for these nights, said and not left out.
4553
4633
  ...(byTheHour || !soldOut.length ? {} : { soldOut }),
4634
+ ...(notSold ? { notSoldHere: notSold } : {}),
4554
4635
  ...flex.structured,
4555
4636
  },
4556
4637
  text: [
@@ -4561,6 +4642,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4561
4642
  terms && flex.text
4562
4643
  ? `${flex.text} (That is the cheapest room of any kind, not only one with ${termWords(terms)}.)`
4563
4644
  : flex.text,
4645
+ notSold,
4564
4646
  deps.vocab.demo ? noteFor(deps.vocab) : '',
4565
4647
  ]
4566
4648
  .filter(Boolean)
@@ -5443,7 +5525,11 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5443
5525
  // Each stay's rate says its terms where they bind - see `ItineraryItem.refundable`.
5444
5526
  const termsOf = (x) => [
5445
5527
  x.refundable === false ? 'non-refundable' : '',
5446
- x.payment === 'prepaid' ? 'paid in advance' : '',
5528
+ x.payment === 'prepaid'
5529
+ ? x.notSoldHere
5530
+ ? 'paid in advance - not booked here'
5531
+ : 'paid in advance'
5532
+ : '',
5447
5533
  ]
5448
5534
  .filter(Boolean)
5449
5535
  .join(', ');