@curless/hotel-storefront 0.1.0 → 0.2.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 (61) hide show
  1. package/dist/__tests__/card-contract.test.js +13 -0
  2. package/dist/__tests__/card-contract.test.js.map +1 -1
  3. package/dist/__tests__/data.test.js +28 -3
  4. package/dist/__tests__/data.test.js.map +1 -1
  5. package/dist/__tests__/demo-hotels.test.js +25 -11
  6. package/dist/__tests__/demo-hotels.test.js.map +1 -1
  7. package/dist/__tests__/present.test.d.ts +2 -0
  8. package/dist/__tests__/present.test.d.ts.map +1 -0
  9. package/dist/__tests__/present.test.js +140 -0
  10. package/dist/__tests__/present.test.js.map +1 -0
  11. package/dist/__tests__/server.test.js +63 -4
  12. package/dist/__tests__/server.test.js.map +1 -1
  13. package/dist/__tests__/tools.test.js +129 -0
  14. package/dist/__tests__/tools.test.js.map +1 -1
  15. package/dist/backend.d.ts +30 -6
  16. package/dist/backend.d.ts.map +1 -1
  17. package/dist/card.generated.d.ts +1 -1
  18. package/dist/card.generated.js +2 -2
  19. package/dist/card.generated.js.map +1 -1
  20. package/dist/config.d.ts +11 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +7 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/data.d.ts.map +1 -1
  25. package/dist/data.js +88 -13
  26. package/dist/data.js.map +1 -1
  27. package/dist/demo-hotels.d.ts +24 -1
  28. package/dist/demo-hotels.d.ts.map +1 -1
  29. package/dist/demo-hotels.js +136 -13
  30. package/dist/demo-hotels.js.map +1 -1
  31. package/dist/fence.d.ts +9 -3
  32. package/dist/fence.d.ts.map +1 -1
  33. package/dist/fence.js +0 -0
  34. package/dist/fence.js.map +1 -1
  35. package/dist/index.js +0 -0
  36. package/dist/members.d.ts +14 -0
  37. package/dist/members.d.ts.map +1 -0
  38. package/dist/members.js +56 -0
  39. package/dist/members.js.map +1 -0
  40. package/dist/present.d.ts +129 -0
  41. package/dist/present.d.ts.map +1 -0
  42. package/dist/present.js +188 -0
  43. package/dist/present.js.map +1 -0
  44. package/dist/server.d.ts +27 -10
  45. package/dist/server.d.ts.map +1 -1
  46. package/dist/server.js +27 -7
  47. package/dist/server.js.map +1 -1
  48. package/dist/surface.d.ts.map +1 -1
  49. package/dist/surface.js +27 -2
  50. package/dist/surface.js.map +1 -1
  51. package/dist/tools.d.ts +174 -1
  52. package/dist/tools.d.ts.map +1 -1
  53. package/dist/tools.js +464 -50
  54. package/dist/tools.js.map +1 -1
  55. package/dist/types.d.ts +57 -1
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/vocab.d.ts +1 -1
  58. package/dist/vocab.d.ts.map +1 -1
  59. package/dist/vocab.js +1 -1
  60. package/dist/vocab.js.map +1 -1
  61. package/package.json +11 -9
@@ -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,0ykTAA0ykT,EAC1ykT,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnB,2FAA2F;AAC3F,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,suqUAAsuqU,EACtuqU,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnB,2FAA2F;AAC3F,MAAM,CAAC,MAAM,qBAAqB,GAChC,kEAAkE,CAAC"}
package/dist/config.d.ts CHANGED
@@ -57,6 +57,13 @@ export type HotelConfig = {
57
57
  file?: string;
58
58
  /** api only: where it answers. The key comes from the environment. */
59
59
  baseUrl?: string;
60
+ /**
61
+ * demo only: how many minutes a booking request holds its rooms.
62
+ *
63
+ * Short on purpose - the demonstration inventory is shared by everyone who
64
+ * opens the connector, and a day-long hold empties it in an afternoon.
65
+ */
66
+ holdMinutes?: number;
60
67
  };
