@curless/hotel-storefront 0.48.1 → 0.49.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/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/demo-hotels.d.ts +7 -0
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +167 -79
- package/dist/demo-hotels.js.map +1 -1
- package/dist/fence.d.ts +10 -0
- package/dist/fence.d.ts.map +1 -1
- package/dist/fence.js +15 -0
- package/dist/fence.js.map +1 -1
- package/dist/listing.d.ts +5 -1
- package/dist/listing.d.ts.map +1 -1
- package/dist/listing.js +6 -2
- package/dist/listing.js.map +1 -1
- package/dist/present.d.ts +27 -0
- package/dist/present.d.ts.map +1 -1
- package/dist/present.js +40 -2
- package/dist/present.js.map +1 -1
- package/dist/same-booking.d.ts +33 -0
- package/dist/same-booking.d.ts.map +1 -0
- package/dist/same-booking.js +73 -0
- package/dist/same-booking.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +3 -3
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts +3 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +86 -40
- package/dist/tools.js.map +1 -1
- package/dist/trip.d.ts +0 -11
- package/dist/trip.d.ts.map +1 -1
- package/dist/trip.js +21 -6
- package/dist/trip.js.map +1 -1
- package/package.json +1 -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,
|
|
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,sxvkCAAsxvkC,EACtxvkC,QAAQ,CACT,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAEnB,6GAA6G;AAC7G,MAAM,CAAC,MAAM,qBAAqB,GAChC,kEAAkE,CAAC;AAErE,0GAA0G;AAC1G,MAAM,CAAC,MAAM,qBAAqB,GAChC,iLAAiL,CAAC"}
|
package/dist/demo-hotels.d.ts
CHANGED
|
@@ -216,9 +216,16 @@ export type DemoHotelsOptions = {
|
|
|
216
216
|
* code AND the email it was made with is added, so it can then change or
|
|
217
217
|
* cancel it with the code alone - the card's buttons send nothing more. Found
|
|
218
218
|
* that way it had "No booking here matches that code" on its own Cancel.
|
|
219
|
+
*
|
|
220
|
+
* ⚠️ `askedAs` IS NEVER ANSWERED EITHER. It is what the booking was asked for
|
|
221
|
+
* as - the stay's rooms and nights and a digest of the guest (`askedAsKey`) -
|
|
222
|
+
* so the same stay asked for again within minutes is found and not booked
|
|
223
|
+
* twice (2026-10-27 audit, F15). It holds nobody's name, and a booking whose
|
|
224
|
+
* stay is changed loses it: it is no longer what was asked for.
|
|
219
225
|
*/
|
|
220
226
|
type StoredBooking = Booking & {
|
|
221
227
|
owners: Set<string>;
|
|
228
|
+
askedAs?: string;
|
|
222
229
|
};
|
|
223
230
|
/**
|
|
224
231
|
* Live holds one conversation may have at once. A demo's inventory is shared:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"demo-hotels.d.ts","sourceRoot":"","sources":["../src/demo-hotels.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"demo-hotels.d.ts","sourceRoot":"","sources":["../src/demo-hotels.ts"],"names":[],"mappings":"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"}
|
package/dist/demo-hotels.js
CHANGED
|
@@ -5,6 +5,7 @@ import { FACILITIES, hasFacility } from './facilities.js';
|
|
|
5
5
|
import { addMoney, formatMoney, minorFactor, money, multiplyMoney, taxOn, toMinor, wholeUnits, } from './money.js';
|
|
6
6
|
import { namedExactly, roomNamesOf } from './names.js';
|
|
7
7
|
import { bedKindsOf } from './room-filters.js';
|
|
8
|
+
import { SAME_BOOKING_MS, guestDigest, singleFlight } from './same-booking.js';
|
|
8
9
|
import { MAX_EXTRA_QUANTITY, MAX_ROOMS_PER_LINE } from './trip.js';
|
|
9
10
|
export const demoError = (code, message) => Object.assign(new Error(message), { code });
|
|
10
11
|
const POLICY_TITLES = {
|
|
@@ -103,8 +104,9 @@ const wholeBooking = (b) => {
|
|
|
103
104
|
isMoney(x.quote?.total));
|
|
104
105
|
};
|
|
105
106
|
const publicBooking = (b) => {
|
|
106
|
-
const { owners, ...rest } = b;
|
|
107
|
+
const { owners, askedAs, ...rest } = b;
|
|
107
108
|
void owners;
|
|
109
|
+
void askedAs;
|
|
108
110
|
return { ...rest };
|
|
109
111
|
};
|
|
110
112
|
/**
|
|
@@ -215,6 +217,8 @@ export const createDemoHotels = (opts) => {
|
|
|
215
217
|
const rng = opts.rng ?? (() => randomInt(0, 2 ** 32) / 2 ** 32);
|
|
216
218
|
const holdMs = opts.holdMs ?? HOLD_MS_DEFAULT;
|
|
217
219
|
const bookings = new Map();
|
|
220
|
+
/** One booking request at a time per conversation and per email - see `singleFlight`. */
|
|
221
|
+
const single = singleFlight();
|
|
218
222
|
/** Saved room preferences, by membership number - see `preferencesFor`. */
|
|
219
223
|
const preferences = new Map();
|
|
220
224
|
const maxHeldRoomNights = opts.maxHeldRoomNights ?? MAX_HELD_ROOM_NIGHTS;
|
|
@@ -647,6 +651,12 @@ export const createDemoHotels = (opts) => {
|
|
|
647
651
|
}
|
|
648
652
|
return there;
|
|
649
653
|
};
|
|
654
|
+
/**
|
|
655
|
+
* The conversation a booking is asked for in: the trip's owner when the card
|
|
656
|
+
* asks on a connection of its own, else the caller - what the same stay asked
|
|
657
|
+
* twice is matched by (a booking made from the card is owned by both).
|
|
658
|
+
*/
|
|
659
|
+
const conversationOf = (who) => who.tripOwner ?? who.sessionId;
|
|
650
660
|
/** Over and done with - nothing held, nothing left to change. */
|
|
651
661
|
const ended = (b) => (b.status !== 'requested' && b.status !== 'confirmed') || b.stay.checkOut < today(b.hotelId);
|
|
652
662
|
/** Room for one more, by forgetting the oldest booking that is over. */
|
|
@@ -675,6 +685,24 @@ export const createDemoHotels = (opts) => {
|
|
|
675
685
|
// reading: the first.
|
|
676
686
|
return part.length > 1 && part.some((r) => r.rate) ? undefined : part[0];
|
|
677
687
|
};
|
|
688
|
+
/**
|
|
689
|
+
* What one stay is asked for as: its lines - hotel, room, nights, party,
|
|
690
|
+
* rooms, hours; an add-on's id, quantity and day - and a digest of the guest
|
|
691
|
+
* (`guestDigest`). The hotel and the room are read as the file names them, so
|
|
692
|
+
* a retry that says "Kerry Hotel" for `hong-kong-kerry` is the same asking.
|
|
693
|
+
* The email is not in it: a booking carries its own, and it is matched there.
|
|
694
|
+
*/
|
|
695
|
+
const askedAsKey = (lines, digest) => `${lines
|
|
696
|
+
.map((l) => {
|
|
697
|
+
const h = byId(l.hotelId);
|
|
698
|
+
const hotel = h?.id ?? l.hotelId;
|
|
699
|
+
if (l.kind !== 'room')
|
|
700
|
+
return `extra|${hotel}|${l.extraId}|${l.quantity}|${l.day ?? ''}`;
|
|
701
|
+
const room = h ? (roomOf(h, l.roomId)?.id ?? l.roomId) : l.roomId;
|
|
702
|
+
return `room|${hotel}|${room}|${l.stay.checkIn}|${l.stay.checkOut}|${l.stay.guests}|${l.rooms ?? 1}|${l.hourly?.start ?? ''}|${l.hourly?.hours ?? ''}`;
|
|
703
|
+
})
|
|
704
|
+
.sort()
|
|
705
|
+
.join(';')}#${digest}`;
|
|
678
706
|
const quoteTrip = async (trip, who, ignore) => {
|
|
679
707
|
sweep();
|
|
680
708
|
const lines = [];
|
|
@@ -1743,85 +1771,142 @@ export const createDemoHotels = (opts) => {
|
|
|
1743
1771
|
.map((l) => `${(byId(l.hotelId)?.extras ?? []).find((e) => e.id === l.extraId)?.name ?? l.extraId} at ${byId(l.hotelId)?.name ?? l.hotelId} goes with a stay there: add a room night at that hotel, or put it on a booking already made there.`)
|
|
1744
1772
|
.join(' ')} The trip is as it was - take that out or change it, then ask again.`);
|
|
1745
1773
|
}
|
|
1746
|
-
// ⚠️
|
|
1747
|
-
//
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
//
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
};
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
const
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
if (over)
|
|
1783
|
-
throw over;
|
|
1784
|
-
const t = now();
|
|
1785
|
-
const made = [];
|
|
1786
|
-
makeRoom(bookable.length);
|
|
1787
|
-
// One at a time, in date order: each is held as it is made, so a later
|
|
1788
|
-
// stay of this same trip sees the rooms an earlier one took.
|
|
1789
|
-
for (const { lines: g } of bookable) {
|
|
1790
|
-
const quote = await quoteTrip({ lines: g }, who);
|
|
1791
|
-
const [firstRoom] = quote.lines.filter((l) => l.kind === 'room');
|
|
1792
|
-
const h = firstRoom ? byId(firstRoom.hotelId) : undefined;
|
|
1793
|
-
if (!firstRoom || !h || !quote.stay || quote.dropped?.length) {
|
|
1794
|
-
failed.push(failedFor(g, quote));
|
|
1795
|
-
continue;
|
|
1774
|
+
// ⚠️ ONE REQUEST AT A TIME PER CONVERSATION AND PER EMAIL (2026-10-27
|
|
1775
|
+
// audit, F15). Pricing is awaited between asking "was this booked
|
|
1776
|
+
// already?" and booking, so two identical requests - a model retrying, two
|
|
1777
|
+
// cards submitting at once - both found nothing and each took the rooms
|
|
1778
|
+
// and counted against the limits. The second waits for the first and
|
|
1779
|
+
// finds its bookings below. The caller and, from the card, the
|
|
1780
|
+
// conversation whose trip it is; the email too, as Huazhu's does.
|
|
1781
|
+
const email = guest.email.trim().toLowerCase();
|
|
1782
|
+
const conversation = conversationOf(who);
|
|
1783
|
+
const locks = [...new Set([who.sessionId, conversation])].map((k) => `s:${k}`);
|
|
1784
|
+
return single([...locks, `e:${email}`], async () => {
|
|
1785
|
+
// ⚠️ THE SAME STAY ASKED FOR AGAIN IS ANSWERED WITH THE FIRST. A stay is
|
|
1786
|
+
// the same when its rooms, nights, party, hours and add-ons are, and the
|
|
1787
|
+
// guest is - name, phone, requests and arrival, NOT the note (free text,
|
|
1788
|
+
// reworded by every retry) - for the same email in the same conversation,
|
|
1789
|
+
// within SAME_BOOKING_MS, and the booking is still live (a cancelled one
|
|
1790
|
+
// is not what was asked for any more). Another NAME on the same email is
|
|
1791
|
+
// another booking: the same room for a friend (robin, 2026-10-23).
|
|
1792
|
+
// Stay by stay, so a trip whose one stay failed is asked again for that
|
|
1793
|
+
// one only, and the ones booked are not booked twice.
|
|
1794
|
+
const digest = guestDigest(guest, requests);
|
|
1795
|
+
const asked = groups.map((lines) => ({ lines, key: askedAsKey(lines, digest) }));
|
|
1796
|
+
const reused = [];
|
|
1797
|
+
const fresh = [];
|
|
1798
|
+
const at = now();
|
|
1799
|
+
for (const ask of asked) {
|
|
1800
|
+
const earlier = [...bookings.values()].find((b) => b.askedAs === ask.key &&
|
|
1801
|
+
!reused.includes(b) &&
|
|
1802
|
+
b.owners.has(conversation) &&
|
|
1803
|
+
b.guest.email.trim().toLowerCase() === email &&
|
|
1804
|
+
at - Date.parse(b.createdAt) < SAME_BOOKING_MS &&
|
|
1805
|
+
!ended(b));
|
|
1806
|
+
if (earlier)
|
|
1807
|
+
reused.push(earlier);
|
|
1808
|
+
else
|
|
1809
|
+
fresh.push(ask);
|
|
1796
1810
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1811
|
+
// ⚠️ SAID AS THE SAME BOOKING, never as a new one (F15): `repeat`.
|
|
1812
|
+
if (reused.length && !fresh.length) {
|
|
1813
|
+
return { booked: reused.map(publicBooking), failed: [], repeat: true };
|
|
1814
|
+
}
|
|
1815
|
+
// ⚠️ EACH STAY ON ITS OWN, AND WHAT WAS BOOKED IS KEPT (robin,
|
|
1816
|
+
// 2026-10-14 - see the seam). A stay that cannot be booked is said, with
|
|
1817
|
+
// why, and stays in the trip; the others are booked. It used to be all or
|
|
1818
|
+
// none, after a Beijing suite that went while the guest typed their name
|
|
1819
|
+
// took the trip's other half with it, unsaid: nothing goes unsaid here.
|
|
1820
|
+
const failed = [];
|
|
1821
|
+
const failedFor = (g, quote, why) => {
|
|
1822
|
+
const room = g.find((l) => l.kind === 'room');
|
|
1823
|
+
const h = room ? byId(room.hotelId) : undefined;
|
|
1824
|
+
const first = quote?.dropped?.[0];
|
|
1825
|
+
return {
|
|
1826
|
+
hotelId: room?.hotelId ?? '',
|
|
1827
|
+
hotelName: h?.name ?? room?.hotelId ?? '',
|
|
1828
|
+
...(h?.names?.name ? { hotelNames: h.names.name } : {}),
|
|
1829
|
+
stay: room?.stay ?? { checkIn: '', checkOut: '', guests: 0 },
|
|
1830
|
+
...(room && h
|
|
1831
|
+
? { roomName: h.rooms.find((r) => r.id === room.roomId)?.name ?? room.roomId }
|
|
1832
|
+
: {}),
|
|
1833
|
+
why: why ?? first?.why ?? 'It could not be booked.',
|
|
1834
|
+
code: why ? 'unavailable' : (first?.code ?? 'unavailable'),
|
|
1835
|
+
...(first?.args && !why ? { args: first.args } : {}),
|
|
1836
|
+
};
|
|
1816
1837
|
};
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1838
|
+
const priced = await Promise.all(fresh.map(async ({ lines: g, key }) => ({
|
|
1839
|
+
lines: g,
|
|
1840
|
+
key,
|
|
1841
|
+
quote: await quoteTrip({ lines: g }, who),
|
|
1842
|
+
})));
|
|
1843
|
+
const bookable = priced.filter(({ lines, quote }) => {
|
|
1844
|
+
const ok = !quote.dropped?.length && quote.lines.some((l) => l.kind === 'room');
|
|
1845
|
+
if (!ok)
|
|
1846
|
+
failed.push(failedFor(lines, quote));
|
|
1847
|
+
return ok;
|
|
1848
|
+
});
|
|
1849
|
+
// The limits are the whole trip's, and count what would be held.
|
|
1850
|
+
const live = [...bookings.values()].filter((b) => b.owners.has(who.sessionId) && b.status === 'requested').length;
|
|
1851
|
+
if (live + bookable.length > MAX_HOLDS_PER_CONVERSATION) {
|
|
1852
|
+
throw demoError('too-many-holds', `Nothing was booked. This conversation already holds ${live} booking${live === 1 ? '' : 's'}, and ${MAX_HOLDS_PER_CONVERSATION} at once is the most - cancel one that is not needed, then ask again.`);
|
|
1853
|
+
}
|
|
1854
|
+
const over = overHolding(bookable.map((b) => b.quote), who, guest.email, 'booked');
|
|
1855
|
+
if (over)
|
|
1856
|
+
throw over;
|
|
1857
|
+
const t = now();
|
|
1858
|
+
const made = [];
|
|
1859
|
+
makeRoom(bookable.length);
|
|
1860
|
+
// One at a time, in date order: each is held as it is made, so a later
|
|
1861
|
+
// stay of this same trip sees the rooms an earlier one took.
|
|
1862
|
+
for (const { lines: g, key } of bookable) {
|
|
1863
|
+
const quote = await quoteTrip({ lines: g }, who);
|
|
1864
|
+
const [firstRoom] = quote.lines.filter((l) => l.kind === 'room');
|
|
1865
|
+
const h = firstRoom ? byId(firstRoom.hotelId) : undefined;
|
|
1866
|
+
if (!firstRoom || !h || !quote.stay || quote.dropped?.length) {
|
|
1867
|
+
failed.push(failedFor(g, quote));
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
const arrival = arrivalInstant(quote.stay.checkIn, h.checkInFrom ?? '15:00', offsetOf(h));
|
|
1871
|
+
const holdUntil = new Date(Math.max(t + 3_600_000, Math.min(t + holdMs, arrival - HOLD_STOPS_BEFORE_ARRIVAL_MS))).toISOString();
|
|
1872
|
+
const booking = {
|
|
1873
|
+
id: `bk_${randomUUID()}`,
|
|
1874
|
+
confirmationCode: confirmationCode(new Set(made.map((m) => m.confirmationCode))),
|
|
1875
|
+
status: 'requested',
|
|
1876
|
+
createdAt: new Date(t).toISOString(),
|
|
1877
|
+
holdUntil,
|
|
1878
|
+
// On the hotel's clock, as the free-cancellation deadline is (2026-10-05 audit, P2).
|
|
1879
|
+
holdUntilLocal: atHotelTime(Date.parse(holdUntil), offsetOf(h)),
|
|
1880
|
+
hotelId: h.id,
|
|
1881
|
+
hotelName: h.name,
|
|
1882
|
+
...(h.names?.name ? { hotelNames: h.names.name } : {}),
|
|
1883
|
+
stay: quote.stay,
|
|
1884
|
+
guest,
|
|
1885
|
+
quote,
|
|
1886
|
+
// The caller, and the conversation whose trip it was - see `Identity.tripOwner`.
|
|
1887
|
+
owners: new Set([who.sessionId, ...(who.tripOwner ? [who.tripOwner] : [])]),
|
|
1888
|
+
...(cleanRequests(requests) ? { requests: cleanRequests(requests) } : {}),
|
|
1889
|
+
askedAs: key,
|
|
1890
|
+
};
|
|
1891
|
+
bookings.set(booking.id, booking);
|
|
1892
|
+
made.push(booking);
|
|
1893
|
+
}
|
|
1894
|
+
if (made.length)
|
|
1895
|
+
changed();
|
|
1896
|
+
// In the trip's own order, as the guest reads it.
|
|
1897
|
+
failed.sort((a, b) => a.stay.checkIn.localeCompare(b.stay.checkIn));
|
|
1898
|
+
// Stays asked for before are in the answer beside the ones made now. When
|
|
1899
|
+
// none was made now - the rest could not be - it is the first asking again.
|
|
1900
|
+
if (reused.length) {
|
|
1901
|
+
const all = [...reused, ...made].sort((x, y) => x.stay.checkIn.localeCompare(y.stay.checkIn));
|
|
1902
|
+
return {
|
|
1903
|
+
booked: all.map(publicBooking),
|
|
1904
|
+
failed,
|
|
1905
|
+
...(made.length ? {} : { repeat: true }),
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
return { booked: made.map(publicBooking), failed };
|
|
1909
|
+
});
|
|
1825
1910
|
},
|
|
1826
1911
|
async listBookings(who, limit = 10) {
|
|
1827
1912
|
sweep();
|
|
@@ -1863,8 +1948,11 @@ export const createDemoHotels = (opts) => {
|
|
|
1863
1948
|
const { preview, quote } = await planChange(b, change, who);
|
|
1864
1949
|
if (!preview.changeable || !preview.to || !quote)
|
|
1865
1950
|
throw demoError('unavailable', preview.rule);
|
|
1951
|
+
// No longer what was asked for - see `StoredBooking.askedAs`.
|
|
1952
|
+
const { askedAs, ...changing } = b;
|
|
1953
|
+
void askedAs;
|
|
1866
1954
|
const updated = {
|
|
1867
|
-
...
|
|
1955
|
+
...changing,
|
|
1868
1956
|
stay: preview.to.stay,
|
|
1869
1957
|
quote,
|
|
1870
1958
|
changedAt: new Date(now()).toISOString(),
|