@curless/hotel-storefront 0.28.0 → 0.29.1

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 (62) hide show
  1. package/dist/backend.d.ts +7 -0
  2. package/dist/backend.d.ts.map +1 -1
  3. package/dist/backend.js.map +1 -1
  4. package/dist/card.generated.d.ts +1 -1
  5. package/dist/card.generated.js +2 -2
  6. package/dist/card.generated.js.map +1 -1
  7. package/dist/config.d.ts +38 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js.map +1 -1
  10. package/dist/demo-hotels.d.ts.map +1 -1
  11. package/dist/demo-hotels.js +7 -1
  12. package/dist/demo-hotels.js.map +1 -1
  13. package/dist/huazhu/backend.d.ts +11 -1
  14. package/dist/huazhu/backend.d.ts.map +1 -1
  15. package/dist/huazhu/backend.js +516 -32
  16. package/dist/huazhu/backend.js.map +1 -1
  17. package/dist/huazhu/bookings.d.ts +37 -1
  18. package/dist/huazhu/bookings.d.ts.map +1 -1
  19. package/dist/huazhu/bookings.js.map +1 -1
  20. package/dist/huazhu/make.d.ts.map +1 -1
  21. package/dist/huazhu/make.js +25 -0
  22. package/dist/huazhu/make.js.map +1 -1
  23. package/dist/huazhu/sandbox-photos.d.ts +7 -0
  24. package/dist/huazhu/sandbox-photos.d.ts.map +1 -1
  25. package/dist/huazhu/sandbox-photos.js +7 -0
  26. package/dist/huazhu/sandbox-photos.js.map +1 -1
  27. package/dist/huazhu/sandbox.d.ts +2 -0
  28. package/dist/huazhu/sandbox.d.ts.map +1 -1
  29. package/dist/huazhu/sandbox.js +14 -6
  30. package/dist/huazhu/sandbox.js.map +1 -1
  31. package/dist/huazhu/sellable.d.ts +17 -0
  32. package/dist/huazhu/sellable.d.ts.map +1 -1
  33. package/dist/huazhu/sellable.js +37 -8
  34. package/dist/huazhu/sellable.js.map +1 -1
  35. package/dist/legal.d.ts.map +1 -1
  36. package/dist/legal.js +19 -8
  37. package/dist/legal.js.map +1 -1
  38. package/dist/payments/checkout.d.ts +71 -0
  39. package/dist/payments/checkout.d.ts.map +1 -0
  40. package/dist/payments/checkout.js +133 -0
  41. package/dist/payments/checkout.js.map +1 -0
  42. package/dist/payments/fake-gateway.d.ts +43 -0
  43. package/dist/payments/fake-gateway.d.ts.map +1 -0
  44. package/dist/payments/fake-gateway.js +135 -0
  45. package/dist/payments/fake-gateway.js.map +1 -0
  46. package/dist/server.d.ts +4 -2
  47. package/dist/server.d.ts.map +1 -1
  48. package/dist/server.js +19 -4
  49. package/dist/server.js.map +1 -1
  50. package/dist/supplier.d.ts +2 -0
  51. package/dist/supplier.d.ts.map +1 -1
  52. package/dist/supplier.js.map +1 -1
  53. package/dist/surface.d.ts.map +1 -1
  54. package/dist/surface.js +7 -2
  55. package/dist/surface.js.map +1 -1
  56. package/dist/tools.d.ts +15 -1
  57. package/dist/tools.d.ts.map +1 -1
  58. package/dist/tools.js +127 -16
  59. package/dist/tools.js.map +1 -1
  60. package/dist/types.d.ts +41 -1
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +1 -1
package/dist/tools.js CHANGED
@@ -208,7 +208,9 @@ const forCapabilities = (tools, n, v, caps) => {
208
208
  // itself: the instructions carried "test environment" while this
209
209
  // line still promised "the hotel's own reservation" (2026-09-30).
210
210
  description: v.sandbox
211
- ? `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. 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.`
211
+ ? `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
212
+ ? '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.'
213
+ : '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.`
212
214
  : `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.`,
213
215
  };
214
216
  }
