@curless/hotel-storefront 0.49.1 → 0.50.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.
Files changed (68) hide show
  1. package/dist/backend.d.ts +1 -1
  2. package/dist/card.generated.d.ts +1 -1
  3. package/dist/card.generated.js +2 -2
  4. package/dist/card.generated.js.map +1 -1
  5. package/dist/config.d.ts +16 -18
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js.map +1 -1
  8. package/dist/demo-hotels.d.ts.map +1 -1
  9. package/dist/demo-hotels.js +3 -1
  10. package/dist/demo-hotels.js.map +1 -1
  11. package/dist/huazhu/backend.d.ts +2 -2
  12. package/dist/huazhu/backend.d.ts.map +1 -1
  13. package/dist/huazhu/backend.js +14 -12
  14. package/dist/huazhu/backend.js.map +1 -1
  15. package/dist/huazhu/bookings.d.ts +8 -5
  16. package/dist/huazhu/bookings.d.ts.map +1 -1
  17. package/dist/huazhu/bookings.js.map +1 -1
  18. package/dist/huazhu/check.d.ts +1 -1
  19. package/dist/huazhu/make.d.ts.map +1 -1
  20. package/dist/huazhu/make.js +6 -32
  21. package/dist/huazhu/make.js.map +1 -1
  22. package/dist/huazhu/sellable.d.ts.map +1 -1
  23. package/dist/huazhu/sellable.js +4 -3
  24. package/dist/huazhu/sellable.js.map +1 -1
  25. package/dist/index.js +0 -0
  26. package/dist/legal.d.ts.map +1 -1
  27. package/dist/legal.js +9 -15
  28. package/dist/legal.js.map +1 -1
  29. package/dist/payments/checkout.d.ts +10 -70
  30. package/dist/payments/checkout.d.ts.map +1 -1
  31. package/dist/payments/checkout.js +14 -131
  32. package/dist/payments/checkout.js.map +1 -1
  33. package/dist/payments/stripe-checkout.js +4 -4
  34. package/dist/payments/stripe-checkout.js.map +1 -1
  35. package/dist/present.d.ts +15 -7
  36. package/dist/present.d.ts.map +1 -1
  37. package/dist/present.js +26 -8
  38. package/dist/present.js.map +1 -1
  39. package/dist/same-booking.d.ts +9 -0
  40. package/dist/same-booking.d.ts.map +1 -1
  41. package/dist/same-booking.js +13 -1
  42. package/dist/same-booking.js.map +1 -1
  43. package/dist/server.d.ts +3 -3
  44. package/dist/server.d.ts.map +1 -1
  45. package/dist/server.js +6 -3
  46. package/dist/server.js.map +1 -1
  47. package/dist/supplier.d.ts +4 -1
  48. package/dist/supplier.d.ts.map +1 -1
  49. package/dist/supplier.js.map +1 -1
  50. package/dist/surface.d.ts +8 -0
  51. package/dist/surface.d.ts.map +1 -1
  52. package/dist/surface.js +23 -7
  53. package/dist/surface.js.map +1 -1
  54. package/dist/tools.d.ts +8 -0
  55. package/dist/tools.d.ts.map +1 -1
  56. package/dist/tools.js +148 -9
  57. package/dist/tools.js.map +1 -1
  58. package/dist/trip.d.ts +18 -2
  59. package/dist/trip.d.ts.map +1 -1
  60. package/dist/trip.js +27 -1
  61. package/dist/trip.js.map +1 -1
  62. package/dist/types.d.ts +11 -4
  63. package/dist/types.d.ts.map +1 -1
  64. package/package.json +13 -14
  65. package/dist/payments/fake-gateway.d.ts +0 -43
  66. package/dist/payments/fake-gateway.d.ts.map +0 -1
  67. package/dist/payments/fake-gateway.js +0 -135
  68. package/dist/payments/fake-gateway.js.map +0 -1
