@curless/hotel-storefront 0.23.1 → 0.24.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/README.md +28 -11
- package/dist/__tests__/__fixtures__/huazhu-fake.d.ts +54 -0
- package/dist/__tests__/__fixtures__/huazhu-fake.d.ts.map +1 -0
- package/dist/__tests__/__fixtures__/huazhu-fake.js +340 -0
- package/dist/__tests__/__fixtures__/huazhu-fake.js.map +1 -0
- package/dist/__tests__/huazhu-backend.test.d.ts +2 -0
- package/dist/__tests__/huazhu-backend.test.d.ts.map +1 -0
- package/dist/__tests__/huazhu-backend.test.js +434 -0
- package/dist/__tests__/huazhu-backend.test.js.map +1 -0
- package/dist/__tests__/huazhu-cancel-policy.test.d.ts +2 -0
- package/dist/__tests__/huazhu-cancel-policy.test.d.ts.map +1 -0
- package/dist/__tests__/huazhu-cancel-policy.test.js +82 -0
- package/dist/__tests__/huazhu-cancel-policy.test.js.map +1 -0
- package/dist/__tests__/huazhu-sellable.test.d.ts +2 -0
- package/dist/__tests__/huazhu-sellable.test.d.ts.map +1 -0
- package/dist/__tests__/huazhu-sellable.test.js +192 -0
- package/dist/__tests__/huazhu-sellable.test.js.map +1 -0
- package/dist/__tests__/server.test.js +129 -4
- package/dist/__tests__/server.test.js.map +1 -1
- package/dist/__tests__/tool-descriptions.test.js +57 -4
- package/dist/__tests__/tool-descriptions.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +2 -0
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/backend.d.ts +27 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +7 -0
- 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 +52 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/facilities.d.ts.map +1 -1
- package/dist/facilities.js +7 -5
- package/dist/facilities.js.map +1 -1
- package/dist/huazhu/backend.d.ts +40 -0
- package/dist/huazhu/backend.d.ts.map +1 -0
- package/dist/huazhu/backend.js +1318 -0
- package/dist/huazhu/backend.js.map +1 -0
- package/dist/huazhu/bookings.d.ts +81 -0
- package/dist/huazhu/bookings.d.ts.map +1 -0
- package/dist/huazhu/bookings.js +189 -0
- package/dist/huazhu/bookings.js.map +1 -0
- package/dist/huazhu/cancel-policy.d.ts +69 -0
- package/dist/huazhu/cancel-policy.d.ts.map +1 -0
- package/dist/huazhu/cancel-policy.js +201 -0
- package/dist/huazhu/cancel-policy.js.map +1 -0
- package/dist/huazhu/catalog.d.ts +75 -0
- package/dist/huazhu/catalog.d.ts.map +1 -0
- package/dist/huazhu/catalog.js +251 -0
- package/dist/huazhu/catalog.js.map +1 -0
- package/dist/huazhu/check.d.ts +5 -0
- package/dist/huazhu/check.d.ts.map +1 -0
- package/dist/huazhu/check.js +170 -0
- package/dist/huazhu/check.js.map +1 -0
- package/dist/huazhu/client.d.ts +100 -0
- package/dist/huazhu/client.d.ts.map +1 -0
- package/dist/huazhu/client.js +232 -0
- package/dist/huazhu/client.js.map +1 -0
- package/dist/huazhu/sellable.d.ts +81 -0
- package/dist/huazhu/sellable.d.ts.map +1 -0
- package/dist/huazhu/sellable.js +219 -0
- package/dist/huazhu/sellable.js.map +1 -0
- package/dist/huazhu/wire.d.ts +389 -0
- package/dist/huazhu/wire.d.ts.map +1 -0
- package/dist/huazhu/wire.js +72 -0
- package/dist/huazhu/wire.js.map +1 -0
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/legal.d.ts +4 -3
- package/dist/legal.d.ts.map +1 -1
- package/dist/legal.js +33 -18
- package/dist/legal.js.map +1 -1
- package/dist/lib.d.ts +8 -3
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +8 -1
- package/dist/lib.js.map +1 -1
- package/dist/server.d.ts +20 -8
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +132 -28
- package/dist/server.js.map +1 -1
- package/dist/surface.d.ts +2 -1
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +37 -10
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +33 -20
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +130 -11
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +21 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { addDays, todayAt } from '../dates.js';
|
|
2
|
+
import { parseCancelPolicy } from './cancel-policy.js';
|
|
3
|
+
import { HuazhuClient, HuazhuRefusal, HuazhuUnreachable } from './client.js';
|
|
4
|
+
import { clockAt, offeredAtAll, sell } from './sellable.js';
|
|
5
|
+
export const checkHuazhu = async (api, env, out = (s) => console.log(s), fetchImpl) => {
|
|
6
|
+
const prefix = api.envPrefix ?? 'HUAZHU';
|
|
7
|
+
const need = ['CLIENT_ID', 'CLIENT_SECRET', 'DISTRIBUTOR_ID'].map((k) => `${prefix}_${k}`);
|
|
8
|
+
const missing = need.filter((n) => !env[n]);
|
|
9
|
+
if (missing.length) {
|
|
10
|
+
out(`✗ not set: ${missing.join(', ')}`);
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const client = new HuazhuClient({
|
|
14
|
+
baseUrl: api.baseUrl,
|
|
15
|
+
authUrl: api.authUrl,
|
|
16
|
+
credentials: {
|
|
17
|
+
clientId: String(env[`${prefix}_CLIENT_ID`]),
|
|
18
|
+
secret: String(env[`${prefix}_CLIENT_SECRET`]),
|
|
19
|
+
distributorId: String(env[`${prefix}_DISTRIBUTOR_ID`]),
|
|
20
|
+
},
|
|
21
|
+
...(fetchImpl ? { fetch: fetchImpl } : {}),
|
|
22
|
+
});
|
|
23
|
+
const tz = api.timezone ?? 'Asia/Shanghai';
|
|
24
|
+
const step = async (what, run) => {
|
|
25
|
+
try {
|
|
26
|
+
const v = await run();
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err instanceof HuazhuRefusal)
|
|
31
|
+
out(`✗ ${what}: Huazhu said ${err.code} ${err.huazhuMessage}${err.detail ? ` - ${err.detail}` : ''}`);
|
|
32
|
+
else if (err instanceof HuazhuUnreachable)
|
|
33
|
+
out(`✗ ${what}: no answer (${err.why}) - is this machine's IP on Huazhu's list?`);
|
|
34
|
+
else
|
|
35
|
+
out(`✗ ${what}: ${err.message}`);
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
out(`Huazhu at ${api.baseUrl} (token from ${api.authUrl}), distributor ${env[`${prefix}_DISTRIBUTOR_ID`]}`);
|
|
40
|
+
const list = await step('hotel list', () => client.hotels());
|
|
41
|
+
if (!list)
|
|
42
|
+
return false;
|
|
43
|
+
out(`✓ token issued, and /hotels lists ${list.length} hotel(s)`);
|
|
44
|
+
const wanted = api.hotels?.length
|
|
45
|
+
? list.filter((h) => api.hotels?.includes(String(h.hotelId)))
|
|
46
|
+
: list;
|
|
47
|
+
if (api.hotels?.length)
|
|
48
|
+
out(` ${wanted.length} of them are in this deployment's hotels list`);
|
|
49
|
+
const first = wanted[0];
|
|
50
|
+
if (!first) {
|
|
51
|
+
out('✗ no hotel to check with');
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const hotel = await step(`hotel ${first.hotelId}`, () => client.hotel(String(first.hotelId)));
|
|
55
|
+
if (!hotel)
|
|
56
|
+
return false;
|
|
57
|
+
const photos = hotel.hotelPhotoList ?? [];
|
|
58
|
+
out(`✓ ${hotel.hotelName} (${hotel.hotelNameEn ?? 'no English name'}) · ${hotel.brandName} · ${hotel.cityName} ${hotel.countyName ?? ''} · ${hotel.tradeCurrency ?? 'no currency given'} · international=${hotel.international}`);
|
|
59
|
+
out(` ${hotel.hotelRoomTypeList?.length ?? 0} room type(s), ${hotel.hotelServiceList?.length ?? 0} facilities, ${photos.length} photo(s); photo hosts: ${[
|
|
60
|
+
...new Set(photos.map((p) => {
|
|
61
|
+
try {
|
|
62
|
+
return new URL(String(p.fileUrl)).origin;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return '?';
|
|
66
|
+
}
|
|
67
|
+
})),
|
|
68
|
+
].join(', ') || 'none'}`);
|
|
69
|
+
if (typeof hotel.openingDate === 'number')
|
|
70
|
+
out(" note: dates arrive as epoch milliseconds, as in the documentation's example");
|
|
71
|
+
const products = await step('products', () => client.products(hotel.hotelId));
|
|
72
|
+
if (!products)
|
|
73
|
+
return false;
|
|
74
|
+
const all = products.products ?? [];
|
|
75
|
+
const allow = new Set(api.allowTags ?? []);
|
|
76
|
+
const offered = all.filter((p) => offeredAtAll(p, allow));
|
|
77
|
+
out(`✓ ${all.length} product(s), ${offered.length} offered here (hour rooms and gated rates left out); sellDays ${products.sellDays}`);
|
|
78
|
+
const tags = [...new Set(all.flatMap((p) => p.tags ?? []))];
|
|
79
|
+
if (tags.length)
|
|
80
|
+
out(` tags seen: ${tags.join(', ')}`);
|
|
81
|
+
const today = todayAt(Date.now(), tz);
|
|
82
|
+
const start = addDays(today, 14);
|
|
83
|
+
const end = addDays(start, 7);
|
|
84
|
+
const [offers, inventory] = await Promise.all([
|
|
85
|
+
step('offers', () => client.offers(hotel.hotelId, start, end)),
|
|
86
|
+
step('inventory', () => client.inventory(hotel.hotelId, start, end)),
|
|
87
|
+
]);
|
|
88
|
+
if (!offers || !inventory)
|
|
89
|
+
return false;
|
|
90
|
+
out(`✓ offers for ${start}..${end}: ${offers.dailyOffers.length} product(s); inventory: ${inventory.dailyInventories.length}`);
|
|
91
|
+
const codes = new Set(offers.dailyOffers.flatMap((o) => o.cancelPolicies.map((c) => c?.code)).filter(Boolean));
|
|
92
|
+
const unread = [...codes].filter((c) => !parseCancelPolicy(c));
|
|
93
|
+
out(` cancellation codes: ${[...codes].join(', ') || 'none'}`);
|
|
94
|
+
if (unread.length)
|
|
95
|
+
out(`✗ these cancellation codes cannot be read here - they would be shown as "the hotel decides": ${unread.join(', ')}`);
|
|
96
|
+
const guarantees = new Set(offers.dailyOffers.flatMap((o) => o.guaranteePolicies.map((g) => g?.code)));
|
|
97
|
+
out(` guarantee codes: ${[...guarantees].join(', ')} - this deployment books as ${api.paymentType ?? 'NGPL'}`);
|
|
98
|
+
const checkIn = start;
|
|
99
|
+
const checkOut = addDays(start, 1);
|
|
100
|
+
const context = {
|
|
101
|
+
checkIn,
|
|
102
|
+
checkOut,
|
|
103
|
+
adults: 2,
|
|
104
|
+
rooms: 1,
|
|
105
|
+
today,
|
|
106
|
+
clock: clockAt(Date.now(), tz),
|
|
107
|
+
paymentType: api.paymentType ?? 'NGPL',
|
|
108
|
+
allowTags: allow,
|
|
109
|
+
};
|
|
110
|
+
const key = (o) => `${o.roomTypeCode}~${o.rateCode}`;
|
|
111
|
+
let cheapest;
|
|
112
|
+
const reasons = new Map();
|
|
113
|
+
for (const p of offered) {
|
|
114
|
+
const offer = offers.dailyOffers.find((o) => key(o) === key(p));
|
|
115
|
+
if (!offer) {
|
|
116
|
+
reasons.set('no offer', (reasons.get('no offer') ?? 0) + 1);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const w = {
|
|
120
|
+
start,
|
|
121
|
+
offer,
|
|
122
|
+
inventory: inventory.dailyInventories.find((i) => key(i) === key(p))?.inventories,
|
|
123
|
+
};
|
|
124
|
+
const res = sell(p, w, hotel.hotelRoomTypeList?.find((r) => r.roomTypeCode === p.roomTypeCode), context);
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
reasons.set(res.kind, (reasons.get(res.kind) ?? 0) + 1);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const total = res.after.reduce((a, b) => a + b, 0);
|
|
130
|
+
if (!cheapest || total < cheapest.after.reduce((a, b) => a + b, 0)) {
|
|
131
|
+
cheapest = {
|
|
132
|
+
product: p,
|
|
133
|
+
after: res.after,
|
|
134
|
+
...(res.before ? { before: res.before } : {}),
|
|
135
|
+
currency: res.currency,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (reasons.size)
|
|
140
|
+
out(` not sellable for ${checkIn}, 1 night, 2 adults: ${[...reasons].map(([k, n]) => `${n} ${k}`).join(', ')}`);
|
|
141
|
+
if (!cheapest) {
|
|
142
|
+
out('✗ nothing here is sellable for that night - try another hotel, or check the payment type the contract allows');
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
out(`✓ cheapest sellable: ${cheapest.product.roomTypeName} / ${cheapest.product.rateCode} at ${cheapest.after[0]} ${cheapest.currency}`);
|
|
146
|
+
const pre = await step('prebook', () => client.prebook({
|
|
147
|
+
hotelId: hotel.hotelId,
|
|
148
|
+
checkinType: 'DAY',
|
|
149
|
+
paymentType: api.paymentType ?? 'NGPL',
|
|
150
|
+
reservations: [
|
|
151
|
+
{
|
|
152
|
+
roomTypeCode: cheapest.product.roomTypeCode,
|
|
153
|
+
rateCode: cheapest.product.rateCode,
|
|
154
|
+
checkin: checkIn,
|
|
155
|
+
checkout: checkOut,
|
|
156
|
+
amountAfterTax: cheapest.after,
|
|
157
|
+
...(cheapest.before ? { amountBeforeTax: cheapest.before } : {}),
|
|
158
|
+
currencyCode: cheapest.currency,
|
|
159
|
+
roomCount: 1,
|
|
160
|
+
adultCount: 2,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
}));
|
|
164
|
+
if (!pre)
|
|
165
|
+
return false;
|
|
166
|
+
out(`✓ prebook accepted our price${pre.preBookToken ? ' and issued a token' : ' (no token returned)'} - nothing was booked`);
|
|
167
|
+
out('All read-only steps passed.');
|
|
168
|
+
return true;
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../src/huazhu/check.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type BookContent, type BookRequest, type CancelContent, type ConsistencyContent, type HotelListItem, type HotelStatic, type InventoryContent, type OffersContent, type PrebookContent, type PrebookRequest, type ProductCandidate, type ProductsContent, type ReservationDetail } from './wire.js';
|
|
2
|
+
export type HuazhuCredentials = {
|
|
3
|
+
clientId: string;
|
|
4
|
+
secret: string;
|
|
5
|
+
/** "渠道Code" - issued by Huazhu with the client id. */
|
|
6
|
+
distributorId: string;
|
|
7
|
+
};
|
|
8
|
+
export type HuazhuClientOptions = {
|
|
9
|
+
/** The business endpoints: https://<bizdomain> - /hotels, /ari/..., /reservation/... */
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
/** The token issuer: https://<authservicedomain> - POST /oauth/token. */
|
|
12
|
+
authUrl: string;
|
|
13
|
+
credentials: HuazhuCredentials;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
now?: () => number;
|
|
16
|
+
/** Milliseconds before an ordinary call is given up on. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
};
|
|
19
|
+
/** Huazhu answered, and the answer was no. Nothing was done. */
|
|
20
|
+
export declare class HuazhuRefusal extends Error {
|
|
21
|
+
readonly code: number;
|
|
22
|
+
readonly huazhuMessage: string;
|
|
23
|
+
readonly detail: string;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
/** A refusal can still carry content - a failed prebook carries today's price. */
|
|
26
|
+
readonly content?: unknown | undefined;
|
|
27
|
+
constructor(code: number, huazhuMessage: string, detail: string, path: string,
|
|
28
|
+
/** A refusal can still carry content - a failed prebook carries today's price. */
|
|
29
|
+
content?: unknown | undefined);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* We do not know what Huazhu did. The documentation's definition: connect or
|
|
33
|
+
* read timeout, a reset, or an HTTP status that is neither 200 nor 4xx.
|
|
34
|
+
*/
|
|
35
|
+
export declare class HuazhuUnreachable extends Error {
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly why: string;
|
|
38
|
+
constructor(path: string, why: string);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The token, shared by every call this process makes.
|
|
42
|
+
*
|
|
43
|
+
* The documentation's rules, each of which is here:
|
|
44
|
+
* - a token lasts `expires_in` seconds (3600 by default);
|
|
45
|
+
* - asking for a new one retires the old one, after a 120-second grace -
|
|
46
|
+
* so two callers that each fetch their own keep invalidating each other;
|
|
47
|
+
* ONE fetch at a time, and everyone waits on it;
|
|
48
|
+
* - refresh early, at a random 180-300 seconds before expiry, so several
|
|
49
|
+
* instances do not all refresh at the same moment.
|
|
50
|
+
*/
|
|
51
|
+
export declare class TokenSource {
|
|
52
|
+
private readonly opts;
|
|
53
|
+
private token?;
|
|
54
|
+
private pending?;
|
|
55
|
+
constructor(opts: Pick<HuazhuClientOptions, 'authUrl' | 'credentials' | 'now'> & {
|
|
56
|
+
fetch: typeof fetch;
|
|
57
|
+
timeoutMs: number;
|
|
58
|
+
});
|
|
59
|
+
private now;
|
|
60
|
+
get(): Promise<string>;
|
|
61
|
+
/** A new token, whatever the old one's clock says - after Huazhu answered 2002. */
|
|
62
|
+
refresh(): Promise<string>;
|
|
63
|
+
private fetchToken;
|
|
64
|
+
}
|
|
65
|
+
export declare class HuazhuClient {
|
|
66
|
+
private readonly opts;
|
|
67
|
+
private readonly fetch;
|
|
68
|
+
private readonly tokens;
|
|
69
|
+
private readonly timeoutMs;
|
|
70
|
+
constructor(opts: HuazhuClientOptions);
|
|
71
|
+
/**
|
|
72
|
+
* One call. Answers the content of a `code: 1000`; throws `HuazhuRefusal`
|
|
73
|
+
* for any other code and `HuazhuUnreachable` when there was no answer.
|
|
74
|
+
*
|
|
75
|
+
* A 2002 (token refused) is retried ONCE with a fresh token - the token can
|
|
76
|
+
* be retired under us by another instance's refresh. Nothing else is
|
|
77
|
+
* retried here: whether a call may be repeated is its caller's to know.
|
|
78
|
+
*/
|
|
79
|
+
call<T>(method: 'GET' | 'POST', path: string, body?: unknown, opts?: {
|
|
80
|
+
timeoutMs?: number;
|
|
81
|
+
}): Promise<T>;
|
|
82
|
+
private once;
|
|
83
|
+
hotels(): Promise<HotelListItem[]>;
|
|
84
|
+
hotel(hotelId: string): Promise<HotelStatic>;
|
|
85
|
+
products(hotelId: string): Promise<ProductsContent>;
|
|
86
|
+
/** At most 30 dates, both ends included - `ariRanges` cuts a longer span. */
|
|
87
|
+
offers(hotelId: string, startDate: string, endDate: string, productCandidates?: ProductCandidate[]): Promise<OffersContent>;
|
|
88
|
+
inventory(hotelId: string, startDate: string, endDate: string, productCandidates?: ProductCandidate[]): Promise<InventoryContent>;
|
|
89
|
+
prebook(req: PrebookRequest): Promise<PrebookContent>;
|
|
90
|
+
/**
|
|
91
|
+
* Make the booking. Throws `HuazhuUnreachable` when the outcome is unknown -
|
|
92
|
+
* see `Reconciler` for what has to happen next, and within what time.
|
|
93
|
+
*/
|
|
94
|
+
book(req: BookRequest): Promise<BookContent>;
|
|
95
|
+
detail(resId: string): Promise<ReservationDetail>;
|
|
96
|
+
cancel(resId: string): Promise<CancelContent>;
|
|
97
|
+
/** "仅限网络异常时调用" - only after /reservation/book went unanswered. */
|
|
98
|
+
consistency(distributorResId: string): Promise<ConsistencyContent>;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/huazhu/client.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,WAAW,EAEhB,KAAK,aAAa,EAElB,KAAK,kBAAkB,EAEvB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EAEvB,MAAM,WAAW,CAAC;AAanB,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,wFAAwF;IACxF,OAAO,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,iBAAiB,CAAC;IAC/B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,gEAAgE;AAChE,qBAAa,aAAc,SAAQ,KAAK;IAEpC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,aAAa,EAAE,MAAM;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,kFAAkF;IAClF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO;gBALjB,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM;IACrB,kFAAkF;IACzE,OAAO,CAAC,EAAE,OAAO,YAAA;CAK7B;AAED;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM;gBADX,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM;CAKvB;AAED;;;;;;;;;;GAUG;AACH,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJvB,OAAO,CAAC,KAAK,CAAC,CAAuC;IACrD,OAAO,CAAC,OAAO,CAAC,CAAkB;gBAGf,IAAI,EAAE,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,aAAa,GAAG,KAAK,CAAC,GAAG;QACpF,KAAK,EAAE,OAAO,KAAK,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;KACnB;IAGH,OAAO,CAAC,GAAG;IAIL,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC;IAK5B,mFAAmF;IACnF,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;YASZ,UAAU;CAuCzB;AAED,qBAAa,YAAY;IAKX,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEN,IAAI,EAAE,mBAAmB;IAYtD;;;;;;;OAOG;IACG,IAAI,CAAC,CAAC,EACV,MAAM,EAAE,KAAK,GAAG,MAAM,EACtB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAChC,OAAO,CAAC,CAAC,CAAC;YAWC,IAAI;IAsDlB,MAAM,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;IAIlC,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAI5C,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAInD,6EAA6E;IAC7E,MAAM,CACJ,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,GACrC,OAAO,CAAC,aAAa,CAAC;IAUzB,SAAS,CACP,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,GACrC,OAAO,CAAC,gBAAgB,CAAC;IAU5B,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAIrD;;;OAGG;IACH,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAI5C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIjD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAS7C,kEAAkE;IAClE,WAAW,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAKnE"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { randomInt, randomUUID } from 'node:crypto';
|
|
2
|
+
import { CODES, SUCCESS, } from './wire.js';
|
|
3
|
+
/** Huazhu answered, and the answer was no. Nothing was done. */
|
|
4
|
+
export class HuazhuRefusal extends Error {
|
|
5
|
+
code;
|
|
6
|
+
huazhuMessage;
|
|
7
|
+
detail;
|
|
8
|
+
path;
|
|
9
|
+
content;
|
|
10
|
+
constructor(code, huazhuMessage, detail, path,
|
|
11
|
+
/** A refusal can still carry content - a failed prebook carries today's price. */
|
|
12
|
+
content) {
|
|
13
|
+
super(`Huazhu ${path} ${code} ${huazhuMessage}${detail ? `: ${detail}` : ''}`);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.huazhuMessage = huazhuMessage;
|
|
16
|
+
this.detail = detail;
|
|
17
|
+
this.path = path;
|
|
18
|
+
this.content = content;
|
|
19
|
+
this.name = 'HuazhuRefusal';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* We do not know what Huazhu did. The documentation's definition: connect or
|
|
24
|
+
* read timeout, a reset, or an HTTP status that is neither 200 nor 4xx.
|
|
25
|
+
*/
|
|
26
|
+
export class HuazhuUnreachable extends Error {
|
|
27
|
+
path;
|
|
28
|
+
why;
|
|
29
|
+
constructor(path, why) {
|
|
30
|
+
super(`Huazhu ${path} did not answer: ${why}`);
|
|
31
|
+
this.path = path;
|
|
32
|
+
this.why = why;
|
|
33
|
+
this.name = 'HuazhuUnreachable';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The token, shared by every call this process makes.
|
|
38
|
+
*
|
|
39
|
+
* The documentation's rules, each of which is here:
|
|
40
|
+
* - a token lasts `expires_in` seconds (3600 by default);
|
|
41
|
+
* - asking for a new one retires the old one, after a 120-second grace -
|
|
42
|
+
* so two callers that each fetch their own keep invalidating each other;
|
|
43
|
+
* ONE fetch at a time, and everyone waits on it;
|
|
44
|
+
* - refresh early, at a random 180-300 seconds before expiry, so several
|
|
45
|
+
* instances do not all refresh at the same moment.
|
|
46
|
+
*/
|
|
47
|
+
export class TokenSource {
|
|
48
|
+
opts;
|
|
49
|
+
token;
|
|
50
|
+
pending;
|
|
51
|
+
constructor(opts) {
|
|
52
|
+
this.opts = opts;
|
|
53
|
+
}
|
|
54
|
+
now() {
|
|
55
|
+
return this.opts.now?.() ?? Date.now();
|
|
56
|
+
}
|
|
57
|
+
async get() {
|
|
58
|
+
if (this.token && this.now() < this.token.refreshAt)
|
|
59
|
+
return this.token.value;
|
|
60
|
+
return this.refresh();
|
|
61
|
+
}
|
|
62
|
+
/** A new token, whatever the old one's clock says - after Huazhu answered 2002. */
|
|
63
|
+
refresh() {
|
|
64
|
+
if (!this.pending) {
|
|
65
|
+
this.pending = this.fetchToken().finally(() => {
|
|
66
|
+
this.pending = undefined;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return this.pending;
|
|
70
|
+
}
|
|
71
|
+
async fetchToken() {
|
|
72
|
+
const { clientId, secret } = this.opts.credentials;
|
|
73
|
+
const url = `${this.opts.authUrl.replace(/\/$/, '')}/oauth/token?scope=ALL&grant_type=client_credentials`;
|
|
74
|
+
let res;
|
|
75
|
+
try {
|
|
76
|
+
res = await this.opts.fetch(url, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`,
|
|
80
|
+
},
|
|
81
|
+
signal: AbortSignal.timeout(this.opts.timeoutMs),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
throw new HuazhuUnreachable('/oauth/token', err.message);
|
|
86
|
+
}
|
|
87
|
+
const body = (await res.json().catch(() => ({})));
|
|
88
|
+
if (!res.ok || !body.access_token) {
|
|
89
|
+
// The one place a 4xx is expected: a wrong client id or secret.
|
|
90
|
+
throw new HuazhuRefusal(CODES.invalidToken, body.error ?? `HTTP ${res.status}`, body.error_description ?? 'the token was refused - check the client id and secret', '/oauth/token');
|
|
91
|
+
}
|
|
92
|
+
const lifeMs = Math.max(60, Number(body.expires_in) || 3600) * 1000;
|
|
93
|
+
const early = randomInt(180, 301) * 1000;
|
|
94
|
+
this.token = {
|
|
95
|
+
value: body.access_token,
|
|
96
|
+
// A token shorter-lived than the early margin is refreshed at half-life.
|
|
97
|
+
refreshAt: this.now() + (lifeMs > early * 2 ? lifeMs - early : lifeMs / 2),
|
|
98
|
+
};
|
|
99
|
+
return body.access_token;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export class HuazhuClient {
|
|
103
|
+
opts;
|
|
104
|
+
fetch;
|
|
105
|
+
tokens;
|
|
106
|
+
timeoutMs;
|
|
107
|
+
constructor(opts) {
|
|
108
|
+
this.opts = opts;
|
|
109
|
+
this.fetch = opts.fetch ?? globalThis.fetch;
|
|
110
|
+
this.timeoutMs = opts.timeoutMs ?? 10_000;
|
|
111
|
+
this.tokens = new TokenSource({
|
|
112
|
+
authUrl: opts.authUrl,
|
|
113
|
+
credentials: opts.credentials,
|
|
114
|
+
now: opts.now,
|
|
115
|
+
fetch: this.fetch,
|
|
116
|
+
timeoutMs: this.timeoutMs,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* One call. Answers the content of a `code: 1000`; throws `HuazhuRefusal`
|
|
121
|
+
* for any other code and `HuazhuUnreachable` when there was no answer.
|
|
122
|
+
*
|
|
123
|
+
* A 2002 (token refused) is retried ONCE with a fresh token - the token can
|
|
124
|
+
* be retired under us by another instance's refresh. Nothing else is
|
|
125
|
+
* retried here: whether a call may be repeated is its caller's to know.
|
|
126
|
+
*/
|
|
127
|
+
async call(method, path, body, opts = {}) {
|
|
128
|
+
try {
|
|
129
|
+
return await this.once(method, path, body, opts, await this.tokens.get());
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof HuazhuRefusal && err.code === CODES.invalidToken) {
|
|
133
|
+
return this.once(method, path, body, opts, await this.tokens.refresh());
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async once(method, path, body, opts, token) {
|
|
139
|
+
const url = `${this.opts.baseUrl.replace(/\/$/, '')}${path}`;
|
|
140
|
+
let res;
|
|
141
|
+
try {
|
|
142
|
+
res = await this.fetch(url, {
|
|
143
|
+
method,
|
|
144
|
+
headers: {
|
|
145
|
+
'Content-Type': 'application/json',
|
|
146
|
+
distributorId: this.opts.credentials.distributorId,
|
|
147
|
+
timestamp: String(this.opts.now?.() ?? Date.now()),
|
|
148
|
+
traceId: randomUUID(),
|
|
149
|
+
Authorization: `Bearer ${token}`,
|
|
150
|
+
},
|
|
151
|
+
...(method === 'POST' ? { body: JSON.stringify(body ?? {}) } : {}),
|
|
152
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? this.timeoutMs),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
throw new HuazhuUnreachable(path, err.message);
|
|
157
|
+
}
|
|
158
|
+
// ⚠️ THE DOCUMENTATION'S LINE: "Http状态码非200且非4XX" is a network error.
|
|
159
|
+
// A 4xx is an answer (our request was wrong); a 5xx or a proxy's 502 is
|
|
160
|
+
// not, and must be treated as "it may have happened".
|
|
161
|
+
if (res.status !== 200 && (res.status < 400 || res.status >= 500)) {
|
|
162
|
+
throw new HuazhuUnreachable(path, `HTTP ${res.status}`);
|
|
163
|
+
}
|
|
164
|
+
let envelope;
|
|
165
|
+
try {
|
|
166
|
+
envelope = (await res.json());
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
// A 200 whose body did not arrive whole is no more an answer than a
|
|
170
|
+
// timeout is.
|
|
171
|
+
if (res.status === 200)
|
|
172
|
+
throw new HuazhuUnreachable(path, 'the answer could not be read');
|
|
173
|
+
throw new HuazhuRefusal(res.status, `HTTP ${res.status}`, err.message, path);
|
|
174
|
+
}
|
|
175
|
+
if (res.status !== 200 || envelope.code !== SUCCESS) {
|
|
176
|
+
throw new HuazhuRefusal(Number(envelope.code) || res.status, envelope.message ?? `HTTP ${res.status}`, envelope.responseDes ?? '', path, envelope.content);
|
|
177
|
+
}
|
|
178
|
+
return envelope.content;
|
|
179
|
+
}
|
|
180
|
+
// ------------------------------------------------------------ endpoints ----
|
|
181
|
+
hotels() {
|
|
182
|
+
return this.call('GET', '/hotels').then((x) => x ?? []);
|
|
183
|
+
}
|
|
184
|
+
hotel(hotelId) {
|
|
185
|
+
return this.call('POST', '/hotel', { hotelId });
|
|
186
|
+
}
|
|
187
|
+
products(hotelId) {
|
|
188
|
+
return this.call('POST', '/ari/products', { hotelId });
|
|
189
|
+
}
|
|
190
|
+
/** At most 30 dates, both ends included - `ariRanges` cuts a longer span. */
|
|
191
|
+
offers(hotelId, startDate, endDate, productCandidates) {
|
|
192
|
+
return this.call('POST', '/ari/daily/offers', {
|
|
193
|
+
hotelId,
|
|
194
|
+
startDate,
|
|
195
|
+
endDate,
|
|
196
|
+
checkinType: 'DAY',
|
|
197
|
+
...(productCandidates?.length ? { productCandidates } : {}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
inventory(hotelId, startDate, endDate, productCandidates) {
|
|
201
|
+
return this.call('POST', '/ari/daily/inventory', {
|
|
202
|
+
hotelId,
|
|
203
|
+
startDate,
|
|
204
|
+
endDate,
|
|
205
|
+
checkinType: 'DAY',
|
|
206
|
+
...(productCandidates?.length ? { productCandidates } : {}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
prebook(req) {
|
|
210
|
+
return this.call('POST', '/reservation/prebook', req, { timeoutMs: 15_000 });
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Make the booking. Throws `HuazhuUnreachable` when the outcome is unknown -
|
|
214
|
+
* see `Reconciler` for what has to happen next, and within what time.
|
|
215
|
+
*/
|
|
216
|
+
book(req) {
|
|
217
|
+
return this.call('POST', '/reservation/book', req, { timeoutMs: 20_000 });
|
|
218
|
+
}
|
|
219
|
+
detail(resId) {
|
|
220
|
+
return this.call('POST', '/reservation/detail', { resId });
|
|
221
|
+
}
|
|
222
|
+
cancel(resId) {
|
|
223
|
+
return this.call('POST', '/reservation/cancel', { resId }, { timeoutMs: 15_000 });
|
|
224
|
+
}
|
|
225
|
+
/** "仅限网络异常时调用" - only after /reservation/book went unanswered. */
|
|
226
|
+
consistency(distributorResId) {
|
|
227
|
+
return this.call('POST', '/reservation/consistency/query', {
|
|
228
|
+
distributorResId,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/huazhu/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type BookWindow, type DailyOffer, type HotelRoomType, type PaymentType, type Product, type RateInfo } from './wire.js';
|
|
2
|
+
/** Arrays in one ARI answer, one element per date from `start`. */
|
|
3
|
+
export type AriWindow = {
|
|
4
|
+
start: string;
|
|
5
|
+
offer: DailyOffer;
|
|
6
|
+
/** Rooms left, per date, from /ari/daily/inventory. */
|
|
7
|
+
inventory?: (number | null)[];
|
|
8
|
+
};
|
|
9
|
+
export type SellContext = {
|
|
10
|
+
checkIn: string;
|
|
11
|
+
checkOut: string;
|
|
12
|
+
/** Per room. The connector's `Stay.guests`. */
|
|
13
|
+
adults: number;
|
|
14
|
+
rooms: number;
|
|
15
|
+
/** The hotel's own date and time of day, now. */
|
|
16
|
+
today: string;
|
|
17
|
+
clock: string;
|
|
18
|
+
paymentType: PaymentType;
|
|
19
|
+
/** Gated tags the contract lets this deployment show. */
|
|
20
|
+
allowTags: ReadonlySet<string>;
|
|
21
|
+
};
|
|
22
|
+
export type Sellable = {
|
|
23
|
+
ok: true;
|
|
24
|
+
product: Product;
|
|
25
|
+
currency: string;
|
|
26
|
+
/** One per night, in the currency's major units, as Huazhu sent them. */
|
|
27
|
+
after: number[];
|
|
28
|
+
/** One per night, or absent when Huazhu did not send every night's. */
|
|
29
|
+
before?: number[];
|
|
30
|
+
rack?: number[];
|
|
31
|
+
guaranteeCode: string;
|
|
32
|
+
cancelCode: string;
|
|
33
|
+
/** The meal on the rate, in words, or absent for room only. */
|
|
34
|
+
meal?: string;
|
|
35
|
+
/** How many of that meal a night, on the arrival night. */
|
|
36
|
+
mealCount: number;
|
|
37
|
+
roomsLeft: number;
|
|
38
|
+
/** Promotions sell at most 3 rooms a booking, list rates 10. */
|
|
39
|
+
maxRooms: number;
|
|
40
|
+
sleeps: number;
|
|
41
|
+
};
|
|
42
|
+
export type Unsellable = {
|
|
43
|
+
ok: false;
|
|
44
|
+
/**
|
|
45
|
+
* Which kind of no, so a hotel with nothing to sell can say the most useful
|
|
46
|
+
* one: a party too big is fixed by a second room, a closed date by another
|
|
47
|
+
* date, prepaid-only by nothing this connector can do.
|
|
48
|
+
*/
|
|
49
|
+
kind: 'party' | 'closed' | 'stay-rule' | 'sold-out' | 'too-many-rooms' | 'prepaid' | 'no-price';
|
|
50
|
+
why: string;
|
|
51
|
+
};
|
|
52
|
+
/** Offered to nobody here: hour rooms, and rates the contract shows to some guests only. */
|
|
53
|
+
export declare const offeredAtAll: (p: Product, allowTags: ReadonlySet<string>) => boolean;
|
|
54
|
+
/** The price entry for this many adults - the smallest one that covers them. */
|
|
55
|
+
export declare const rateFor: (rates: RateInfo[], adults: number) => RateInfo | undefined;
|
|
56
|
+
/** The most people one room of this product sleeps, from everything Huazhu says about it. */
|
|
57
|
+
export declare const sleepsOf: (p: Product, offer: DailyOffer | undefined, rt?: HotelRoomType) => number;
|
|
58
|
+
/**
|
|
59
|
+
* "预订窗口 … 非此时间范围内不展示该报价": outside its booking window a rate is
|
|
60
|
+
* not shown at all. A night-snack rate sells from 20:00; at 14:00 it does not
|
|
61
|
+
* exist.
|
|
62
|
+
*/
|
|
63
|
+
export declare const bookableNow: (w: BookWindow | undefined, checkIn: string, clock: string) => boolean;
|
|
64
|
+
/**
|
|
65
|
+
* One rate, one stay: sellable with its figures, or the reason it is not.
|
|
66
|
+
*
|
|
67
|
+
* `window` must cover every night AND the departure date - "close to
|
|
68
|
+
* departure" is a restriction on the day the guest leaves.
|
|
69
|
+
*/
|
|
70
|
+
export declare const sell: (product: Product, w: AriWindow, roomType: HotelRoomType | undefined, c: SellContext) => Sellable | Unsellable;
|
|
71
|
+
/**
|
|
72
|
+
* Cut a span of dates into the pieces the ARI endpoints accept: at most thirty
|
|
73
|
+
* dates each, both ends included ("endDate-startDate=29").
|
|
74
|
+
*/
|
|
75
|
+
export declare const ariRanges: (start: string, end: string) => {
|
|
76
|
+
start: string;
|
|
77
|
+
end: string;
|
|
78
|
+
}[];
|
|
79
|
+
/** The hotel's time of day, HH:MM. */
|
|
80
|
+
export declare const clockAt: (now: number, tz: string) => string;
|
|
81
|
+
//# sourceMappingURL=sellable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sellable.d.ts","sourceRoot":"","sources":["../../src/huazhu/sellable.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,UAAU,EAEf,KAAK,aAAa,EAGlB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,QAAQ,EACd,MAAM,WAAW,CAAC;AAanB,mEAAmE;AACnE,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,CAAC;IAClB,uDAAuD;IACvD,SAAS,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,WAAW,CAAC;IACzB,yDAAyD;IACzD,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,EAAE,EAAE,KAAK,CAAC;IACV;;;;OAIG;IACH,IAAI,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,UAAU,GAAG,gBAAgB,GAAG,SAAS,GAAG,UAAU,CAAC;IAChG,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAEF,4FAA4F;AAC5F,eAAO,MAAM,YAAY,GAAI,GAAG,OAAO,EAAE,WAAW,WAAW,CAAC,MAAM,CAAC,KAAG,OACsB,CAAC;AAMjG,gFAAgF;AAChF,eAAO,MAAM,OAAO,GAAI,OAAO,QAAQ,EAAE,EAAE,QAAQ,MAAM,KAAG,QAAQ,GAAG,SAGd,CAAC;AAE1D,6FAA6F;AAC7F,eAAO,MAAM,QAAQ,GAAI,GAAG,OAAO,EAAE,OAAO,UAAU,GAAG,SAAS,EAAE,KAAK,aAAa,KAAG,MAUxF,CAAC;AAOF;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,GAAG,UAAU,GAAG,SAAS,EAAE,SAAS,MAAM,EAAE,OAAO,MAAM,KAAG,OAUvF,CAAC;AAUF;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GACf,SAAS,OAAO,EAChB,GAAG,SAAS,EACZ,UAAU,aAAa,GAAG,SAAS,EACnC,GAAG,WAAW,KACb,QAAQ,GAAG,UA8Ib,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,KAAK,MAAM,KAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EAOpF,CAAC;AAEF,sCAAsC;AACtC,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,EAAE,IAAI,MAAM,KAAG,MAWjD,CAAC"}
|