@curless/hotel-storefront 0.24.4 → 0.26.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 (90) hide show
  1. package/dist/__tests__/__helpers__/sessions.d.ts +10 -0
  2. package/dist/__tests__/__helpers__/sessions.d.ts.map +1 -0
  3. package/dist/__tests__/__helpers__/sessions.js +37 -0
  4. package/dist/__tests__/__helpers__/sessions.js.map +1 -0
  5. package/dist/__tests__/card-contract.test.js +20 -0
  6. package/dist/__tests__/card-contract.test.js.map +1 -1
  7. package/dist/__tests__/data.test.js +7 -1
  8. package/dist/__tests__/data.test.js.map +1 -1
  9. package/dist/__tests__/dates.test.js +13 -1
  10. package/dist/__tests__/dates.test.js.map +1 -1
  11. package/dist/__tests__/huazhu-audit.test.js +69 -4
  12. package/dist/__tests__/huazhu-audit.test.js.map +1 -1
  13. package/dist/__tests__/huazhu-backend.test.js +93 -0
  14. package/dist/__tests__/huazhu-backend.test.js.map +1 -1
  15. package/dist/__tests__/huazhu-sandbox.test.js +7 -1
  16. package/dist/__tests__/huazhu-sandbox.test.js.map +1 -1
  17. package/dist/__tests__/server.test.js +112 -3
  18. package/dist/__tests__/server.test.js.map +1 -1
  19. package/dist/__tests__/state.test.d.ts +2 -0
  20. package/dist/__tests__/state.test.d.ts.map +1 -0
  21. package/dist/__tests__/state.test.js +204 -0
  22. package/dist/__tests__/state.test.js.map +1 -0
  23. package/dist/__tests__/tools.test.js +18 -0
  24. package/dist/__tests__/tools.test.js.map +1 -1
  25. package/dist/backend.d.ts +5 -0
  26. package/dist/backend.d.ts.map +1 -1
  27. package/dist/backend.js.map +1 -1
  28. package/dist/card.generated.d.ts +1 -1
  29. package/dist/card.generated.js +2 -2
  30. package/dist/card.generated.js.map +1 -1
  31. package/dist/config.d.ts +7 -0
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +0 -11
  34. package/dist/config.js.map +1 -1
  35. package/dist/dates.d.ts +9 -0
  36. package/dist/dates.d.ts.map +1 -1
  37. package/dist/dates.js +32 -0
  38. package/dist/dates.js.map +1 -1
  39. package/dist/demo-hotels.d.ts +21 -1
  40. package/dist/demo-hotels.d.ts.map +1 -1
  41. package/dist/demo-hotels.js +83 -11
  42. package/dist/demo-hotels.js.map +1 -1
  43. package/dist/huazhu/backend.d.ts +5 -0
  44. package/dist/huazhu/backend.d.ts.map +1 -1
  45. package/dist/huazhu/backend.js +67 -9
  46. package/dist/huazhu/backend.js.map +1 -1
  47. package/dist/index.js +9 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/legal.d.ts +8 -1
  50. package/dist/legal.d.ts.map +1 -1
  51. package/dist/legal.js +22 -8
  52. package/dist/legal.js.map +1 -1
  53. package/dist/limits.d.ts +49 -0
  54. package/dist/limits.d.ts.map +1 -1
  55. package/dist/limits.js +45 -0
  56. package/dist/limits.js.map +1 -1
  57. package/dist/members.d.ts +3 -1
  58. package/dist/members.d.ts.map +1 -1
  59. package/dist/members.js +24 -6
  60. package/dist/members.js.map +1 -1
  61. package/dist/money.d.ts.map +1 -1
  62. package/dist/money.js +8 -1
  63. package/dist/money.js.map +1 -1
  64. package/dist/server.d.ts +22 -8
  65. package/dist/server.d.ts.map +1 -1
  66. package/dist/server.js +121 -37
  67. package/dist/server.js.map +1 -1
  68. package/dist/sessions.d.ts +19 -0
  69. package/dist/sessions.d.ts.map +1 -0
  70. package/dist/sessions.js +96 -0
  71. package/dist/sessions.js.map +1 -0
  72. package/dist/state.d.ts +32 -0
  73. package/dist/state.d.ts.map +1 -0
  74. package/dist/state.js +118 -0
  75. package/dist/state.js.map +1 -0
  76. package/dist/tools.d.ts +13 -0
  77. package/dist/tools.d.ts.map +1 -1
  78. package/dist/tools.js +69 -5
  79. package/dist/tools.js.map +1 -1
  80. package/dist/trip-ids.d.ts +8 -1
  81. package/dist/trip-ids.d.ts.map +1 -1
  82. package/dist/trip-ids.js +24 -1
  83. package/dist/trip-ids.js.map +1 -1
  84. package/dist/trip.d.ts +3 -1
  85. package/dist/trip.d.ts.map +1 -1
  86. package/dist/trip.js +23 -6
  87. package/dist/trip.js.map +1 -1
  88. package/dist/types.d.ts +8 -0
  89. package/dist/types.d.ts.map +1 -1
  90. package/package.json +1 -1
