@voyant-travel/storefront 0.120.1
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 +231 -0
- package/dist/booking-intents.d.ts +42 -0
- package/dist/booking-intents.d.ts.map +1 -0
- package/dist/booking-intents.js +83 -0
- package/dist/customer-portal/index.d.ts +16 -0
- package/dist/customer-portal/index.d.ts.map +1 -0
- package/dist/customer-portal/index.js +23 -0
- package/dist/customer-portal/route-runtime.d.ts +16 -0
- package/dist/customer-portal/route-runtime.d.ts.map +1 -0
- package/dist/customer-portal/route-runtime.js +27 -0
- package/dist/customer-portal/routes-public.d.ts +1936 -0
- package/dist/customer-portal/routes-public.d.ts.map +1 -0
- package/dist/customer-portal/routes-public.js +165 -0
- package/dist/customer-portal/routes.d.ts +43 -0
- package/dist/customer-portal/routes.d.ts.map +1 -0
- package/dist/customer-portal/routes.js +17 -0
- package/dist/customer-portal/service-public-impl.d.ts +138 -0
- package/dist/customer-portal/service-public-impl.d.ts.map +1 -0
- package/dist/customer-portal/service-public-impl.js +1808 -0
- package/dist/customer-portal/service-public.d.ts +2 -0
- package/dist/customer-portal/service-public.d.ts.map +1 -0
- package/dist/customer-portal/service-public.js +1 -0
- package/dist/customer-portal/validation-public/bookings.d.ts +551 -0
- package/dist/customer-portal/validation-public/bookings.d.ts.map +1 -0
- package/dist/customer-portal/validation-public/bookings.js +132 -0
- package/dist/customer-portal/validation-public/common.d.ts +162 -0
- package/dist/customer-portal/validation-public/common.d.ts.map +1 -0
- package/dist/customer-portal/validation-public/common.js +139 -0
- package/dist/customer-portal/validation-public/profile.d.ts +749 -0
- package/dist/customer-portal/validation-public/profile.d.ts.map +1 -0
- package/dist/customer-portal/validation-public/profile.js +308 -0
- package/dist/customer-portal/validation-public.d.ts +3 -0
- package/dist/customer-portal/validation-public.d.ts.map +1 -0
- package/dist/customer-portal/validation-public.js +2 -0
- package/dist/guest-booking-guard.d.ts +24 -0
- package/dist/guest-booking-guard.d.ts.map +1 -0
- package/dist/guest-booking-guard.js +55 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/product-extra-ref.d.ts +238 -0
- package/dist/product-extra-ref.d.ts.map +1 -0
- package/dist/product-extra-ref.js +22 -0
- package/dist/routes-admin.d.ts +220 -0
- package/dist/routes-admin.d.ts.map +1 -0
- package/dist/routes-admin.js +28 -0
- package/dist/routes-public.d.ts +1475 -0
- package/dist/routes-public.d.ts.map +1 -0
- package/dist/routes-public.js +362 -0
- package/dist/service-booking-session-bootstrap.d.ts +227 -0
- package/dist/service-booking-session-bootstrap.d.ts.map +1 -0
- package/dist/service-booking-session-bootstrap.js +287 -0
- package/dist/service-boundary-resource-sql.d.ts +18 -0
- package/dist/service-boundary-resource-sql.d.ts.map +1 -0
- package/dist/service-boundary-resource-sql.js +73 -0
- package/dist/service-boundary-sql.d.ts +103 -0
- package/dist/service-boundary-sql.d.ts.map +1 -0
- package/dist/service-boundary-sql.js +307 -0
- package/dist/service-departures-core.d.ts +41 -0
- package/dist/service-departures-core.d.ts.map +1 -0
- package/dist/service-departures-core.js +92 -0
- package/dist/service-departures-extensions.d.ts +46 -0
- package/dist/service-departures-extensions.d.ts.map +1 -0
- package/dist/service-departures-extensions.js +81 -0
- package/dist/service-departures-offers.d.ts +220 -0
- package/dist/service-departures-offers.d.ts.map +1 -0
- package/dist/service-departures-offers.js +177 -0
- package/dist/service-departures-price-preview.d.ts +306 -0
- package/dist/service-departures-price-preview.d.ts.map +1 -0
- package/dist/service-departures-price-preview.js +383 -0
- package/dist/service-departures-pricing-context.d.ts +115 -0
- package/dist/service-departures-pricing-context.d.ts.map +1 -0
- package/dist/service-departures-pricing-context.js +237 -0
- package/dist/service-departures-pricing.d.ts +5 -0
- package/dist/service-departures-pricing.d.ts.map +1 -0
- package/dist/service-departures-pricing.js +4 -0
- package/dist/service-departures.d.ts +192 -0
- package/dist/service-departures.d.ts.map +1 -0
- package/dist/service-departures.js +213 -0
- package/dist/service-intake.d.ts +130 -0
- package/dist/service-intake.d.ts.map +1 -0
- package/dist/service-intake.js +274 -0
- package/dist/service-transport-eligibility.d.ts +10 -0
- package/dist/service-transport-eligibility.d.ts.map +1 -0
- package/dist/service-transport-eligibility.js +198 -0
- package/dist/service.d.ts +1062 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +332 -0
- package/dist/transport-eligibility.d.ts +4 -0
- package/dist/transport-eligibility.d.ts.map +1 -0
- package/dist/transport-eligibility.js +2 -0
- package/dist/validation/departures.d.ts +1669 -0
- package/dist/validation/departures.d.ts.map +1 -0
- package/dist/validation/departures.js +397 -0
- package/dist/validation/intake.d.ts +147 -0
- package/dist/validation/intake.d.ts.map +1 -0
- package/dist/validation/intake.js +69 -0
- package/dist/validation/offers.d.ts +340 -0
- package/dist/validation/offers.d.ts.map +1 -0
- package/dist/validation/offers.js +117 -0
- package/dist/validation-settings.d.ts +609 -0
- package/dist/validation-settings.d.ts.map +1 -0
- package/dist/validation-settings.js +235 -0
- package/dist/validation-transport-eligibility.d.ts +314 -0
- package/dist/validation-transport-eligibility.d.ts.map +1 -0
- package/dist/validation-transport-eligibility.js +97 -0
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +4 -0
- package/dist/verification/index.d.ts +12 -0
- package/dist/verification/index.d.ts.map +1 -0
- package/dist/verification/index.js +18 -0
- package/dist/verification/routes-public.d.ts +121 -0
- package/dist/verification/routes-public.d.ts.map +1 -0
- package/dist/verification/routes-public.js +125 -0
- package/dist/verification/schema.d.ts +273 -0
- package/dist/verification/schema.d.ts.map +1 -0
- package/dist/verification/schema.js +50 -0
- package/dist/verification/service.d.ts +114 -0
- package/dist/verification/service.d.ts.map +1 -0
- package/dist/verification/service.js +283 -0
- package/dist/verification/validation.d.ts +98 -0
- package/dist/verification/validation.d.ts.map +1 -0
- package/dist/verification/validation.js +54 -0
- package/package.json +148 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes-public.d.ts","sourceRoot":"","sources":["../src/routes-public.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAQnC,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,cAAc,CAAA;AAmCrB,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAO1F;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAC3C,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,EACf,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,CAAC,CAAC,CAmBZ;AAED,KAAK,GAAG,GAAG;IACT,QAAQ,EAAE,cAAc,CAAA;IACxB,SAAS,EAAE;QACT,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,GAAG,eAAe,CAAA;CACpB,CAAA;AA+CD,wBAAgB,4BAA4B,CAAC,OAAO,CAAC,EAAE,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0BAwW9E;AAED,MAAM,MAAM,sBAAsB,GAAG,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAA"}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// agent-quality: file-size exception -- owner: storefront; existing route module stays co-located until a dedicated split preserves behavior and tests.
|
|
2
|
+
import { checkoutCapabilityActions, checkoutCapabilityCookie, issueCheckoutCapability, } from "@voyant-travel/bookings/checkout-capability";
|
|
3
|
+
import { enqueueWriteIntent, getWriteIntent } from "@voyant-travel/db/write-intents";
|
|
4
|
+
import { idempotencyKey, parseJsonBody, parseQuery, } from "@voyant-travel/hono";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { BOOKING_BOOTSTRAP_INTENT_EVENT, BOOKING_BOOTSTRAP_INTENT_KIND, } from "./booking-intents.js";
|
|
7
|
+
import { createStorefrontService, } from "./service.js";
|
|
8
|
+
import { storefrontBookingSessionBootstrapInputSchema, storefrontDepartureListQuerySchema, storefrontDeparturePricePreviewInputSchema, storefrontLeadIntakeInputSchema, storefrontNewsletterSubscribeInputSchema, storefrontOfferApplyInputSchema, storefrontOfferRedeemInputSchema, storefrontProductAvailabilitySummaryQuerySchema, storefrontProductExtensionsQuerySchema, storefrontPromotionalOfferListQuerySchema, } from "./validation.js";
|
|
9
|
+
import { storefrontTransportEligibilityInputSchema } from "./validation-transport-eligibility.js";
|
|
10
|
+
/**
|
|
11
|
+
* Shared-cache marker for non-personalized catalog reads (departure
|
|
12
|
+
* detail/list, itineraries). Same data for every visitor; the framework
|
|
13
|
+
* cache layer (`publicResponseCache` in @voyant-travel/hono) and the platform
|
|
14
|
+
* dispatcher only cache responses explicitly marked like this. Applied
|
|
15
|
+
* to success responses only.
|
|
16
|
+
*/
|
|
17
|
+
const PUBLIC_CACHE_CONTROL = "public, s-maxage=60, stale-while-revalidate=300";
|
|
18
|
+
/**
|
|
19
|
+
* KV read-model TTL for the departure list (RFC voyant#1687 Phase 2.2).
|
|
20
|
+
* Departure availability shifts with every booking, so unlike the
|
|
21
|
+
* product-detail documents (24h + exact invalidation in products) this
|
|
22
|
+
* is purely TTL-bounded: browse-grade freshness within 2 minutes, and
|
|
23
|
+
* checkout always re-verifies capacity on the live transactional path.
|
|
24
|
+
*/
|
|
25
|
+
const DEPARTURES_DOC_TTL_SECONDS = 120;
|
|
26
|
+
export function departuresDocKey(productId, query) {
|
|
27
|
+
const entries = Object.entries(query)
|
|
28
|
+
.filter(([, value]) => value !== undefined && value !== null)
|
|
29
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
30
|
+
.map(([key, value]) => `${key}=${String(value)}`)
|
|
31
|
+
.join("&");
|
|
32
|
+
return `rm:v1:departures:${productId}:${entries || "default"}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Read-through KV cache for a departures payload. Best-effort: any KV
|
|
36
|
+
* failure (or a missing CACHE binding) degrades to the live query.
|
|
37
|
+
*/
|
|
38
|
+
export async function readThroughDepartures(c, key, compute) {
|
|
39
|
+
const kv = c.env?.CACHE;
|
|
40
|
+
if (kv) {
|
|
41
|
+
try {
|
|
42
|
+
const hit = await kv.get(key, { type: "json" });
|
|
43
|
+
if (hit !== null && hit !== undefined)
|
|
44
|
+
return hit;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// fall through to live
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const data = await compute();
|
|
51
|
+
if (kv && data !== null && data !== undefined) {
|
|
52
|
+
try {
|
|
53
|
+
await kv.put(key, JSON.stringify(data), { expirationTtl: DEPARTURES_DOC_TTL_SECONDS });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// best-effort
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
function getRuntimeEnv(c) {
|
|
62
|
+
const processEnv = globalThis.process?.env ?? {};
|
|
63
|
+
return {
|
|
64
|
+
...processEnv,
|
|
65
|
+
...(c.env ?? {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function sessionConflictError(status) {
|
|
69
|
+
switch (status) {
|
|
70
|
+
case "insufficient_capacity":
|
|
71
|
+
return "Insufficient slot capacity";
|
|
72
|
+
case "slot_unavailable":
|
|
73
|
+
return "Availability slot is not bookable";
|
|
74
|
+
case "pricing_unavailable":
|
|
75
|
+
return "Pricing is not available for the selected booking session items";
|
|
76
|
+
case "stale_quote":
|
|
77
|
+
return "Booking session quote is stale";
|
|
78
|
+
case "invalid_slot":
|
|
79
|
+
return "Booking session slot does not match the requested departure";
|
|
80
|
+
default:
|
|
81
|
+
return "Unable to bootstrap booking session";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function attachCheckoutCapability(session, issued) {
|
|
85
|
+
return {
|
|
86
|
+
...session,
|
|
87
|
+
checkoutCapability: {
|
|
88
|
+
token: issued.token,
|
|
89
|
+
expiresAt: issued.expiresAt.toISOString(),
|
|
90
|
+
actions: [...checkoutCapabilityActions],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function createStorefrontPublicRoutes(options) {
|
|
95
|
+
const storefrontService = createStorefrontService(options);
|
|
96
|
+
function getRequestContext(c) {
|
|
97
|
+
return {
|
|
98
|
+
db: c.get("db"),
|
|
99
|
+
eventBus: c.get("eventBus"),
|
|
100
|
+
env: c.env,
|
|
101
|
+
context: c,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function runIntakeGuard(input) {
|
|
105
|
+
const decision = await storefrontService.checkIntakeGuard(input);
|
|
106
|
+
if (!decision || decision.allowed)
|
|
107
|
+
return null;
|
|
108
|
+
return {
|
|
109
|
+
status: decision.status ?? 403,
|
|
110
|
+
error: decision.error ?? "Storefront intake rejected",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return new Hono()
|
|
114
|
+
.get("/settings", async (c) => {
|
|
115
|
+
return c.json({ data: await storefrontService.resolveSettings(getRequestContext(c)) });
|
|
116
|
+
})
|
|
117
|
+
.post("/leads", async (c) => {
|
|
118
|
+
const context = getRequestContext(c);
|
|
119
|
+
const body = await parseJsonBody(c, storefrontLeadIntakeInputSchema);
|
|
120
|
+
const rejected = await runIntakeGuard({ kind: "lead", body, context });
|
|
121
|
+
if (rejected)
|
|
122
|
+
return c.json({ error: rejected.error }, rejected.status);
|
|
123
|
+
return c.json({
|
|
124
|
+
data: await storefrontService.createLead({
|
|
125
|
+
body,
|
|
126
|
+
context,
|
|
127
|
+
}),
|
|
128
|
+
}, 201);
|
|
129
|
+
})
|
|
130
|
+
.post("/newsletter/subscribe", async (c) => {
|
|
131
|
+
const context = getRequestContext(c);
|
|
132
|
+
const body = await parseJsonBody(c, storefrontNewsletterSubscribeInputSchema);
|
|
133
|
+
const rejected = await runIntakeGuard({ kind: "newsletter", body, context });
|
|
134
|
+
if (rejected)
|
|
135
|
+
return c.json({ error: rejected.error }, rejected.status);
|
|
136
|
+
return c.json({
|
|
137
|
+
data: await storefrontService.subscribeNewsletter({
|
|
138
|
+
body,
|
|
139
|
+
context,
|
|
140
|
+
}),
|
|
141
|
+
}, 202);
|
|
142
|
+
})
|
|
143
|
+
.get("/departures/:departureId", async (c) => {
|
|
144
|
+
const departure = await storefrontService.getDeparture(c.get("db"), c.req.param("departureId"));
|
|
145
|
+
if (!departure)
|
|
146
|
+
return c.json({ error: "Storefront departure not found" }, 404);
|
|
147
|
+
c.header("Cache-Control", PUBLIC_CACHE_CONTROL);
|
|
148
|
+
return c.json({ data: departure });
|
|
149
|
+
})
|
|
150
|
+
.get("/products/:productId/departures", async (c) => {
|
|
151
|
+
const productId = c.req.param("productId");
|
|
152
|
+
const query = await parseQuery(c, storefrontDepartureListQuerySchema);
|
|
153
|
+
const result = await readThroughDepartures(c, departuresDocKey(productId, query), () => storefrontService.listProductDepartures(c.get("db"), productId, query));
|
|
154
|
+
c.header("Cache-Control", PUBLIC_CACHE_CONTROL);
|
|
155
|
+
return c.json(result);
|
|
156
|
+
})
|
|
157
|
+
.post("/departures/:departureId/price", async (c) => {
|
|
158
|
+
const preview = await storefrontService.previewDeparturePrice(c.get("db"), c.req.param("departureId"), await parseJsonBody(c, storefrontDeparturePricePreviewInputSchema), getRequestContext(c));
|
|
159
|
+
return preview
|
|
160
|
+
? c.json({ data: preview })
|
|
161
|
+
: c.json({ error: "Storefront departure not found" }, 404);
|
|
162
|
+
})
|
|
163
|
+
.post("/bookings/sessions/bootstrap", idempotencyKey({
|
|
164
|
+
scope: "POST /v1/public/bookings/sessions/bootstrap",
|
|
165
|
+
replayResponses: false,
|
|
166
|
+
}), async (c) => {
|
|
167
|
+
// Async mode (RFC voyant#1687 Phase 3.2): `?async=1` or
|
|
168
|
+
// `Prefer: respond-async` stores a write intent + durably emits
|
|
169
|
+
// its event (outbox), and answers 202 + a status URL — under a
|
|
170
|
+
// booking spike, callers get instant 202s and the reserve
|
|
171
|
+
// transactions drain at the outbox's pace instead of
|
|
172
|
+
// thundering-herding the slot locks. The handler is
|
|
173
|
+
// `createBookingBootstrapIntentHandler` (booking-intents.ts),
|
|
174
|
+
// registered on the app bus by the deployment.
|
|
175
|
+
// Async mode is honored ONLY when the deployment wired the
|
|
176
|
+
// intent handler (`bookingIntents` option) — otherwise a 202'd
|
|
177
|
+
// intent would never be settled and the caller would watch it
|
|
178
|
+
// stale-fail. Unwired deployments silently get the sync path.
|
|
179
|
+
const wantsAsync = Boolean(options?.bookingIntents) &&
|
|
180
|
+
(c.req.query("async") === "1" ||
|
|
181
|
+
(c.req.header("prefer") ?? "").toLowerCase().includes("respond-async"));
|
|
182
|
+
if (wantsAsync) {
|
|
183
|
+
const idempotencyKey = c.req.header("idempotency-key")?.trim();
|
|
184
|
+
if (!idempotencyKey) {
|
|
185
|
+
return c.json({ error: "Idempotency-Key header is required for async bootstrap" }, 428);
|
|
186
|
+
}
|
|
187
|
+
const body = await parseJsonBody(c, storefrontBookingSessionBootstrapInputSchema);
|
|
188
|
+
const db = c.get("db");
|
|
189
|
+
const { intent, created } = await enqueueWriteIntent(db, {
|
|
190
|
+
kind: BOOKING_BOOTSTRAP_INTENT_KIND,
|
|
191
|
+
payload: {
|
|
192
|
+
input: body,
|
|
193
|
+
userId: c.get("userId"),
|
|
194
|
+
},
|
|
195
|
+
idempotencyKey,
|
|
196
|
+
});
|
|
197
|
+
if (created) {
|
|
198
|
+
const eventBus = c.get("eventBus");
|
|
199
|
+
await eventBus?.emit(BOOKING_BOOTSTRAP_INTENT_EVENT, { intentId: intent.id });
|
|
200
|
+
}
|
|
201
|
+
return c.json({
|
|
202
|
+
data: {
|
|
203
|
+
intentId: intent.id,
|
|
204
|
+
status: intent.status,
|
|
205
|
+
statusUrl: `/v1/public/bookings/intents/${intent.id}`,
|
|
206
|
+
},
|
|
207
|
+
}, 202);
|
|
208
|
+
}
|
|
209
|
+
const result = await storefrontService.bootstrapBookingSession(getRequestContext(c), await parseJsonBody(c, storefrontBookingSessionBootstrapInputSchema), c.get("userId"));
|
|
210
|
+
if (result.status === "departure_not_found") {
|
|
211
|
+
return c.json({ error: "Storefront departure not found" }, 404);
|
|
212
|
+
}
|
|
213
|
+
if (result.status === "slot_not_found") {
|
|
214
|
+
return c.json({ error: "Availability slot not found" }, 404);
|
|
215
|
+
}
|
|
216
|
+
if (result.status !== "ok") {
|
|
217
|
+
return c.json({
|
|
218
|
+
error: sessionConflictError(result.status),
|
|
219
|
+
...(result.status === "stale_quote" && "repricing" in result
|
|
220
|
+
? { data: { repricing: result.repricing } }
|
|
221
|
+
: {}),
|
|
222
|
+
}, result.status === "invalid_slot" ? 400 : 409);
|
|
223
|
+
}
|
|
224
|
+
if (!("bootstrap" in result)) {
|
|
225
|
+
return c.json({ error: "Unable to bootstrap booking session" }, 409);
|
|
226
|
+
}
|
|
227
|
+
const { bootstrap } = result;
|
|
228
|
+
const capability = await issueCheckoutCapability(bootstrap.session.sessionId, getRuntimeEnv(c));
|
|
229
|
+
c.header("Set-Cookie", checkoutCapabilityCookie(capability.token, capability.expiresAt), {
|
|
230
|
+
append: true,
|
|
231
|
+
});
|
|
232
|
+
return c.json({
|
|
233
|
+
data: {
|
|
234
|
+
...bootstrap,
|
|
235
|
+
session: attachCheckoutCapability(bootstrap.session, capability),
|
|
236
|
+
},
|
|
237
|
+
}, 201);
|
|
238
|
+
})
|
|
239
|
+
.get("/bookings/intents/:intentId", async (c) => {
|
|
240
|
+
const db = c.get("db");
|
|
241
|
+
const intent = await getWriteIntent(db, c.req.param("intentId"));
|
|
242
|
+
if (!intent || intent.kind !== BOOKING_BOOTSTRAP_INTENT_KIND) {
|
|
243
|
+
return c.json({ error: "Booking intent not found" }, 404);
|
|
244
|
+
}
|
|
245
|
+
if (intent.status === "succeeded") {
|
|
246
|
+
const stored = intent.result;
|
|
247
|
+
const bootstrap = stored?.bootstrap;
|
|
248
|
+
if (bootstrap?.session?.sessionId) {
|
|
249
|
+
// The checkout capability is issued at POLL time (it's a
|
|
250
|
+
// signed short-lived token derived from the sessionId — the
|
|
251
|
+
// async handler has no response to attach a cookie to).
|
|
252
|
+
const capability = await issueCheckoutCapability(bootstrap.session.sessionId, getRuntimeEnv(c));
|
|
253
|
+
c.header("Set-Cookie", checkoutCapabilityCookie(capability.token, capability.expiresAt), {
|
|
254
|
+
append: true,
|
|
255
|
+
});
|
|
256
|
+
return c.json({
|
|
257
|
+
data: {
|
|
258
|
+
intentId: intent.id,
|
|
259
|
+
status: "succeeded",
|
|
260
|
+
...bootstrap,
|
|
261
|
+
session: attachCheckoutCapability(bootstrap.session, capability),
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (intent.status === "failed") {
|
|
267
|
+
const detail = (intent.result ?? {});
|
|
268
|
+
return c.json({
|
|
269
|
+
data: {
|
|
270
|
+
intentId: intent.id,
|
|
271
|
+
status: "failed",
|
|
272
|
+
error: detail.conflict
|
|
273
|
+
? sessionConflictError(detail.conflict)
|
|
274
|
+
: (intent.error ?? "Booking intent failed"),
|
|
275
|
+
...(detail.conflict ? { conflict: detail.conflict } : {}),
|
|
276
|
+
...(detail.httpStatus ? { httpStatus: detail.httpStatus } : {}),
|
|
277
|
+
...(detail.repricing !== undefined ? { repricing: detail.repricing } : {}),
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return c.json({ data: { intentId: intent.id, status: "pending" } });
|
|
282
|
+
})
|
|
283
|
+
.post("/departures/:departureId/eligibility", async (c) => {
|
|
284
|
+
return c.json({
|
|
285
|
+
data: await storefrontService.checkDepartureTransportEligibility({
|
|
286
|
+
departureId: c.req.param("departureId"),
|
|
287
|
+
body: await parseJsonBody(c, storefrontTransportEligibilityInputSchema),
|
|
288
|
+
context: getRequestContext(c),
|
|
289
|
+
}),
|
|
290
|
+
});
|
|
291
|
+
})
|
|
292
|
+
.post("/products/:productId/departures/:departureId/eligibility", async (c) => {
|
|
293
|
+
return c.json({
|
|
294
|
+
data: await storefrontService.checkDepartureTransportEligibility({
|
|
295
|
+
departureId: c.req.param("departureId"),
|
|
296
|
+
productId: c.req.param("productId"),
|
|
297
|
+
body: await parseJsonBody(c, storefrontTransportEligibilityInputSchema),
|
|
298
|
+
context: getRequestContext(c),
|
|
299
|
+
}),
|
|
300
|
+
});
|
|
301
|
+
})
|
|
302
|
+
.get("/products/:productId/extensions", async (c) => {
|
|
303
|
+
const query = await parseQuery(c, storefrontProductExtensionsQuerySchema);
|
|
304
|
+
return c.json({
|
|
305
|
+
data: await storefrontService.getProductExtensions(c.get("db"), c.req.param("productId"), query.optionId),
|
|
306
|
+
});
|
|
307
|
+
})
|
|
308
|
+
.get("/products/:productId/availability", async (c) => {
|
|
309
|
+
return c.json({
|
|
310
|
+
data: await storefrontService.getProductAvailabilitySummary(c.get("db"), c.req.param("productId"), await parseQuery(c, storefrontProductAvailabilitySummaryQuerySchema)),
|
|
311
|
+
});
|
|
312
|
+
})
|
|
313
|
+
.get("/products/:productId/departures/:departureId/itinerary", async (c) => {
|
|
314
|
+
const itinerary = await storefrontService.getDepartureItinerary(c.get("db"), {
|
|
315
|
+
departureId: c.req.param("departureId"),
|
|
316
|
+
productId: c.req.param("productId"),
|
|
317
|
+
});
|
|
318
|
+
if (!itinerary)
|
|
319
|
+
return c.json({ error: "Storefront itinerary not found" }, 404);
|
|
320
|
+
c.header("Cache-Control", PUBLIC_CACHE_CONTROL);
|
|
321
|
+
return c.json({ data: itinerary });
|
|
322
|
+
})
|
|
323
|
+
.get("/products/:productId/offers", async (c) => {
|
|
324
|
+
const query = await parseQuery(c, storefrontPromotionalOfferListQuerySchema);
|
|
325
|
+
return c.json({
|
|
326
|
+
data: await storefrontService.listApplicableOffers({
|
|
327
|
+
productId: c.req.param("productId"),
|
|
328
|
+
departureId: query.departureId,
|
|
329
|
+
locale: query.locale,
|
|
330
|
+
context: getRequestContext(c),
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
})
|
|
334
|
+
.get("/offers/:slug", async (c) => {
|
|
335
|
+
const query = await parseQuery(c, storefrontPromotionalOfferListQuerySchema);
|
|
336
|
+
const offer = await storefrontService.getOfferBySlug({
|
|
337
|
+
slug: c.req.param("slug"),
|
|
338
|
+
locale: query.locale,
|
|
339
|
+
context: getRequestContext(c),
|
|
340
|
+
});
|
|
341
|
+
return offer ? c.json({ data: offer }) : c.json({ error: "Storefront offer not found" }, 404);
|
|
342
|
+
})
|
|
343
|
+
.post("/offers/:slug/apply", async (c) => {
|
|
344
|
+
const result = await storefrontService.applyOffer({
|
|
345
|
+
slug: c.req.param("slug"),
|
|
346
|
+
body: await parseJsonBody(c, storefrontOfferApplyInputSchema),
|
|
347
|
+
context: getRequestContext(c),
|
|
348
|
+
});
|
|
349
|
+
return result
|
|
350
|
+
? c.json({ data: result })
|
|
351
|
+
: c.json({ error: "Storefront offer application is not configured" }, 501);
|
|
352
|
+
})
|
|
353
|
+
.post("/offers/redeem", async (c) => {
|
|
354
|
+
const result = await storefrontService.redeemOffer({
|
|
355
|
+
body: await parseJsonBody(c, storefrontOfferRedeemInputSchema),
|
|
356
|
+
context: getRequestContext(c),
|
|
357
|
+
});
|
|
358
|
+
return result
|
|
359
|
+
? c.json({ data: result })
|
|
360
|
+
: c.json({ error: "Storefront offer redemption is not configured" }, 501);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { type PaymentPolicy } from "@voyant-travel/finance";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
import type { StorefrontBookingSessionBootstrapInput } from "./validation.js";
|
|
4
|
+
export interface StorefrontBootstrapRequestContext {
|
|
5
|
+
db: PostgresJsDatabase;
|
|
6
|
+
env?: unknown;
|
|
7
|
+
context?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface StorefrontBookingSessionBootstrapOptions {
|
|
10
|
+
paymentPolicy?: PaymentPolicy;
|
|
11
|
+
resolvePaymentPolicy?: (input: StorefrontBookingSessionBootstrapInput & StorefrontBootstrapRequestContext) => Promise<PaymentPolicy | null | undefined> | PaymentPolicy | null | undefined;
|
|
12
|
+
today?: Date;
|
|
13
|
+
}
|
|
14
|
+
export declare function bootstrapStorefrontBookingSession(context: StorefrontBootstrapRequestContext, input: StorefrontBookingSessionBootstrapInput, options: StorefrontBookingSessionBootstrapOptions | undefined, userId?: string): Promise<{
|
|
15
|
+
status: Exclude<string, "ok">;
|
|
16
|
+
} | {
|
|
17
|
+
status: "stale_quote";
|
|
18
|
+
repricing: {
|
|
19
|
+
originalQuote: {
|
|
20
|
+
currencyCode: string;
|
|
21
|
+
totalSellAmountCents: number;
|
|
22
|
+
quotedAt?: string | null | undefined;
|
|
23
|
+
expiresAt?: string | null | undefined;
|
|
24
|
+
};
|
|
25
|
+
current: {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
catalogId: string | null;
|
|
28
|
+
currencyCode: string;
|
|
29
|
+
totalSellAmountCents: number;
|
|
30
|
+
items: {
|
|
31
|
+
inputIndex: number;
|
|
32
|
+
itemId: string;
|
|
33
|
+
title: string;
|
|
34
|
+
productId: string | null;
|
|
35
|
+
optionId: string | null;
|
|
36
|
+
optionUnitId: string | null;
|
|
37
|
+
optionUnitName: string | null;
|
|
38
|
+
optionUnitType: string | null;
|
|
39
|
+
pricingCategoryId: string | null;
|
|
40
|
+
quantity: number;
|
|
41
|
+
pricingMode: string;
|
|
42
|
+
unitSellAmountCents: number | null;
|
|
43
|
+
totalSellAmountCents: number | null;
|
|
44
|
+
warnings: string[];
|
|
45
|
+
}[];
|
|
46
|
+
warnings: never[];
|
|
47
|
+
appliedToSession: boolean;
|
|
48
|
+
};
|
|
49
|
+
deltaAmountCents: number;
|
|
50
|
+
staleQuote: boolean;
|
|
51
|
+
};
|
|
52
|
+
bootstrap?: undefined;
|
|
53
|
+
} | {
|
|
54
|
+
status: "ok";
|
|
55
|
+
bootstrap: {
|
|
56
|
+
session: {
|
|
57
|
+
sessionId: string;
|
|
58
|
+
bookingNumber: string;
|
|
59
|
+
status: "draft" | "confirmed" | "cancelled" | "on_hold" | "awaiting_payment" | "in_progress" | "completed" | "expired";
|
|
60
|
+
externalBookingRef: string | null;
|
|
61
|
+
communicationLanguage: string | null;
|
|
62
|
+
sellCurrency: string;
|
|
63
|
+
sellAmountCents: number | null;
|
|
64
|
+
startDate: string | null;
|
|
65
|
+
endDate: string | null;
|
|
66
|
+
pax: number | null;
|
|
67
|
+
holdExpiresAt: string | null;
|
|
68
|
+
confirmedAt: string | null;
|
|
69
|
+
expiredAt: string | null;
|
|
70
|
+
cancelledAt: string | null;
|
|
71
|
+
completedAt: string | null;
|
|
72
|
+
travelers: {
|
|
73
|
+
id: string;
|
|
74
|
+
participantType: "other" | "traveler" | "occupant";
|
|
75
|
+
travelerCategory: "other" | "adult" | "child" | "infant" | "senior" | null;
|
|
76
|
+
firstName: string;
|
|
77
|
+
lastName: string;
|
|
78
|
+
email: string | null;
|
|
79
|
+
phone: string | null;
|
|
80
|
+
preferredLanguage: string | null;
|
|
81
|
+
specialRequests: string | null;
|
|
82
|
+
isPrimary: boolean;
|
|
83
|
+
notes: string | null;
|
|
84
|
+
}[];
|
|
85
|
+
items: {
|
|
86
|
+
id: string;
|
|
87
|
+
title: string;
|
|
88
|
+
description: string | null;
|
|
89
|
+
itemType: "service" | "unit" | "other" | "extra" | "fee" | "tax" | "discount" | "adjustment" | "accommodation" | "transport";
|
|
90
|
+
status: "draft" | "confirmed" | "cancelled" | "fulfilled" | "on_hold" | "expired";
|
|
91
|
+
serviceDate: string | null;
|
|
92
|
+
startsAt: string | null;
|
|
93
|
+
endsAt: string | null;
|
|
94
|
+
quantity: number;
|
|
95
|
+
sellCurrency: string;
|
|
96
|
+
unitSellAmountCents: number | null;
|
|
97
|
+
totalSellAmountCents: number | null;
|
|
98
|
+
costCurrency: string | null;
|
|
99
|
+
unitCostAmountCents: number | null;
|
|
100
|
+
totalCostAmountCents: number | null;
|
|
101
|
+
notes: string | null;
|
|
102
|
+
productId: string | null;
|
|
103
|
+
optionId: string | null;
|
|
104
|
+
optionUnitId: string | null;
|
|
105
|
+
pricingCategoryId: string | null;
|
|
106
|
+
travelerLinks: {
|
|
107
|
+
id: string;
|
|
108
|
+
travelerId: string;
|
|
109
|
+
role: string;
|
|
110
|
+
isPrimary: boolean;
|
|
111
|
+
}[];
|
|
112
|
+
}[];
|
|
113
|
+
allocations: {
|
|
114
|
+
id: string;
|
|
115
|
+
bookingItemId: string;
|
|
116
|
+
productId: string | null;
|
|
117
|
+
optionId: string | null;
|
|
118
|
+
optionUnitId: string | null;
|
|
119
|
+
pricingCategoryId: string | null;
|
|
120
|
+
availabilitySlotId: string | null;
|
|
121
|
+
quantity: number;
|
|
122
|
+
allocationType: "unit" | "pickup" | "resource";
|
|
123
|
+
status: "confirmed" | "cancelled" | "fulfilled" | "expired" | "held" | "released";
|
|
124
|
+
holdExpiresAt: string | null;
|
|
125
|
+
confirmedAt: string | null;
|
|
126
|
+
releasedAt: string | null;
|
|
127
|
+
}[];
|
|
128
|
+
checklist: {
|
|
129
|
+
hasTravelers: boolean;
|
|
130
|
+
hasPrimaryTraveler: boolean;
|
|
131
|
+
hasItems: boolean;
|
|
132
|
+
hasAllocations: boolean;
|
|
133
|
+
readyForConfirmation: boolean;
|
|
134
|
+
};
|
|
135
|
+
state: {
|
|
136
|
+
sessionId: string;
|
|
137
|
+
stateKey: "wizard";
|
|
138
|
+
currentStep: string | null;
|
|
139
|
+
completedSteps: string[];
|
|
140
|
+
payload: Record<string, unknown>;
|
|
141
|
+
version: number;
|
|
142
|
+
createdAt: string;
|
|
143
|
+
updatedAt: string;
|
|
144
|
+
} | null;
|
|
145
|
+
};
|
|
146
|
+
paymentPlan: {
|
|
147
|
+
source: "storefront_default";
|
|
148
|
+
depositKind: import("@voyant-travel/finance").DepositKind;
|
|
149
|
+
depositPercent: number | null;
|
|
150
|
+
depositAmountCents: number | null;
|
|
151
|
+
requiresFullPayment: boolean;
|
|
152
|
+
};
|
|
153
|
+
paymentSchedule: {
|
|
154
|
+
id: string;
|
|
155
|
+
scheduleType: "other" | "deposit" | "installment" | "balance" | "hold";
|
|
156
|
+
status: "pending" | "cancelled" | "waived" | "expired" | "paid" | "due";
|
|
157
|
+
dueDate: string;
|
|
158
|
+
currency: string;
|
|
159
|
+
amountCents: number;
|
|
160
|
+
notes: string | null;
|
|
161
|
+
}[];
|
|
162
|
+
repricing: {
|
|
163
|
+
originalQuote: {
|
|
164
|
+
currencyCode: string;
|
|
165
|
+
totalSellAmountCents: number;
|
|
166
|
+
quotedAt?: string | null | undefined;
|
|
167
|
+
expiresAt?: string | null | undefined;
|
|
168
|
+
};
|
|
169
|
+
current: {
|
|
170
|
+
sessionId: string;
|
|
171
|
+
items: {
|
|
172
|
+
itemId: string;
|
|
173
|
+
title: string;
|
|
174
|
+
productId: string | null;
|
|
175
|
+
optionId: string | null;
|
|
176
|
+
optionUnitId: string | null;
|
|
177
|
+
optionUnitName: string | null;
|
|
178
|
+
optionUnitType: string | null;
|
|
179
|
+
pricingCategoryId: string | null;
|
|
180
|
+
quantity: number;
|
|
181
|
+
pricingMode: string;
|
|
182
|
+
unitSellAmountCents: number | null;
|
|
183
|
+
totalSellAmountCents: number | null;
|
|
184
|
+
warnings: string[];
|
|
185
|
+
}[];
|
|
186
|
+
catalogId: string | null;
|
|
187
|
+
currencyCode: string;
|
|
188
|
+
totalSellAmountCents: number;
|
|
189
|
+
warnings: never[];
|
|
190
|
+
appliedToSession: boolean;
|
|
191
|
+
};
|
|
192
|
+
deltaAmountCents: number;
|
|
193
|
+
staleQuote: boolean;
|
|
194
|
+
};
|
|
195
|
+
availability: {
|
|
196
|
+
departureId: string;
|
|
197
|
+
slotId: string;
|
|
198
|
+
productId: string;
|
|
199
|
+
optionId: string | null;
|
|
200
|
+
dateLocal: string | Date;
|
|
201
|
+
startsAt: string | null;
|
|
202
|
+
endsAt: string | null;
|
|
203
|
+
timezone: string;
|
|
204
|
+
status: import("./service-boundary-sql.js").StorefrontSlotStatus;
|
|
205
|
+
capacity: number | null;
|
|
206
|
+
remaining: number | null;
|
|
207
|
+
};
|
|
208
|
+
allocation: {
|
|
209
|
+
id: string;
|
|
210
|
+
bookingItemId: string;
|
|
211
|
+
productId: string | null;
|
|
212
|
+
optionId: string | null;
|
|
213
|
+
optionUnitId: string | null;
|
|
214
|
+
pricingCategoryId: string | null;
|
|
215
|
+
availabilitySlotId: string | null;
|
|
216
|
+
quantity: number;
|
|
217
|
+
allocationType: "unit" | "pickup" | "resource";
|
|
218
|
+
status: "confirmed" | "cancelled" | "fulfilled" | "expired" | "held" | "released";
|
|
219
|
+
holdExpiresAt: string | null;
|
|
220
|
+
confirmedAt: string | null;
|
|
221
|
+
releasedAt: string | null;
|
|
222
|
+
}[];
|
|
223
|
+
currency: string;
|
|
224
|
+
};
|
|
225
|
+
repricing?: undefined;
|
|
226
|
+
}>;
|
|
227
|
+
//# sourceMappingURL=service-booking-session-bootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-booking-session-bootstrap.d.ts","sourceRoot":"","sources":["../src/service-booking-session-bootstrap.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAGjE,OAAO,KAAK,EAAE,sCAAsC,EAAE,MAAM,iBAAiB,CAAA;AAE7E,MAAM,WAAW,iCAAiC;IAChD,EAAE,EAAE,kBAAkB,CAAA;IACtB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,wCAAwC;IACvD,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B,oBAAoB,CAAC,EAAE,CACrB,KAAK,EAAE,sCAAsC,GAAG,iCAAiC,KAC9E,OAAO,CAAC,aAAa,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,aAAa,GAAG,IAAI,GAAG,SAAS,CAAA;IACjF,KAAK,CAAC,EAAE,IAAI,CAAA;CACb;AAqPD,wBAAsB,iCAAiC,CACrD,OAAO,EAAE,iCAAiC,EAC1C,KAAK,EAAE,sCAAsC,EAC7C,OAAO,EAAE,wCAAwC,GAAG,SAAS,EAC7D,MAAM,CAAC,EAAE,MAAM;;;;;;;;;;;;;;;;;4BAhKD,MAAM;wBACV,MAAM;uBACP,MAAM;2BACF,MAAM,GAAG,IAAI;0BACd,MAAM,GAAG,IAAI;8BACT,MAAM,GAAG,IAAI;gCACX,MAAM,GAAG,IAAI;gCACb,MAAM,GAAG,IAAI;mCACV,MAAM,GAAG,IAAI;0BACtB,MAAM;6BACH,MAAM;qCACE,MAAM,GAAG,IAAI;sCACZ,MAAM,GAAG,IAAI;0BACzB,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAXX,MAAM;+BACF,MAAM,GAAG,IAAI;8BACd,MAAM,GAAG,IAAI;kCACT,MAAM,GAAG,IAAI;oCACX,MAAM,GAAG,IAAI;oCACb,MAAM,GAAG,IAAI;uCACV,MAAM,GAAG,IAAI;8BACtB,MAAM;iCACH,MAAM;yCACE,MAAM,GAAG,IAAI;0CACZ,MAAM,GAAG,IAAI;8BACzB,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmTrB"}
|