@curless/hotel-storefront 0.37.1 → 0.38.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 (51) hide show
  1. package/dist/backend.d.ts +43 -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 +16 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +4 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/huazhu/backend.d.ts +7 -0
  12. package/dist/huazhu/backend.d.ts.map +1 -1
  13. package/dist/huazhu/backend.js +306 -7
  14. package/dist/huazhu/backend.js.map +1 -1
  15. package/dist/huazhu/bookings.d.ts +9 -0
  16. package/dist/huazhu/bookings.d.ts.map +1 -1
  17. package/dist/huazhu/bookings.js.map +1 -1
  18. package/dist/huazhu/fake.d.ts.map +1 -1
  19. package/dist/huazhu/fake.js +5 -0
  20. package/dist/huazhu/fake.js.map +1 -1
  21. package/dist/huazhu/make.d.ts.map +1 -1
  22. package/dist/huazhu/make.js +28 -1
  23. package/dist/huazhu/make.js.map +1 -1
  24. package/dist/huazhu/wire.d.ts +17 -0
  25. package/dist/huazhu/wire.d.ts.map +1 -1
  26. package/dist/huazhu/wire.js.map +1 -1
  27. package/dist/legal.d.ts.map +1 -1
  28. package/dist/legal.js +23 -4
  29. package/dist/legal.js.map +1 -1
  30. package/dist/lib.d.ts +1 -1
  31. package/dist/lib.d.ts.map +1 -1
  32. package/dist/lib.js.map +1 -1
  33. package/dist/payments/fake-unionpay.d.ts +22 -0
  34. package/dist/payments/fake-unionpay.d.ts.map +1 -0
  35. package/dist/payments/fake-unionpay.js +89 -0
  36. package/dist/payments/fake-unionpay.js.map +1 -0
  37. package/dist/payments/unionpay.d.ts +125 -0
  38. package/dist/payments/unionpay.d.ts.map +1 -0
  39. package/dist/payments/unionpay.js +192 -0
  40. package/dist/payments/unionpay.js.map +1 -0
  41. package/dist/server.d.ts +2 -2
  42. package/dist/server.d.ts.map +1 -1
  43. package/dist/server.js +12 -3
  44. package/dist/server.js.map +1 -1
  45. package/dist/tools.d.ts +24 -0
  46. package/dist/tools.d.ts.map +1 -1
  47. package/dist/tools.js +124 -2
  48. package/dist/tools.js.map +1 -1
  49. package/dist/types.d.ts +14 -5
  50. package/dist/types.d.ts.map +1 -1
  51. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"card.generated.js","sourceRoot":"","sources":["../src/card.generated.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,wEAAwE;AACxE,gFAAgF;AAChF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,eAAe,GAAW,MAAM,CAAC,IAAI,CAChD,82yzBAA82yzB,EAC92yzB,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnB,6GAA6G;AAC7G,MAAM,CAAC,MAAM,qBAAqB,GAChC,kEAAkE,CAAC"}