@@ -864,13 +866,13 @@ const CHANGE_SCHEMA = {
864
866
  * policies, and every trip a member badge and saving (2026-10-05 audit) -
865
867
  * fields no answer there ever carries.
866
868
  */
867
- export const outputSchemasFor = (schemas, caps) => {
869
+ export const outputSchemasFor = (schemas, caps, lang, also = {}) => {
868
870
  const gone = [
869
871
  ...(caps.members ? [] : ['member', 'memberSaving']),
870
872
  ...(caps.extras ? [] : ['extras']),
871
873
  ...(caps.policies === false ? ['policies'] : []),
872
874
  ];
873
- if (!gone.length && !caps.hourly)
875
+ if (!gone.length && !caps.hourly && !lang && !also.logoDark && !also.accent)
874
876
  return schemas;
875
877
  return Object.fromEntries(Object.entries(schemas).map(([name, schema]) => {
876
878
  const s = schema;
@@ -882,6 +884,15 @@ export const outputSchemasFor = (schemas, caps) => {
882
884
  // On every answer, as `common` puts it: rooms by the hour are sold here.
883
885
  if (caps.hourly)
884
886
  properties.hourly = BOOL;
887
+ // On every answer too: the language the card draws its own words in.
888
+ if (lang)
889
+ properties.lang = { enum: ['zh', 'en'] };
890
+ // And the dark page's logo, where the group gave one.
891
+ if (also.logoDark)
892
+ properties.logoDarkUrl = STR;
893
+ // And the group's own colour, where its deployment gives one.
894
+ if (also.accent)
895
+ properties.accent = { type: 'string', pattern: '^#[0-9a-fA-F]{6}$' };
885
896
  return [name, { ...s, properties }];
886
897
  }));
887
898
  };
@@ -1189,7 +1200,48 @@ const holderOf = (deps, supplier) => {
1189
1200
  * room - and a booking whose confirmation has not come back yet is neither,
1190
1201
  * and is said as exactly that.
1191
1202
  */
1203
+ /**
1204
+ * A booking as the card is given it: its money as strings, like every other
1205
+ * price - the card never does arithmetic, and an amount sent as
1206
+ * `{ minor, currency }` was drawn as "[object Object]" (香格里拉, 2026-10-12).
1207
+ */
1208
+ const bookingForCard = (b) => ({
1209
+ ...b,
1210
+ quote: quoteForCard(b.quote),
1211
+ ...(b.payment ? { payment: { ...b.payment, amount: formatMoney(b.payment.amount) } } : {}),
1212
+ });
1213
+ /**
1214
+ * A booking paid here in advance, as the guest is told it - by where its
1215
+ * money stands. Undefined for any other booking.
1216
+ */
1217
+ const paidLine = (deps, b) => {
1218
+ const p = b.payment;
1219
+ if (!p)
1220
+ return undefined;
1221
+ const h = holderOf(deps, b.supplier);
1222
+ const amount = formatMoney(p.amount);
1223
+ const test = h.sandbox
1224
+ ? ' This is a test: the card page is in test mode, no real money moves, and the booking is in the simulated system.'
1225
+ : '';
1226
+ switch (p.status) {
1227
+ case 'awaiting':
1228
+ return `**Not booked yet, and nothing paid.** ${amount} is 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.${test}`;
1229
+ case 'paid':
1230
+ return b.status === 'confirmed'
1231
+ ? `${amount} paid in advance by card, and booked with ${h.name}: nothing more is paid at the hotel.${test}`
1232
+ : `${amount} paid in advance by card. ${h.name} has not confirmed the booking yet - ${deps.tools.listBookings} shows when it has; if it does not take it, all ${amount} goes back.${test}`;
1233
+ case 'lapsed':
1234
+ return `Not paid in time: nothing was booked and nothing was charged.${test}`;
1235
+ case 'refunding':
1236
+ return `All ${amount} is going back to the card it was paid with${p.refundReason ? ` - ${p.refundReason}` : ''}.${test}`;
1237
+ case 'refunded':
1238
+ return `All ${amount} has gone back to the card it was paid with${p.refundReason ? ` - ${p.refundReason}` : ''}.${test}`;
1239
+ }
1240
+ };
1192
1241
  const bookedLine = (deps, b) => {
1242
+ const paid = paidLine(deps, b);
1243
+ if (paid)
1244
+ return paid;
1193
1245
  const total = formatMoney(b.quote.total);
1194
1246
  const h = holderOf(deps, b.supplier);
1195
1247
  if (h.caps.booking === 'pay-at-hotel') {
@@ -1224,6 +1276,8 @@ const common = (deps, key) => {
1224
1276
  return {
1225
1277
  brand: deps.vocab.brand,
1226
1278
  ...(deps.logoUrl ? { logoUrl: deps.logoUrl } : {}),
1279
+ ...(deps.logoDarkUrl ? { logoDarkUrl: deps.logoDarkUrl } : {}),
1280
+ ...(deps.accent ? { accent: deps.accent } : {}),
1227
1281
  ...(deps.showName ? { showName: true } : {}),
1228
1282
  toolNames: deps.offered
1229
1283
  ? Object.fromEntries(Object.entries(deps.tools).filter(([, name]) => deps.offered?.has(name)))
@@ -1245,8 +1299,19 @@ const common = (deps, key) => {
1245
1299
  vocab: { noun: deps.vocab.noun, nounPlural: deps.vocab.nounPlural },
1246
1300
  ...(deps.vocab.demo ? { demo: true, demoNote: DEMO_NOTE } : { demo: false }),
1247
1301
  ...(key ? memberBadge(deps, key) : {}),
1302
+ ...(langOf(deps, key) ? { lang: langOf(deps, key) } : {}),
1248
1303
  };
1249
1304
  };
1305
+ /**
1306
+ * The language the CARD speaks to this guest - its own buttons, labels and
1307
+ * dates. What the server writes for the model stays English.
1308
+ *
1309
+ * ⚠️ PER TRIP, NOT ONLY PER BRAND (总线程, 2026-10-12): today it is the
1310
+ * deployment's (Huazhu's single store is Chinese), and a guest's own choice -
1311
+ * as the market's \`set_language\` keeps one per buyer - is meant to come in
1312
+ * HERE, by \`key\`, without the card or the schema changing.
1313
+ */
1314
+ const langOf = (deps, _key) => deps.lang;
1250
1315
  /**
1251
1316
  * When a room stops being free to cancel, the way a guest reads it: the
1252
1317
  * hotel's own clock when the backend knows it, else the instant in UTC - never
@@ -1278,6 +1343,12 @@ const quoteForCard = (quote, groupOf) => ({
1278
1343
  total: formatMoney(l.total),
1279
1344
  refundable: l.refundable,
1280
1345
  freeCancellationUntil: l.freeCancellationUntil,
1346
+ // The deadline on the hotel's clock, where the backend knows it - the
1347
+ // review restates the terms beside the total (round-2 jury).
1348
+ ...(l.freeCancellationLocal ? { freeCancellationLocal: l.freeCancellationLocal } : {}),
1349
+ // Paid in advance: the review says so, and its button leads to the
1350
+ // payment page rather than to a booking (香格里拉's card, 2026-10-12).
1351
+ ...(l.payment ? { payment: l.payment } : {}),
1281
1352
  day: l.day,
1282
1353
  })),
1283
1354
  subtotal: formatMoney(quote.subtotal),
@@ -1541,6 +1612,13 @@ const labelWords = (r) => {
1541
1612
  ];
1542
1613
  return words.length ? ` [${words.join(', ')}]` : '';
1543
1614
  };
1615
+ const PAYMENT_WORDS = {
1616
+ awaiting: 'to be paid on the card page',
1617
+ paid: 'paid in advance by card',
1618
+ lapsed: 'not paid in time - nothing booked or charged',
1619
+ refunding: 'refund in full under way',
1620
+ refunded: 'refunded in full',
1621
+ };
1544
1622
  const bookingLine = (b) => {
1545
1623
  const kinds = b.requests?.kinds ?? [];
1546
1624
  return [
@@ -1550,6 +1628,9 @@ const bookingLine = (b) => {
1550
1628
  ? 'unconfirmed: whether it was made is not known yet, and a person is confirming it'
1551
1629
  : b.status}`,
1552
1630
  kinds.length ? ` · asked for ${kinds.map(bare).join(', ')}` : '',
1631
+ b.payment
1632
+ ? ` · ${PAYMENT_WORDS[b.payment.status]}${b.payment.status === 'awaiting' && b.payment.url ? ` by ${b.payment.payByLocal ?? b.payment.payBy}: ${b.payment.url}` : ''}`
1633
+ : '',
1553
1634
  ].join('');
1554
1635
  };
1555
1636
  /**
@@ -1594,6 +1675,13 @@ const dateFlex = (dates, stay) => {
1594
1675
  // the other two are worse, which is a claim about prices that are equal.
1595
1676
  const beats = best?.from && chosen?.from && best.from.minor < chosen.from.minor ? best : undefined;
1596
1677
  const beatsBy = beats?.from?.minor;
1678
+ // The cheapest of the window - the chosen date too - when two or more are
1679
+ // priced in one currency; ties all marked. `best` stays what it was: a date
1680
+ // that beats the guest's own.
1681
+ const priced = dates.filter((d) => d.from);
1682
+ const lowest = priced.length >= 2 && new Set(priced.map((d) => d.from?.currency)).size === 1
1683
+ ? Math.min(...priced.map((d) => d.from?.minor ?? Number.POSITIVE_INFINITY))
1684
+ : undefined;
1597
1685
  const structured = {
1598
1686
  dates: dates.map((d) => ({
1599
1687
  checkIn: d.checkIn,
@@ -1602,6 +1690,7 @@ const dateFlex = (dates, stay) => {
1602
1690
  ...(d.chosen ? { chosen: true } : {}),
1603
1691
  // The chosen date cannot match: `beatsBy` is a price strictly below it.
1604
1692
  ...(beatsBy !== undefined && d.from?.minor === beatsBy ? { best: true } : {}),
1693
+ ...(lowest !== undefined && d.from?.minor === lowest ? { lowest: true } : {}),
1605
1694
  })),
1606
1695
  };
1607
1696
  if (!best?.from)
@@ -2434,8 +2523,19 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
2434
2523
  .join(' '),
2435
2524
  };
2436
2525
  }
2526
+ // ⚠️ THE CHEAPEST, SAID BY US - the card never compares two prices it was
2527
+ // handed as text. Only on named nights, in one currency, with two or more
2528
+ // priced to compare; every hotel that ties is marked (总线程, 2026-10-12).
2529
+ // A room by the hour is priced for its own hours, and is not in the race.
2530
+ const stays = hotels.filter((h) => h.forStay && !h.forStay.hours);
2531
+ const lowest = stays.length >= 2 && new Set(stays.map((h) => h.forStay?.total.currency)).size === 1
2532
+ ? Math.min(...stays.map((h) => h.forStay?.total.minor ?? Number.POSITIVE_INFINITY))
2533
+ : undefined;
2437
2534
  const forCard = hotels.map((h) => ({
2438
2535
  ...h,
2536
+ ...(lowest !== undefined && h.forStay && !h.forStay.hours && h.forStay.total.minor === lowest
2537
+ ? { lowest: true }
2538
+ : {}),
2439
2539
  summary: hotelText('description', h.name, h.summary, DESCRIPTION_MAX).text,
2440
2540
  fromNightly: h.fromNightly ? formatMoney(h.fromNightly) : undefined,
2441
2541
  ...(h.forStay
@@ -3037,6 +3137,10 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3037
3137
  error: 'no-such-room',
3038
3138
  query: roomRef,
3039
3139
  rooms: dated.rooms.map((r) => ({ id: r.id, name: r.name })),
3140
+ // The hotel and the nights, so the card can take the guest straight
3141
+ // back to its rooms rather than to the start (round-2 jury).
3142
+ hotel: { id: detail.hotel.id, name: detail.hotel.name },
3143
+ stay,
3040
3144
  },
3041
3145
  text: `${detail.hotel.name} has no room type by that name for those nights. It has: ${dated.rooms.map((r) => r.name).join(', ')}.`,
3042
3146
  };
@@ -3192,16 +3296,20 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3192
3296
  const several = booked.length > 1;
3193
3297
  return {
3194
3298
  structured: {
3299
+ // ⚠️ 'booking' EVEN WHEN IT IS AWAITING PAYMENT: the card draws a view
3300
+ // only once it knows it, and the card-contract test holds every view
3301
+ // here to that. 香格里拉's card turns this into its own 'payment' step
3302
+ // (booking.status 'awaiting-payment', booking.payment) - and moves the
3303
+ // view with it.
3195
3304
  view: 'booking',
3196
3305
  ...common(deps, key),
3197
- booking: { ...first.booking, quote: quoteForCard(first.booking.quote) },
3306
+ booking: bookingForCard(first.booking),
3198
3307
  // ⚠️ ONE PER STAY - see `requestBooking`. The card draws each with its
3199
3308
  // own code; `booking` stays the first, for a host that reads one.
3200
3309
  ...(several
3201
3310
  ? {
3202
3311
  bookings: booked.map(({ booking, asked }) => ({
3203
- ...booking,
3204
- quote: quoteForCard(booking.quote),
3312
+ ...bookingForCard(booking),
3205
3313
  total: formatMoney(booking.quote.total),
3206
3314
  ...(asked.length ? { requested: asked } : {}),
3207
3315
  // Each its own warning colour - it rode on the first alone.
@@ -3290,7 +3398,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3290
3398
  structured: {
3291
3399
  view: 'bookings',
3292
3400
  ...common(deps, key),
3293
- bookings: bookings.map((b) => ({ ...b, quote: quoteForCard(b.quote) })),
3401
+ bookings: bookings.map(bookingForCard),
3294
3402
  count: bookings.length,
3295
3403
  // What was looked for, so a card can say THAT did not match rather
3296
3404
  // than "nothing in this conversation".
@@ -3424,7 +3532,7 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3424
3532
  structured: {
3425
3533
  view: 'changed',
3426
3534
  ...common(deps, key),
3427
- booking: { ...changed, quote: quoteForCard(changed.quote) },
3535
+ booking: bookingForCard(changed),
3428
3536
  hotelName: preview.hotelName,
3429
3537
  from: stayOf(preview.from),
3430
3538
  ...(preview.to ? { to: stayOf(preview.to) } : {}),
@@ -3525,19 +3633,22 @@ const answerTool = async (deps, name, rawArgs, ctx) => {
3525
3633
  structured: {
3526
3634
  view: 'cancelled',
3527
3635
  ...common(deps, key),
3528
- booking: { ...cancelled, quote: quoteForCard(cancelled.quote) },
3636
+ booking: bookingForCard(cancelled),
3529
3637
  fee: formatMoney(charged),
3530
3638
  rule: preview.rule,
3531
3639
  cancellable: false,
3532
3640
  },
3533
3641
  text: [
3534
- h.sandbox
3535
- ? // A test booking cancelled in the simulation: no hotel, no charge (2026-10-05 audit, P2).
3536
- `${cancelled.confirmationCode} is cancelled in the simulated system. ${charged.minor === 0 ? 'No fee.' : `A real booking on this rate would be charged ${formatMoney(charged)}; this one is a test, and nothing is charged.`}`
3537
- : h.demo
3538
- ? // Nobody charges a demonstration anything (2026-10-05 audit, P2).
3539
- `${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.`}`
3540
- : `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `The hotel charges ${formatMoney(charged)}.`}`,
3642
+ cancelled.payment
3643
+ ? // Paid here in advance: what becomes of the money is the answer.
3644
+ `${cancelled.confirmationCode} is ${cancelled.status === 'expired' ? 'called off before it was paid' : 'cancelled'}. ${paidLine(deps, cancelled)}`
3645
+ : h.sandbox
3646
+ ? // A test booking cancelled in the simulation: no hotel, no charge (2026-10-05 audit, P2).
3647
+ `${cancelled.confirmationCode} is cancelled in the simulated system. ${charged.minor === 0 ? 'No fee.' : `A real booking on this rate would be charged ${formatMoney(charged)}; this one is a test, and nothing is charged.`}`
3648
+ : h.demo
3649
+ ? // Nobody charges a demonstration anything (2026-10-05 audit, P2).
3650
+ `${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.`}`
3651
+ : `${cancelled.confirmationCode} is cancelled. ${charged.minor === 0 ? 'No fee.' : `The hotel charges ${formatMoney(charged)}.`}`,
3541
3652
  demoBookingNote(deps, cancelled.supplier),
3542
3653
  deps.vocab.demo ? DEMO_NOTE : '',
3543
3654
  ]