61
68
  /**
62
69
  * Taking money. Absent - and it IS absent today, by decision - the connector
@@ -117,6 +124,10 @@ export declare const toolNames: (config: Pick<HotelConfig, "naming">) => {
117
124
  readonly previewCancellation: `preview_${string}cancellation`;
118
125
  readonly cancelBooking: `cancel_${string}booking`;
119
126
  readonly policies: `get_${string}${string}_policies`;
127
+ readonly compare: `compare_${string}${string}`;
128
+ readonly itinerary: `show_${string}itinerary`;
129
+ readonly signIn: `sign_in_${string}member`;
130
+ readonly signOut: `sign_out_${string}member`;
120
131
  };
121
132
  export type ToolNames = ReturnType<typeof toolNames>;
122
133
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,WAAW,GAAG;IACxB,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAC;IAEX,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC;IAEd,qDAAqD;IACrD,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IAEF;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,sFAAsF;QACtF,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IAEF,iCAAiC;IACjC,IAAI,EAAE;QACJ;;;;;;WAMG;QACH,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;QACvB,gFAAgF;QAChF,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,sEAAsE;QACtE,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IAEF;;;;;OAKG;IACH,OAAO,CAAC,EAAE;QACR,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;KACzB,CAAC;IAEF,2GAA2G;IAC3G,IAAI,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,6EAA6E;QAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IAEF,KAAK,EAAE;QACL,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,mEAAmE;QACnE,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH,CAAC;AAQF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,GAAI,QAAQ,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC;;;;;;;;;;;;CAiB5D,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,WAAW,GAAG;IACxB,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAC;IAEX,oFAAoF;IACpF,KAAK,EAAE,MAAM,CAAC;IAEd,qDAAqD;IACrD,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB;;;;;;WAMG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IAEF;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE;QACP,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,sFAAsF;QACtF,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IAEF,iCAAiC;IACjC,IAAI,EAAE;QACJ;;;;;;WAMG;QACH,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;QACvB,gFAAgF;QAChF,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,sEAAsE;QACtE,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB;;;;;WAKG;QACH,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IAEF;;;;;OAKG;IACH,OAAO,CAAC,EAAE;QACR,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAC;KACzB,CAAC;IAEF,2GAA2G;IAC3G,IAAI,EAAE;QACJ,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE;QACN,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,6EAA6E;QAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IAEF,KAAK,EAAE;QACL,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,mEAAmE;QACnE,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH,CAAC;AAQF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,GAAI,QAAQ,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC;;;;;;;;;;;;;;;;CAwB5D,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC"}
package/dist/config.js CHANGED
@@ -37,6 +37,13 @@ export const toolNames = (config) => {
37
37
  previewCancellation: `preview_${prefix}cancellation`,
38
38
  cancelBooking: `cancel_${prefix}booking`,
39
39
  policies: `get_${prefix}${noun}_policies`,
40
+ // The two that carry the MODEL's judgment rather than a tool's answer -
41
+ // which hotels sit beside each other, what the plan's days are. Every fact
42
+ // on the card is still joined by the server; see present.ts.
43
+ compare: `compare_${prefix}${plural}`,
44
+ itinerary: `show_${prefix}itinerary`,
45
+ signIn: `sign_in_${prefix}member`,
46
+ signOut: `sign_out_${prefix}member`,
40
47
  };
41
48
  };
42
49
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAmGH,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,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,QAAQ,EAAE,OAAO,MAAM,GAAG,IAAI,WAAW;KACjC,CAAC;AACb,CAAC,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA0GH,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,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,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;KAC3B,CAAC;AACb,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAsB,YAAY,EAAW,MAAM,kBAAkB,CAAC;AA0KlF,eAAO,MAAM,cAAc,GAAI,KAAK,OAAO,KAAG,YAU7C,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG,YAS3D,CAAC"}
1
+ {"version":3,"file":"data.d.ts","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAsB,YAAY,EAA0B,MAAM,kBAAkB,CAAC;AAkPjG,eAAO,MAAM,cAAc,GAAI,KAAK,OAAO,KAAG,YAc7C,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG,YAS3D,CAAC"}
package/dist/data.js CHANGED
@@ -10,6 +10,25 @@ import { dirname, resolve } from 'node:path';
10
10
  * it prints a plausible number rather than an error.
11
11
  */
12
12
  const KINDS = ['transfer', 'dining', 'experience', 'family', 'wellness'];