1
+ {"version":3,"file":"card.generated.js","sourceRoot":"","sources":["../src/card.generated.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,wEAAwE;AACxE,gFAAgF;AAChF,4DAA4D;AAC5D,MAAM,CAAC,MAAM,eAAe,GAAW,MAAM,CAAC,IAAI,CAChD,sm01BAAsm01B,EACtm01B,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnB,6GAA6G;AAC7G,MAAM,CAAC,MAAM,qBAAqB,GAChC,kEAAkE,CAAC"}
package/dist/config.d.ts CHANGED
@@ -263,6 +263,21 @@ export type ApiSource = {
263
263
  merchantUrl?: string;
264
264
  /** Only `card` is taken yet. */
265
265
  method?: 'card';
266
+ /**
267
+ * A second way to pay a prepaid rate: UnionPay's page, whose one-time card
268
+ * the hotel's system charges (Huazhu's CCPN) - robin, 2026-10-15: "OPN 走
269
+ * stripe,信用卡走银联,两个都接上". Through the agentbank gateway, on a
270
+ * test-mode merchant: its key in `<prefix>_GATEWAY_KEY`, an agent key of
271
+ * the same organisation in `<prefix>_AGENT_KEY`. Huazhu's test
272
+ * environment or the simulation only. See docs/huazhu-unionpay-apop.md.
273
+ */
274
+ unionpay?: {
275
+ gatewayUrl: string;
276
+ merchantId: string;
277
+ /** On UnionPay's page - ASCII only. Default: the brand. */
278
+ merchantName?: string;
279
+ merchantUrl?: string;
280
+ };
266
281
  };
267
282
  /**
268
283
  * A TEST deployment: `baseUrl` and `authUrl` answer from the simulated
@@ -356,6 +371,7 @@ export declare const toolNames: (config: Pick<HotelConfig, "naming">) => {
356
371
  readonly signIn: `sign_in_${string}member`;
357
372
  readonly signOut: `sign_out_${string}member`;
358
373
  readonly forgetPreferences: `forget_${string}preferences`;
374
+ readonly payWithUnionPay: `pay_${string}booking_with_unionpay`;
359
375
  };
360
376
  export type ToolNames = ReturnType<typeof toolNames>;
361
377
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -42,6 +42,10 @@ export const toolNames = (config) => {
42
42
  signIn: `sign_in_${prefix}member`,
43
43
  signOut: `sign_out_${prefix}member`,
44
44
  forgetPreferences: `forget_${prefix}preferences`,
45
+ // The card's own, never the model's (`ui.visibility: ['app']`): a stay
46
+ // waiting for its payment, paid with UnionPay instead - offered only where
47
+ // the backend can (`capabilities.unionpay`).
48
+ payWithUnionPay: `pay_${prefix}booking_with_unionpay`,
45
49
  };
46
50
  };
47
51
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAqVA,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE,CACjC,CAAC;KACE,WAAW,EAAE;KACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;KAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAE3B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAmC,EAAE,EAAE;IAC/D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7E,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,IAAI,OAAO,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC;IAC7D,OAAO;QACL,UAAU,EAAE,QAAQ,MAAM,GAAG,MAAM,EAAE;QACrC,QAAQ,EAAE,OAAO,MAAM,GAAG,IAAI,EAAE;QAChC,yEAAyE;QACzE,0EAA0E;QAC1E,+CAA+C;QAC/C,MAAM,EAAE,WAAW,MAAM,GAAG,MAAM,EAAE;QACpC,UAAU,EAAE,QAAQ,MAAM,QAAQ;QAClC,SAAS,EAAE,UAAU,MAAM,MAAM;QACjC,QAAQ,EAAE,QAAQ,MAAM,MAAM;QAC9B,SAAS,EAAE,SAAS,MAAM,MAAM;QAChC,cAAc,EAAE,WAAW,MAAM,SAAS;QAC1C,YAAY,EAAE,WAAW,MAAM,UAAU;QACzC,mBAAmB,EAAE,WAAW,MAAM,cAAc;QACpD,aAAa,EAAE,UAAU,MAAM,SAAS;QACxC,yEAAyE;QACzE,kEAAkE;QAClE,aAAa,EAAE,WAAW,MAAM,gBAAgB;QAChD,aAAa,EAAE,UAAU,MAAM,SAAS;QACxC,QAAQ,EAAE,OAAO,MAAM,GAAG,IAAI,WAAW;QACzC,wEAAwE;QACxE,2EAA2E;QAC3E,6DAA6D;QAC7D,OAAO,EAAE,WAAW,MAAM,GAAG,MAAM,EAAE;QACrC,SAAS,EAAE,QAAQ,MAAM,WAAW;QACpC,MAAM,EAAE,WAAW,MAAM,QAAQ;QACjC,OAAO,EAAE,YAAY,MAAM,QAAQ;QACnC,iBAAiB,EAAE,UAAU,MAAM,aAAa;KACxC,CAAC;AACb,CAAC,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAoWA,MAAM,IAAI,GAAG,CAAC,CAAS,EAAU,EAAE,CACjC,CAAC;KACE,WAAW,EAAE;KACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;KAC3B,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAE3B;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAmC,EAAE,EAAE;IAC/D,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7E,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,IAAI,OAAO,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC;IAC7D,OAAO;QACL,UAAU,EAAE,QAAQ,MAAM,GAAG,MAAM,EAAE;QACrC,QAAQ,EAAE,OAAO,MAAM,GAAG,IAAI,EAAE;QAChC,yEAAyE;QACzE,0EAA0E;QAC1E,+CAA+C;QAC/C,MAAM,EAAE,WAAW,MAAM,GAAG,MAAM,EAAE;QACpC,UAAU,EAAE,QAAQ,MAAM,QAAQ;QAClC,SAAS,EAAE,UAAU,MAAM,MAAM;QACjC,QAAQ,EAAE,QAAQ,MAAM,MAAM;QAC9B,SAAS,EAAE,SAAS,MAAM,MAAM;QAChC,cAAc,EAAE,WAAW,MAAM,SAAS;QAC1C,YAAY,EAAE,WAAW,MAAM,UAAU;QACzC,mBAAmB,EAAE,WAAW,MAAM,cAAc;QACpD,aAAa,EAAE,UAAU,MAAM,SAAS;QACxC,yEAAyE;QACzE,kEAAkE;QAClE,aAAa,EAAE,WAAW,MAAM,gBAAgB;QAChD,aAAa,EAAE,UAAU,MAAM,SAAS;QACxC,QAAQ,EAAE,OAAO,MAAM,GAAG,IAAI,WAAW;QACzC,wEAAwE;QACxE,2EAA2E;QAC3E,6DAA6D;QAC7D,OAAO,EAAE,WAAW,MAAM,GAAG,MAAM,EAAE;QACrC,SAAS,EAAE,QAAQ,MAAM,WAAW;QACpC,MAAM,EAAE,WAAW,MAAM,QAAQ;QACjC,OAAO,EAAE,YAAY,MAAM,QAAQ;QACnC,iBAAiB,EAAE,UAAU,MAAM,aAAa;QAChD,uEAAuE;QACvE,2EAA2E;QAC3E,6CAA6C;QAC7C,eAAe,EAAE,OAAO,MAAM,uBAAuB;KAC7C,CAAC;AACb,CAAC,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { type BackendCapabilities, type HotelBackend } from '../backend.js';
2
2
  import { type BookingLimits } from '../limits.js';
3
3
  import type { Checkout } from '../payments/checkout.js';
4
+ import { type UnionPayHandoff } from '../payments/unionpay.js';
4
5
  import type { Quote, ShownQuote } from '../types.js';
5
6
  import { type BookingStore, type Reconciler, type StoredBooking } from './bookings.js';
6
7
  import { type Catalog } from './catalog.js';
@@ -37,6 +38,12 @@ export type HuazhuHotelsOptions = {
37
38
  */
38
39
  prepaid?: {
39
40
  checkout: Checkout;
41
+ /**
42
+ * The other way to pay a prepaid rate: UnionPay's page, whose one-time
43
+ * card Huazhu charges (CCPN) - robin, 2026-10-15: "OPN 走stripe,信用卡走
44
+ * 银联,两个都接上". See docs/huazhu-unionpay-apop.md.
45
+ */
46
+ unionpay?: UnionPayHandoff;
40
47
  payWithinMs?: number;
41
48
  };
42
49
  now?: () => number;
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/huazhu/backend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../../src/huazhu/backend.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,mBAAmB,EAExB,KAAK,YAAY,EAQlB,MAAM,eAAe,CAAC;AAavB,OAAO,EAAkB,KAAK,aAAa,EAAkC,MAAM,cAAc,CAAC;AAElG,OAAO,KAAK,EAAE,QAAQ,EAAiC,MAAM,yBAAyB,CAAC;AACvF,OAAO,EAEL,KAAK,eAAe,EAIrB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAsBV,KAAK,EAML,UAAU,EAIX,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,YAAY,EAGjB,KAAK,UAAU,EAEf,KAAK,aAAa,EAGnB,MAAM,eAAe,CAAC;AAWvB,OAAO,EAAE,KAAK,OAAO,EAAoD,MAAM,cAAc,CAAC;AAC9F,OAAO,EAAE,KAAK,YAAY,EAAoC,MAAM,aAAa,CAAC;AAgBlF,OAAO,EAQL,KAAK,WAAW,EAChB,KAAK,WAAW,EAKjB,MAAM,WAAW,CAAC;AAsBnB,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,kFAAkF;IAClF,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,YAAY,CAAC;IACpB,UAAU,EAAE,UAAU,CAAC;IACvB,gGAAgG;IAChG,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;IACnD,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,+EAA+E;IAC/E,EAAE,EAAE,MAAM,CAAC;IACX,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wEAAwE;IACxE,8FAA8F;IAC9F,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE;QACR,QAAQ,EAAE,QAAQ,CAAC;QACnB;;;;WAIG;QACH,QAAQ,CAAC,EAAE,eAAe,CAAC;QAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iIAAiI;IACjI,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uCAAuC;IACvC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,mBAiBjC,CAAC;AAEF,0GAA0G;AAC1G,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,OAGjD,CAAC;AAaF,eAAO,MAAM,aAAa,GAAI,OAAO,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,KAAG,MAKhE,CAAC;AAKF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B,sFAAsF;AACtF,eAAO,MAAM,WAAW,KAAK,CAAC;AAsR9B;;;GAGG;AACH,eAAO,MAAM,UAAU,GAAI,GAAG,WAAW,EAAE,OAAO,MAAM,KAAG,OAiB1D,CAAC;AA2BF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,GAAI,QAAQ,WAAW,EAAE,EAAE,KAAK,MAAM,KAAG,WAAW,GAAG,SAGhF,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,WAAW,GAAI,QAAQ,WAAW,EAAE,EAAE,KAAK,MAAM,KAAG,WAAW,EAK3E,CAAC;AAUF,oFAAoF;AACpF,eAAO,MAAM,WAAW,GAAI,QAAQ,WAAW,EAAE,EAAE,KAAK,MAAM,KAAG,WAAW,EAO3E,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,IAAI,MAAM,EAAE,WAAU,IAAI,GAAG,IAAW,KAAG,MAKtF,CAAC;AAiCF;;;GAGG;AACH,eAAO,MAAM,UAAU,GACrB,OAAO,UAAU,GAAG,SAAS,EAC7B,GAAG,KAAK;AACR,kGAAkG;AAClG,eAAY,KACX,MAAM,GAAG,SAwBX,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,MAAM,mBAAmB,KACxB,YAAY,GAAG;IAChB,YAAY,EAAE,mBAAmB,CAAC;IAClC,YAAY,IAAI,MAAM,EAAE,CAAC;IACzB,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAAC;IACnE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAulG9B,CAAC"}
@@ -6,6 +6,7 @@ import { FACILITIES, hasFacility } from '../facilities.js';
6
6
  import { endOf, startProblem } from '../hourly.js';
7
7
  import { BOOKING_LIMITS, DAY_MS, HOUR_MS, WindowCounter } from '../limits.js';
8
8
  import { formatMoney, minorFactor, money, multiplyMoney, toMinor } from '../money.js';
9
+ import { expiryMMyy, lastFour, } from '../payments/unionpay.js';
9
10
  import { MAX_ROOMS_PER_LINE, quoteLineKey } from '../trip.js';
10
11
  import { IN_REQUEST_AT_MS, StoreWriteError, nextCheckAt, statusOf, } from './bookings.js';
11
12
  import { describePolicy, feeFor, freeUntilOf, nightAuditOf, parseCancelPolicy, penaltyAt, refundableFrom, } from './cancel-policy.js';
@@ -473,6 +474,7 @@ export const createHuazhuHotels = (opts) => {
473
474
  if (opts.prepaid && !opts.sandbox && !opts.testEnvironment)
474
475
  throw new Error("prepaid rates are sold against the simulated Huazhu or Huazhu's test environment only, until robin decides who takes the money (docs/huazhu-products-plan.md)");
475
476
  const checkout = opts.prepaid?.checkout;
477
+ const unionpay = opts.prepaid?.unionpay;
476
478
  /** Unpaid this long, the page is closed and the booking lapses - inside the card page's own ~31 minutes. */
477
479
  const payWithin = opts.prepaid?.payWithinMs ?? 30 * 60_000;
478
480
  const prepaidType = checkout ? 'OPN' : undefined;
@@ -1255,6 +1257,37 @@ export const createHuazhuHotels = (opts) => {
1255
1257
  /** The money's side of a booking paid in advance, as the guest is shown it. */
1256
1258
  const paymentOf = (b) => {
1257
1259
  const p = b.prepaid;
1260
+ if (p.method === 'unionpay') {
1261
+ // The hotel charges the one-time card and gives back to it - nothing
1262
+ // here is taken or refunded.
1263
+ const status = b.status === 'cancelled' && p.issuedAt
1264
+ ? 'hotel-refunds'
1265
+ : p.lapsedAt || p.declined
1266
+ ? 'lapsed'
1267
+ : b.status === 'confirmed'
1268
+ ? 'paid'
1269
+ : p.issuedAt
1270
+ ? 'issued'
1271
+ : 'awaiting';
1272
+ return {
1273
+ method: 'unionpay',
1274
+ amount: b.quote.total,
1275
+ status,
1276
+ ...(status === 'awaiting' && p.url ? { url: p.url } : {}),
1277
+ ...(status === 'awaiting'
1278
+ ? {
1279
+ payBy: new Date(p.payBy).toISOString(),
1280
+ payByLocal: deadlineAt(p.payBy, b.tz, en ? 'en' : 'zh'),
1281
+ payByLocalIso: localIsoAt(p.payBy, b.tz),
1282
+ }
1283
+ : {}),
1284
+ ...(status === 'hotel-refunds'
1285
+ ? {
1286
+ refundReason: 'Huazhu charged the UnionPay card, and gives back to that same card what its cancellation terms allow',
1287
+ }
1288
+ : {}),
1289
+ };
1290
+ }
1258
1291
  const refund = p.refund;
1259
1292
  const status = refund
1260
1293
  ? refund.status === 'refunded'
@@ -1547,15 +1580,17 @@ export const createHuazhuHotels = (opts) => {
1547
1580
  * reconciler, whose write-off or "nobody could confirm it" refunds it too
1548
1581
  * (`settlePrepaid`).
1549
1582
  */
1550
- const bookPaid = async (id) => {
1583
+ const bookPaid = async (id, card) => {
1551
1584
  const row = store.get(id);
1552
1585
  const p = row?.prepaid;
1553
- if (!row || !p?.paidAt || row.status !== 'awaiting-payment')
1586
+ // Paid on the card page (OPN), or a one-time card UnionPay issued (CCPN).
1587
+ if (!row || !p || !(p.paidAt || (card && p.issuedAt)) || row.status !== 'awaiting-payment')
1554
1588
  return;
1555
1589
  const contact = contactOf(row.guest);
1556
1590
  if (typeof contact === 'string') {
1557
- store.update(id, { status: 'failed' });
1558
- await refundOf(id, "the guest's name could not be sent to Huazhu as written");
1591
+ const why = "the guest's name could not be sent to Huazhu as written";
1592
+ store.update(id, { status: 'failed', ...(card ? { prepaid: { ...p, declined: why } } : {}) });
1593
+ await refundOf(id, why);
1559
1594
  return;
1560
1595
  }
1561
1596
  const comment = commentOf(row.requests, row.guest.notes);
@@ -1564,7 +1599,19 @@ export const createHuazhuHotels = (opts) => {
1564
1599
  checkinType: row.hourly ? 'HOUR' : 'DAY',
1565
1600
  contactPerson: contact,
1566
1601
  distributorResId: row.distributorResId,
1567
- paymentType: row.paymentType,
1602
+ paymentType: card ? 'CCPN' : row.paymentType,
1603
+ // ⚠️ THE ONE-TIME CARD, in this request and nowhere else: not on disk,
1604
+ // not in a log, not in an answer (docs/huazhu-unionpay-apop.md).
1605
+ ...(card
1606
+ ? {
1607
+ paymentCard: {
1608
+ cardType: 'CU',
1609
+ cardHolderName: row.guest.name.replace(/\s+/g, ' ').trim().slice(0, 64),
1610
+ cardNumber: card.number,
1611
+ expireDate: expiryMMyy(card),
1612
+ },
1613
+ }
1614
+ : {}),
1568
1615
  reservations: [
1569
1616
  {
1570
1617
  roomTypeCode: row.roomTypeCode,
@@ -1617,6 +1664,19 @@ export const createHuazhuHotels = (opts) => {
1617
1664
  });
1618
1665
  return;
1619
1666
  }
1667
+ if (refusal && card) {
1668
+ // Paid with UnionPay: the hotel charges the card, and a booking it did
1669
+ // not make it never charged - there is nothing here to give back.
1670
+ store.update(id, {
1671
+ status: 'failed',
1672
+ pendingSince: undefined,
1673
+ prepaid: {
1674
+ ...p,
1675
+ declined: `Huazhu did not take the booking: ${refusal.huazhuMessage} (${refusal.code})`.replace(/\d{12,19}/g, '****'),
1676
+ },
1677
+ });
1678
+ return;
1679
+ }
1620
1680
  if (refusal) {
1621
1681
  store.update(id, { status: 'failed', pendingSince: undefined });
1622
1682
  // A price that moved since the prebook is one of these (5209): the
@@ -1654,6 +1714,67 @@ export const createHuazhuHotels = (opts) => {
1654
1714
  }
1655
1715
  }
1656
1716
  };
1717
+ /**
1718
+ * A confirmation that lands a little after `payBy` was made in time - the
1719
+ * callback is seconds behind the guest. Later than this, it is never used.
1720
+ */
1721
+ const ISSUED_GRACE_MS = 5 * 60_000;
1722
+ /**
1723
+ * One step for a booking paid with UnionPay: confirmed on UnionPay's page →
1724
+ * the one-time card, once, straight into a CCPN booking with Huazhu;
1725
+ * unconfirmed past its time → lapsed. Nothing here is ever refunded: the
1726
+ * hotel charges the card, and gives back to it.
1727
+ */
1728
+ const settleUnionPay = async (row) => {
1729
+ const p = row.prepaid;
1730
+ if (row.status !== 'awaiting-payment' || !unionpay)
1731
+ return;
1732
+ const lapse = () => store.update(row.id, {
1733
+ status: 'expired',
1734
+ prepaid: { ...p, lapsedAt: now(), url: undefined },
1735
+ });
1736
+ if (!p.sessionId) {
1737
+ if (now() >= p.payBy)
1738
+ lapse();
1739
+ return;
1740
+ }
1741
+ let state;
1742
+ try {
1743
+ state = await unionpay.state(p.sessionId);
1744
+ }
1745
+ catch {
1746
+ return; // asked again on the next pass
1747
+ }
1748
+ if (state === 'issued' && now() < p.payBy + ISSUED_GRACE_MS) {
1749
+ // ⚠️ ONCE: the gateway hands the card over a single time, then forgets it.
1750
+ let card;
1751
+ try {
1752
+ card = await unionpay.card(p.sessionId);
1753
+ }
1754
+ catch {
1755
+ return; // not handed over - asked again on the next pass
1756
+ }
1757
+ if (!card) {
1758
+ const why = 'UnionPay confirmed the payment, but its one-time card could not be read here - nothing was charged';
1759
+ store.update(row.id, {
1760
+ status: 'failed',
1761
+ prepaid: { ...p, url: undefined, declined: why },
1762
+ });
1763
+ console.error(`[huazhu prepaid ${row.id}] ${why}`);
1764
+ return;
1765
+ }
1766
+ store.update(row.id, {
1767
+ prepaid: { ...p, issuedAt: now(), cardLastFour: lastFour(card), url: undefined },
1768
+ });
1769
+ console.log(`[huazhu prepaid ${row.id}] UnionPay issued a one-time card ···· ${lastFour(card)}: booking it with Huazhu as CCPN`);
1770
+ await bookPaid(row.id, card);
1771
+ return;
1772
+ }
1773
+ // Unconfirmed in time: lapsed. A confirmation after this is never handed
1774
+ // on, so nothing is ever charged for it.
1775
+ if (state === 'closed' || now() >= p.payBy)
1776
+ lapse();
1777
+ };
1657
1778
  /** Whether a booking paid in advance still has money work to do. */
1658
1779
  let ticking = false;
1659
1780
  const moneyPending = (b) => {
@@ -1662,6 +1783,9 @@ export const createHuazhuHotels = (opts) => {
1662
1783
  return false;
1663
1784
  if (b.status === 'awaiting-payment')
1664
1785
  return true;
1786
+ // Paid with UnionPay, the hotel charges and refunds the card: nothing here after the booking.
1787
+ if (p.method === 'unionpay')
1788
+ return false;
1665
1789
  if (!p.paidAt)
1666
1790
  return false;
1667
1791
  // Booked: Huazhu's order number still to be written onto the payment, once.
@@ -1682,6 +1806,8 @@ export const createHuazhuHotels = (opts) => {
1682
1806
  const p = row.prepaid;
1683
1807
  if (!p || !checkout)
1684
1808
  return;
1809
+ if (p.method === 'unionpay')
1810
+ return settleUnionPay(row);
1685
1811
  if (row.status === 'awaiting-payment') {
1686
1812
  if (!p.sessionId) {
1687
1813
  if (now() >= p.payBy)
@@ -2157,6 +2283,158 @@ export const createHuazhuHotels = (opts) => {
2157
2283
  }
2158
2284
  return { booked: made.map((b) => publicBooking(store.get(b.id) ?? b)), failed };
2159
2285
  };
2286
+ /**
2287
+ * Waits its turn behind the pass and the landing page - the one at a time
2288
+ * every step on a booking paid in advance takes - then runs.
2289
+ */
2290
+ const inTurn = async (fn) => {
2291
+ for (let i = 0; ticking && i < 400; i += 1)
2292
+ await new Promise((r) => setTimeout(r, 25));
2293
+ if (ticking)
2294
+ throw new Error('A payment here is being settled right now. Ask again in a moment.');
2295
+ ticking = true;
2296
+ try {
2297
+ return await fn();
2298
+ }
2299
+ finally {
2300
+ ticking = false;
2301
+ }
2302
+ };
2303
+ /**
2304
+ * Pay a booking waiting for payment with UnionPay instead of the card page
2305
+ * (robin, 2026-10-15). The card page is closed first - paid on it in the
2306
+ * meantime, it goes on as that payment. The stay is prebooked again as
2307
+ * CCPN, the way it will be booked, and UnionPay's page opened for the total
2308
+ * the guest was shown. No card bound yet: UnionPay's page to bind one.
2309
+ */
2310
+ const unionPay = async (code, email, who) => {
2311
+ if (!unionpay || !checkout)
2312
+ return undefined;
2313
+ // ⚠️ THE CODE AND THE EMAIL IT WAS MADE WITH, always - never the code
2314
+ // alone (香格里拉, 2026-10-15): the pair a front desk asks for.
2315
+ const asked = email.trim().toLowerCase();
2316
+ const found = findOwn(code, { ...who, email: asked });
2317
+ if (!found || found.guest.email.trim().toLowerCase() !== asked)
2318
+ return undefined;
2319
+ const refused = (why) => ({ kind: 'refused', why });
2320
+ const notWaiting = (b) => !b.prepaid
2321
+ ? `${b.confirmationCode} is paid at the hotel - there is nothing to pay now.`
2322
+ : b.status !== 'awaiting-payment'
2323
+ ? `${b.confirmationCode} is ${b.status} - it is not waiting for a payment.`
2324
+ : undefined;
2325
+ const first = store.get(found.id) ?? found;
2326
+ const no = notWaiting(first);
2327
+ if (no)
2328
+ return refused(no);
2329
+ if (first.prepaid?.method === 'unionpay' && first.prepaid.url)
2330
+ return { kind: 'confirm', url: first.prepaid.url, booking: publicBooking(first) };
2331
+ let cards;
2332
+ try {
2333
+ cards = await unionpay.cardsOf(first.guest.email);
2334
+ }
2335
+ catch (err) {
2336
+ return refused(`UnionPay could not be reached (${err.message}). Nothing changed - the card page still works, or ask again.`);
2337
+ }
2338
+ const card = cards[0];
2339
+ // No card yet: UnionPay's page to bind one - and nothing else changes.
2340
+ if (!card) {
2341
+ try {
2342
+ return { kind: 'enroll', url: (await unionpay.enroll(first.guest.email)).url };
2343
+ }
2344
+ catch (err) {
2345
+ return refused(`UnionPay's page for adding a card could not be opened (${err.message}). Nothing changed - ask again.`);
2346
+ }
2347
+ }
2348
+ return inTurn(async () => {
2349
+ const row = store.get(first.id) ?? first;
2350
+ const again = notWaiting(row);
2351
+ if (again)
2352
+ return refused(again);
2353
+ const p = row.prepaid;
2354
+ if (p.method === 'unionpay' && p.url)
2355
+ return { kind: 'confirm', url: p.url, booking: publicBooking(row) };
2356
+ // The card page first: closed, it cannot be paid after this.
2357
+ if (p.method !== 'unionpay' && p.sessionId) {
2358
+ let state;
2359
+ try {
2360
+ state = await checkout.close(p.sessionId);
2361
+ }
2362
+ catch (err) {
2363
+ return refused(`The card page could not be closed (${err.message}). Nothing changed - ask again.`);
2364
+ }
2365
+ if (state === 'paid') {
2366
+ await settlePrepaid(row);
2367
+ return { kind: 'paid-on-card', booking: publicBooking(store.get(row.id) ?? row) };
2368
+ }
2369
+ store.update(row.id, { prepaid: { ...p, sessionId: undefined, url: undefined } });
2370
+ }
2371
+ const title = row.quote.lines[0]?.title ?? row.hotelName;
2372
+ // Prebooked again the way it will be booked: Huazhu's token holds only
2373
+ // for the same request (wire.ts).
2374
+ let token;
2375
+ try {
2376
+ token = (await client.prebook({
2377
+ hotelId: row.hotelId,
2378
+ checkinType: row.hourly ? 'HOUR' : 'DAY',
2379
+ paymentType: 'CCPN',
2380
+ reservations: [
2381
+ {
2382
+ roomTypeCode: row.roomTypeCode,
2383
+ rateCode: row.rateCode,
2384
+ ...stayDatesOf(row),
2385
+ amountAfterTax: p.after,
2386
+ ...(p.before ? { amountBeforeTax: p.before } : {}),
2387
+ currencyCode: p.currency,
2388
+ roomCount: row.rooms,
2389
+ adultCount: row.stay.guests,
2390
+ },
2391
+ ],
2392
+ }))?.preBookToken;
2393
+ }
2394
+ catch (err) {
2395
+ if (err instanceof HuazhuRefusal) {
2396
+ store.update(row.id, {
2397
+ status: 'expired',
2398
+ prepaid: { ...p, sessionId: undefined, url: undefined, lapsedAt: now() },
2399
+ });
2400
+ return refused(`Nothing was booked or charged: ${refusalWords(err, `${row.hotelName} - ${title}`, p.currency)}`);
2401
+ }
2402
+ return refused(`Huazhu could not be reached to check the stay (${err.message}). Nothing was charged - ask again.`);
2403
+ }
2404
+ const names = hotelNamesOf(row.hotelId);
2405
+ const nights = row.quote.lines[0]?.nights ?? 1;
2406
+ let opened;
2407
+ try {
2408
+ opened = await unionpay.open({
2409
+ ref: row.distributorResId,
2410
+ code: row.confirmationCode,
2411
+ title: `${row.hotelName} · ${title}`.slice(0, 200),
2412
+ // UnionPay's page reads ASCII only: the hotel's English name.
2413
+ label: `${names?.en ?? 'Huazhu hotel'} - ${nights} night${nights === 1 ? '' : 's'} from ${row.stay.checkIn}`,
2414
+ amountMinor: row.quote.total.minor,
2415
+ currency: row.quote.total.currency,
2416
+ email: row.guest.email,
2417
+ name: row.guest.name,
2418
+ cardId: card.id,
2419
+ });
2420
+ }
2421
+ catch (err) {
2422
+ return refused(`UnionPay's page could not be opened (${err.message}). Nothing was charged - ask again.`);
2423
+ }
2424
+ const updated = store.update(row.id, {
2425
+ paymentType: 'CCPN',
2426
+ prepaid: {
2427
+ ...p,
2428
+ method: 'unionpay',
2429
+ sessionId: opened.sessionId,
2430
+ url: opened.url,
2431
+ ...(token ? { preBookToken: token } : {}),
2432
+ payBy: now() + payWithin,
2433
+ },
2434
+ }) ?? row;
2435
+ return { kind: 'confirm', url: opened.url, booking: publicBooking(updated) };
2436
+ });
2437
+ };
2160
2438
  /** When each payment page last moved its booking on, for the page a paid guest lands on. */
2161
2439
  const returned = new Map();
2162
2440
  /** What the landing page says of a booking - never the guest's name, email or phone. */
@@ -2232,6 +2510,7 @@ export const createHuazhuHotels = (opts) => {
2232
2510
  ...HUAZHU_CAPABILITIES,
2233
2511
  ...(opts.hourly ? { hourly: true } : {}),
2234
2512
  ...(checkout ? { prepaid: true } : {}),
2513
+ ...(checkout && unionpay ? { unionpay: true } : {}),
2235
2514
  },
2236
2515
  /**
2237
2516
  * One pass over the bookings paid in advance - every 10 seconds while the
@@ -2239,6 +2518,7 @@ export const createHuazhuHotels = (opts) => {
2239
2518
  * money going back.
2240
2519
  */
2241
2520
  ...(checkout ? { paymentReturn } : {}),
2521
+ ...(checkout && unionpay ? { unionPay } : {}),
2242
2522
  async prepaidTick() {
2243
2523
  // ⚠️ ONE PASS AT A TIME: a pass slower than the ten seconds between
2244
2524
  // them would otherwise book a paid stay, or give money back, twice.
@@ -2845,12 +3125,15 @@ export const createHuazhuHotels = (opts) => {
2845
3125
  .all()
2846
3126
  .filter((b) => b.owners.includes(who.sessionId) &&
2847
3127
  // A refused booking is nothing to show - unless the guest paid for
2848
- // it: their money going back is.
3128
+ // it: their money going back is. Or confirmed it on UnionPay: that
3129
+ // the hotel turned it down, and charged nothing, is.
2849
3130
  !(b.status === 'failed' &&
2850
3131
  !b.resId &&
2851
3132
  b.pendingSince === undefined &&
2852
3133
  !b.attention &&
2853
- !b.prepaid?.paidAt))
3134
+ !b.prepaid?.paidAt &&
3135
+ !b.prepaid?.issuedAt &&
3136
+ !b.prepaid?.declined))
2854
3137
  .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
2855
3138
  .slice(0, most);
2856
3139
  const fresh = await pool(mine, 3, (b) => refresh(b));
@@ -2961,6 +3244,22 @@ export const createHuazhuHotels = (opts) => {
2961
3244
  if (!found)
2962
3245
  return null;
2963
3246
  const b = await refresh(found);
3247
+ // Waiting on UnionPay: a confirmation already made is being booked by
3248
+ // the pass - which alone takes the card, once; one not made never will be.
3249
+ if (b.status === 'awaiting-payment' && b.prepaid?.method === 'unionpay') {
3250
+ const state = b.prepaid.sessionId && unionpay
3251
+ ? await unionpay.state(b.prepaid.sessionId).catch(() => undefined)
3252
+ : 'closed';
3253
+ if (state !== 'issued' && state !== undefined) {
3254
+ return publicBooking(store.update(b.id, {
3255
+ status: 'expired',
3256
+ prepaid: { ...b.prepaid, lapsedAt: now(), url: undefined },
3257
+ }) ?? b);
3258
+ }
3259
+ throw new Error(state === 'issued'
3260
+ ? 'The guest has just confirmed on UnionPay, so the stay is being booked with Huazhu now. Ask again in a moment to cancel it.'
3261
+ : 'UnionPay could not be reached to say whether the guest confirmed. Ask again in a moment - nothing is booked until it can.');
3262
+ }
2964
3263
  // Not paid yet: the page is closed, so it cannot be paid after this.
2965
3264
  if (b.status === 'awaiting-payment' && b.prepaid?.sessionId && checkout) {
2966
3265
  const state = await checkout.close(b.prepaid.sessionId).catch(() => undefined);