@voyant-travel/charters 0.117.2
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 +16 -0
- package/dist/adapters/index.d.ts +254 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/memoize.d.ts +28 -0
- package/dist/adapters/memoize.d.ts.map +1 -0
- package/dist/adapters/memoize.js +121 -0
- package/dist/adapters/mock.d.ts +50 -0
- package/dist/adapters/mock.d.ts.map +1 -0
- package/dist/adapters/mock.js +194 -0
- package/dist/adapters/registry.d.ts +24 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +40 -0
- package/dist/booking-extension.d.ts +895 -0
- package/dist/booking-extension.d.ts.map +1 -0
- package/dist/booking-extension.js +339 -0
- package/dist/catalog-policy.d.ts +23 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +400 -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 +29 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +63 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/lib/key.d.ts +22 -0
- package/dist/lib/key.d.ts.map +1 -0
- package/dist/lib/key.js +24 -0
- package/dist/routes-public.d.ts +785 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +234 -0
- package/dist/routes.d.ts +1744 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +543 -0
- package/dist/schema-core.d.ts +815 -0
- package/dist/schema-core.d.ts.map +1 -0
- package/dist/schema-core.js +98 -0
- package/dist/schema-itinerary.d.ts +239 -0
- package/dist/schema-itinerary.d.ts.map +1 -0
- package/dist/schema-itinerary.js +30 -0
- package/dist/schema-pricing.d.ts +385 -0
- package/dist/schema-pricing.d.ts.map +1 -0
- package/dist/schema-pricing.js +62 -0
- package/dist/schema-shared.d.ts +8 -0
- package/dist/schema-shared.d.ts.map +1 -0
- package/dist/schema-shared.js +37 -0
- package/dist/schema-sourced-content.d.ts +253 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +44 -0
- package/dist/schema-yachts.d.ts +367 -0
- package/dist/schema-yachts.d.ts.map +1 -0
- package/dist/schema-yachts.js +30 -0
- package/dist/schema.d.ts +8 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +7 -0
- package/dist/service-bookings-helpers.d.ts +20 -0
- package/dist/service-bookings-helpers.d.ts.map +1 -0
- package/dist/service-bookings-helpers.js +67 -0
- package/dist/service-bookings-local.d.ts +5 -0
- package/dist/service-bookings-local.d.ts.map +1 -0
- package/dist/service-bookings-local.js +177 -0
- package/dist/service-bookings-types.d.ts +88 -0
- package/dist/service-bookings-types.d.ts.map +1 -0
- package/dist/service-bookings-types.js +1 -0
- package/dist/service-bookings.d.ts +36 -0
- package/dist/service-bookings.d.ts.map +1 -0
- package/dist/service-bookings.js +267 -0
- package/dist/service-catalog-plane.d.ts +58 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +145 -0
- package/dist/service-content-synthesizer.d.ts +42 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +122 -0
- package/dist/service-content.d.ts +43 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +248 -0
- package/dist/service-myba.d.ts +85 -0
- package/dist/service-myba.d.ts.map +1 -0
- package/dist/service-myba.js +88 -0
- package/dist/service-pricing.d.ts +64 -0
- package/dist/service-pricing.d.ts.map +1 -0
- package/dist/service-pricing.js +167 -0
- package/dist/service.d.ts +131 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +279 -0
- package/dist/validation-core.d.ts +152 -0
- package/dist/validation-core.d.ts.map +1 -0
- package/dist/validation-core.js +66 -0
- package/dist/validation-itinerary.d.ts +43 -0
- package/dist/validation-itinerary.d.ts.map +1 -0
- package/dist/validation-itinerary.js +19 -0
- package/dist/validation-pricing.d.ts +103 -0
- package/dist/validation-pricing.d.ts.map +1 -0
- package/dist/validation-pricing.js +28 -0
- package/dist/validation-shared.d.ts +61 -0
- package/dist/validation-shared.d.ts.map +1 -0
- package/dist/validation-shared.js +60 -0
- package/dist/validation-yachts.d.ts +76 -0
- package/dist/validation-yachts.d.ts.map +1 -0
- package/dist/validation-yachts.js +36 -0
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +5 -0
- package/package.json +116 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Charter content service — `getCharterContent` with locale-resolved
|
|
3
|
+
* cache reads, SWR refresh, and synthesizer fallback.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `service-content.ts` in the products / cruises / accommodations
|
|
6
|
+
* packages but charter-shaped. The charter content aggregate (§3.2 /
|
|
7
|
+
* §3.6) is `{ charter, yacht, voyages[], suites[], schedule_days[],
|
|
8
|
+
* policies[] }` — one payload returned by a single getContent.
|
|
9
|
+
* Pricing stays out (volatile-live, flows through `liveResolve`).
|
|
10
|
+
*/
|
|
11
|
+
import { type ContentLocaleResolution, type InvalidateOnDrift, type SourceAdapter, type SourceAdapterContext } from "@voyant-travel/catalog";
|
|
12
|
+
import type { SourceAdapterRegistry } from "@voyant-travel/catalog/booking-engine";
|
|
13
|
+
import type { AnyDrizzleDb } from "@voyant-travel/db";
|
|
14
|
+
import { type CharterContent } from "./content-shape.js";
|
|
15
|
+
export interface CharterContentScope {
|
|
16
|
+
preferredLocales: ReadonlyArray<string>;
|
|
17
|
+
market?: string;
|
|
18
|
+
currency?: string;
|
|
19
|
+
acceptMachineTranslated?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface GetCharterContentOptions {
|
|
22
|
+
registry: SourceAdapterRegistry;
|
|
23
|
+
buildAdapterContext?: (adapter: SourceAdapter) => SourceAdapterContext;
|
|
24
|
+
onOverlayError?: (event: {
|
|
25
|
+
field_path: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}) => void;
|
|
28
|
+
}
|
|
29
|
+
export interface ResolvedCharterContent {
|
|
30
|
+
content: CharterContent;
|
|
31
|
+
resolution: ContentLocaleResolution<{
|
|
32
|
+
locale: string;
|
|
33
|
+
payload: CharterContent;
|
|
34
|
+
}>;
|
|
35
|
+
source: "sourced-cache" | "sourced-fresh" | "synthesized";
|
|
36
|
+
served_stale: boolean;
|
|
37
|
+
synthesized: boolean;
|
|
38
|
+
machine_translated: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function getCharterContent(db: AnyDrizzleDb, entityId: string, scope: CharterContentScope, options: GetCharterContentOptions): Promise<ResolvedCharterContent | null>;
|
|
41
|
+
/** Drift event consumer for the charters content cache. Per §3.4.1. */
|
|
42
|
+
export declare const invalidateCharterContentOnDrift: InvalidateOnDrift;
|
|
43
|
+
//# 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;;;;;;;;;GASG;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,cAAc,EAIpB,MAAM,oBAAoB,CAAA;AAc3B,MAAM,WAAW,mBAAmB;IAClC,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,wBAAwB;IACvC,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,sBAAsB;IACrC,OAAO,EAAE,cAAc,CAAA;IACvB,UAAU,EAAE,uBAAuB,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,cAAc,CAAA;KAAE,CAAC,CAAA;IAChF,MAAM,EAAE,eAAe,GAAG,eAAe,GAAG,aAAa,CAAA;IACzD,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CA0FxC;AA2ND,uEAAuE;AACvE,eAAO,MAAM,+BAA+B,EAAE,iBAG7C,CAAA"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Charter content service — `getCharterContent` with locale-resolved
|
|
3
|
+
* cache reads, SWR refresh, and synthesizer fallback.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `service-content.ts` in the products / cruises / accommodations
|
|
6
|
+
* packages but charter-shaped. The charter content aggregate (§3.2 /
|
|
7
|
+
* §3.6) is `{ charter, yacht, voyages[], suites[], schedule_days[],
|
|
8
|
+
* policies[] }` — one payload returned by a single getContent.
|
|
9
|
+
* Pricing stays out (volatile-live, flows through `liveResolve`).
|
|
10
|
+
*/
|
|
11
|
+
import { createInvalidateOnDrift, fetchOverlaysForEntity, isStale, pickBestCachedLocale, readSourcedEntry, withContentRefreshLock, } from "@voyant-travel/catalog";
|
|
12
|
+
import { and, eq } from "drizzle-orm";
|
|
13
|
+
import { CHARTERS_CONTENT_SCHEMA_VERSION, charterContentSchema, mergeOverlaysIntoCharterContent, validateCharterContent, } from "./content-shape.js";
|
|
14
|
+
import { CHARTERS_CONTENT_MARKET_ANY, chartersSourcedContentTable, } from "./schema-sourced-content.js";
|
|
15
|
+
import { synthesizeCharterContent, } from "./service-content-synthesizer.js";
|
|
16
|
+
/** Charters cache TTL is 24h — same as products / cruises. */
|
|
17
|
+
const CHARTERS_DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
export async function getCharterContent(db, entityId, scope, options) {
|
|
19
|
+
const sourcedEntry = await readSourcedEntry(db, "charters", entityId);
|
|
20
|
+
if (!sourcedEntry)
|
|
21
|
+
return null;
|
|
22
|
+
const provenance = {
|
|
23
|
+
kind: "sourced",
|
|
24
|
+
provenance: {
|
|
25
|
+
source_kind: sourcedEntry.source_kind,
|
|
26
|
+
source_provider: sourcedEntry.source_provider ?? undefined,
|
|
27
|
+
source_connection_id: sourcedEntry.source_connection_id ?? undefined,
|
|
28
|
+
source_ref: sourcedEntry.source_ref ?? undefined,
|
|
29
|
+
source_freshness: sourcedEntry.source_freshness,
|
|
30
|
+
last_sourced_at: sourcedEntry.last_sourced_at ?? undefined,
|
|
31
|
+
},
|
|
32
|
+
entry_id: sourcedEntry.id,
|
|
33
|
+
status: sourcedEntry.status,
|
|
34
|
+
projection: sourcedEntry.projection,
|
|
35
|
+
projection_etag: sourcedEntry.projection_etag,
|
|
36
|
+
projection_seen_at: sourcedEntry.projection_seen_at,
|
|
37
|
+
first_seen_at: sourcedEntry.first_seen_at,
|
|
38
|
+
last_seen_at: sourcedEntry.last_seen_at,
|
|
39
|
+
};
|
|
40
|
+
const adapter = sourcedEntry.source_connection_id
|
|
41
|
+
? (options.registry.resolveByConnection(sourcedEntry.source_connection_id) ??
|
|
42
|
+
options.registry.byKind(sourcedEntry.source_kind)[0]?.adapter)
|
|
43
|
+
: options.registry.byKind(sourcedEntry.source_kind)[0]?.adapter;
|
|
44
|
+
const adapterCtx = options.buildAdapterContext?.(adapter) ?? {
|
|
45
|
+
connection_id: sourcedEntry.source_connection_id ?? sourcedEntry.source_kind,
|
|
46
|
+
};
|
|
47
|
+
const market = scope.market ?? CHARTERS_CONTENT_MARKET_ANY;
|
|
48
|
+
const acceptMT = scope.acceptMachineTranslated ?? true;
|
|
49
|
+
const cachedRows = await fetchCacheCandidates(db, entityId, market);
|
|
50
|
+
const eligibleRows = acceptMT ? cachedRows : cachedRows.filter((r) => !r.machine_translated);
|
|
51
|
+
const best = pickBestCachedLocale(eligibleRows.map((row) => ({ ...row, locale: row.locale })), scope.preferredLocales);
|
|
52
|
+
if (best && !isStale(best.candidate)) {
|
|
53
|
+
return finalizeFromCache(db, entityId, best, false, options);
|
|
54
|
+
}
|
|
55
|
+
if (best && isStale(best.candidate)) {
|
|
56
|
+
if (adapter?.getContent) {
|
|
57
|
+
void scheduleRefresh(db, adapter, adapterCtx, {
|
|
58
|
+
entity_module: "charters",
|
|
59
|
+
entity_id: entityId,
|
|
60
|
+
locale: scope.preferredLocales[0] ?? best.candidate.locale,
|
|
61
|
+
market,
|
|
62
|
+
currency: scope.currency,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return finalizeFromCache(db, entityId, best, true, options);
|
|
66
|
+
}
|
|
67
|
+
if (!adapter?.getContent) {
|
|
68
|
+
const overlays = await fetchOverlaysForEntity(db, "charters", entityId);
|
|
69
|
+
const synthesized = synthesizeCharterContent({ locale: scope.preferredLocales[0] ?? "en-GB" }, {
|
|
70
|
+
provenance,
|
|
71
|
+
overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
|
|
72
|
+
});
|
|
73
|
+
return wrapSynthesized(synthesized, scope, false);
|
|
74
|
+
}
|
|
75
|
+
const fresh = await fetchFreshContent(db, adapter, adapterCtx, {
|
|
76
|
+
entity_module: "charters",
|
|
77
|
+
entity_id: entityId,
|
|
78
|
+
locale: scope.preferredLocales[0] ?? "en-GB",
|
|
79
|
+
market,
|
|
80
|
+
currency: scope.currency,
|
|
81
|
+
});
|
|
82
|
+
if (!fresh) {
|
|
83
|
+
const overlays = await fetchOverlaysForEntity(db, "charters", entityId);
|
|
84
|
+
const synthesized = synthesizeCharterContent({ locale: scope.preferredLocales[0] ?? "en-GB" }, {
|
|
85
|
+
provenance,
|
|
86
|
+
overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
|
|
87
|
+
});
|
|
88
|
+
return wrapSynthesized(synthesized, scope, false);
|
|
89
|
+
}
|
|
90
|
+
return finalizeFresh(db, entityId, fresh, scope, options);
|
|
91
|
+
}
|
|
92
|
+
async function fetchCacheCandidates(db, entityId, market) {
|
|
93
|
+
return db
|
|
94
|
+
.select()
|
|
95
|
+
.from(chartersSourcedContentTable)
|
|
96
|
+
.where(and(eq(chartersSourcedContentTable.entity_id, entityId), eq(chartersSourcedContentTable.market, market), eq(chartersSourcedContentTable.content_schema_version, CHARTERS_CONTENT_SCHEMA_VERSION)));
|
|
97
|
+
}
|
|
98
|
+
async function fetchFreshContent(db, adapter, ctx, request) {
|
|
99
|
+
const result = await withContentRefreshLock(db, {
|
|
100
|
+
entityModule: request.entity_module,
|
|
101
|
+
entityId: request.entity_id,
|
|
102
|
+
locale: request.locale,
|
|
103
|
+
market: request.market,
|
|
104
|
+
}, async () => {
|
|
105
|
+
const got = await adapter.getContent(ctx, request);
|
|
106
|
+
const validation = validateCharterContent(got.content);
|
|
107
|
+
if (!validation.valid) {
|
|
108
|
+
throw new Error(`charters getContent for ${request.entity_id} failed validation: ${validation.reason}`);
|
|
109
|
+
}
|
|
110
|
+
await writeCacheRow(db, request, got);
|
|
111
|
+
return got;
|
|
112
|
+
});
|
|
113
|
+
return result ?? null;
|
|
114
|
+
}
|
|
115
|
+
function scheduleRefresh(db, adapter, ctx, request) {
|
|
116
|
+
void withContentRefreshLock(db, {
|
|
117
|
+
entityModule: request.entity_module,
|
|
118
|
+
entityId: request.entity_id,
|
|
119
|
+
locale: request.locale,
|
|
120
|
+
market: request.market,
|
|
121
|
+
}, async () => {
|
|
122
|
+
const got = await adapter.getContent(ctx, request);
|
|
123
|
+
const validation = validateCharterContent(got.content);
|
|
124
|
+
if (!validation.valid)
|
|
125
|
+
return;
|
|
126
|
+
await writeCacheRow(db, request, got);
|
|
127
|
+
}).catch(() => {
|
|
128
|
+
// intentional swallow — see §3.4 SWR refresh contract
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async function writeCacheRow(db, request, result) {
|
|
132
|
+
const market = request.market ?? CHARTERS_CONTENT_MARKET_ANY;
|
|
133
|
+
const now = new Date();
|
|
134
|
+
// Coerce JSON-string dates to Date — see products writeCacheRow.
|
|
135
|
+
const sourceUpdatedAt = toDateOrNull(result.source_updated_at);
|
|
136
|
+
const freshUntil = toDateOrNull(result.fresh_until) ?? new Date(now.getTime() + CHARTERS_DEFAULT_TTL_MS);
|
|
137
|
+
await db
|
|
138
|
+
.insert(chartersSourcedContentTable)
|
|
139
|
+
.values({
|
|
140
|
+
entity_id: request.entity_id,
|
|
141
|
+
locale: request.locale,
|
|
142
|
+
market,
|
|
143
|
+
payload: result.content,
|
|
144
|
+
content_schema_version: result.content_schema_version,
|
|
145
|
+
returned_locale: result.returned_locale,
|
|
146
|
+
machine_translated: result.machine_translated ?? false,
|
|
147
|
+
source_updated_at: sourceUpdatedAt,
|
|
148
|
+
fetched_at: now,
|
|
149
|
+
fresh_until: freshUntil,
|
|
150
|
+
etag: result.etag ?? null,
|
|
151
|
+
fetch_status: "ok",
|
|
152
|
+
fetch_error: null,
|
|
153
|
+
})
|
|
154
|
+
.onConflictDoUpdate({
|
|
155
|
+
target: [
|
|
156
|
+
chartersSourcedContentTable.entity_id,
|
|
157
|
+
chartersSourcedContentTable.locale,
|
|
158
|
+
chartersSourcedContentTable.market,
|
|
159
|
+
],
|
|
160
|
+
set: {
|
|
161
|
+
payload: result.content,
|
|
162
|
+
content_schema_version: result.content_schema_version,
|
|
163
|
+
returned_locale: result.returned_locale,
|
|
164
|
+
machine_translated: result.machine_translated ?? false,
|
|
165
|
+
source_updated_at: sourceUpdatedAt,
|
|
166
|
+
fetched_at: now,
|
|
167
|
+
fresh_until: freshUntil,
|
|
168
|
+
etag: result.etag ?? null,
|
|
169
|
+
fetch_status: "ok",
|
|
170
|
+
fetch_error: null,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function finalizeFromCache(db, entityId, best, servedStale, options) {
|
|
175
|
+
const validation = validateCharterContent(best.candidate.payload);
|
|
176
|
+
if (!validation.valid) {
|
|
177
|
+
throw new Error(`charters cache row for ${entityId} (${best.candidate.locale}) failed validation: ${validation.reason}`);
|
|
178
|
+
}
|
|
179
|
+
const overlays = await fetchOverlaysForEntity(db, "charters", entityId);
|
|
180
|
+
const merged = mergeOverlaysIntoCharterContent(validation.content, overlays.map((o) => ({ field_path: o.field_path, value: o.value })), {
|
|
181
|
+
onOverlayError: options.onOverlayError
|
|
182
|
+
? (e) => options.onOverlayError({
|
|
183
|
+
field_path: e.overlay.field_path,
|
|
184
|
+
reason: e.reason,
|
|
185
|
+
})
|
|
186
|
+
: undefined,
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
content: merged,
|
|
190
|
+
resolution: {
|
|
191
|
+
candidate: { locale: best.candidate.locale, payload: merged },
|
|
192
|
+
served_locale: best.candidate.returned_locale,
|
|
193
|
+
match_kind: best.match_kind,
|
|
194
|
+
},
|
|
195
|
+
source: "sourced-cache",
|
|
196
|
+
served_stale: servedStale,
|
|
197
|
+
synthesized: false,
|
|
198
|
+
machine_translated: best.candidate.machine_translated,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function finalizeFresh(db, entityId, fresh, scope, options) {
|
|
202
|
+
const cachedContent = charterContentSchema.parse(fresh.content);
|
|
203
|
+
const overlays = await fetchOverlaysForEntity(db, "charters", entityId);
|
|
204
|
+
const merged = mergeOverlaysIntoCharterContent(cachedContent, overlays.map((o) => ({ field_path: o.field_path, value: o.value })), {
|
|
205
|
+
onOverlayError: options.onOverlayError
|
|
206
|
+
? (e) => options.onOverlayError({
|
|
207
|
+
field_path: e.overlay.field_path,
|
|
208
|
+
reason: e.reason,
|
|
209
|
+
})
|
|
210
|
+
: undefined,
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
content: merged,
|
|
214
|
+
resolution: {
|
|
215
|
+
candidate: { locale: scope.preferredLocales[0] ?? fresh.returned_locale, payload: merged },
|
|
216
|
+
served_locale: fresh.returned_locale,
|
|
217
|
+
match_kind: scope.preferredLocales[0] === fresh.returned_locale ? "exact" : "language_match",
|
|
218
|
+
},
|
|
219
|
+
source: "sourced-fresh",
|
|
220
|
+
served_stale: false,
|
|
221
|
+
synthesized: false,
|
|
222
|
+
machine_translated: fresh.machine_translated ?? false,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function wrapSynthesized(synthesized, scope, servedStale) {
|
|
226
|
+
return {
|
|
227
|
+
content: synthesized.content,
|
|
228
|
+
resolution: {
|
|
229
|
+
candidate: { locale: synthesized.served_locale, payload: synthesized.content },
|
|
230
|
+
served_locale: synthesized.served_locale,
|
|
231
|
+
match_kind: scope.preferredLocales[0] === synthesized.served_locale ? "exact" : "any",
|
|
232
|
+
},
|
|
233
|
+
source: "synthesized",
|
|
234
|
+
served_stale: servedStale,
|
|
235
|
+
synthesized: true,
|
|
236
|
+
machine_translated: false,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Drift event consumer for the charters content cache. Per §3.4.1. */
|
|
240
|
+
export const invalidateCharterContentOnDrift = createInvalidateOnDrift(chartersSourcedContentTable, { entityModule: "charters" });
|
|
241
|
+
function toDateOrNull(value) {
|
|
242
|
+
if (!value)
|
|
243
|
+
return null;
|
|
244
|
+
if (value instanceof Date)
|
|
245
|
+
return value;
|
|
246
|
+
const parsed = new Date(value);
|
|
247
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
248
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import { type BookingCharterDetail } from "./booking-extension.js";
|
|
3
|
+
/**
|
|
4
|
+
* Minimal subset of `@voyant-travel/legal`'s `contractsService` we depend on.
|
|
5
|
+
* Defined structurally so charters does NOT take a hard dependency on
|
|
6
|
+
* the legal package — templates wire the real service in. Mirrors the
|
|
7
|
+
* dependency-inversion pattern legal already uses for document
|
|
8
|
+
* generators / event resolveDb.
|
|
9
|
+
*/
|
|
10
|
+
export interface CharterContractsService {
|
|
11
|
+
getDefaultTemplate(db: PostgresJsDatabase, query: {
|
|
12
|
+
scope?: string;
|
|
13
|
+
language?: string;
|
|
14
|
+
slug?: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
id: string;
|
|
17
|
+
currentVersionId: string | null;
|
|
18
|
+
slug: string;
|
|
19
|
+
} | null>;
|
|
20
|
+
getTemplateById(db: PostgresJsDatabase, id: string): Promise<{
|
|
21
|
+
id: string;
|
|
22
|
+
currentVersionId: string | null;
|
|
23
|
+
slug: string;
|
|
24
|
+
} | null>;
|
|
25
|
+
createContract(db: PostgresJsDatabase, data: {
|
|
26
|
+
scope: "customer" | "supplier" | "partner" | "channel" | "other";
|
|
27
|
+
title: string;
|
|
28
|
+
templateVersionId?: string | null;
|
|
29
|
+
personId?: string | null;
|
|
30
|
+
organizationId?: string | null;
|
|
31
|
+
bookingId?: string | null;
|
|
32
|
+
variables?: Record<string, unknown> | null;
|
|
33
|
+
language?: string;
|
|
34
|
+
metadata?: Record<string, unknown> | null;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
id: string;
|
|
37
|
+
} | null>;
|
|
38
|
+
}
|
|
39
|
+
export type GenerateMybaContractInput = {
|
|
40
|
+
bookingId: string;
|
|
41
|
+
/** Override the template that the booking_charter_details snapshotted. */
|
|
42
|
+
templateIdOverride?: string | null;
|
|
43
|
+
/** Locale for the contract; defaults to "en". */
|
|
44
|
+
language?: string;
|
|
45
|
+
/** Extra Liquid variables passed to the template renderer. Merged on top of
|
|
46
|
+
* the defaults generated from the booking + charter snapshot. */
|
|
47
|
+
extraVariables?: Record<string, unknown>;
|
|
48
|
+
title?: string;
|
|
49
|
+
};
|
|
50
|
+
export type GenerateMybaContractResult = {
|
|
51
|
+
status: "ok";
|
|
52
|
+
contractId: string;
|
|
53
|
+
detail: BookingCharterDetail;
|
|
54
|
+
} | {
|
|
55
|
+
status: "not_found";
|
|
56
|
+
} | {
|
|
57
|
+
status: "wrong_mode";
|
|
58
|
+
bookingMode: string;
|
|
59
|
+
} | {
|
|
60
|
+
status: "no_template";
|
|
61
|
+
detail: BookingCharterDetail;
|
|
62
|
+
} | {
|
|
63
|
+
status: "template_not_found";
|
|
64
|
+
templateId: string;
|
|
65
|
+
} | {
|
|
66
|
+
status: "contract_create_failed";
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Generate a MYBA contract for a whole-yacht booking and link it back via
|
|
70
|
+
* `booking_charter_details.mybaContractId`. Idempotent in the
|
|
71
|
+
* already-generated case: if `mybaContractId` is set, returns ok without
|
|
72
|
+
* recreating.
|
|
73
|
+
*
|
|
74
|
+
* Resolves the template id with this precedence:
|
|
75
|
+
* 1. `input.templateIdOverride`
|
|
76
|
+
* 2. `booking_charter_details.mybaTemplateIdSnapshot` (recorded at
|
|
77
|
+
* booking-creation time from voyage override or product default)
|
|
78
|
+
* 3. legal's default contract template (scope='customer', the slug
|
|
79
|
+
* conventionally used for MYBA — caller can also pass a slug via
|
|
80
|
+
* override).
|
|
81
|
+
*/
|
|
82
|
+
export declare const mybaService: {
|
|
83
|
+
generateContract(db: PostgresJsDatabase, contractsService: CharterContractsService, input: GenerateMybaContractInput): Promise<GenerateMybaContractResult>;
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=service-myba.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-myba.d.ts","sourceRoot":"","sources":["../src/service-myba.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAAE,KAAK,oBAAoB,EAAyB,MAAM,wBAAwB,CAAA;AAEzF;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,kBAAkB,CAChB,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1D,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAEhF,eAAe,CACb,EAAE,EAAE,kBAAkB,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAEhF,cAAc,CACZ,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE;QACJ,KAAK,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAA;QAChE,KAAK,EAAE,MAAM,CAAA;QACb,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACjC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACxB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;QAC1C,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;KAC1C,GACA,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;CAClC;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;sEACkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,0BAA0B,GAClC;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GAClE;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,GACvB;IAAE,MAAM,EAAE,YAAY,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,oBAAoB,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACpD;IAAE,MAAM,EAAE,wBAAwB,CAAA;CAAE,CAAA;AAExC;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW;yBAEhB,kBAAkB,oBACJ,uBAAuB,SAClC,yBAAyB,GAC/B,OAAO,CAAC,0BAA0B,CAAC;CAqEvC,CAAA"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { bookingCharterDetails } from "./booking-extension.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generate a MYBA contract for a whole-yacht booking and link it back via
|
|
5
|
+
* `booking_charter_details.mybaContractId`. Idempotent in the
|
|
6
|
+
* already-generated case: if `mybaContractId` is set, returns ok without
|
|
7
|
+
* recreating.
|
|
8
|
+
*
|
|
9
|
+
* Resolves the template id with this precedence:
|
|
10
|
+
* 1. `input.templateIdOverride`
|
|
11
|
+
* 2. `booking_charter_details.mybaTemplateIdSnapshot` (recorded at
|
|
12
|
+
* booking-creation time from voyage override or product default)
|
|
13
|
+
* 3. legal's default contract template (scope='customer', the slug
|
|
14
|
+
* conventionally used for MYBA — caller can also pass a slug via
|
|
15
|
+
* override).
|
|
16
|
+
*/
|
|
17
|
+
export const mybaService = {
|
|
18
|
+
async generateContract(db, contractsService, input) {
|
|
19
|
+
const [detail] = await db
|
|
20
|
+
.select()
|
|
21
|
+
.from(bookingCharterDetails)
|
|
22
|
+
.where(eq(bookingCharterDetails.bookingId, input.bookingId))
|
|
23
|
+
.limit(1);
|
|
24
|
+
if (!detail)
|
|
25
|
+
return { status: "not_found" };
|
|
26
|
+
if (detail.bookingMode !== "whole_yacht") {
|
|
27
|
+
return { status: "wrong_mode", bookingMode: detail.bookingMode };
|
|
28
|
+
}
|
|
29
|
+
if (detail.mybaContractId) {
|
|
30
|
+
return { status: "ok", contractId: detail.mybaContractId, detail };
|
|
31
|
+
}
|
|
32
|
+
const templateId = input.templateIdOverride ?? detail.mybaTemplateIdSnapshot;
|
|
33
|
+
let template = null;
|
|
34
|
+
if (templateId) {
|
|
35
|
+
template = await contractsService.getTemplateById(db, templateId);
|
|
36
|
+
if (!template)
|
|
37
|
+
return { status: "template_not_found", templateId };
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
template = await contractsService.getDefaultTemplate(db, {
|
|
41
|
+
scope: "customer",
|
|
42
|
+
language: input.language ?? "en",
|
|
43
|
+
});
|
|
44
|
+
if (!template)
|
|
45
|
+
return { status: "no_template", detail };
|
|
46
|
+
}
|
|
47
|
+
const variables = {
|
|
48
|
+
bookingId: detail.bookingId,
|
|
49
|
+
voyageId: detail.voyageId,
|
|
50
|
+
voyageDisplayName: detail.voyageDisplayName,
|
|
51
|
+
yachtName: detail.yachtName,
|
|
52
|
+
yachtId: detail.yachtId,
|
|
53
|
+
charterArea: detail.charterAreaSnapshot,
|
|
54
|
+
guestCount: detail.guestCount,
|
|
55
|
+
currency: detail.quotedCurrency,
|
|
56
|
+
charterFee: detail.quotedCharterFee,
|
|
57
|
+
apaPercent: detail.apaPercent,
|
|
58
|
+
apaAmount: detail.apaAmount,
|
|
59
|
+
total: detail.quotedTotal,
|
|
60
|
+
...(input.extraVariables ?? {}),
|
|
61
|
+
};
|
|
62
|
+
const contract = await contractsService.createContract(db, {
|
|
63
|
+
scope: "customer",
|
|
64
|
+
title: input.title ??
|
|
65
|
+
`MYBA charter agreement — ${detail.voyageDisplayName ?? detail.voyageId ?? detail.bookingId}`,
|
|
66
|
+
templateVersionId: template.currentVersionId,
|
|
67
|
+
bookingId: detail.bookingId,
|
|
68
|
+
variables,
|
|
69
|
+
language: input.language ?? "en",
|
|
70
|
+
metadata: {
|
|
71
|
+
source: "charters",
|
|
72
|
+
bookingMode: "whole_yacht",
|
|
73
|
+
templateId: template.id,
|
|
74
|
+
templateSlug: template.slug,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
if (!contract)
|
|
78
|
+
return { status: "contract_create_failed" };
|
|
79
|
+
const [updated] = await db
|
|
80
|
+
.update(bookingCharterDetails)
|
|
81
|
+
.set({ mybaContractId: contract.id, updatedAt: new Date() })
|
|
82
|
+
.where(eq(bookingCharterDetails.bookingId, input.bookingId))
|
|
83
|
+
.returning();
|
|
84
|
+
if (!updated)
|
|
85
|
+
return { status: "contract_create_failed" };
|
|
86
|
+
return { status: "ok", contractId: contract.id, detail: updated };
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
import { type CharterVoyage } from "./schema-core.js";
|
|
3
|
+
import { type CharterSuite } from "./schema-pricing.js";
|
|
4
|
+
export type PerSuiteQuote = {
|
|
5
|
+
mode: "per_suite";
|
|
6
|
+
voyageId: string;
|
|
7
|
+
suiteId: string;
|
|
8
|
+
suiteName: string;
|
|
9
|
+
currency: string;
|
|
10
|
+
suitePrice: string;
|
|
11
|
+
portFee: string | null;
|
|
12
|
+
total: string;
|
|
13
|
+
};
|
|
14
|
+
export type ComposePerSuiteQuoteInput = {
|
|
15
|
+
voyageId: string;
|
|
16
|
+
suite: Pick<CharterSuite, "id" | "suiteName" | "pricesByCurrency" | "portFeesByCurrency">;
|
|
17
|
+
currency: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function composePerSuiteQuote(input: ComposePerSuiteQuoteInput): PerSuiteQuote;
|
|
20
|
+
export type WholeYachtQuote = {
|
|
21
|
+
mode: "whole_yacht";
|
|
22
|
+
voyageId: string;
|
|
23
|
+
currency: string;
|
|
24
|
+
charterFee: string;
|
|
25
|
+
apaPercent: string;
|
|
26
|
+
apaAmount: string;
|
|
27
|
+
total: string;
|
|
28
|
+
};
|
|
29
|
+
export type ComposeWholeYachtQuoteInput = {
|
|
30
|
+
voyage: Pick<CharterVoyage, "id" | "wholeYachtPricesByCurrency" | "apaPercentOverride">;
|
|
31
|
+
/**
|
|
32
|
+
* The product's defaultApaPercent — passed in by the caller so this stays
|
|
33
|
+
* a pure function. Voyage-level override takes precedence.
|
|
34
|
+
*/
|
|
35
|
+
productDefaultApaPercent: string | null;
|
|
36
|
+
currency: string;
|
|
37
|
+
};
|
|
38
|
+
export declare function composeWholeYachtQuote(input: ComposeWholeYachtQuoteInput): WholeYachtQuote;
|
|
39
|
+
/**
|
|
40
|
+
* Compute APA amount from a charter fee and a percent. Useful for finance-side
|
|
41
|
+
* recalculation without needing to re-quote the whole voyage.
|
|
42
|
+
*/
|
|
43
|
+
export declare function computeApaAmount(charterFee: string, apaPercent: string): string;
|
|
44
|
+
export declare const pricingService: {
|
|
45
|
+
quotePerSuite(db: PostgresJsDatabase, args: {
|
|
46
|
+
suiteId: string;
|
|
47
|
+
currency: string;
|
|
48
|
+
}): Promise<PerSuiteQuote>;
|
|
49
|
+
quoteWholeYacht(db: PostgresJsDatabase, args: {
|
|
50
|
+
voyageId: string;
|
|
51
|
+
currency: string;
|
|
52
|
+
}): Promise<WholeYachtQuote>;
|
|
53
|
+
/**
|
|
54
|
+
* Lowest published per-suite price for a voyage, in the requested currency.
|
|
55
|
+
* Returns `null` when no available suite has that currency. The previous
|
|
56
|
+
* `lowestSuitePriceUSD` shape was renamed and generalized as part of #355
|
|
57
|
+
* (browse-currency policy is a deployment choice, not a hardcoded USD).
|
|
58
|
+
*/
|
|
59
|
+
lowestSuitePriceForCurrency(db: PostgresJsDatabase, voyageId: string, currency: string): Promise<{
|
|
60
|
+
suiteId: string;
|
|
61
|
+
price: string;
|
|
62
|
+
} | null>;
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=service-pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-pricing.d.ts","sourceRoot":"","sources":["../src/service-pricing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAAE,KAAK,aAAa,EAAmC,MAAM,kBAAkB,CAAA;AACtF,OAAO,EAAE,KAAK,YAAY,EAAiB,MAAM,qBAAqB,CAAA;AAgEtE,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,WAAW,GAAG,kBAAkB,GAAG,oBAAoB,CAAC,CAAA;IACzF,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,aAAa,CAqBpF;AAID,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,aAAa,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CAMd,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,4BAA4B,GAAG,oBAAoB,CAAC,CAAA;IACvF;;;OAGG;IACH,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAA;IACvC,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,GAAG,eAAe,CA2B1F;AAID;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAG/E;AAID,eAAO,MAAM,cAAc;sBAEnB,kBAAkB,QAChB;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAC1C,OAAO,CAAC,aAAa,CAAC;wBAenB,kBAAkB,QAChB;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAC3C,OAAO,CAAC,eAAe,CAAC;IAqB3B;;;;;OAKG;oCAEG,kBAAkB,YACZ,MAAM,YACN,MAAM,GACf,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAsBtD,CAAA"}
|