@curless/hotel-storefront 0.31.2 → 0.32.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.
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,mBAAmB,EASzB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAa,YAAY,EAAgB,MAAM,cAAc,CAAC;AAC1E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAK7C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAkBhD,OAAO,EAIL,KAAK,SAAS,EAIf,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAkBV,eAAe,EAKhB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAa,KAAK,UAAU,EAA+B,MAAM,YAAY,CAAC;AAuFrF,+GAA+G;AAC/G,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B;;;;GAIG;AACH,eAAO,MAAM,UAAU,GACrB,GAAG,SAAS,EACZ,GAAG,UAAU,EACb,OAAM,mBAAuC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IACC,CAAC;AAEjD,mEAAmE;AACnE,eAAO,MAAM,aAAa,GAAI,GAAG,SAAS,EAAE,MAAM,mBAAmB,KAAG,GAAG,CAAC,MAAM,CAM9E,CAAC;AA6zBL;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,MAAM,mBAAmB,EACzB,OAAO,IAAI,EACX,OAAM;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,KACtF,MAAM,CAAC,MAAM,EAAE,OAAO,CA6CxB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,GAAG,SAAS,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CA0OtE,CAAC;AAEH,qDAAqD;AACrD,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE/B;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9F,MAAM,MAAM,QAAQ,GAAG;IACrB,OAAO,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oFAAoF;IACpF,OAAO,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,0FAA0F;IAC1F,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAmnCF;;;;GAIG;AACH,eAAO,MAAM,eAAe,IAAI,CAAC;AAyJjC;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,QAAQ,KAAG,UAMzC,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,MAAM,QAAQ,EACd,MAAM,MAAM,EACZ,SAAS,OAAO,EAChB,KAAK,WAAW,KACf,OAAO,CAAC,UAAU,CAMpB,CAAC"}
package/dist/tools.js CHANGED
@@ -1033,6 +1033,14 @@ export const buildOutputSchemas = (n) => ({
1033
1033
  requested: ARR,
1034
1034
  /** An accessible room was asked for, which this booking does not reserve. */
1035
1035
  accessibility: BOOL,
1036
+ /** One per stay, when the trip held several. */
1037
+ bookings: ARR,
1038
+ /**
1039
+ * The stays that could not be booked when the others were - each with
1040
+ * why, still in the trip, and what could be booked instead (robin,
1041
+ * 2026-10-14).
1042
+ */
1043
+ failed: ARR,
1036
1044
  },
1037
1045
  required: ['view'],
1038
1046
  },
@@ -1198,6 +1206,66 @@ const memberBadge = (deps, key) => {
1198
1206
  : { signedIn: false };
1199
1207
  };
1200
1208
  /** A market's group by its id or its name, as a guest or a model would say it. */