13
+ /**
14
+ * The largest rate a currency can plausibly write for one night, used to catch
15
+ * a price typed in MINOR units by mistake - 380000 for 3800 - which would look
16
+ * like a real, if eye-watering, number on a card.
17
+ *
18
+ * It has to be per currency. 2,400,000 is an ordinary room in Jakarta and a
19
+ * typo in Hong Kong, and a single cap either lets the typo through everywhere
20
+ * or refuses every hotel in Indonesia, Mongolia and Sri Lanka.
21
+ */
22
+ const BIG_MONEY = {
23
+ IDR: 50_000_000,
24
+ VND: 500_000_000,
25
+ KRW: 50_000_000,
26
+ MNT: 20_000_000,
27
+ MMK: 20_000_000,
28
+ LKR: 5_000_000,
29
+ JPY: 5_000_000,
30
+ };
31
+ const maxPrice = (currency) => BIG_MONEY[currency.toUpperCase()] ?? 200_000;
13
32
  const TOPICS = [
14
33
  'cancellation',
15
34
  'check-in',
@@ -44,7 +63,7 @@ const time = (v, where, what) => {
44
63
  throw new DataError(where, `${what} must look like 15:00`);
45
64
  return s;
46
65
  };
47
- const parseRoom = (raw, where) => {
66
+ const parseRoom = (raw, where, currency) => {
48
67
  const r = raw;
49
68
  const id = str(r.id, where, 'room id');
50
69
  const at = `${where} room ${id}`;
@@ -61,16 +80,16 @@ const parseRoom = (raw, where) => {
61
80
  beds: r.beds === undefined ? undefined : str(r.beds, at, 'beds'),
62
81
  view: r.view === undefined ? undefined : str(r.view, at, 'view'),
63
82
  breakfast: r.breakfast === true,
64
- // A rate is in whole currency units, the way the hotel writes it. 380000
65
- // would be a rate typed in minor units by mistake, and it would look like a
66
- // real - if eye-watering - price on the card.
67
- nightly: num(r.nightly, at, 'nightly', 1, 200_000),
83
+ imageUrl: r.imageUrl === undefined ? undefined : str(r.imageUrl, at, 'imageUrl'),
84
+ // A rate is in whole currency units, the way the hotel writes it - see
85
+ // BIG_MONEY for why the ceiling is the currency's and not one number.
86
+ nightly: num(r.nightly, at, 'nightly', 1, maxPrice(currency)),
68
87
  roomsLeft: num(r.roomsLeft, at, 'roomsLeft', 0, 500),
69
88
  refundable,
70
89
  freeCancelHours: refundable ? num(r.freeCancelHours, at, 'freeCancelHours', 0, 720) : undefined,
71
90
  };
72
91
  };
73
- const parseExtra = (raw, where) => {
92
+ const parseExtra = (raw, where, currency) => {
74
93
  const e = raw;
75
94
  const id = str(e.id, where, 'extra id');
76
95
  const at = `${where} extra ${id}`;
@@ -88,7 +107,7 @@ const parseExtra = (raw, where) => {
88
107
  name: str(e.name, at, 'name'),
89
108
  kind,
90
109
  description: str(e.description, at, 'description'),
91
- price: num(e.price, at, 'price', 1, 200_000),
110
+ price: num(e.price, at, 'price', 1, maxPrice(currency)),
92
111
  per,
93
112
  when,
94
113
  };
@@ -98,7 +117,11 @@ const parseHotel = (raw, index) => {
98
117
  const where = `hotel[${index}]`;
99
118
  const id = str(h.id, where, 'id');
100
119
  const at = `hotel ${id}`;
101
- const rooms = Array.isArray(h.rooms) ? h.rooms.map((r) => parseRoom(r, at)) : [];
120
+ // Read before the rooms are: a room's ceiling is its hotel's currency.
121
+ const currency = str(h.currency, at, 'currency');
122
+ if (!/^[A-Z]{3}$/.test(currency))
123
+ throw new DataError(at, `currency ${currency} must be a 3-letter code like HKD`);
124
+ const rooms = Array.isArray(h.rooms) ? h.rooms.map((r) => parseRoom(r, at, currency)) : [];
102
125
  if (rooms.length === 0)
103
126
  throw new DataError(at, 'a hotel with no room types cannot be booked; leave it out instead');
104
127
  const seen = new Set();
@@ -114,15 +137,13 @@ const parseHotel = (raw, index) => {
114
137
  throw new DataError(at, `policy topic ${topic} is not one we show`);
115
138
  policies[topic] = str(text, at, `policy ${topic}`);
116
139
  }
117
- const currency = str(h.currency, at, 'currency');
118
- if (!/^[A-Z]{3}$/.test(currency))
119
- throw new DataError(at, `currency ${currency} must be a 3-letter code like HKD`);
120
140
  return {
121
141
  id,
122
142
  name: str(h.name, at, 'name'),
123
143
  brand: str(h.brand, at, 'brand'),
124
144
  city: str(h.city, at, 'city'),
125
145
  cityId: str(h.cityId, at, 'cityId'),
146
+ country: h.country === undefined ? undefined : str(h.country, at, 'country'),
126
147
  area: h.area === undefined ? undefined : str(h.area, at, 'area'),
127
148
  summary: str(h.summary, at, 'summary'),
128
149
  amenities: Array.isArray(h.amenities)
@@ -141,7 +162,7 @@ const parseHotel = (raw, index) => {
141
162
  utcOffset: h.utcOffset === undefined ? undefined : str(h.utcOffset, at, 'utcOffset'),
142
163
  rating: h.rating === undefined ? undefined : parseRating(h.rating, at),
143
164
  rooms,
144
- extras: Array.isArray(h.extras) ? h.extras.map((e) => parseExtra(e, at)) : [],
165
+ extras: Array.isArray(h.extras) ? h.extras.map((e) => parseExtra(e, at, currency)) : [],
145
166
  policies,
146
167
  };
147
168
  };
@@ -155,6 +176,56 @@ const parseRating = (raw, where) => {
155
176
  count: num(r.count, where, 'rating.count', 1, 10_000_000),
156
177
  };
157
178
  };
179
+ /**
180
+ * The loyalty programme, validated like everything else that can print a
181
+ * number: a tier's discount is a FRACTION, and a member without a tier that
182
+ * exists would silently price at the public rate while calling itself a member.
183
+ */
184
+ const parseMembership = (raw) => {
185
+ const m = raw;
186
+ const where = 'membership';
187
+ const tiers = Array.isArray(m.tiers)
188
+ ? m.tiers.map((t) => {
189
+ const x = t;
190
+ const id = str(x.id, where, 'tier id');
191
+ return {
192
+ id,
193
+ name: str(x.name, `${where} ${id}`, 'name'),
194
+ // 0.1, not 10 - the same mistake as taxRate, and it would take 1000%
195
+ // off a rate and still render.
196
+ discount: num(x.discount, `${where} ${id}`, 'discount', 0, 0.9),
197
+ pointsPerUnit: num(x.pointsPerUnit, `${where} ${id}`, 'pointsPerUnit', 0, 100),
198
+ };
199
+ })
200
+ : [];
201
+ if (tiers.length === 0)
202
+ throw new DataError(where, 'a programme with no tiers cannot price anything');
203
+ const ids = new Set(tiers.map((t) => t.id));
204
+ const members = Array.isArray(m.members)
205
+ ? m.members.map((r) => {
206
+ const x = r;
207
+ const number = str(x.number, where, 'member number');
208
+ const at = `${where} ${number}`;
209
+ const tier = str(x.tier, at, 'tier');
210
+ if (!ids.has(tier))
211
+ throw new DataError(at, `tier ${tier} is not one this programme has`);
212
+ return {
213
+ number,
214
+ surname: str(x.surname, at, 'surname'),
215
+ given: str(x.given, at, 'given'),
216
+ tier,
217
+ points: num(x.points, at, 'points', 0, 100_000_000),
218
+ };
219
+ })
220
+ : [];
221
+ const seen = new Set();
222
+ for (const x of members) {
223
+ if (seen.has(x.number))
224
+ throw new DataError(where, `two members share the number ${x.number}`);
225
+ seen.add(x.number);
226
+ }
227
+ return { programme: str(m.programme, where, 'programme'), tiers, members };
228
+ };
158
229
  export const parseHotelData = (raw) => {
159
230
  const d = raw;
160
231
  const hotels = Array.isArray(d.hotels) ? d.hotels.map(parseHotel) : [];
@@ -166,7 +237,11 @@ export const parseHotelData = (raw) => {
166
237
  throw new DataError('hotels file', `two hotels share the id ${h.id}`);
167
238
  ids.add(h.id);
168
239
  }
169
- return { brand: str(d.brand, 'hotels file', 'brand'), hotels };
240
+ return {
241
+ brand: str(d.brand, 'hotels file', 'brand'),
242
+ hotels,
243
+ ...(d.membership ? { membership: parseMembership(d.membership) } : {}),
244
+ };
170
245
  };
171
246
  /** Read a brand's hotels file. `from` is the config file it was named in. */
172
247
  export const loadHotelData = (file, from) => {
package/dist/data.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"data.js","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAI7C;;;;;;;;GAQG;AAEH,MAAM,KAAK,GAAgB,CAAC,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;AACtF,MAAM,MAAM,GAAuB;IACjC,cAAc;IACd,UAAU;IACV,UAAU;IACV,MAAM;IACN,SAAS;IACT,eAAe;CAChB,CAAC;AAEF,MAAM,SAAU,SAAQ,KAAK;IAC3B,YAAY,KAAa,EAAE,IAAY;QACrC,KAAK,CAAC,GAAG,KAAK,KAAK,IAAI,EAAE,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF;AAED,MAAM,GAAG,GAAG,CAAC,CAAU,EAAE,KAAa,EAAE,IAAY,EAAU,EAAE;IAC9D,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;QAC1C,MAAM,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,IAAI,6BAA6B,CAAC,CAAC;IACnE,OAAO,CAAC,CAAC;AACX,CAAC,CAAC;AAEF,MAAM,GAAG,GAAG,CAAC,CAAU,EAAE,KAAa,EAAE,IAAY,EAAE,GAAW,EAAE,GAAW,EAAU,EAAE;IACxF,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC9C,MAAM,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,IAAI,mBAAmB,CAAC,CAAC;IACzD,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,GAAG;QAAE,MAAM,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,IAAI,OAAO,CAAC,aAAa,GAAG,KAAK,GAAG,EAAE,CAAC,CAAC;IAC9F,OAAO,CAAC,CAAC;AACX,CAAC,CAAC;AAEF,MAAM,IAAI,GAAG,CAAC,CAAU,EAAE,KAAa,EAAE,IAAY,EAAsB,EAAE;IAC3E,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACtC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,KAAK,EAAE,GAAG,IAAI,uBAAuB,CAAC,CAAC;IACzF,OAAO,CAAC,CAAC;AACX,CAAC,CAAC;AAEF,MAAM,SAAS,GAAG,CAAC,GAAY,EAAE,KAAa,EAAW,EAAE;IACzD,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;IACvC,MAAM,EAAE,GAAG,GAAG,KAAK,SAAS,EAAE,EAAE,CAAC;IACjC,MAAM,UAAU,GAAG,CAAC,CAAC,UAAU,KAAK,IAAI,CAAC;IACzC,IAAI,UAAU,IAAI,CAAC,CAAC,eAAe,KAAK,SAAS;QAC/C,MAAM,IAAI,SAAS,CACjB,EAAE,EACF,gGAAgG,CACjG,CAAC;IACJ,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC,eAAe,KAAK,SAAS;QAChD,MAAM,IAAI,SAAS,CACjB,EAAE,EACF,wFAAwF,CACzF,CAAC;IACJ,OAAO;QACL,EAAE;QACF,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC;QAC7B,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;QACzC,OAAO,EAAE,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,GAAG,CAAC;QACpF,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC;QAChE,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC;QAChE,SAAS,EAAE,CAAC,CAAC,SAAS,KAAK,IAAI;QAC/B,yEAAyE;QACzE,4EAA4E;QAC5E,8CAA8C;QAC9C,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,OAAO,CAAC;QAClD,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,GAAG,CAAC;QACpD,UAAU;QACV,eAAe,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,eAAe,EAAE,EAAE,EAAE,iBAAiB,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;KAChG,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,UAAU,GAAG,CAAC,GAAY,EAAE,KAAa,EAAY,EAAE;IAC3D,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,UAAU,CAAC,CAAC;IACxC,MAAM,EAAE,GAAG,GAAG,KAAK,UAAU,EAAE,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAc,CAAC;IAClD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QACvB,MAAM,IAAI,SAAS,CAAC,EAAE,EAAE,QAAQ,IAAI,kBAAkB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5E,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;IAClC,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,OAAO;QACvD,MAAM,IAAI,SAAS,CAAC,EAAE,EAAE,mCAAmC,CAAC,CAAC;IAC/D,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;IACxE,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,KAAK;QACpF,MAAM,IAAI,SAAS,CAAC,EAAE,EAAE,wCAAwC,CAAC,CAAC;IACpE,OAAO;QACL,EAAE;QACF,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC;QAC7B,IAAI;QACJ,WAAW,EAAE,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,EAAE,aAAa,CAAC;QAClD,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC;QAC5C,GAAG;QACH,IAAI;KACL,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,UAAU,GAAG,CAAC,GAAY,EAAE,KAAa,EAAY,EAAE;IAC3D,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,MAAM,KAAK,GAAG,SAAS,KAAK,GAAG,CAAC;IAChC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAClC,MAAM,EAAE,GAAG,SAAS,EAAE,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACjF,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QACpB,MAAM,IAAI,SAAS,CAAC,EAAE,EAAE,mEAAmE,CAAC,CAAC;IAC/F,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,EAAE,EAAE,+BAA+B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACnF,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IACD,MAAM,QAAQ,GAA8C,EAAE,CAAC;IAC/D,MAAM,WAAW,GAAG,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAA4B,CAAC;IAClE,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACxD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAyB,CAAC;YAC7C,MAAM,IAAI,SAAS,CAAC,EAAE,EAAE,gBAAgB,KAAK,qBAAqB,CAAC,CAAC;QACtE,QAAQ,CAAC,KAAyB,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,UAAU,KAAK,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,EAAE,UAAU,CAAC,CAAC;IACjD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC9B,MAAM,IAAI,SAAS,CAAC,EAAE,EAAE,YAAY,QAAQ,mCAAmC,CAAC,CAAC;IACnF,OAAO;QACL,EAAE;QACF,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC;QAC7B,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,EAAE,OAAO,CAAC;QAChC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC;QAC7B,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,QAAQ,CAAC;QACnC,IAAI,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC;QAChE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,SAAS,CAAC;QACtC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;YACnC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;YACvD,CAAC,CAAC,EAAE;QACN,OAAO,EAAE,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,SAAS,CAAC;QAC5E,GAAG,EAAE,CAAC,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,KAAK,CAAC;QAC5D,QAAQ,EAAE,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,EAAE,UAAU,CAAC;QAChF,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,EAAE,aAAa,CAAC;QACnD,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,UAAU,EAAE,EAAE,EAAE,YAAY,CAAC;QAChD,QAAQ;QACR,2EAA2E;QAC3E,8DAA8D;QAC9D,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,GAAG,CAAC;QAC9C,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9D,SAAS,EAAE,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,EAAE,EAAE,WAAW,CAAC;QACpF,MAAM,EAAE,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;QACtE,KAAK;QACL,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE;QAC7E,QAAQ;KACT,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,WAAW,GAAG,CAAC,GAAY,EAAE,KAAa,EAAsB,EAAE;IACtE,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;IACnE,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,QAAQ,CAAC;QACvD,QAAQ;QACR,iEAAiE;QACjE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,EAAE,UAAU,CAAC;KAC1D,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAY,EAAgB,EAAE;IAC3D,MAAM,CAAC,GAAG,GAA8B,CAAC;IACzC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;IAC7E,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,MAAM,IAAI,SAAS,CAAC,aAAa,EAAE,2BAA2B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACzF,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAChB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;AACjE,CAAC,CAAC;AAEF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAY,EAAE,IAAa,EAAgB,EAAE;IACzE,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,SAAS,CAAC,aAAa,EAAE,eAAe,IAAI,MAAO,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IACxF,CAAC;IACD,OAAO,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1C,CAAC,CAAC"}
1
+ {"version":3,"file":"data.js","sourceRoot":"","sources":["../src/data.ts"],"names":[],"mappings":"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"}
@@ -24,6 +24,7 @@ export type RawRoom = {
24
24
  beds?: string;
25
25
  view?: string;
26
26
  breakfast: boolean;
27
+ imageUrl?: string;
27
28
  /** Written the way a human writes it - 3800, not 380000. */
28
29
  nightly: number;
29
30
  roomsLeft: number;
@@ -46,6 +47,7 @@ export type RawHotel = {
46
47
  brand: string;
47
48
  city: string;
48
49
  cityId: string;
50
+ country?: string;
49
51
  area?: string;
50
52
  summary: string;
51
53
  amenities: string[];
@@ -70,16 +72,37 @@ export type RawHotel = {
70
72
  extras?: RawExtra[];
71
73
  policies?: Partial<Record<HotelPolicyTopic, string>>;
72
74
  };
75
+ export type RawTier = {
76
+ id: string;
77
+ name: string;
78
+ discount: number;
79
+ pointsPerUnit: number;
80
+ };
81
+ export type RawMember = {
82
+ number: string;
83
+ surname: string;
84
+ given: string;
85
+ tier: string;
86
+ points: number;
87
+ };
88
+ export type RawMembership = {
89
+ programme: string;
90
+ tiers: RawTier[];
91
+ members: RawMember[];
92
+ };
73
93
  export type RawHotelData = {
74
94
  brand: string;
75
95
  hotels: RawHotel[];
96
+ membership?: RawMembership;
76
97
  };
77
98
  export type DemoError = Error & {
78
- code: 'not-found' | 'bad-stay' | 'unavailable' | 'mixed-currency' | 'empty-trip';
99
+ code: 'not-found' | 'bad-stay' | 'unavailable' | 'mixed-currency' | 'empty-trip' | 'no-guest';
79
100
  };
80
101
  export declare const demoError: (code: DemoError["code"], message: string) => DemoError;
81
102
  export type DemoHotelsOptions = {
82
103
  data: RawHotelData;
104
+ /** How long a booking request holds its rooms. Default two hours - see HOLD_MS_DEFAULT. */
105
+ holdMs?: number;
83
106
  /** Injected so tests do not depend on the day they run. */
84
107
  now?: () => number;
85
108
  rng?: () => number;
@@ -1 +1 @@
1
- {"version":3,"file":"demo-hotels.d.ts","sourceRoot":"","sources":["../src/demo-hotels.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"demo-hotels.d.ts","sourceRoot":"","sources":["../src/demo-hotels.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { arrivalInstant, eachNight, isWeekendNight, nights as nightsBetween, stayProblem, todayAt, } from './dates.js';
2
- import { addMoney, formatMoney, money, multiplyMoney, taxOn, toMinor } from './money.js';
2
+ import { addMoney, formatMoney, minorFactor, money, multiplyMoney, taxOn, toMinor, } from './money.js';
3
3
  export const demoError = (code, message) => Object.assign(new Error(message), { code });
4
4
  const POLICY_TITLES = {
5
5
  cancellation: 'Cancellation',
@@ -10,7 +10,19 @@ const POLICY_TITLES = {
10
10
  accessibility: 'Accessibility',
11
11
  };
12
12
  const DEFAULT_OFFSET = '+08:00';
13
- const HOLD_HOURS = 24;
13
+ /**
14
+ * How long a request holds the rooms.
15
+ *
16
+ * ⚠️ TWO HOURS, NOT THE TWENTY-FOUR A REAL DESK WOULD GIVE, and the reason is
17
+ * that this backend's inventory is a demonstration everyone shares. One
18
+ * afternoon of evals and demos booked the Island Shangri-La's Deluxe City View
19
+ * six times, each hold lasting a day, and the room simply vanished from the
20
+ * card - a colleague opening the demo was told it was sold out, by us, about a
21
+ * room that does not exist (2026-09-18). Inventory exhaustion is silent and
22
+ * hits everybody; a hold lapsing is loud, local, and printed on the booking.
23
+ * A real backend sets its own window.
24
+ */
25
+ const HOLD_MS_DEFAULT = 2 * 3_600_000;
14
26
  /** A hold never runs past arrival - a room held for a guest who never arrives is a room nobody got. */
15
27
  const HOLD_STOPS_BEFORE_ARRIVAL_MS = 2 * 3_600_000;
16
28
  const CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
@@ -20,6 +32,7 @@ export const createDemoHotels = (opts) => {
20
32
  const hotels = opts.data.hotels;
21
33
  const now = opts.now ?? (() => Date.now());
22
34
  const rng = opts.rng ?? Math.random;
35
+ const holdMs = opts.holdMs ?? HOLD_MS_DEFAULT;
23
36
  const bookings = new Map();
24
37
  let seq = 0;
25
38
  const byId = (id) => {
@@ -28,6 +41,43 @@ export const createDemoHotels = (opts) => {
28
41
  hotels.find((h) => h.name.toLowerCase() === wanted) ??
29
42
  hotels.find((h) => h.name.toLowerCase().includes(wanted) && wanted.length >= 6));
30
43
  };
44
+ /**
45
+ * The member, by number and the surname on it.
46
+ *
47
+ * ⚠️ BOTH, AND CASE-FOLDED ONLY. A number alone would let anyone who can
48
+ * guess a format read somebody's tier and point balance; the surname is the
49
+ * second half of what a desk checks. It is not a password and this is not
50
+ * authentication - which is exactly why the tier only ever buys a DISCOUNT
51
+ * here and never reaches a booking somebody else made.
52
+ */
53
+ const memberFor = (number, surname) => {
54
+ const m = opts.data.membership;
55
+ if (!m)
56
+ return null;
57
+ const wanted = number.replace(/\s+/g, '').toUpperCase();
58
+ const hit = m.members.find((x) => x.number.replace(/\s+/g, '').toUpperCase() === wanted &&
59
+ x.surname.trim().toLowerCase() === surname.trim().toLowerCase());
60
+ if (!hit)
61
+ return null;
62
+ const tier = m.tiers.find((t) => t.id === hit.tier);
63
+ if (!tier)
64
+ return null;
65
+ return {
66
+ number: hit.number,
67
+ name: `${hit.given} ${hit.surname}`,
68
+ surname: hit.surname,
69
+ programme: m.programme,
70
+ tier: {
71
+ id: tier.id,
72
+ name: tier.name,
73
+ discount: tier.discount,
74
+ pointsPerUnit: tier.pointsPerUnit,
75
+ },
76
+ points: hit.points,
77
+ };
78
+ };
79
+ /** The tier's discount, or none. One place, so a rate cannot be discounted twice. */
80
+ const discountOf = (who) => who?.member?.tier.discount ?? 0;
31
81
  const offsetOf = (h) => h.utcOffset ?? DEFAULT_OFFSET;
32
82
  const today = (hotelId) => {
33
83
  const h = hotelId ? byId(hotelId) : undefined;
@@ -54,10 +104,18 @@ export const createDemoHotels = (opts) => {
54
104
  b.status = 'expired';
55
105
  }
56
106
  };
57
- const priceRoom = (h, r, stay) => {
107
+ const priceRoom = (h, r, stay, who) => {
58
108
  const perNight = eachNight(stay.checkIn, stay.checkOut).map((day) => money(toMinor(r.nightly, h.currency) * (isWeekendNight(day) ? 1 + h.weekendUplift : 1), h.currency));
59
- const roomSubtotal = perNight.reduce((a, b) => addMoney(a, b), money(0, h.currency));
109
+ const publicSubtotal = perNight.reduce((a, b) => addMoney(a, b), money(0, h.currency));
110
+ // ⚠️ THE DISCOUNT COMES OFF THE ROOM, NOT OFF THE TAX. Tax is charged on
111
+ // what is actually paid, so it is computed after - the other order quietly
112
+ // hands the member a second, invisible discount on the government's share.
113
+ const discount = discountOf(who);
114
+ const roomSubtotal = discount
115
+ ? money(publicSubtotal.minor * (1 - discount), h.currency)
116
+ : publicSubtotal;
60
117
  const taxes = taxOn(roomSubtotal, h.taxRate);
118
+ const publicTaxes = taxOn(publicSubtotal, h.taxRate);
61
119
  const freeUntil = r.refundable && r.freeCancelHours !== undefined
62
120
  ? new Date(arrivalInstant(stay.checkIn, h.checkInFrom ?? '15:00', offsetOf(h)) -
63
121
  r.freeCancelHours * 3_600_000).toISOString()
@@ -70,11 +128,23 @@ export const createDemoHotels = (opts) => {
70
128
  beds: r.beds,
71
129
  view: r.view,
72
130
  breakfast: r.breakfast,
131
+ imageUrl: r.imageUrl,
73
132
  // The average, because a Friday costs more than a Tuesday. `stayTotal` is
74
133
  // the number to compare hotels on, and it is exact.
75
134
  nightly: money(roomSubtotal.minor / perNight.length, h.currency),
76
135
  stayTotal: addMoney(roomSubtotal, taxes),
77
136
  taxesAndFees: taxes,
137
+ ...(discount
138
+ ? {
139
+ publicNightly: money(publicSubtotal.minor / perNight.length, h.currency),
140
+ publicStayTotal: addMoney(publicSubtotal, publicTaxes),
141
+ memberRate: {
142
+ programme: who?.member?.programme ?? '',
143
+ tier: who?.member?.tier.name ?? '',
144
+ discount,
145
+ },
146
+ }
147
+ : {}),
78
148
  nights: perNight.length,
79
149
  refundable: r.refundable,
80
150
  freeCancellationUntil: freeUntil,
@@ -89,6 +159,7 @@ export const createDemoHotels = (opts) => {
89
159
  brand: h.brand,
90
160
  city: h.city,
91
161
  cityId: h.cityId,
162
+ country: h.country,
92
163
  area: h.area,
93
164
  summary: h.summary,
94
165
  amenities: h.amenities,
@@ -126,13 +197,16 @@ export const createDemoHotels = (opts) => {
126
197
  };
127
198
  const mine = (b, who) => b.sessionId === who.sessionId ||
128
199
  (!!who.email && b.guest.email.toLowerCase() === who.email.toLowerCase());
129
- const quoteTrip = async (trip) => {
200
+ const quoteTrip = async (trip, who) => {
130
201
  sweep();
131
202
  const lines = [];
132
203
  const dropped = [];
133
204
  let currency;
134
205
  let subtotal = 0;
135
206
  let taxes = 0;
207
+ // What the tier took off, added up as we go - so the card can say what
208
+ // membership was worth on THIS trip rather than quoting a percentage.
209
+ let saved = 0;
136
210
  for (const line of trip.lines) {
137
211
  const h = byId(line.hotelId);
138
212
  if (!h) {
@@ -162,7 +236,7 @@ export const createDemoHotels = (opts) => {
162
236
  });
163
237
  continue;
164
238
  }
165
- const room = priceRoom(h, raw, line.stay);
239
+ const room = priceRoom(h, raw, line.stay, who);
166
240
  const wanted = Math.max(1, Math.floor(line.rooms ?? 1));
167
241
  if (room.roomsLeft < wanted) {
168
242
  dropped.push({
@@ -183,6 +257,11 @@ export const createDemoHotels = (opts) => {
183
257
  const preTax = money((room.stayTotal.minor - room.taxesAndFees.minor) * wanted, h.currency);
184
258
  subtotal += preTax.minor;
185
259
  taxes += room.taxesAndFees.minor * wanted;
260
+ // What the tier took off this line, all in - so the trip can say what
261
+ // membership was worth in money rather than quoting a percentage.
262
+ if (room.publicStayTotal) {
263
+ saved += (room.publicStayTotal.minor - room.stayTotal.minor) * wanted;
264
+ }
186
265
  lines.push({
187
266
  kind: 'room',
188
267
  hotelId: h.id,
@@ -247,6 +326,7 @@ export const createDemoHotels = (opts) => {
247
326
  guests: Math.max(...roomStays.map((s) => s.guests)),
248
327
  }
249
328
  : undefined;
329
+ const member = who?.member;
250
330
  return {
251
331
  lines,
252
332
  subtotal: money(subtotal, cur),
@@ -254,6 +334,17 @@ export const createDemoHotels = (opts) => {
254
334
  total: money(subtotal + taxes, cur),
255
335
  stay,
256
336
  dropped: dropped.length ? dropped : undefined,
337
+ ...(member && lines.length
338
+ ? {
339
+ member: {
340
+ programme: member.programme,
341
+ tier: member.tier.name,
342
+ saved: money(saved, cur),
343
+ // Points on what is actually paid, in whole currency units.
344
+ points: Math.round(((subtotal + taxes) / minorFactor(cur)) * member.tier.pointsPerUnit),
345
+ },
346
+ }
347
+ : {}),
257
348
  };
258
349
  };
259
350
  /** What cancelling this booking costs, line by line, in the hotel's own terms. */
@@ -299,18 +390,37 @@ export const createDemoHotels = (opts) => {
299
390
  .filter((t) => t.length > 1);
300
391
  const city = q.city?.trim().toLowerCase();
301
392
  const matches = hotels.filter((h) => {
302
- if (city && h.cityId.toLowerCase() !== city && h.city.toLowerCase() !== city)
393
+ // A country counts as a city here: "Japan" and "Malaysia" are how a
394
+ // guest narrows a group that runs hotels in twenty-odd countries, and
395
+ // refusing it would answer "no hotels" for a place with five.
396
+ if (city &&
397
+ h.cityId.toLowerCase() !== city &&
398
+ h.city.toLowerCase() !== city &&
399
+ (h.country ?? '').toLowerCase() !== city)
303
400
  return false;
304
401
  if (terms.length === 0)
305
402
  return true;
306
- const hay = [h.name, h.brand, h.city, h.cityId, h.area, h.summary, ...h.amenities]
403
+ const hay = [
404
+ h.name,
405
+ h.brand,
406
+ h.city,
407
+ h.cityId,
408
+ h.country,
409
+ h.area,
410
+ h.summary,
411
+ ...h.amenities,
412
+ ]
307
413
  .join(' ')
308
414
  .toLowerCase();
309
415
  return terms.every((t) => hay.includes(t));
310
416
  });
311
- return matches.slice(0, q.limit ?? 20).map(toHotel);
417
+ // `total` is what MATCHED, not what is being returned. They were the same
418
+ // number while the file held nine hotels; with ninety-six they are not,
419
+ // and a card that says "20 hotels" about a group with forty in China is
420
+ // wrong in the one direction a booking connector cannot be wrong in.
421
+ return { hotels: matches.slice(0, q.limit ?? 20).map(toHotel), total: matches.length };
312
422
  },
313
- async getHotel(id, stay) {
423
+ async getHotel(id, stay, who) {
314
424
  const h = byId(id);
315
425
  if (!h)
316
426
  return null;
@@ -329,7 +439,7 @@ export const createDemoHotels = (opts) => {
329
439
  noRooms: `No room here sleeps ${stay.guests}. The largest is ${Math.max(...h.rooms.map((r) => r.sleeps))}; two rooms would work.`,
330
440
  };
331
441
  }
332
- const rooms = fits.map((r) => priceRoom(h, r, stay)).filter((r) => r.roomsLeft > 0);
442
+ const rooms = fits.map((r) => priceRoom(h, r, stay, who)).filter((r) => r.roomsLeft > 0);
333
443
  return {
334
444
  ...detail,
335
445
  stay,
@@ -345,8 +455,21 @@ export const createDemoHotels = (opts) => {
345
455
  return (h.extras ?? []).map((e) => toExtra(h, e));
346
456
  },
347
457
  quote: quoteTrip,
458
+ async signInMember(number, surname) {
459
+ return memberFor(number, surname);
460
+ },
348
461
  async requestBooking(trip, guest, who) {
349
- const quote = await quoteTrip(trip);
462
+ const quote = await quoteTrip(trip, who);
463
+ // ⚠️ A ROOM HELD FOR NOBODY. The tool's schema says name and email are
464
+ // required, but a schema is what the model is TOLD, not what it must do -
465
+ // `request_booking {}` came back with a confirmation code and
466
+ // `guest: { name: '', email: '' }` on the live connector (2026-09-18).
467
+ // The hotel would be holding a room for nobody, and the guest could never
468
+ // find the booking by email. Refused at the seam the real API sits behind
469
+ // as well as in the tool.
470
+ if (!guest.name.trim() || !guest.email.includes('@')) {
471
+ throw demoError('no-guest', "A booking needs the guest's name and an email to send it to.");
472
+ }
350
473
  const [firstRoom] = quote.lines.filter((l) => l.kind === 'room');
351
474
  const first = firstRoom ? byId(firstRoom.hotelId) : undefined;
352
475
  const stay = quote.stay;
@@ -356,7 +479,7 @@ export const createDemoHotels = (opts) => {
356
479
  }
357
480
  const arrival = arrivalInstant(stay.checkIn, first.checkInFrom ?? '15:00', offsetOf(first));
358
481
  const t = now();
359
- const holdUntil = new Date(Math.max(t + 3_600_000, Math.min(t + HOLD_HOURS * 3_600_000, arrival - HOLD_STOPS_BEFORE_ARRIVAL_MS))).toISOString();
482
+ const holdUntil = new Date(Math.max(t + 3_600_000, Math.min(t + holdMs, arrival - HOLD_STOPS_BEFORE_ARRIVAL_MS))).toISOString();
360
483
  seq += 1;
361
484
  const booking = {
362
485
  id: `bk_${Date.now().toString(36)}_${seq}`,