@@ -0,0 +1,204 @@
1
+ import { mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { beforeEach, describe, expect, it } from 'vitest';
6
+ import { loadHotelData } from '../data.js';
7
+ import { KEEP_AFTER_CHECKOUT_DAYS, createDemoHotels } from '../demo-hotels.js';
8
+ import { MEMBER_TTL_MS, MemberStore } from '../members.js';
9
+ import { StateDir, StateFile } from '../state.js';
10
+ import { TripIds } from '../trip-ids.js';
11
+ import { TripStore } from '../trip.js';
12
+ // ⚠️ A DEPLOY USED TO FORGET EVERY GUEST (2026-10-05 audit): a confirmation
13
+ // code stopped opening its booking, a trip went empty, a member was signed
14
+ // out. Each store here is built, written, thrown away and built again from the
15
+ // same directory - which is what a restart is.
16
+ const DATA = loadHotelData(fileURLToPath(new URL('./__fixtures__/brand.json', import.meta.url)));
17
+ const OCT_1 = Date.parse('2026-10-01T02:00:00Z');
18
+ const STAY = { checkIn: '2026-10-19', checkOut: '2026-10-21', guests: 2 };
19
+ const guest = { name: 'Robin Ruan', email: 'robin@example.com' };
20
+ const me = { sessionId: 'session-a' };
21
+ let dir;
22
+ let clock = OCT_1;
23
+ const now = () => clock;
24
+ beforeEach(() => {
25
+ dir = mkdtempSync(join(tmpdir(), 'hotel-state-'));
26
+ clock = OCT_1;
27
+ });
28
+ /** One store's file, as a fresh process would open it. */
29
+ const fileFor = (name) => new StateFile(join(dir, name));
30
+ describe('the state file', () => {
31
+ it('writes only once asked to flush, owner-only, and reads back what it wrote', () => {
32
+ const f = fileFor('x.json');
33
+ f.bind(() => ({ hello: 'world' }));
34
+ f.changed();
35
+ expect(readdirSync(dir)).toEqual([]); // coalesced, not yet written
36
+ f.flush();
37
+ expect(fileFor('x.json').read()).toEqual({ hello: 'world' });
38
+ expect(statSync(join(dir, 'x.json')).mode & 0o777).toBe(0o600);
39
+ });
40
+ // Overwriting a file that did not parse would destroy the only copy of
41
+ // somebody's booking.
42
+ it('sets a file that does not parse aside instead of starting over it', () => {
43
+ writeFileSync(join(dir, 'bad.json'), '{"bookings": [');
44
+ expect(fileFor('bad.json').read()).toBeUndefined();
45
+ expect(readdirSync(dir).some((n) => n.startsWith('bad.json.corrupt-'))).toBe(true);
46
+ });
47
+ it('writes nothing anywhere when the deployment has no directory', () => {
48
+ const none = new StateDir(undefined);
49
+ expect(none.persisted).toBe(false);
50
+ expect(none.file('trips.json')).toBeUndefined();
51
+ });
52
+ });
53
+ describe('across a restart', () => {
54
+ it('keeps the trip, under the same key', () => {
55
+ const f = fileFor('trips.json');
56
+ const first = new TripStore(now, f);
57
+ first.set('conv hotel', {
58
+ kind: 'room',
59
+ hotelId: 'hong-kong-island',
60
+ roomId: 'deluxe-city',
61
+ stay: STAY,
62
+ rooms: 2,
63
+ });
64
+ first.set('other hotel', {
65
+ kind: 'room',
66
+ hotelId: 'hong-kong-island',
67
+ roomId: 'suite',
68
+ stay: STAY,
69
+ });
70
+ f.flush();
71
+ const again = new TripStore(now, fileFor('trips.json'));
72
+ expect(again.read('conv hotel')).toEqual([
73
+ { kind: 'room', hotelId: 'hong-kong-island', roomId: 'deluxe-city', stay: STAY, rooms: 2 },
74
+ ]);
75
+ expect(again.size).toBe(2);
76
+ });
77
+ // The card names its trip by id; an id that did not survive the deploy sent
78
+ // the card's rooms into a trip of their own.
79
+ it('keeps the id the card names its trip by', () => {
80
+ const f = fileFor('trip-ids.json');
81
+ const ids = new TripIds(now, f);
82
+ const id = ids.idFor('conv hotel');
83
+ f.flush();
84
+ expect(new TripIds(now, fileFor('trip-ids.json')).keyFor(id)).toBe('conv hotel');
85
+ });
86
+ it('keeps a sign-in for its two hours, and takes it off the disk after', () => {
87
+ const member = { number: 'SLC-4471-8820', name: 'Robin Ruan', surname: 'Ruan' };
88
+ const f = fileFor('members.json');
89
+ const members = new MemberStore(now, f);
90
+ members.set('conv', member);
91
+ f.flush();
92
+ expect(new MemberStore(now, fileFor('members.json')).read('conv')?.number).toBe('SLC-4471-8820');
93
+ clock += MEMBER_TTL_MS + 1;
94
+ const later = fileFor('members.json');
95
+ const expired = new MemberStore(now, later);
96
+ expect(expired.read('conv')).toBeUndefined();
97
+ later.flush();
98
+ expect(readFileSync(join(dir, 'members.json'), 'utf8')).not.toContain('SLC-4471-8820');
99
+ });
100
+ });
101
+ describe('demo bookings across a restart', () => {
102
+ const trip = {
103
+ lines: [{ kind: 'room', hotelId: 'hong-kong-kerry', roomId: 'deluxe', stay: STAY }],
104
+ };
105
+ const make = (file = fileFor('demo.json')) => ({ file, hotels: createDemoHotels({ data: DATA, now, rng: () => 0.5, state: file }) });
106
+ it('opens the same booking by its code after the restart, and still holds its room', async () => {
107
+ const first = make();
108
+ const [booking] = await first.hotels.requestBooking(trip, guest, me);
109
+ if (!booking)
110
+ throw new Error('no booking');
111
+ first.file.flush();
112
+ const { hotels } = make();
113
+ expect((await hotels.listBookings(me)).map((b) => b.confirmationCode)).toEqual([
114
+ booking.confirmationCode,
115
+ ]);
116
+ const found = await hotels.findBooking(booking.confirmationCode, {
117
+ sessionId: 'another-conversation',
118
+ email: guest.email,
119
+ });
120
+ expect(found?.id).toBe(booking.id);
121
+ // The hold is still a hold: the room is still off what the next guest sees.
122
+ const shown = await hotels.getHotel('hong-kong-kerry', STAY);
123
+ expect(shown?.rooms?.find((r) => r.id === 'deluxe')?.roomsLeft).toBe(9);
124
+ });
125
+ it('keeps who owns a booking on disk, and never in an answer', async () => {
126
+ const first = make();
127
+ const [booking] = await first.hotels.requestBooking(trip, guest, me);
128
+ first.file.flush();
129
+ expect(readFileSync(join(dir, 'demo.json'), 'utf8')).toContain('session-a');
130
+ expect(JSON.stringify(booking)).not.toContain('session-a');
131
+ const { hotels } = make();
132
+ expect(JSON.stringify(await hotels.listBookings(me))).not.toContain('session-a');
133
+ });
134
+ it('keeps saved preferences for the membership', async () => {
135
+ const first = make();
136
+ const member = await first.hotels.signInMember?.('SLC-4471-8820', 'Ruan');
137
+ if (!member)
138
+ throw new Error('no member');
139
+ await first.hotels.rememberPreferences?.(member, ['high-floor']);
140
+ first.file.flush();
141
+ const again = await make().hotels.signInMember?.('SLC-4471-8820', 'Ruan');
142
+ expect(again?.preferences).toEqual(['high-floor']);
143
+ });
144
+ // The privacy page says how long; this is that sentence, enforced.
145
+ it(`forgets a booking ${KEEP_AFTER_CHECKOUT_DAYS} days after its check-out`, async () => {
146
+ const first = make();
147
+ const [booking] = await first.hotels.requestBooking(trip, guest, me);
148
+ first.file.flush();
149
+ clock = Date.parse('2026-10-21T00:00:00Z') + KEEP_AFTER_CHECKOUT_DAYS * 86_400_000 - 3_600_000;
150
+ expect((await make().hotels.listBookings(me)).map((b) => b.id)).toEqual([booking?.id]);
151
+ clock += 2 * 86_400_000;
152
+ const later = make();
153
+ expect(await later.hotels.listBookings(me)).toEqual([]);
154
+ later.file.flush();
155
+ expect(readFileSync(join(dir, 'demo.json'), 'utf8')).not.toContain(guest.email);
156
+ });
157
+ });
158
+ describe('the demo-wide cap on held rooms', () => {
159
+ const stayOf = (from, nights) => {
160
+ const end = new Date(Date.parse(`${from}T00:00:00Z`) + nights * 86_400_000);
161
+ return { checkIn: from, checkOut: end.toISOString().slice(0, 10), guests: 2 };
162
+ };
163
+ // ⚠️ Twelve made-up conversations, one booking each, emptied a hotel for a
164
+ // month. The per-conversation cap cannot see that; this can.
165
+ it('refuses a hold that would take more than the demo allows, whoever asks', async () => {
166
+ const hotels = createDemoHotels({ data: DATA, now, rng: () => 0.5, maxHeldRoomNights: 10 });
167
+ const book = (session, hotelId, roomId, nights, rooms = 1) => hotels.requestBooking({ lines: [{ kind: 'room', hotelId, roomId, stay: stayOf('2026-11-02', nights), rooms }] }, guest, { sessionId: session });
168
+ await book('one', 'hong-kong-kerry', 'deluxe', 4, 2); // 8 room-nights
169
+ await expect(book('two', 'hong-kong-kowloon', 'deluxe', 3)).rejects.toMatchObject({
170
+ code: 'demo-busy',
171
+ });
172
+ // Two room-nights still fit.
173
+ expect(await book('three', 'hong-kong-kowloon', 'deluxe', 2)).toHaveLength(1);
174
+ });
175
+ it('counts only live holds - a lapsed or cancelled one gives its share back', async () => {
176
+ const hotels = createDemoHotels({
177
+ data: DATA,
178
+ now,
179
+ rng: () => 0.5,
180
+ maxHeldRoomNights: 4,
181
+ holdMs: 3_600_000,
182
+ });
183
+ const one = {
184
+ lines: [
185
+ {
186
+ kind: 'room',
187
+ hotelId: 'hong-kong-kerry',
188
+ roomId: 'deluxe',
189
+ stay: stayOf('2026-11-02', 4),
190
+ },
191
+ ],
192
+ };
193
+ const [first] = await hotels.requestBooking(one, guest, { sessionId: 'a' });
194
+ await expect(hotels.requestBooking(one, guest, { sessionId: 'b' })).rejects.toMatchObject({
195
+ code: 'demo-busy',
196
+ });
197
+ await hotels.cancelBooking(first?.confirmationCode ?? '', { sessionId: 'a' });
198
+ const [second] = await hotels.requestBooking(one, guest, { sessionId: 'b' });
199
+ expect(second?.status).toBe('requested');
200
+ clock += 3_600_000 + 1; // lapsed
201
+ expect(await hotels.requestBooking(one, guest, { sessionId: 'c' })).toHaveLength(1);
202
+ });
203
+ });
204
+ //# sourceMappingURL=state.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.test.js","sourceRoot":"","sources":["../../src/__tests__/state.test.ts"],"names":[],"mappings":"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"}
@@ -106,6 +106,15 @@ describe('one hotel', () => {
106
106
  expect(twoRooms?.roomsTotal).toBe('HK$17,176');
107
107
  expect(two.text).toContain('HK$17,176 for 2 rooms');
108
108
  });
109
+ // ⚠️ "until 2026-10-17" for a window that closes at 15:00 that day: a guest
110
+ // cancelling that evening would have paid a night (2026-10-05 audit).
111
+ it('gives the free-cancellation deadline with its hour, on the hotel clock', async () => {
112
+ const res = await call(t.getHotel, { hotel: 'hong-kong-island', ...STAY });
113
+ const room = res.structured.rooms.find((r) => r.id === 'deluxe-city');
114
+ // 48 hours before a 15:00 check-in on Monday 19 October, in Hong Kong.
115
+ expect(room?.freeCancellationLocal).toBe('Sat 17 Oct, 15:00');
116
+ expect(res.text).toContain('free to cancel until Sat 17 Oct, 15:00');
117
+ });
109
118
  it('says so plainly once the held rooms mean nothing can supply that many', async () => {
110
119
  // Island's largest type has six; two held leaves four, and four is under
111
120
  // the five being asked for. Held inventory changing what a room count can
@@ -275,6 +284,15 @@ describe('membership', () => {
275
284
  expect(room?.publicStayTotal).toBe('HK$8,588');
276
285
  expect(room?.stayTotal).toBe('HK$7,729');
277
286
  });
287
+ // ⚠️ One room's public rate struck through over two rooms' member rate put
288
+ // the "was" price BELOW the price a member pays (2026-10-05 audit).
289
+ it('strikes through the public rate for as many rooms as the member price beside it', async () => {
290
+ await signIn();
291
+ const res = await call(t.getHotel, { hotel: 'hong-kong-island', ...STAY, rooms: 2 });
292
+ const room = res.structured.rooms.find((r) => r.id === 'deluxe-city');
293
+ expect(room?.roomsTotal).toBe('HK$15,458');
294
+ expect(room?.publicRoomsTotal).toBe('HK$17,176');
295
+ });
278
296
  it('says what membership was worth on THIS trip, in money', async () => {
279
297
  await signIn();
280
298
  await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-harbour', ...STAY });