package/dist/tools.js CHANGED
@@ -8,7 +8,8 @@ import { namedExactly, roomNamesOf, roomTypeNamed, withTraditional } from './nam
8
8
  import { MAX_COMPARED, MAX_CONS, MAX_DAYS, MAX_ITEMS_PER_DAY, MAX_MOMENTS, MAX_PROS, buildItinerary, candidatesFor, compareHotels, namesMoreThanOne, } from './present.js';
9
9
  import { regionOf } from './regions.js';
10
10
  import { activeKeysOf, byRoomType, facetsOf, keyWords, matchesTerms, optionsOf, roomTermsOf, termWords, } from './room-filters.js';
11
- import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, clampCount, pickedKey, shownOf, tripKey, } from './trip.js';
11
+ import { SAME_BOOKING_MS } from './same-booking.js';
12
+ import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE, clampCount, pickedKey, shownOf, stampOf, tripKey, } from './trip.js';
12
13
  import { DEMO_NOTE, branded, catalogue, noteFor, upFirst } from './vocab.js';
13
14
  // The eleven things a guest can do: find hotels, look at one on dates, see what
14
15
  // else it offers, build a trip, ask for the rooms, and deal with a booking they
@@ -993,6 +994,8 @@ const TRIP_SCHEMA = {
993
994
  dropped: ARR,
994
995
  /** A name that fits more than one hotel: the ones it fits (`namesLike`). */
995
996
  candidates: ARR,
997
+ /** The trip as this answer drew it - the card sends it back with its booking form (`stampOf`). */
998
+ stamp: STR,
996
999
  },
997
1000
  required: ['view'],
998
1001
  };
@@ -1200,6 +1203,12 @@ const BOOKING_SCHEMA = {
1200
1203
  repeat: BOOL,
1201
1204
  /** A form whose room is no longer the one picked: the pick that would put it back (`pickChanged`). */
1202
1205
  again: OBJ,
1206
+ /** Of `bookings`, the ids asked for minutes before, when the others are new (`BookingResult.reused`). */
1207
+ repeated: STRS,
1208
+ /** `trip`, drawn again: see `stampOf`. */
1209
+ stamp: STR,
1210
+ /** The trip changed after the card that sent the form was drawn, so nothing was booked (`stampOf`). */
1211
+ tripChanged: BOOL,
1203
1212
  },
1204
1213
  required: ['view'],
1205
1214
  };