1209
+ /** How many stays that failed are each given alternatives - each is a search. */
1210
+ const MAX_FAILED_WITH_ALTERNATIVES = 3;
1211
+ /** How many alternatives a stay that failed is given. */
1212
+ const MAX_ALTERNATIVES = 3;
1213
+ /** A trip line of the stay that failed - its room, or an add-on on its nights. */
1214
+ const inFailedStay = (l, f) => l.hotelId === f.hotelId &&
1215
+ (l.kind === 'room'
1216
+ ? l.stay.checkIn === f.stay.checkIn &&
1217
+ l.stay.checkOut === f.stay.checkOut &&
1218
+ (l.hourly?.start ?? '') === (f.hourly?.start ?? '')
1219
+ : !l.day || (f.stay.checkIn <= l.day && l.day <= f.stay.checkOut));
1220
+ /** How many rooms the stay that failed asked for. */
1221
+ const roomsIn = (lines, f) => Math.max(1, lines
1222
+ .filter((l) => l.kind === 'room' && inFailedStay(l, f))
1223
+ .reduce((n, l) => n + (l.kind === 'room' ? (l.rooms ?? 1) : 0), 0));
1224
+ /**
1225
+ * What could be booked instead of a stay that failed: hotels in its city with
1226
+ * a room for the same nights, the same party and as many rooms - the group's
1227
+ * own brand first, then the nearest in price to what the guest was shown
1228
+ * (robin, 2026-10-14: 给客人备选方案). Found when it failed, so a guest is
1229
+ * never left with "no" and nothing else; none found, none offered.
1230
+ */
1231
+ const alternativesFor = async (deps, f, lines, shown, who) => {
1232
+ try {
1233
+ const failedAt = (await deps.backend.getHotel(f.hotelId))?.hotel;
1234
+ if (!failedAt)
1235
+ return { hotels: [] };
1236
+ const rooms = roomsIn(lines, f);
1237
+ const found = await deps.backend.searchHotels({
1238
+ city: failedAt.city,
1239
+ stay: f.stay,
1240
+ rooms,
1241
+ limit: 20,
1242
+ ...(f.hourly ? { filters: { hourly: true } } : {}),
1243
+ }, who);
1244
+ // What the guest was shown for it: the stay's room lines, before taxes -
1245
+ // only to put the nearest first.
1246
+ const was = Object.entries(shown?.lines ?? {})
1247
+ .filter(([k]) => k.startsWith(`room|${f.hotelId}|`) &&
1248
+ k.includes(`|${f.stay.checkIn}|${f.stay.checkOut}|`))
1249
+ .reduce((n, [, v]) => n + v.total.minor, 0);
1250
+ const hotels = found.hotels
1251
+ .filter((h) => h.id !== f.hotelId && h.forStay && !h.noRoomsForStay)
1252
+ .map((h, i) => ({
1253
+ h,
1254
+ i,
1255
+ own: h.brand === failedAt.brand ? 0 : 1,
1256
+ off: was && h.forStay ? Math.abs(h.forStay.total.minor - was) : 0,
1257
+ }))
1258
+ .sort((a, b) => a.own - b.own || a.off - b.off || a.i - b.i)
1259
+ .slice(0, MAX_ALTERNATIVES)
1260
+ .map(({ h }) => h);
1261
+ return { city: failedAt.city, hotels };
1262
+ }
1263
+ catch {
1264
+ // An alternative is a kindness, not the booking: a search that fails
1265
+ // leaves the stay said, with its other ways on.
1266
+ return { hotels: [] };
1267
+ }
1268
+ };
1201
1269
  const groupNamed = (deps, said) => {
1202
1270
  const want = said.trim().toLowerCase();
1203
1271
  return deps.backend
@@ -1279,6 +1347,21 @@ const paidLine = (deps, b) => {
1279
1347
  return `All ${amount} has gone back to the card it was paid with${p.refundReason ? ` - ${p.refundReason}` : ''}.${test}`;
1280
1348
  }
1281
1349
  };
1350
+ /**
1351
+ * A stay sent to the hotel's own system whose answer is not in yet: being
1352
+ * confirmed - never counted as booked, and given no code until it is (总线程,
1353
+ * 2026-10-14). A hold or a demonstration's request IS the booking.
1354
+ */
1355
+ const confirming = (deps, b) => holderOf(deps, b.supplier).caps.booking === 'pay-at-hotel' &&
1356
+ (b.status === 'requested' || b.status === 'unconfirmed');
1357
+ /** A booking for the card - with no code while it is being confirmed. */
1358
+ const madeForCard = (deps, b) => {
1359
+ const card = bookingForCard(b);
1360
+ if (!confirming(deps, b))
1361
+ return card;
1362
+ const { confirmationCode: _unconfirmed, ...rest } = card;
1363
+ return { ...rest, pending: true };
1364
+ };
1282
1365
  const bookedLine = (deps, b) => {
1283
1366
  const paid = paidLine(deps, b);
1284
1367
  if (paid)
@@ -3162,6 +3245,23 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3162
3245
  : 'Which nights? A room needs a check-in and a check-out date before it can go into the trip.',
3163
3246
  };
3164
3247
  }
3248
+ // ⚠️ TAKING A ROOM OUT NEEDS NO ROOM FOR SALE. The lookup below is of
3249
+ // what is for sale, so a room that had gone since it went into the trip -
3250
+ // the stay a booking could not make - could not be taken out of it
3251
+ // (2026-10-14). The line in the trip is the one asked about.
3252
+ if (args.rooms !== undefined && clampCount(args.rooms, MAX_ROOMS_PER_LINE) === 0) {
3253
+ const held = deps.trips
3254
+ .read(key)
3255
+ .find((l) => l.kind === 'room' &&
3256
+ l.hotelId === hotelId &&
3257
+ l.roomId === roomRef &&
3258
+ l.stay.checkIn === stay.checkIn &&
3259
+ l.stay.checkOut === stay.checkOut);
3260
+ if (held) {
3261
+ deps.trips.set(key, unset(held));
3262
+ return currentTrip(deps, key, identity(deps, ctx, key));
3263
+ }
3264
+ }
3165
3265
  // The backend owns the calendar - it knows each hotel's own timezone, and a
