@voyant-travel/accommodations 0.105.17
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/LICENSE +201 -0
- package/README.md +11 -0
- package/dist/booking-engine/handler.d.ts +103 -0
- package/dist/booking-engine/handler.d.ts.map +1 -0
- package/dist/booking-engine/handler.js +254 -0
- package/dist/booking-engine/index.d.ts +8 -0
- package/dist/booking-engine/index.d.ts.map +1 -0
- package/dist/booking-engine/index.js +7 -0
- package/dist/catalog-policy.d.ts +23 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +441 -0
- package/dist/content-shape.d.ts +5 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +13 -0
- package/dist/draft-shape.d.ts +35 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +84 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/routes-content.d.ts +31 -0
- package/dist/routes-content.d.ts.map +1 -0
- package/dist/routes-content.js +87 -0
- package/dist/schema-bookings.d.ts +582 -0
- package/dist/schema-bookings.d.ts.map +1 -0
- package/dist/schema-bookings.js +62 -0
- package/dist/schema-inventory.d.ts +1361 -0
- package/dist/schema-inventory.d.ts.map +1 -0
- package/dist/schema-inventory.js +125 -0
- package/dist/schema-shared.d.ts +5 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +24 -0
- package/dist/schema-sourced-content.d.ts +254 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +48 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +4 -0
- package/dist/service-catalog-plane.d.ts +55 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +202 -0
- package/dist/service-content-owned.d.ts +13 -0
- package/dist/service-content-owned.d.ts.map +1 -0
- package/dist/service-content-owned.js +205 -0
- package/dist/service-content-synthesizer.d.ts +43 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +149 -0
- package/dist/service-content.d.ts +47 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +304 -0
- package/package.json +101 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-plane integration for the accommodation service.
|
|
3
|
+
*
|
|
4
|
+
* The accommodation vertical's catalog entry is the **room type** — the
|
|
5
|
+
* sellable variant within a property. Properties live in
|
|
6
|
+
* `packages/facilities` and are referenced via `propertyId`.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the pattern in `packages/products/src/service-catalog-plane.ts`.
|
|
9
|
+
*
|
|
10
|
+
* See `docs/architecture/catalog-architecture.md` §9.1.
|
|
11
|
+
*/
|
|
12
|
+
import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, resolveEntityView, } from "@voyant-travel/catalog";
|
|
13
|
+
import { and, eq } from "drizzle-orm";
|
|
14
|
+
import { accommodationCatalogPolicy } from "./catalog-policy.js";
|
|
15
|
+
import { roomTypes } from "./schema-inventory.js";
|
|
16
|
+
import { ACCOMMODATION_CONTENT_MARKET_ANY, accommodationSourcedContentTable, } from "./schema-sourced-content.js";
|
|
17
|
+
let _registry;
|
|
18
|
+
function getAccommodationRegistry() {
|
|
19
|
+
if (!_registry) {
|
|
20
|
+
_registry = createFieldPolicyRegistry(accommodationCatalogPolicy);
|
|
21
|
+
}
|
|
22
|
+
return _registry;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Maps a room-type row to a field-keyed projection. Provenance covers sourced
|
|
26
|
+
* and direct-supplier lodging inventory; the caller declares the source kind.
|
|
27
|
+
*/
|
|
28
|
+
export function roomTypeRowToProjection(row, context) {
|
|
29
|
+
return new Map([
|
|
30
|
+
// Provenance
|
|
31
|
+
["source.kind", context.sourceKind ?? "direct"],
|
|
32
|
+
["source.ref", context.sourceRef],
|
|
33
|
+
["seller.operator_id", context.sellerOperatorId],
|
|
34
|
+
// Identity
|
|
35
|
+
["id", row.id],
|
|
36
|
+
["code", row.code],
|
|
37
|
+
["createdAt", row.createdAt],
|
|
38
|
+
["updatedAt", row.updatedAt],
|
|
39
|
+
// Cross-module reference
|
|
40
|
+
["propertyId", row.propertyId],
|
|
41
|
+
["supplierId", row.supplierId],
|
|
42
|
+
// Merchandisable
|
|
43
|
+
["name", row.name],
|
|
44
|
+
["description", row.description],
|
|
45
|
+
["accessibilityNotes", row.accessibilityNotes],
|
|
46
|
+
["thumbnailUrl", pickThumbnailUrl(row.metadata)],
|
|
47
|
+
// Structural / facets
|
|
48
|
+
["inventoryMode", row.inventoryMode],
|
|
49
|
+
["roomClass", row.roomClass],
|
|
50
|
+
["active", row.active],
|
|
51
|
+
["smokingAllowed", row.smokingAllowed],
|
|
52
|
+
["sortOrder", row.sortOrder],
|
|
53
|
+
// Occupancy
|
|
54
|
+
["maxAdults", row.maxAdults],
|
|
55
|
+
["maxChildren", row.maxChildren],
|
|
56
|
+
["maxInfants", row.maxInfants],
|
|
57
|
+
["standardOccupancy", row.standardOccupancy],
|
|
58
|
+
["maxOccupancy", row.maxOccupancy],
|
|
59
|
+
["minOccupancy", row.minOccupancy],
|
|
60
|
+
// Physical
|
|
61
|
+
["bedroomCount", row.bedroomCount],
|
|
62
|
+
["bathroomCount", row.bathroomCount],
|
|
63
|
+
["areaValue", row.areaValue],
|
|
64
|
+
["areaUnit", row.areaUnit],
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
export function roomTypeProvenance(_row, context) {
|
|
68
|
+
return {
|
|
69
|
+
source_kind: context.sourceKind ?? "direct",
|
|
70
|
+
source_freshness: context.sourceKind && context.sourceKind !== "direct" ? "sync" : "static",
|
|
71
|
+
source_ref: context.sourceRef,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export async function getResolvedRoomTypeById(db, id, context) {
|
|
75
|
+
const rows = await db.select().from(roomTypes).where(eq(roomTypes.id, id)).limit(1);
|
|
76
|
+
const row = rows[0];
|
|
77
|
+
if (!row)
|
|
78
|
+
return null;
|
|
79
|
+
const projection = roomTypeRowToProjection(row, {
|
|
80
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
81
|
+
sourceKind: context.sourceKind,
|
|
82
|
+
sourceRef: context.sourceRef,
|
|
83
|
+
});
|
|
84
|
+
return resolveEntityView(db, getAccommodationRegistry(), "accommodations", id, projection, context.scope);
|
|
85
|
+
}
|
|
86
|
+
export async function listResolvedRoomTypes(db, rows, context) {
|
|
87
|
+
const registry = getAccommodationRegistry();
|
|
88
|
+
const views = [];
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
const projection = roomTypeRowToProjection(row, {
|
|
91
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
92
|
+
sourceKind: context.sourceKind,
|
|
93
|
+
sourceRef: context.sourceRef,
|
|
94
|
+
});
|
|
95
|
+
const view = await resolveEntityView(db, registry, "accommodations", row.id, projection, context.scope);
|
|
96
|
+
views.push(view);
|
|
97
|
+
}
|
|
98
|
+
return views;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build a `CaptureSnapshotInput` for a accommodation room type. Used by
|
|
102
|
+
* booking commit flows to capture the room-type view at booking time.
|
|
103
|
+
*/
|
|
104
|
+
export async function buildRoomTypeSnapshotInput(db, roomTypeId, context) {
|
|
105
|
+
const view = await getResolvedRoomTypeById(db, roomTypeId, context);
|
|
106
|
+
if (!view)
|
|
107
|
+
return null;
|
|
108
|
+
return buildSnapshotInputFromView(view, {
|
|
109
|
+
entityModule: "accommodations",
|
|
110
|
+
entityId: roomTypeId,
|
|
111
|
+
sourceKind: context.sourceKind ?? "direct",
|
|
112
|
+
sourceRef: context.sourceRef,
|
|
113
|
+
pricingBasis: context.pricingBasis,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
// Indexer document emission
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
export function createRoomTypeDocumentEmitter(context) {
|
|
120
|
+
const registry = getAccommodationRegistry();
|
|
121
|
+
return {
|
|
122
|
+
vertical: "accommodations",
|
|
123
|
+
emit(source, slice) {
|
|
124
|
+
const projection = roomTypeRowToProjection(source, {
|
|
125
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
126
|
+
sourceKind: context.sourceKind,
|
|
127
|
+
sourceRef: context.sourceRef,
|
|
128
|
+
});
|
|
129
|
+
return buildIndexerDocument(registry, projection, slice, source.id);
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function createRoomTypeDocumentBuilder(db, context) {
|
|
134
|
+
const registry = getAccommodationRegistry();
|
|
135
|
+
return async (entityId, slice) => {
|
|
136
|
+
const rows = await db.select().from(roomTypes).where(eq(roomTypes.id, entityId)).limit(1);
|
|
137
|
+
const row = rows[0];
|
|
138
|
+
if (!row)
|
|
139
|
+
return null;
|
|
140
|
+
const projection = new Map(roomTypeRowToProjection(row, {
|
|
141
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
142
|
+
sourceKind: context.sourceKind,
|
|
143
|
+
sourceRef: context.sourceRef,
|
|
144
|
+
}));
|
|
145
|
+
const sourcedThumbnailUrl = await fetchSourcedContentThumbnailUrl(db, entityId, slice);
|
|
146
|
+
if (sourcedThumbnailUrl) {
|
|
147
|
+
projection.set("thumbnailUrl", sourcedThumbnailUrl);
|
|
148
|
+
}
|
|
149
|
+
return buildIndexerDocument(registry, projection, slice, entityId);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function fetchSourcedContentThumbnailUrl(db, entityId, slice) {
|
|
153
|
+
const rows = await db
|
|
154
|
+
.select({
|
|
155
|
+
market: accommodationSourcedContentTable.market,
|
|
156
|
+
payload: accommodationSourcedContentTable.payload,
|
|
157
|
+
})
|
|
158
|
+
.from(accommodationSourcedContentTable)
|
|
159
|
+
.where(and(eq(accommodationSourcedContentTable.entity_id, entityId), eq(accommodationSourcedContentTable.locale, slice.locale)));
|
|
160
|
+
const row = rows.find((candidate) => candidate.market === slice.market) ??
|
|
161
|
+
rows.find((candidate) => candidate.market === ACCOMMODATION_CONTENT_MARKET_ANY) ??
|
|
162
|
+
rows[0];
|
|
163
|
+
return pickThumbnailUrl(row?.payload);
|
|
164
|
+
}
|
|
165
|
+
function pickThumbnailUrl(value) {
|
|
166
|
+
const record = asRecord(value);
|
|
167
|
+
if (!record)
|
|
168
|
+
return null;
|
|
169
|
+
return (firstString(record.thumbnailUrl, record.heroImageUrl, record.hero_image_url, record.imageUrl, record.image_url) ??
|
|
170
|
+
firstMediaUrl(record.media) ??
|
|
171
|
+
firstStringFromArray(record.images) ??
|
|
172
|
+
firstStringFromArray(record.galleryUrls));
|
|
173
|
+
}
|
|
174
|
+
function firstMediaUrl(value) {
|
|
175
|
+
if (!Array.isArray(value))
|
|
176
|
+
return null;
|
|
177
|
+
for (const item of value) {
|
|
178
|
+
const media = asRecord(item);
|
|
179
|
+
if (!media)
|
|
180
|
+
continue;
|
|
181
|
+
const type = typeof media.type === "string" ? media.type : null;
|
|
182
|
+
if (type && type !== "image")
|
|
183
|
+
continue;
|
|
184
|
+
const url = firstString(media.url, media.src);
|
|
185
|
+
if (url)
|
|
186
|
+
return url;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function firstStringFromArray(value) {
|
|
191
|
+
if (!Array.isArray(value))
|
|
192
|
+
return null;
|
|
193
|
+
return value.find((item) => typeof item === "string" && item.length > 0) ?? null;
|
|
194
|
+
}
|
|
195
|
+
function firstString(...values) {
|
|
196
|
+
return (values.find((value) => typeof value === "string" && value.length > 0) ?? null);
|
|
197
|
+
}
|
|
198
|
+
function asRecord(value) {
|
|
199
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
200
|
+
? value
|
|
201
|
+
: null;
|
|
202
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ContentLocaleMatchKind } from "@voyant-travel/catalog";
|
|
2
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
3
|
+
import { type AccommodationContent } from "./content-shape.js";
|
|
4
|
+
export interface BuildOwnedAccommodationContentOptions {
|
|
5
|
+
preferredLocales: ReadonlyArray<string>;
|
|
6
|
+
}
|
|
7
|
+
export interface BuildOwnedAccommodationContentResult {
|
|
8
|
+
content: AccommodationContent;
|
|
9
|
+
servedLocale: string;
|
|
10
|
+
matchKind: ContentLocaleMatchKind;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildOwnedAccommodationContent(db: AnyDrizzleDb, entityId: string, options: BuildOwnedAccommodationContentOptions): Promise<BuildOwnedAccommodationContentResult | null>;
|
|
13
|
+
//# sourceMappingURL=service-content-owned.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-content-owned.d.ts","sourceRoot":"","sources":["../src/service-content-owned.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAQrD,OAAO,EACL,KAAK,oBAAoB,EAI1B,MAAM,oBAAoB,CAAA;AAS3B,MAAM,WAAW,qCAAqC;IACpD,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACxC;AAED,MAAM,WAAW,oCAAoC;IACnD,OAAO,EAAE,oBAAoB,CAAA;IAC7B,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,sBAAsB,CAAA;CAClC;AAED,wBAAsB,8BAA8B,CAClD,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,qCAAqC,GAC7C,OAAO,CAAC,oCAAoC,GAAG,IAAI,CAAC,CAsItD"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { facilities, facilityAddressProjections, facilityFeatures, properties, } from "@voyant-travel/operations";
|
|
2
|
+
import { and, asc, eq, inArray } from "drizzle-orm";
|
|
3
|
+
import { accommodationContentSchema, validateAccommodationContent, } from "./content-shape.js";
|
|
4
|
+
import { mealPlans, ratePlanRoomTypes, ratePlans, roomTypeBedConfigs, roomTypes, } from "./schema-inventory.js";
|
|
5
|
+
export async function buildOwnedAccommodationContent(db, entityId, options) {
|
|
6
|
+
// biome-ignore lint/suspicious/noExplicitAny: AnyDrizzleDb widens drizzle's row inference. -- owner: accommodations; existing suppression is intentional pending typed cleanup.
|
|
7
|
+
const roomRow = (await db.select().from(roomTypes).where(eq(roomTypes.id, entityId)).limit(1))[0];
|
|
8
|
+
if (!roomRow)
|
|
9
|
+
return null;
|
|
10
|
+
// biome-ignore lint/suspicious/noExplicitAny: AnyDrizzleDb widens drizzle's row inference. -- owner: accommodations; existing suppression is intentional pending typed cleanup.
|
|
11
|
+
const propertyRow = (await db.select().from(properties).where(eq(properties.id, roomRow.propertyId)).limit(1))[0];
|
|
12
|
+
if (!propertyRow)
|
|
13
|
+
return null;
|
|
14
|
+
const ownedRows = await Promise.all([
|
|
15
|
+
db.select().from(facilities).where(eq(facilities.id, propertyRow.facilityId)).limit(1),
|
|
16
|
+
db
|
|
17
|
+
.select()
|
|
18
|
+
.from(facilityAddressProjections)
|
|
19
|
+
.where(eq(facilityAddressProjections.facilityId, propertyRow.facilityId))
|
|
20
|
+
.limit(1),
|
|
21
|
+
db
|
|
22
|
+
.select()
|
|
23
|
+
.from(facilityFeatures)
|
|
24
|
+
.where(eq(facilityFeatures.facilityId, propertyRow.facilityId))
|
|
25
|
+
.orderBy(asc(facilityFeatures.sortOrder), asc(facilityFeatures.name)),
|
|
26
|
+
db
|
|
27
|
+
.select()
|
|
28
|
+
.from(roomTypes)
|
|
29
|
+
.where(and(eq(roomTypes.propertyId, propertyRow.id), eq(roomTypes.active, true)))
|
|
30
|
+
.orderBy(asc(roomTypes.sortOrder), asc(roomTypes.name)),
|
|
31
|
+
db
|
|
32
|
+
.select()
|
|
33
|
+
.from(mealPlans)
|
|
34
|
+
.where(and(eq(mealPlans.propertyId, propertyRow.id), eq(mealPlans.active, true)))
|
|
35
|
+
.orderBy(asc(mealPlans.sortOrder), asc(mealPlans.name)),
|
|
36
|
+
db
|
|
37
|
+
.select()
|
|
38
|
+
.from(ratePlans)
|
|
39
|
+
.where(and(eq(ratePlans.propertyId, propertyRow.id), eq(ratePlans.active, true)))
|
|
40
|
+
.orderBy(asc(ratePlans.sortOrder), asc(ratePlans.name)),
|
|
41
|
+
]);
|
|
42
|
+
const facilityRows = ownedRows[0];
|
|
43
|
+
const addressRows = ownedRows[1];
|
|
44
|
+
const featureRows = ownedRows[2];
|
|
45
|
+
const roomRows = ownedRows[3];
|
|
46
|
+
const mealPlanRows = ownedRows[4];
|
|
47
|
+
const ratePlanRows = ownedRows[5];
|
|
48
|
+
const roomIds = roomRows.map((row) => row.id);
|
|
49
|
+
const ratePlanIds = ratePlanRows.map((row) => row.id);
|
|
50
|
+
const [bedRows, ratePlanRoomRows] = await Promise.all([
|
|
51
|
+
roomIds.length > 0
|
|
52
|
+
? db
|
|
53
|
+
.select()
|
|
54
|
+
.from(roomTypeBedConfigs)
|
|
55
|
+
.where(inArray(roomTypeBedConfigs.roomTypeId, roomIds))
|
|
56
|
+
.orderBy(asc(roomTypeBedConfigs.isPrimary), asc(roomTypeBedConfigs.createdAt))
|
|
57
|
+
: [],
|
|
58
|
+
ratePlanIds.length > 0
|
|
59
|
+
? db
|
|
60
|
+
.select()
|
|
61
|
+
.from(ratePlanRoomTypes)
|
|
62
|
+
.where(and(inArray(ratePlanRoomTypes.ratePlanId, ratePlanIds), eq(ratePlanRoomTypes.active, true)))
|
|
63
|
+
.orderBy(asc(ratePlanRoomTypes.sortOrder), asc(ratePlanRoomTypes.createdAt))
|
|
64
|
+
: [],
|
|
65
|
+
]);
|
|
66
|
+
const facilityRow = facilityRows[0] ?? null;
|
|
67
|
+
const addressRow = addressRows[0] ?? null;
|
|
68
|
+
const content = accommodationContentSchema.parse({
|
|
69
|
+
hotel: {
|
|
70
|
+
id: propertyRow.id,
|
|
71
|
+
name: facilityRow?.name ?? roomRow.name,
|
|
72
|
+
description: facilityRow?.description ?? roomRow.description ?? null,
|
|
73
|
+
star_rating: normalizeStarRating(propertyRow.rating, propertyRow.ratingScale),
|
|
74
|
+
hero_image_url: firstStringFromMetadata(roomRow.metadata, "images"),
|
|
75
|
+
highlights: featureRows
|
|
76
|
+
.filter((feature) => feature.highlighted)
|
|
77
|
+
.map((feature) => feature.name),
|
|
78
|
+
brand: propertyRow.brandName ?? propertyRow.groupName ?? null,
|
|
79
|
+
country: addressRow?.country ?? null,
|
|
80
|
+
city: addressRow?.city ?? null,
|
|
81
|
+
address: addressRow?.fullText ?? addressRow?.address ?? addressRow?.line1 ?? null,
|
|
82
|
+
postal_code: addressRow?.postalCode ?? null,
|
|
83
|
+
latitude: addressRow?.latitude ?? null,
|
|
84
|
+
longitude: addressRow?.longitude ?? null,
|
|
85
|
+
check_in_time: propertyRow.checkInTime ?? null,
|
|
86
|
+
check_out_time: propertyRow.checkOutTime ?? null,
|
|
87
|
+
},
|
|
88
|
+
room_types: roomRows.map((room) => ownedRoomTypeToContent(room, bedRows)),
|
|
89
|
+
rate_plans: ratePlanRows.map((plan) => ownedRatePlanToContent(plan, ratePlanRoomRows)),
|
|
90
|
+
meal_plans: mealPlanRows.map((plan) => ({
|
|
91
|
+
id: plan.id,
|
|
92
|
+
code: plan.code,
|
|
93
|
+
name: plan.name,
|
|
94
|
+
description: plan.description ?? null,
|
|
95
|
+
basis: mealPlanBasis(plan),
|
|
96
|
+
inclusions: mealPlanInclusions(plan),
|
|
97
|
+
})),
|
|
98
|
+
amenities: featureRows
|
|
99
|
+
.filter((feature) => feature.category === "amenity")
|
|
100
|
+
.map((feature) => ({
|
|
101
|
+
id: feature.code ?? feature.id,
|
|
102
|
+
category: feature.category,
|
|
103
|
+
name: feature.name,
|
|
104
|
+
description: feature.description ?? feature.valueText ?? null,
|
|
105
|
+
is_free: undefined,
|
|
106
|
+
})),
|
|
107
|
+
policies: propertyRow.policyNotes
|
|
108
|
+
? [{ kind: "supplier_notes", body: propertyRow.policyNotes }]
|
|
109
|
+
: [],
|
|
110
|
+
});
|
|
111
|
+
const validation = validateAccommodationContent(content);
|
|
112
|
+
if (!validation.valid) {
|
|
113
|
+
throw new Error(`owned accommodation ${entityId} projection failed validation: ${validation.reason}`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
content,
|
|
117
|
+
servedLocale: options.preferredLocales[0] ?? "en-GB",
|
|
118
|
+
matchKind: options.preferredLocales.length > 0 ? "exact" : "any",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function ownedRoomTypeToContent(room, bedRows) {
|
|
122
|
+
const roomBeds = bedRows.filter((bed) => bed.roomTypeId === room.id);
|
|
123
|
+
return {
|
|
124
|
+
id: room.id,
|
|
125
|
+
code: room.code ?? null,
|
|
126
|
+
name: room.name,
|
|
127
|
+
description: room.description ?? null,
|
|
128
|
+
room_class: room.roomClass ?? null,
|
|
129
|
+
view: stringFromMetadata(room.metadata, "view"),
|
|
130
|
+
bedrooms: room.bedroomCount ?? null,
|
|
131
|
+
beds: roomBeds.map((bed) => bed.quantity > 1 ? `${bed.quantity} ${bed.bedType}` : bed.bedType),
|
|
132
|
+
size_sqm: room.areaUnit === "sqm" ? room.areaValue : null,
|
|
133
|
+
max_adults: room.maxAdults ?? null,
|
|
134
|
+
max_children: room.maxChildren ?? null,
|
|
135
|
+
max_occupancy: room.maxOccupancy ?? room.standardOccupancy ?? null,
|
|
136
|
+
amenities: stringArrayFromMetadata(room.metadata, "amenities"),
|
|
137
|
+
images: stringArrayFromMetadata(room.metadata, "images"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function ownedRatePlanToContent(plan, ratePlanRoomRows) {
|
|
141
|
+
return {
|
|
142
|
+
id: plan.id,
|
|
143
|
+
code: plan.code,
|
|
144
|
+
name: plan.name,
|
|
145
|
+
description: plan.description ?? null,
|
|
146
|
+
charge_frequency: contentChargeFrequency(plan.chargeFrequency),
|
|
147
|
+
applies_to_room_type_ids: ratePlanRoomRows
|
|
148
|
+
.filter((row) => row.ratePlanId === plan.id)
|
|
149
|
+
.map((row) => row.roomTypeId),
|
|
150
|
+
cancellation_policy: plan.cancellationPolicyId ?? null,
|
|
151
|
+
inclusions: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function contentChargeFrequency(value) {
|
|
155
|
+
return value === "per_stay" || value === "per_person_per_stay" ? "per_stay" : "per_night";
|
|
156
|
+
}
|
|
157
|
+
function mealPlanBasis(plan) {
|
|
158
|
+
if (plan.includesBreakfast && plan.includesLunch && plan.includesDinner && plan.includesDrinks) {
|
|
159
|
+
return "all_inclusive";
|
|
160
|
+
}
|
|
161
|
+
if (plan.includesBreakfast && plan.includesLunch && plan.includesDinner)
|
|
162
|
+
return "full_board";
|
|
163
|
+
if (plan.includesBreakfast && plan.includesDinner)
|
|
164
|
+
return "half_board";
|
|
165
|
+
if (plan.includesBreakfast)
|
|
166
|
+
return "bed_breakfast";
|
|
167
|
+
return "room_only";
|
|
168
|
+
}
|
|
169
|
+
function mealPlanInclusions(plan) {
|
|
170
|
+
const inclusions = [];
|
|
171
|
+
if (plan.includesBreakfast)
|
|
172
|
+
inclusions.push("Breakfast");
|
|
173
|
+
if (plan.includesLunch)
|
|
174
|
+
inclusions.push("Lunch");
|
|
175
|
+
if (plan.includesDinner)
|
|
176
|
+
inclusions.push("Dinner");
|
|
177
|
+
if (plan.includesDrinks)
|
|
178
|
+
inclusions.push("Drinks");
|
|
179
|
+
return inclusions;
|
|
180
|
+
}
|
|
181
|
+
function normalizeStarRating(value, scale) {
|
|
182
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
183
|
+
return null;
|
|
184
|
+
if (typeof scale === "number" && Number.isFinite(scale) && scale > 0 && scale !== 5) {
|
|
185
|
+
return Math.max(0, Math.min(5, (value / scale) * 5));
|
|
186
|
+
}
|
|
187
|
+
return Math.max(0, Math.min(5, value));
|
|
188
|
+
}
|
|
189
|
+
function firstStringFromMetadata(metadata, key) {
|
|
190
|
+
return stringArrayFromMetadata(metadata, key)[0] ?? null;
|
|
191
|
+
}
|
|
192
|
+
function stringArrayFromMetadata(metadata, key) {
|
|
193
|
+
if (!metadata || typeof metadata !== "object")
|
|
194
|
+
return [];
|
|
195
|
+
const value = metadata[key];
|
|
196
|
+
if (!Array.isArray(value))
|
|
197
|
+
return [];
|
|
198
|
+
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
199
|
+
}
|
|
200
|
+
function stringFromMetadata(metadata, key) {
|
|
201
|
+
if (!metadata || typeof metadata !== "object")
|
|
202
|
+
return null;
|
|
203
|
+
const value = metadata[key];
|
|
204
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
205
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accommodation content synthesizer — fallback for thin adapters that
|
|
3
|
+
* declare `supportsContentFetch: false`.
|
|
4
|
+
*
|
|
5
|
+
* Produces the most complete `AccommodationContent` blob we can
|
|
6
|
+
* legitimately synthesize from the durable sourced-entry projection +
|
|
7
|
+
* locale-aware overlays + plane-level provenance. Fields the
|
|
8
|
+
* projection doesn't carry render as typed empty states (`room_types:
|
|
9
|
+
* []`, `rate_plans: []`).
|
|
10
|
+
*
|
|
11
|
+
* Per §3.6: never invents plausible-but-unverified fields ("hotels
|
|
12
|
+
* usually have a pool" is not a basis for an amenity), never
|
|
13
|
+
* machine-translates, never mines snapshots, never caches its own
|
|
14
|
+
* output.
|
|
15
|
+
*/
|
|
16
|
+
import { type ProvenanceReadResult } from "@voyant-travel/catalog";
|
|
17
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
18
|
+
import { type AccommodationContent } from "./content-shape.js";
|
|
19
|
+
export interface SynthesizeAccommodationContentOptions {
|
|
20
|
+
provenance: Extract<ProvenanceReadResult, {
|
|
21
|
+
kind: "sourced";
|
|
22
|
+
}>;
|
|
23
|
+
overlays?: ReadonlyArray<{
|
|
24
|
+
field_path: string;
|
|
25
|
+
value: unknown;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
export interface SynthesizedAccommodationContent {
|
|
29
|
+
content: AccommodationContent;
|
|
30
|
+
content_schema_version: string;
|
|
31
|
+
served_locale: string;
|
|
32
|
+
source_kind: string;
|
|
33
|
+
source_provider?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function synthesizeAccommodationContent(scope: {
|
|
36
|
+
locale: string;
|
|
37
|
+
}, options: SynthesizeAccommodationContentOptions): SynthesizedAccommodationContent;
|
|
38
|
+
export declare function synthesizeAccommodationContentFromDb(db: AnyDrizzleDb, scope: {
|
|
39
|
+
locale: string;
|
|
40
|
+
}, provenance: Extract<ProvenanceReadResult, {
|
|
41
|
+
kind: "sourced";
|
|
42
|
+
}>): Promise<SynthesizedAccommodationContent>;
|
|
43
|
+
//# sourceMappingURL=service-content-synthesizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-content-synthesizer.d.ts","sourceRoot":"","sources":["../src/service-content-synthesizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,EAEL,KAAK,oBAAoB,EAE1B,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,qCAAqC;IACpD,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IAC9D,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CACjE;AAED,MAAM,WAAW,+BAA+B;IAC9C,OAAO,EAAE,oBAAoB,CAAA;IAC7B,sBAAsB,EAAE,MAAM,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,OAAO,EAAE,qCAAqC,GAC7C,+BAA+B,CAmCjC;AAED,wBAAsB,oCAAoC,CACxD,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAC7D,OAAO,CAAC,+BAA+B,CAAC,CAO1C"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accommodation content synthesizer — fallback for thin adapters that
|
|
3
|
+
* declare `supportsContentFetch: false`.
|
|
4
|
+
*
|
|
5
|
+
* Produces the most complete `AccommodationContent` blob we can
|
|
6
|
+
* legitimately synthesize from the durable sourced-entry projection +
|
|
7
|
+
* locale-aware overlays + plane-level provenance. Fields the
|
|
8
|
+
* projection doesn't carry render as typed empty states (`room_types:
|
|
9
|
+
* []`, `rate_plans: []`).
|
|
10
|
+
*
|
|
11
|
+
* Per §3.6: never invents plausible-but-unverified fields ("hotels
|
|
12
|
+
* usually have a pool" is not a basis for an amenity), never
|
|
13
|
+
* machine-translates, never mines snapshots, never caches its own
|
|
14
|
+
* output.
|
|
15
|
+
*/
|
|
16
|
+
import { fetchOverlaysForEntity, mergeOverlaysIntoContent, } from "@voyant-travel/catalog";
|
|
17
|
+
import { ACCOMMODATION_CONTENT_SCHEMA_VERSION, accommodationContentSchema, } from "./content-shape.js";
|
|
18
|
+
export function synthesizeAccommodationContent(scope, options) {
|
|
19
|
+
const projection = options.provenance.projection;
|
|
20
|
+
const hotel = pickHotelSummary(projection, options.provenance);
|
|
21
|
+
const amenities = pickAmenities(projection);
|
|
22
|
+
const policies = pickPolicies(projection);
|
|
23
|
+
const baseContent = {
|
|
24
|
+
hotel,
|
|
25
|
+
room_types: [],
|
|
26
|
+
rate_plans: [],
|
|
27
|
+
meal_plans: [],
|
|
28
|
+
amenities,
|
|
29
|
+
policies,
|
|
30
|
+
};
|
|
31
|
+
let merged = baseContent;
|
|
32
|
+
if (options.overlays && options.overlays.length > 0) {
|
|
33
|
+
const result = mergeOverlaysIntoContent(baseContent, options.overlays, {
|
|
34
|
+
validate(p) {
|
|
35
|
+
const r = accommodationContentSchema.safeParse(p);
|
|
36
|
+
return r.success
|
|
37
|
+
? { valid: true }
|
|
38
|
+
: { valid: false, reason: r.error.issues[0]?.message ?? "invalid" };
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
merged = accommodationContentSchema.parse(result);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
content: merged,
|
|
45
|
+
content_schema_version: ACCOMMODATION_CONTENT_SCHEMA_VERSION,
|
|
46
|
+
served_locale: scope.locale,
|
|
47
|
+
source_kind: options.provenance.provenance.source_kind,
|
|
48
|
+
source_provider: options.provenance.provenance.source_provider,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function synthesizeAccommodationContentFromDb(db, scope, provenance) {
|
|
52
|
+
const entityId = entityIdFromProvenance(provenance);
|
|
53
|
+
const overlays = await fetchOverlaysForEntity(db, "accommodations", entityId);
|
|
54
|
+
return synthesizeAccommodationContent(scope, {
|
|
55
|
+
provenance,
|
|
56
|
+
overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function entityIdFromProvenance(provenance) {
|
|
60
|
+
const fromProjection = provenance.projection.id;
|
|
61
|
+
if (typeof fromProjection === "string" && fromProjection.length > 0) {
|
|
62
|
+
return fromProjection;
|
|
63
|
+
}
|
|
64
|
+
return provenance.entry_id;
|
|
65
|
+
}
|
|
66
|
+
function pickHotelSummary(projection, provenance) {
|
|
67
|
+
return {
|
|
68
|
+
id: stringOr(projection.id, "") || provenance.entry_id,
|
|
69
|
+
name: stringOr(projection.name, "") || stringOr(projection.title, "") || "Unnamed property",
|
|
70
|
+
description: stringOr(projection.description, null),
|
|
71
|
+
star_rating: numberOr(projection.star_rating, null),
|
|
72
|
+
hero_image_url: stringOr(projection.hero_image_url, null),
|
|
73
|
+
highlights: stringArrayOr(projection.highlights, []),
|
|
74
|
+
brand: stringOr(projection.brand, null) ?? provenance.provenance.source_provider ?? null,
|
|
75
|
+
country: stringOr(projection.country, null),
|
|
76
|
+
city: stringOr(projection.city, null),
|
|
77
|
+
address: stringOr(projection.address, null),
|
|
78
|
+
postal_code: stringOr(projection.postal_code, null),
|
|
79
|
+
latitude: numberOr(projection.latitude, null),
|
|
80
|
+
longitude: numberOr(projection.longitude, null),
|
|
81
|
+
check_in_time: stringOr(projection.check_in_time, null),
|
|
82
|
+
check_out_time: stringOr(projection.check_out_time, null),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function pickAmenities(projection) {
|
|
86
|
+
// Bedbanks commonly emit a flat string array for amenities. We map
|
|
87
|
+
// each into the structured shape with no inferred category — that's
|
|
88
|
+
// genuinely unknown without a real getContent.
|
|
89
|
+
const list = projection.amenities;
|
|
90
|
+
if (!Array.isArray(list))
|
|
91
|
+
return [];
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const item of list) {
|
|
94
|
+
if (typeof item === "string" && item.length > 0) {
|
|
95
|
+
out.push({
|
|
96
|
+
id: slugifyAmenityId(item),
|
|
97
|
+
category: null,
|
|
98
|
+
name: item,
|
|
99
|
+
description: null,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (item && typeof item === "object") {
|
|
103
|
+
const obj = item;
|
|
104
|
+
const name = stringOr(obj.name, null) ?? stringOr(obj.label, null);
|
|
105
|
+
if (!name)
|
|
106
|
+
continue;
|
|
107
|
+
out.push({
|
|
108
|
+
id: stringOr(obj.id, null) ?? slugifyAmenityId(name),
|
|
109
|
+
category: stringOr(obj.category, null),
|
|
110
|
+
name,
|
|
111
|
+
description: stringOr(obj.description, null),
|
|
112
|
+
is_free: typeof obj.is_free === "boolean" ? obj.is_free : undefined,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
function pickPolicies(projection) {
|
|
119
|
+
const out = [];
|
|
120
|
+
const cancel = stringOr(projection.cancellation_policy, null);
|
|
121
|
+
if (cancel)
|
|
122
|
+
out.push({ kind: "cancellation", body: cancel });
|
|
123
|
+
const payment = stringOr(projection.payment_terms, null);
|
|
124
|
+
if (payment)
|
|
125
|
+
out.push({ kind: "payment", body: payment });
|
|
126
|
+
const checkin = stringOr(projection.check_in_policy, null);
|
|
127
|
+
if (checkin)
|
|
128
|
+
out.push({ kind: "check_in", body: checkin });
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function slugifyAmenityId(value) {
|
|
132
|
+
return (value
|
|
133
|
+
.toLowerCase()
|
|
134
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
135
|
+
.replace(/^_+|_+$/g, "")
|
|
136
|
+
.slice(0, 64) || "amenity");
|
|
137
|
+
}
|
|
138
|
+
function stringOr(value, fallback) {
|
|
139
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
140
|
+
}
|
|
141
|
+
function numberOr(value, fallback) {
|
|
142
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
143
|
+
}
|
|
144
|
+
function stringArrayOr(value, fallback) {
|
|
145
|
+
if (!Array.isArray(value))
|
|
146
|
+
return fallback;
|
|
147
|
+
const out = value.filter((v) => typeof v === "string");
|
|
148
|
+
return out.length > 0 ? out : fallback;
|
|
149
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accommodation content service — `getAccommodationContent` with locale-
|
|
3
|
+
* resolved cache reads, SWR refresh, and synthesizer fallback.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `service-content.ts` in the products and cruises packages
|
|
6
|
+
* but accommodation-shaped. The accommodation content aggregate (§3.2 /
|
|
7
|
+
* §3.6) is `{ hotel, room_types[], rate_plans[], meal_plans[],
|
|
8
|
+
* amenities[], policies[] }` — one payload returned by a single
|
|
9
|
+
* getContent. Pricing stays out (volatile-live, flows through
|
|
10
|
+
* `liveResolve`); rate plans here are the structural plan definitions,
|
|
11
|
+
* not their per-night rates.
|
|
12
|
+
*/
|
|
13
|
+
import { type ContentLocaleResolution, type InvalidateOnDrift, type SourceAdapter, type SourceAdapterContext } from "@voyant-travel/catalog";
|
|
14
|
+
import type { SourceAdapterRegistry } from "@voyant-travel/catalog/booking-engine";
|
|
15
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
16
|
+
import { type AccommodationContent } from "./content-shape.js";
|
|
17
|
+
export type { BuildOwnedAccommodationContentOptions, BuildOwnedAccommodationContentResult, } from "./service-content-owned.js";
|
|
18
|
+
export { buildOwnedAccommodationContent } from "./service-content-owned.js";
|
|
19
|
+
export interface AccommodationContentScope {
|
|
20
|
+
preferredLocales: ReadonlyArray<string>;
|
|
21
|
+
market?: string;
|
|
22
|
+
currency?: string;
|
|
23
|
+
acceptMachineTranslated?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface GetAccommodationContentOptions {
|
|
26
|
+
registry: SourceAdapterRegistry;
|
|
27
|
+
buildAdapterContext?: (adapter: SourceAdapter) => SourceAdapterContext;
|
|
28
|
+
onOverlayError?: (event: {
|
|
29
|
+
field_path: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
}) => void;
|
|
32
|
+
}
|
|
33
|
+
export interface ResolvedAccommodationContent {
|
|
34
|
+
content: AccommodationContent;
|
|
35
|
+
resolution: ContentLocaleResolution<{
|
|
36
|
+
locale: string;
|
|
37
|
+
payload: AccommodationContent;
|
|
38
|
+
}>;
|
|
39
|
+
source: "sourced-cache" | "sourced-fresh" | "synthesized" | "owned";
|
|
40
|
+
served_stale: boolean;
|
|
41
|
+
synthesized: boolean;
|
|
42
|
+
machine_translated: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function getAccommodationContent(db: AnyDrizzleDb, entityId: string, scope: AccommodationContentScope, options: GetAccommodationContentOptions): Promise<ResolvedAccommodationContent | null>;
|
|
45
|
+
/** Drift event consumer for the accommodation content cache. Per §3.4.1. */
|
|
46
|
+
export declare const invalidateAccommodationContentOnDrift: InvalidateOnDrift;
|
|
47
|
+
//# sourceMappingURL=service-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-content.d.ts","sourceRoot":"","sources":["../src/service-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,KAAK,uBAAuB,EAK5B,KAAK,iBAAiB,EAKtB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAE1B,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAA;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,EAEL,KAAK,oBAAoB,EAI1B,MAAM,oBAAoB,CAAA;AAY3B,YAAY,EACV,qCAAqC,EACrC,oCAAoC,GACrC,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,8BAA8B,EAAE,MAAM,4BAA4B,CAAA;AAS3E,MAAM,WAAW,yBAAyB;IACxC,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAClC;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,oBAAoB,CAAA;IACtE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzE;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,oBAAoB,CAAA;IAC7B,UAAU,EAAE,uBAAuB,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,oBAAoB,CAAA;KAAE,CAAC,CAAA;IACtF,MAAM,EAAE,eAAe,GAAG,eAAe,GAAG,aAAa,GAAG,OAAO,CAAA;IACnE,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,yBAAyB,EAChC,OAAO,EAAE,8BAA8B,GACtC,OAAO,CAAC,4BAA4B,GAAG,IAAI,CAAC,CA0I9C;AA6OD,4EAA4E;AAC5E,eAAO,MAAM,qCAAqC,EAAE,iBAGnD,CAAA"}
|