@@ -1236,6 +1245,8 @@ export const buildOutputSchemas = (n) => ({
1236
1245
  widened: OBJ,
1237
1246
  /** The deployment's only hotel, answered with its page - see HOTEL_PAGE. */
1238
1247
  only: BOOL,
1248
+ /** Only where the client shows no cards: nothing was drawn - see `LIST_NOT_DRAWN`. */
1249
+ notDrawn: STR,
1239
1250
  ...HOTEL_PAGE,
1240
1251
  },
1241
1252
  required: ['view'],
@@ -1377,7 +1388,7 @@ export const buildOutputSchemas = (n) => ({
1377
1388
  inTrip: { type: 'number' },
1378
1389
  planned: { type: 'number' },
1379
1390
  dropped: ARR,
1380
- /** The stays not booked yet, together - only when all are priced in one currency. */
1391
+ /** The stays not in the trip or booked yet - what "add them" adds - together, only when all are priced in one currency. */
1381
1392
  staysTotal: STR,
1382
1393
  /** What the text says of a long stay at one hotel, in the same words. */
1383
1394
  nudges: STRS,
@@ -2646,6 +2657,8 @@ const currentTrip = async (deps, key, who, store = key) => {
2646
2657
  // card's own trip view.
2647
2658
  if (lines.length)
2648
2659
  deps.trips.show(store, shownOf(parts));
2660
+ // What this answer drew, for the card's form to send back - see `stampOf`.
2661
+ const stamp = lines.length ? { stamp: stampOf(lines, deps.trips.shown(store)) } : {};
2649
2662
  const [only] = parts;
2650
2663
  const orphans = needsRoomIn(deps.tools, lines, parts);
2651
2664
  const needs = orphans.needsRoom.length ? { needsRoom: orphans.needsRoom } : {};
@@ -2659,6 +2672,7 @@ const currentTrip = async (deps, key, who, store = key) => {
2659
2672
  ...common(deps, key),
2660
2673
  ...quoteForCard(only, groupOf),
2661
2674
  ...needs,
2675
+ ...stamp,
2662
2676
  count: lines.length,
2663
2677
  },
2664
2678
  text: [
@@ -2700,6 +2714,7 @@ const currentTrip = async (deps, key, who, store = key) => {
2700
2714
  .join(' · '),
2701
2715
  ...needs,
2702
2716
  ...(shaped.some((p) => p.dropped) ? { dropped: shaped.flatMap((p) => p.dropped ?? []) } : {}),
2717
+ ...stamp,
2703
2718
  count: lines.length,
2704
2719
  },
2705
2720
  text: [
@@ -3014,6 +3029,29 @@ export const PLAN_DRAWN_ONCE = 'The plan on the card is this one. Something to c
3014
3029
  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.';
3015
3030
  /** A comparison's answer, for a client that shows no cards - see `PLAN_NOT_DRAWN`. */
3016
3031
  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.";
3032
+ /**
3033
+ * A list's answer, for a client that shows no cards - see `PLAN_NOT_DRAWN`.
3034
+ *
3035
+ * ⚠️ A LIST WITH NO CARD IS THE HOTELS IN WORDS (2026-10-29 audit, H4). The
3036
+ * answer said "12 Huazhu hotels in 上海." and nothing else - the hotels were on
3037
+ * the card - so a client that draws none had a count to tell the guest.
3038
+ */
3039
+ export const LIST_NOT_DRAWN = "Nothing was drawn: this app shows no cards, so the guest has seen none of these hotels. Tell them in your reply - each one's name, area and price as given here:";
3040
+ /** One hotel of a list as a line, for a client that shows no cards. */
3041
+ const listedInWords = (h) => {
3042
+ const where = [h.area, h.city]
3043
+ .filter((x) => !!x)
3044
+ .map((x) => hotelName(x))
3045
+ .join(', ');
3046
+ const price = h.forStay
3047
+ ? `${formatMoney(h.forStay.total)} for the stay incl. taxes and fees, ${hotelName(h.forStay.room)}${h.forStay.rooms > 1 ? ` × ${h.forStay.rooms}` : ''}`
3048
+ : h.noRoomsForStay
3049
+ ? 'no room for those nights'
3050
+ : h.fromNightly
3051
+ ? `from ${formatMoney(h.fromNightly)} a night`
3052
+ : '';
3053
+ return `- ${hotelName(h.name)} (\`${h.id}\`)${where ? ` · ${where}` : ''}${price ? `: ${price}` : ''}`;
3054
+ };
3017
3055
  /**
3018
3056
  * One column of a comparison as a line, for a client that shows no cards:
3019
3057
  * what the card's rows say - where, the rating, the room, the price - and the
@@ -3022,7 +3060,11 @@ export const COMPARISON_NOT_DRAWN = "Nothing was drawn: this app shows no cards,
3022
3060
  const comparedInWords = (c) => {
3023
3061
  const h = c.hotel;
3024
3062
  const about = [
3025
- [h.area, h.city].filter(Boolean).join(', '),
3063
+ // The group's own words, as the name is (2026-10-29 audit, H9).
3064
+ [h.area, h.city]
3065
+ .filter((x) => !!x)
3066
+ .map((x) => hotelName(x))
3067
+ .join(', '),
3026
3068
  h.rating ? `${h.rating.value}/${h.rating.scaleMax} from ${h.rating.count} reviews` : '',
3027
3069
  ]
3028
3070
  .filter(Boolean)
@@ -3226,6 +3268,73 @@ const pickChanged = (deps, key, args) => {
3226
3268
  text: words,
3227
3269
  };
3228
3270
  };
3271
+ /** A booking that is still one - what `bookedFromThisForm` may answer with. */
3272
+ const LIVE_STATUSES = new Set([
3273
+ 'requested',
3274
+ 'confirmed',
3275
+ 'unconfirmed',
3276
+ 'awaiting-payment',
3277
+ ]);
3278
+ /**
3279
+ * The booking this very form made a few minutes ago, when the slot is empty
3280
+ * because of it - or undefined.
3281
+ *
3282
+ * ⚠️ THE GUEST'S OWN BOOKING IS NOT "ANOTHER CARD'S" (2026-10-29 audit, H2c).
3283
+ * A booking empties the pick; when its answer is lost on the way (a host that
3284
+ * gives up before 华住 has answered), the guest presses again on the same form
3285
+ * and was told another card had booked or let go of the room - about the
3286
+ * booking they had just made. A live booking of this conversation, for this
3287
+ * email, this room, these nights and this count, made within the minutes a
3288
+ * repeat is recognised in (`SAME_BOOKING_MS`), is that booking: said as one,
3289
+ * and nothing is booked again.
3290
+ */
3291
+ const bookedFromThisForm = async (deps, ctx, key, args) => {
3292
+ const email = str(args.email).trim().toLowerCase();
3293
+ const room = str(args.room);
3294
+ if (!email || !room)
3295
+ return undefined;
3296
+ const said = (v) => v !== undefined && v !== null && v !== '';
3297
+ const rooms = said(args.rooms) ? Number(args.rooms) : 1;
3298
+ const at = deps.trips.now();
3299
+ let mine;
3300
+ try {
3301
+ mine = await deps.backend.listBookings(identity(deps, ctx, key, { email }));
3302
+ }
3303
+ catch {
3304
+ return undefined;
3305
+ }
3306
+ const b = mine.find((x) => LIVE_STATUSES.has(x.status) &&
3307
+ at - Date.parse(x.createdAt) < SAME_BOOKING_MS &&
3308
+ x.hotelId === str(args.hotel) &&
3309
+ x.guest.email.trim().toLowerCase() === email &&
3310
+ (!said(args.guests) || Number(args.guests) === x.stay.guests) &&
3311
+ (said(args.startTime)
3312
+ ? x.hourly?.start === str(args.startTime)
3313
+ : (!said(args.checkIn) || str(args.checkIn) === x.stay.checkIn) &&
3314
+ (!said(args.checkOut) || str(args.checkOut) === x.stay.checkOut)) &&
3315
+ x.quote.lines.some((l) => l.kind === 'room' && l.roomRef === room && (l.quantity ?? 1) === rooms));
3316
+ if (!b)
3317
+ return undefined;
3318
+ return {
3319
+ structured: {
3320
+ view: 'booking',
3321
+ ...common(deps, key),
3322
+ booking: madeForCard(deps, b),
3323
+ paid: false,
3324
+ holdUntil: b.holdUntil,
3325
+ total: formatMoney(b.quote.total),
3326
+ repeat: true,
3327
+ },
3328
+ text: [
3329
+ 'NOTHING NEW WAS BOOKED: this booking form already booked this room a few minutes ago, and that answer did not reach the card - this is the booking made then.',
3330
+ `**${confirming(deps, b) ? 'Being confirmed' : b.confirmationCode}** · ${b.hotelName} · ${stayWords(b.stay, b.hourly)}`,
3331
+ bookedLine(deps, b) ?? '',
3332
+ deps.vocab.demo ? noteFor(deps.vocab) : '',
3333
+ ]
3334
+ .filter(Boolean)
3335
+ .join('\n'),
3336
+ };
3337
+ };
3229
3338
  /**
3230
3339
  * A room line, validated and priced against the hotel's calendar and put into
3231
3340
  * one slot of the trip store - THE ONE ROOM-LINE CODE, for `add_to_trip` (`store`
@@ -3460,6 +3569,20 @@ const bookTripLines = async (deps, t, ctx, key, store, args, nothingThere) => {
3460
3569
  text: nothingThere,
3461
3570
  };
3462
3571
  }
3572
+ // ⚠️ THE TRIP THE CARD DREW, OR NOTHING - see `stampOf`. The assistant books
3573
+ // the trip as it is now (no stamp); a card's form books the one it showed.
3574
+ const drawn = str(args.stamp);
3575
+ if (drawn && drawn !== stampOf(inTrip, deps.trips.shown(store))) {
3576
+ const again = await currentTrip(deps, key, identity(deps, ctx, key), store);
3577
+ return {
3578
+ ...again,
3579
+ structured: { ...again.structured, tripChanged: true },
3580
+ text: [
3581
+ 'Nothing was booked: the trip changed after the card the guest pressed Book on was drawn - a stay was added, taken out or changed in this conversation, or its price moved. It is shown again as it is now; the guest checks it and presses Book again.',
3582
+ again.text,
3583
+ ].join('\n\n'),
3584
+ };
3585
+ }
3463
3586
  // ⚠️ ONE GROUP AT A TIME, in a market: a booking cannot be all-or-nothing
3464
3587
  // across two groups' systems (Marketplace, §6-1). `group` books one
3465
3588
  // group's stays and leaves the rest in the trip.
@@ -3506,7 +3629,7 @@ const bookTripLines = async (deps, t, ctx, key, store, args, nothingThere) => {
3506
3629
  const { requests, dropped } = readRequests(args);
3507
3630
  try {
3508
3631
  const shown = deps.trips.shown(store);
3509
- const { booked: made, failed, repeat, } = await deps.backend.requestBooking({ lines, ...(shown ? { shown } : {}) }, {
3632
+ const { booked: made, failed, repeat, reused, } = await deps.backend.requestBooking({ lines, ...(shown ? { shown } : {}) }, {
3510
3633
  ...guest,
3511
3634
  ...(str(args.phone) ? { phone: str(args.phone) } : {}),
3512
3635
  ...(requests?.note ? { notes: requests.note } : {}),
@@ -3590,6 +3713,8 @@ const bookTripLines = async (deps, t, ctx, key, store, args, nothingThere) => {
3590
3713
  }));
3591
3714
  const [first] = booked;
3592
3715
  const several = booked.length > 1;
3716
+ // The stays asked for minutes before, beside new ones - see `reused`.
3717
+ const earlier = booked.filter(({ booking }) => reused?.includes(booking.id));
3593
3718
  const waiting = booked.filter(({ booking }) => confirming(deps, booking)).length;
3594
3719
  const notBooked = failed.map((f, i) => ({
3595
3720
  hotelId: f.hotelId,
@@ -3683,13 +3808,16 @@ const bookTripLines = async (deps, t, ctx, key, store, args, nothingThere) => {
3683
3808
  : {}),
3684
3809
  ...(notBooked.length ? { failed: notBooked } : {}),
3685
3810
  ...(repeat ? { repeat: true } : {}),
3811
+ ...(earlier.length ? { repeated: earlier.map(({ booking }) => booking.id) } : {}),
3686
3812
  },
3687
3813
  text: [
3688
3814
  // ⚠️ THE SAME BOOKING ASKED FOR AGAIN IS SAID TO BE THAT, never a new
3689
3815
  // one (2026-10-27 audit, F2): it was answered as if just booked.
3690
3816
  repeat
3691
3817
  ? `NOTHING NEW WAS BOOKED: the same ${booked.length > 1 ? 'stays' : 'stay'} for the same guest ${booked.length > 1 ? 'were' : 'was'} asked for a few minutes ago, and ${booked.length > 1 ? 'these are the bookings' : 'this is the booking'} made then. A booking for someone else is made under their name.`
3692
- : '',
3818
+ : earlier.length
3819
+ ? `NOT ALL NEW: ${listAnd(earlier.map(({ booking }) => booking.confirmationCode || booking.hotelName))} ${earlier.length > 1 ? 'were' : 'was'} asked for a few minutes ago for the same guest and ${earlier.length > 1 ? 'are the bookings' : 'is the booking'} made then; the rest were booked now.`
3820
+ : '',
3693
3821
  // ⚠️ BOOKED IS CONFIRMED: a stay still being confirmed is said as
3694
3822
  // that, and not counted (总线程, 2026-10-14).
3695
3823
  notBooked.length || (booked.length > 1 && waiting)
@@ -3736,7 +3864,10 @@ const bookTripLines = async (deps, t, ctx, key, store, args, nothingThere) => {
3736
3864
  structured: {
3737
3865
  view: 'error',
3738
3866
  ...common(deps, key),
3739
- error: 'not-bookable',
3867
+ // ⚠️ NOT KNOWN IS NOT "NOT BOOKED" (2026-10-29 audit, H2): the card
3868
+ // said a fault as "could not be booked, try again later" - the one
3869
+ // thing a guest must not do after a booking that may have gone through.
3870
+ error: words === BOOKING_FAULT ? 'uncertain' : 'not-bookable',
3740
3871
  detail: words,
3741
3872
  },
3742
3873
  text: words,
@@ -4177,6 +4308,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4177
4308
  ...(answer.overBudget ? { overBudget: answer.overBudget } : {}),
4178
4309
  ...(widened?.found ? { widened } : {}),
4179
4310
  ...(answer.currencies.length > 1 ? { currencies: answer.currencies } : {}),
4311
+ // In both, like a plan's: a host with no card may hand its model this
4312
+ // rather than the text.
4313
+ ...(ctx.noCards ? { notDrawn: LIST_NOT_DRAWN } : {}),
4180
4314
  },
4181
4315
  text: [
4182
4316
  `${total} ${branded(deps.vocab, total === 1 ? deps.vocab.noun : deps.vocab.nounPlural)}${where}.`,
@@ -4189,6 +4323,9 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4189
4323
  : '',
4190
4324
  ...priceNotes,
4191
4325
  widened?.found ? `Nothing matched ${widened.from}, so this is ${widened.to}.` : '',
4326
+ ctx.noCards && hotels.length
4327
+ ? `\n${LIST_NOT_DRAWN}\n${hotels.map(listedInWords).join('\n')}\n`
4328
+ : '',
4192
4329
  forPlans,
4193
4330
  ]
4194
4331
  .filter(Boolean)
@@ -4648,8 +4785,10 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
4648
4785
  // a trip is - and the trip left as it is.
4649
4786
  if (name === t.bookPicked) {
4650
4787
  const changed = pickChanged(deps, key, args);
4651
- if (changed)
4652
- return direct(changed);
4788
+ if (changed) {
4789
+ const emptied = !deps.trips.read(pickedKey(key)).some((l) => l.kind === 'room');
4790
+ return direct((emptied && (await bookedFromThisForm(deps, ctx, key, args))) || changed);
4791
+ }
4653
4792
  return direct(await bookTripLines(deps, t, ctx, key, pickedKey(key), args, 'Nothing is picked: the guest presses Reserve on a room first.'));
4654
4793
  }
4655
4794
  if (name === t.listBookings) {
@@ -5267,7 +5406,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
5267
5406
  `Priced stays (${plan.stay?.guests ?? 2} guests; ${t.addToTrip} takes each one's hotel id, room id and dates):`,
5268
5407
  ...stays,
5269
5408
  ...(plan.staysTotal
5270
- ? [`Together, the stays not booked yet: ${plan.staysTotal}.`]
5409
+ ? [`Together, the stays not in the trip or booked yet: ${plan.staysTotal}.`]
5271
5410
  : []),
5272
5411
  ...nudges,
5273
5412
  ]