3166
3266
  // second opinion computed from this process's clock would disagree with it
3167
3267
  // for eight hours a day.
@@ -3270,7 +3370,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3270
3370
  const { requests, dropped } = readRequests(args);
3271
3371
  try {
3272
3372
  const shown = deps.trips.shown(key);
3273
- const made = await deps.backend.requestBooking({ lines, ...(shown ? { shown } : {}) }, {
3373
+ const { booked: made, failed } = await deps.backend.requestBooking({ lines, ...(shown ? { shown } : {}) }, {
3274
3374
  ...guest,
3275
3375
  ...(str(args.phone) ? { phone: str(args.phone) } : {}),
3276
3376
  ...(requests?.note ? { notes: requests.note } : {}),
@@ -3284,16 +3384,26 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3284
3384
  // only place an Identity is built; the guest's email still overrides,
3285
3385
  // because a booking is found by the address it was made with.
3286
3386
  { ...identity(deps, ctx, key), email: guest.email }, requests);
3287
- if (lines.length === inTrip.length)
3387
+ // ⚠️ WHAT WAS BOOKED LEAVES THE TRIP; WHAT WAS NOT STAYS IN IT, add-ons
3388
+ // and all - to ask again, or to put another hotel in its place (robin,
3389
+ // 2026-10-14).
3390
+ const stillOut = (l) => failed.some((f) => inFailedStay(l, f));
3391
+ if (lines.length === inTrip.length && !lines.some(stillOut))
3288
3392
  deps.trips.clear(key);
3289
3393
  else
3290
3394
  for (const l of lines)
3291
- deps.trips.set(key, unset(l));
3395
+ if (!stillOut(l))
3396
+ deps.trips.set(key, unset(l));
3292
3397
  // What is left is priced again, and that price is what the next
3293
- // group's booking is held to (ShownQuote).
3294
- const rest = lines.length === inTrip.length
3398
+ // booking is held to (ShownQuote).
3399
+ const rest = lines.length === inTrip.length && !failed.length
3295
3400
  ? undefined
3296
3401
  : await currentTrip(deps, key, identity(deps, ctx, key));
3402
+ const others = rest && lines.length !== inTrip.length;
3403
+ // What could be booked instead of each, found now (robin, 2026-10-14).
3404
+ const instead = await Promise.all(failed.map((f, i) => i < MAX_FAILED_WITH_ALTERNATIVES
3405
+ ? alternativesFor(deps, f, lines, shown, identity(deps, ctx, key))
3406
+ : Promise.resolve({ hotels: [] })));
3297
3407
  // ⚠️ REMEMBERED ONLY WHEN ASKED - see `rememberPreferences`. Said either
3298
3408
  // way, so neither the guest nor the model assumes something was kept.
3299
3409
  const signedIn = deps.members.read(key);
@@ -3343,9 +3453,53 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3343
3453
  };
3344
3454
  }));
3345
3455
  const [first] = booked;
3346
- if (!first)
3347
- throw new Error('Nothing was booked.');
3348
3456
  const several = booked.length > 1;
