@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.
- package/dist/__tests__/__helpers__/sessions.d.ts +10 -0
- package/dist/__tests__/__helpers__/sessions.d.ts.map +1 -0
- package/dist/__tests__/__helpers__/sessions.js +37 -0
- package/dist/__tests__/__helpers__/sessions.js.map +1 -0
- package/dist/__tests__/card-contract.test.js +20 -0
- package/dist/__tests__/card-contract.test.js.map +1 -1
- package/dist/__tests__/data.test.js +7 -1
- package/dist/__tests__/data.test.js.map +1 -1
- package/dist/__tests__/dates.test.js +13 -1
- package/dist/__tests__/dates.test.js.map +1 -1
- package/dist/__tests__/huazhu-audit.test.js +69 -4
- package/dist/__tests__/huazhu-audit.test.js.map +1 -1
- package/dist/__tests__/huazhu-backend.test.js +93 -0
- package/dist/__tests__/huazhu-backend.test.js.map +1 -1
- package/dist/__tests__/huazhu-sandbox.test.js +7 -1
- package/dist/__tests__/huazhu-sandbox.test.js.map +1 -1
- package/dist/__tests__/server.test.js +112 -3
- package/dist/__tests__/server.test.js.map +1 -1
- package/dist/__tests__/state.test.d.ts +2 -0
- package/dist/__tests__/state.test.d.ts.map +1 -0
- package/dist/__tests__/state.test.js +204 -0
- package/dist/__tests__/state.test.js.map +1 -0
- package/dist/__tests__/tools.test.js +18 -0
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/backend.d.ts +5 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js.map +1 -1
- package/dist/card.generated.d.ts +1 -1
- package/dist/card.generated.js +2 -2
- package/dist/card.generated.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -11
- package/dist/config.js.map +1 -1
- package/dist/dates.d.ts +9 -0
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +32 -0
- package/dist/dates.js.map +1 -1
- package/dist/demo-hotels.d.ts +21 -1
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +83 -11
- package/dist/demo-hotels.js.map +1 -1
- package/dist/huazhu/backend.d.ts +5 -0
- package/dist/huazhu/backend.d.ts.map +1 -1
- package/dist/huazhu/backend.js +67 -9
- package/dist/huazhu/backend.js.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/legal.d.ts +8 -1
- package/dist/legal.d.ts.map +1 -1
- package/dist/legal.js +22 -8
- package/dist/legal.js.map +1 -1
- package/dist/limits.d.ts +49 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +45 -0
- package/dist/limits.js.map +1 -1
- package/dist/members.d.ts +3 -1
- package/dist/members.d.ts.map +1 -1
- package/dist/members.js +24 -6
- package/dist/members.js.map +1 -1
- package/dist/money.d.ts.map +1 -1
- package/dist/money.js +8 -1
- package/dist/money.js.map +1 -1
- package/dist/server.d.ts +22 -8
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +121 -37
- package/dist/server.js.map +1 -1
- package/dist/sessions.d.ts +19 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +96 -0
- package/dist/sessions.js.map +1 -0
- package/dist/state.d.ts +32 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +118 -0
- package/dist/state.js.map +1 -0
- package/dist/tools.d.ts +13 -0
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +69 -5
- package/dist/tools.js.map +1 -1
- package/dist/trip-ids.d.ts +8 -1
- package/dist/trip-ids.d.ts.map +1 -1
- package/dist/trip-ids.js +24 -1
- package/dist/trip-ids.js.map +1 -1
- package/dist/trip.d.ts +3 -1
- package/dist/trip.d.ts.map +1 -1
- package/dist/trip.js +23 -6
- package/dist/trip.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- 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 });
|