3457
+ const waiting = booked.filter(({ booking }) => confirming(deps, booking)).length;
3458
+ const notBooked = failed.map((f, i) => ({
3459
+ hotelId: f.hotelId,
3460
+ hotelName: f.hotelName,
3461
+ stay: f.stay,
3462
+ ...(f.hourly ? { hourly: f.hourly } : {}),
3463
+ ...(f.roomName ? { roomName: f.roomName } : {}),
3464
+ rooms: roomsIn(lines, f),
3465
+ // What to take out of the trip to put another hotel in its place.
3466
+ roomRefs: lines.flatMap((l) => (l.kind === 'room' && inFailedStay(l, f) ? [l.roomId] : [])),
3467
+ extraRefs: lines.flatMap((l) => l.kind === 'extra' && inFailedStay(l, f) ? [l.extraId] : []),
3468
+ why: f.why,
3469
+ ...(f.code ? { code: f.code } : {}),
3470
+ ...(f.args ? { args: f.args } : {}),
3471
+ ...(instead[i]?.city ? { city: instead[i]?.city } : {}),
3472
+ alternatives: (instead[i]?.hotels ?? []).map((h) => ({
3473
+ hotelId: h.id,
3474
+ name: h.name,
3475
+ brand: h.brand,
3476
+ ...(h.forStay
3477
+ ? {
3478
+ total: formatMoney(h.forStay.total),
3479
+ room: h.forStay.room,
3480
+ freeCancellation: h.forStay.freeCancellation,
3481
+ }
3482
+ : {}),
3483
+ })),
3484
+ }));
3485
+ const failedText = notBooked.map((f) => `NOT BOOKED - ${f.hotelName} · ${stayWords(f.stay, f.hourly)}: ${f.why} It is still in the trip.${f.alternatives.length
3486
+ ? ` Bookable instead for the same nights: ${f.alternatives.map((a) => `${a.name}${a.room ? ` (${a.room}` : ''}${a.total ? `${a.room ? ', ' : ' ('}${a.total} for the stay)` : a.room ? ')' : ''} [hotel \`${a.hotelId}\`]`).join('; ')}.`
3487
+ : ''} The guest can ask again, take another hotel, or leave it.`);
3488
+ if (!first) {
3489
+ // Nothing booked at all: every stay said, each with its way on.
3490
+ return {
3491
+ structured: {
3492
+ view: 'booking',
3493
+ ...common(deps, key),
3494
+ paid: false,
3495
+ failed: notBooked,
3496
+ },
3497
+ text: [
3498
+ `Nothing was booked: ${notBooked.length === 1 ? 'the stay' : `none of the ${notBooked.length} stays`} could be.`,
3499
+ ...failedText,
3500
+ ].join('\n'),
3501
+ };
3502
+ }
3349
3503
  return {
3350
3504
  structured: {
3351
3505
  // ⚠️ 'booking' EVEN WHEN IT IS AWAITING PAYMENT: the card draws a view
@@ -3355,13 +3509,13 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3355
3509
  // view with it.
3356
3510
  view: 'booking',
3357
3511
  ...common(deps, key),
3358
- booking: bookingForCard(first.booking),
3512
+ booking: madeForCard(deps, first.booking),
3359
3513
  // ⚠️ ONE PER STAY - see `requestBooking`. The card draws each with its
3360
3514
  // own code; `booking` stays the first, for a host that reads one.
3361
3515
  ...(several
3362
3516
  ? {
3363
3517
  bookings: booked.map(({ booking, asked }) => ({
3364
- ...bookingForCard(booking),
3518
+ ...madeForCard(deps, booking),
3365
3519
  total: formatMoney(booking.quote.total),
3366
3520
  ...(asked.length ? { requested: asked } : {}),
3367
3521
  // Each its own warning colour - it rode on the first alone.
@@ -3379,18 +3533,25 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3379
3533
  ...(first.booking.requests?.kinds.includes('accessible-room')
3380
3534
  ? { accessibility: true }
3381
3535
  : {}),
3536
+ ...(notBooked.length ? { failed: notBooked } : {}),
3382
3537
  },
3383
3538
  text: [
3539
+ // ⚠️ BOOKED IS CONFIRMED: a stay still being confirmed is said as
3540
+ // that, and not counted (总线程, 2026-10-14).
3541
+ notBooked.length || (booked.length > 1 && waiting)
3542
+ ? `${booked.length - waiting} of ${booked.length + notBooked.length} stays booked${waiting ? `, ${waiting} being confirmed by the hotel - no code until it is` : ''}${notBooked.length ? `; ${notBooked.length === 1 ? 'one' : notBooked.length} could not be, said below` : ''} - what was booked stays booked.`
3543
+ : '',
3384
3544
  several
3385
3545
  ? `${booked.length} bookings, one for each stay - each hotel keeps its own nights under its own code, and each is ${holderOf(deps, first.booking.supplier).caps.changes ? 'changed or cancelled' : 'cancelled'} on its own.`
3386
3546
  : '',
3387
3547
  ...booked.flatMap(({ booking, asked }) => [
3388
- `**${booking.confirmationCode}** · ${booking.hotelName} · ${stayWords(booking.stay, booking.hourly)}`,
3548
+ `**${confirming(deps, booking) ? 'Being confirmed' : booking.confirmationCode}** · ${booking.hotelName} · ${stayWords(booking.stay, booking.hourly)}`,
3389
3549
  bookedLine(deps, booking),
3390
3550
  ...asked,
3391
3551
  ]),
3552
+ ...failedText,
3392
3553
  remembered?.line ?? '',
3393
- rest ? `The trip's other stays are still in it, not booked:\n${rest.text}` : '',
3554
+ others ? `The trip's other stays are still in it, not booked:\n${rest?.text ?? ''}` : '',
3394
3555
  deps.vocab.demo ? DEMO_NOTE : '',
3395
3556
  ]
3396
3557
  .filter(Boolean)