@voyantjs/bookings 0.119.2 → 0.119.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pricing-assignment/age.d.ts +33 -0
- package/dist/pricing-assignment/age.d.ts.map +1 -0
- package/dist/pricing-assignment/age.js +109 -0
- package/dist/pricing-assignment/draft.d.ts +69 -0
- package/dist/pricing-assignment/draft.d.ts.map +1 -0
- package/dist/pricing-assignment/draft.js +284 -0
- package/dist/pricing-assignment/types.d.ts +113 -0
- package/dist/pricing-assignment/types.d.ts.map +1 -0
- package/dist/pricing-assignment/types.js +30 -0
- package/dist/pricing-assignment/unit-helpers.d.ts +11 -0
- package/dist/pricing-assignment/unit-helpers.d.ts.map +1 -0
- package/dist/pricing-assignment/unit-helpers.js +19 -0
- package/dist/pricing-assignment/verify.d.ts +67 -0
- package/dist/pricing-assignment/verify.d.ts.map +1 -0
- package/dist/pricing-assignment/verify.js +121 -0
- package/dist/pricing-assignment.d.ts +4 -275
- package/dist/pricing-assignment.d.ts.map +1 -1
- package/dist/pricing-assignment.js +4 -559
- package/dist/routes-admin.d.ts +2894 -0
- package/dist/routes-admin.d.ts.map +1 -0
- package/dist/routes-admin.js +2111 -0
- package/dist/routes.d.ts +1 -2893
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +1 -2111
- package/dist/service-core.d.ts +6240 -0
- package/dist/service-core.d.ts.map +1 -0
- package/dist/service-core.js +4350 -0
- package/dist/service-public-core.d.ts +907 -0
- package/dist/service-public-core.d.ts.map +1 -0
- package/dist/service-public-core.js +1176 -0
- package/dist/service-public.d.ts +1 -906
- package/dist/service-public.d.ts.map +1 -1
- package/dist/service-public.js +1 -1176
- package/dist/service.d.ts +1 -6239
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +1 -4350
- package/package.json +5 -5
package/dist/service-public.js
CHANGED
|
@@ -1,1176 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { and, asc, desc, eq, inArray, or } from "drizzle-orm";
|
|
3
|
-
import { departurePriceOverridesRef, optionPriceRulesRef, optionUnitPriceRulesRef, optionUnitTiersRef, priceCatalogsRef, pricingCategoriesRef, } from "./pricing-ref.js";
|
|
4
|
-
import { optionUnitsRef, productOptionsRef, productsRef } from "./products-ref.js";
|
|
5
|
-
import { bookingAllocations, bookingDocuments, bookingFulfillments, bookingItems, bookingItemTravelers, bookingSessionStates, bookings, bookingTravelers, } from "./schema.js";
|
|
6
|
-
import { bookingsService } from "./service.js";
|
|
7
|
-
import { publicBookingSessionTravelerInputSchema } from "./validation-public.js";
|
|
8
|
-
const BOOKING_PERSON_SOURCE = "storefront-booking";
|
|
9
|
-
async function safeResolveTravelerPerson(db, resolver, participant, bookingId) {
|
|
10
|
-
if (!resolver)
|
|
11
|
-
return null;
|
|
12
|
-
const contact = {
|
|
13
|
-
firstName: participant.firstName,
|
|
14
|
-
lastName: participant.lastName,
|
|
15
|
-
email: participant.email,
|
|
16
|
-
phone: participant.phone,
|
|
17
|
-
preferredLanguage: participant.preferredLanguage,
|
|
18
|
-
};
|
|
19
|
-
try {
|
|
20
|
-
return await resolver(db, contact, {
|
|
21
|
-
bookingId,
|
|
22
|
-
source: BOOKING_PERSON_SOURCE,
|
|
23
|
-
sourceRef: bookingId,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
// Person resolution is best-effort — a CRM hiccup must not block
|
|
28
|
-
// the actual booking. Log and fall back to `null` so the traveler
|
|
29
|
-
// row still lands without a person link.
|
|
30
|
-
console.error("[bookings] resolveTravelerPerson failed for booking", bookingId, error);
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
async function safeResolveBillingPerson(db, resolver, contact, bookingId) {
|
|
35
|
-
if (!resolver)
|
|
36
|
-
return null;
|
|
37
|
-
try {
|
|
38
|
-
return await resolver(db, contact, {
|
|
39
|
-
bookingId,
|
|
40
|
-
source: BOOKING_PERSON_SOURCE,
|
|
41
|
-
sourceRef: bookingId,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
console.error("[bookings] resolveBillingPerson failed for booking", bookingId, error);
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const travelerParticipantTypeValues = ["traveler", "occupant"];
|
|
50
|
-
const travelerParticipantTypes = new Set(travelerParticipantTypeValues);
|
|
51
|
-
const WIZARD_STATE_KEY = "wizard";
|
|
52
|
-
function normalizeDate(value) {
|
|
53
|
-
if (!value) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
if (value instanceof Date) {
|
|
57
|
-
return value.toISOString().slice(0, 10);
|
|
58
|
-
}
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
function normalizeDateTime(value) {
|
|
62
|
-
if (!value) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
return value instanceof Date ? value.toISOString() : value;
|
|
66
|
-
}
|
|
67
|
-
function getRecord(value) {
|
|
68
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
69
|
-
? value
|
|
70
|
-
: null;
|
|
71
|
-
}
|
|
72
|
-
function getNestedRecord(record, keys) {
|
|
73
|
-
for (const key of keys) {
|
|
74
|
-
const value = getRecord(record?.[key]);
|
|
75
|
-
if (value) {
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
function getRecordString(record, keys) {
|
|
82
|
-
for (const key of keys) {
|
|
83
|
-
const value = record?.[key];
|
|
84
|
-
if (typeof value === "string" && value.length > 0) {
|
|
85
|
-
return value;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
function getBillingPartyType(record) {
|
|
91
|
-
const value = getRecordString(record, ["partyType", "type"]);
|
|
92
|
-
return value === "individual" || value === "company" ? value : null;
|
|
93
|
-
}
|
|
94
|
-
function getArray(value) {
|
|
95
|
-
return Array.isArray(value) ? value : null;
|
|
96
|
-
}
|
|
97
|
-
function extractBookingContactFromStatePayload(payload) {
|
|
98
|
-
const root = getRecord(payload);
|
|
99
|
-
const stepData = getNestedRecord(root, ["stepData", "steps"]);
|
|
100
|
-
const billingRecord = getNestedRecord(root, ["billing", "billingContact", "contact"]) ??
|
|
101
|
-
getNestedRecord(stepData, ["billing", "billingContact", "contact"]);
|
|
102
|
-
const billing = getNestedRecord(billingRecord, ["billing", "contact"]) ?? billingRecord;
|
|
103
|
-
if (!billing) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
contactFirstName: getRecordString(billing, ["firstName"]),
|
|
108
|
-
contactLastName: getRecordString(billing, ["lastName"]),
|
|
109
|
-
contactPartyType: getBillingPartyType(billing),
|
|
110
|
-
contactTaxId: getRecordString(billing, ["taxId", "vatNumber", "vatId"]),
|
|
111
|
-
contactEmail: getRecordString(billing, ["email"]),
|
|
112
|
-
contactPhone: getRecordString(billing, ["phone"]),
|
|
113
|
-
contactCountry: getRecordString(billing, ["country"]),
|
|
114
|
-
contactRegion: getRecordString(billing, ["state", "region"]),
|
|
115
|
-
contactCity: getRecordString(billing, ["city"]),
|
|
116
|
-
contactAddressLine1: getRecordString(billing, ["addressLine1", "address1", "line1"]),
|
|
117
|
-
contactAddressLine2: getRecordString(billing, ["addressLine2", "address2", "line2"]),
|
|
118
|
-
contactPostalCode: getRecordString(billing, ["postalCode", "postal", "zip"]),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
function extractBookingTravelersFromStatePayload(payload) {
|
|
122
|
-
const root = getRecord(payload);
|
|
123
|
-
const stepData = getNestedRecord(root, ["stepData", "steps"]);
|
|
124
|
-
const travelersRecord = getNestedRecord(root, ["travelers"]) ?? getNestedRecord(stepData, ["travelers"]);
|
|
125
|
-
const travelers = getArray(travelersRecord?.travelers) ??
|
|
126
|
-
getArray(root?.travelers) ??
|
|
127
|
-
getArray(stepData?.travelers);
|
|
128
|
-
if (!travelers) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
const parsed = publicBookingSessionTravelerInputSchema.array().safeParse(travelers);
|
|
132
|
-
return parsed.success ? parsed.data : null;
|
|
133
|
-
}
|
|
134
|
-
function countTravelerParticipants(participants) {
|
|
135
|
-
return participants.filter((participant) => travelerParticipantTypes.has(participant.participantType)).length;
|
|
136
|
-
}
|
|
137
|
-
async function resolveTravelerPersonForParticipant(db, resolver, participant, bookingId, existingPersonId) {
|
|
138
|
-
if (existingPersonId) {
|
|
139
|
-
return existingPersonId;
|
|
140
|
-
}
|
|
141
|
-
return safeResolveTravelerPerson(db, resolver, {
|
|
142
|
-
firstName: participant.firstName,
|
|
143
|
-
lastName: participant.lastName,
|
|
144
|
-
email: participant.email ?? null,
|
|
145
|
-
phone: participant.phone ?? null,
|
|
146
|
-
preferredLanguage: participant.preferredLanguage ?? null,
|
|
147
|
-
}, bookingId);
|
|
148
|
-
}
|
|
149
|
-
async function syncTravelerRowsFromStatePayload(db, bookingId, payload, resolvers, userId) {
|
|
150
|
-
const travelers = extractBookingTravelersFromStatePayload(payload);
|
|
151
|
-
if (!travelers) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const existingTravelers = await db
|
|
155
|
-
.select()
|
|
156
|
-
.from(bookingTravelers)
|
|
157
|
-
.where(and(eq(bookingTravelers.bookingId, bookingId), inArray(bookingTravelers.participantType, [...travelerParticipantTypeValues])))
|
|
158
|
-
.orderBy(asc(bookingTravelers.createdAt));
|
|
159
|
-
const existingById = new Map(existingTravelers.map((traveler) => [traveler.id, traveler]));
|
|
160
|
-
const usedExistingIds = new Set();
|
|
161
|
-
for (const participant of travelers) {
|
|
162
|
-
const matchingExisting = (participant.id ? existingById.get(participant.id) : undefined) ??
|
|
163
|
-
(participant.id
|
|
164
|
-
? undefined
|
|
165
|
-
: existingTravelers.find((traveler) => !usedExistingIds.has(traveler.id)));
|
|
166
|
-
const hasStableContactPoint = Boolean(participant.email?.trim() || participant.phone?.trim());
|
|
167
|
-
const reusablePersonId = participant.id || !hasStableContactPoint ? (matchingExisting?.personId ?? null) : null;
|
|
168
|
-
const personId = await resolveTravelerPersonForParticipant(db, resolvers.resolveTravelerPerson, participant, bookingId, reusablePersonId);
|
|
169
|
-
const travelerRecord = {
|
|
170
|
-
participantType: participant.participantType,
|
|
171
|
-
travelerCategory: participant.travelerCategory ?? null,
|
|
172
|
-
firstName: participant.firstName,
|
|
173
|
-
lastName: participant.lastName,
|
|
174
|
-
email: participant.email ?? null,
|
|
175
|
-
phone: participant.phone ?? null,
|
|
176
|
-
preferredLanguage: participant.preferredLanguage ?? null,
|
|
177
|
-
specialRequests: participant.specialRequests ?? null,
|
|
178
|
-
isPrimary: participant.isPrimary,
|
|
179
|
-
notes: participant.notes ?? null,
|
|
180
|
-
personId,
|
|
181
|
-
};
|
|
182
|
-
if (matchingExisting) {
|
|
183
|
-
await bookingsService.updateTravelerRecord(db, matchingExisting.id, travelerRecord);
|
|
184
|
-
usedExistingIds.add(matchingExisting.id);
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
const created = await bookingsService.createTravelerRecord(db, bookingId, travelerRecord, userId);
|
|
188
|
-
if (created) {
|
|
189
|
-
usedExistingIds.add(created.id);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
for (const existing of existingTravelers) {
|
|
193
|
-
if (!usedExistingIds.has(existing.id)) {
|
|
194
|
-
await bookingsService.deleteTravelerRecord(db, existing.id);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
const travelerCount = countTravelerParticipants(travelers);
|
|
198
|
-
await bookingsService.updateBooking(db, bookingId, {
|
|
199
|
-
pax: travelerCount > 0 ? travelerCount : null,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
function normalizeSessionState(bookingId, state) {
|
|
203
|
-
if (!state) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
return {
|
|
207
|
-
sessionId: bookingId,
|
|
208
|
-
stateKey: WIZARD_STATE_KEY,
|
|
209
|
-
currentStep: state.currentStep ?? null,
|
|
210
|
-
completedSteps: state.completedSteps ?? [],
|
|
211
|
-
payload: state.payload ?? {},
|
|
212
|
-
version: state.version,
|
|
213
|
-
createdAt: normalizeDateTime(state.createdAt),
|
|
214
|
-
updatedAt: normalizeDateTime(state.updatedAt),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
async function getWizardSessionState(db, bookingId) {
|
|
218
|
-
const [state] = await db
|
|
219
|
-
.select()
|
|
220
|
-
.from(bookingSessionStates)
|
|
221
|
-
.where(and(eq(bookingSessionStates.bookingId, bookingId), eq(bookingSessionStates.stateKey, WIZARD_STATE_KEY)))
|
|
222
|
-
.limit(1);
|
|
223
|
-
return normalizeSessionState(bookingId, state);
|
|
224
|
-
}
|
|
225
|
-
async function upsertWizardSessionState(db, bookingId, input) {
|
|
226
|
-
const [existing] = await db
|
|
227
|
-
.select()
|
|
228
|
-
.from(bookingSessionStates)
|
|
229
|
-
.where(and(eq(bookingSessionStates.bookingId, bookingId), eq(bookingSessionStates.stateKey, WIZARD_STATE_KEY)))
|
|
230
|
-
.limit(1);
|
|
231
|
-
const payload = input.replacePayload
|
|
232
|
-
? input.payload
|
|
233
|
-
: {
|
|
234
|
-
...(existing?.payload ?? {}),
|
|
235
|
-
...input.payload,
|
|
236
|
-
};
|
|
237
|
-
const completedSteps = input.completedSteps ?? existing?.completedSteps ?? [];
|
|
238
|
-
const currentStep = input.currentStep === undefined ? (existing?.currentStep ?? null) : (input.currentStep ?? null);
|
|
239
|
-
if (existing) {
|
|
240
|
-
const [updated] = await db
|
|
241
|
-
.update(bookingSessionStates)
|
|
242
|
-
.set({
|
|
243
|
-
currentStep,
|
|
244
|
-
completedSteps,
|
|
245
|
-
payload,
|
|
246
|
-
version: existing.version + 1,
|
|
247
|
-
updatedAt: new Date(),
|
|
248
|
-
})
|
|
249
|
-
.where(eq(bookingSessionStates.id, existing.id))
|
|
250
|
-
.returning();
|
|
251
|
-
return normalizeSessionState(bookingId, updated ?? existing);
|
|
252
|
-
}
|
|
253
|
-
const [created] = await db
|
|
254
|
-
.insert(bookingSessionStates)
|
|
255
|
-
.values({
|
|
256
|
-
bookingId,
|
|
257
|
-
stateKey: WIZARD_STATE_KEY,
|
|
258
|
-
currentStep,
|
|
259
|
-
completedSteps,
|
|
260
|
-
payload,
|
|
261
|
-
version: 1,
|
|
262
|
-
})
|
|
263
|
-
.returning();
|
|
264
|
-
return normalizeSessionState(bookingId, created);
|
|
265
|
-
}
|
|
266
|
-
function resolveTierAmount(tiers, quantity, fallbackAmount) {
|
|
267
|
-
const tier = tiers.find((candidate) => quantity >= candidate.minQuantity &&
|
|
268
|
-
(candidate.maxQuantity === null || quantity <= candidate.maxQuantity));
|
|
269
|
-
return tier?.sellAmountCents ?? fallbackAmount;
|
|
270
|
-
}
|
|
271
|
-
function computeLineTotal(pricingMode, unitSellAmountCents, quantity, fallbackAmount) {
|
|
272
|
-
switch (pricingMode) {
|
|
273
|
-
case "free":
|
|
274
|
-
case "included":
|
|
275
|
-
return 0;
|
|
276
|
-
case "on_request":
|
|
277
|
-
return null;
|
|
278
|
-
case "per_unit":
|
|
279
|
-
case "per_person":
|
|
280
|
-
return unitSellAmountCents === null ? null : unitSellAmountCents * quantity;
|
|
281
|
-
default:
|
|
282
|
-
return unitSellAmountCents ?? fallbackAmount;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
async function generateBookingNumber(db) {
|
|
286
|
-
const now = new Date();
|
|
287
|
-
const y = now.getFullYear().toString().slice(-2);
|
|
288
|
-
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
289
|
-
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
290
|
-
const suffix = String(Math.floor(Math.random() * 900000) + 100000);
|
|
291
|
-
const bookingNumber = `BK-${y}${m}-${suffix}`;
|
|
292
|
-
const [existing] = await db
|
|
293
|
-
.select({ id: bookings.id })
|
|
294
|
-
.from(bookings)
|
|
295
|
-
.where(eq(bookings.bookingNumber, bookingNumber))
|
|
296
|
-
.limit(1);
|
|
297
|
-
if (!existing) {
|
|
298
|
-
return bookingNumber;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
throw new Error("Unable to generate a unique booking number");
|
|
302
|
-
}
|
|
303
|
-
async function buildOverviewSnapshot(db, query) {
|
|
304
|
-
const bookingLookupNumber = query.bookingNumber ?? query.bookingCode ?? null;
|
|
305
|
-
const [booking] = await db
|
|
306
|
-
.select()
|
|
307
|
-
.from(bookings)
|
|
308
|
-
.where(query.bookingId
|
|
309
|
-
? eq(bookings.id, query.bookingId)
|
|
310
|
-
: bookingLookupNumber
|
|
311
|
-
? eq(bookings.bookingNumber, bookingLookupNumber)
|
|
312
|
-
: eq(bookings.id, "__missing__"))
|
|
313
|
-
.limit(1);
|
|
314
|
-
if (!booking) {
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
const [participants, items, itemParticipantLinks, documents, fulfillments] = await Promise.all([
|
|
318
|
-
db
|
|
319
|
-
.select()
|
|
320
|
-
.from(bookingTravelers)
|
|
321
|
-
.where(eq(bookingTravelers.bookingId, booking.id))
|
|
322
|
-
.orderBy(asc(bookingTravelers.createdAt)),
|
|
323
|
-
db
|
|
324
|
-
.select()
|
|
325
|
-
.from(bookingItems)
|
|
326
|
-
.where(eq(bookingItems.bookingId, booking.id))
|
|
327
|
-
.orderBy(asc(bookingItems.createdAt)),
|
|
328
|
-
db
|
|
329
|
-
.select({
|
|
330
|
-
id: bookingItemTravelers.id,
|
|
331
|
-
bookingItemId: bookingItemTravelers.bookingItemId,
|
|
332
|
-
travelerId: bookingItemTravelers.travelerId,
|
|
333
|
-
role: bookingItemTravelers.role,
|
|
334
|
-
isPrimary: bookingItemTravelers.isPrimary,
|
|
335
|
-
})
|
|
336
|
-
.from(bookingItemTravelers)
|
|
337
|
-
.innerJoin(bookingItems, eq(bookingItems.id, bookingItemTravelers.bookingItemId))
|
|
338
|
-
.where(eq(bookingItems.bookingId, booking.id))
|
|
339
|
-
.orderBy(asc(bookingItemTravelers.createdAt)),
|
|
340
|
-
db
|
|
341
|
-
.select()
|
|
342
|
-
.from(bookingDocuments)
|
|
343
|
-
.where(eq(bookingDocuments.bookingId, booking.id))
|
|
344
|
-
.orderBy(asc(bookingDocuments.createdAt)),
|
|
345
|
-
db
|
|
346
|
-
.select()
|
|
347
|
-
.from(bookingFulfillments)
|
|
348
|
-
.where(eq(bookingFulfillments.bookingId, booking.id))
|
|
349
|
-
.orderBy(asc(bookingFulfillments.createdAt)),
|
|
350
|
-
]);
|
|
351
|
-
const email = query.email?.trim().toLowerCase() ?? null;
|
|
352
|
-
if (email) {
|
|
353
|
-
const authorized = participants.some((participant) => constantTimeEqualString(participant.email?.trim().toLowerCase() ?? "", email));
|
|
354
|
-
if (!authorized) {
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
const itemLinksByItemId = new Map();
|
|
359
|
-
for (const link of itemParticipantLinks) {
|
|
360
|
-
const existing = itemLinksByItemId.get(link.bookingItemId) ?? [];
|
|
361
|
-
existing.push({
|
|
362
|
-
id: link.id,
|
|
363
|
-
travelerId: link.travelerId,
|
|
364
|
-
role: link.role,
|
|
365
|
-
isPrimary: link.isPrimary,
|
|
366
|
-
});
|
|
367
|
-
itemLinksByItemId.set(link.bookingItemId, existing);
|
|
368
|
-
}
|
|
369
|
-
return {
|
|
370
|
-
bookingId: booking.id,
|
|
371
|
-
bookingNumber: booking.bookingNumber,
|
|
372
|
-
status: booking.status,
|
|
373
|
-
sellCurrency: booking.sellCurrency,
|
|
374
|
-
sellAmountCents: booking.sellAmountCents ?? null,
|
|
375
|
-
startDate: normalizeDate(booking.startDate),
|
|
376
|
-
endDate: normalizeDate(booking.endDate),
|
|
377
|
-
pax: booking.pax ?? null,
|
|
378
|
-
confirmedAt: normalizeDateTime(booking.confirmedAt),
|
|
379
|
-
cancelledAt: normalizeDateTime(booking.cancelledAt),
|
|
380
|
-
completedAt: normalizeDateTime(booking.completedAt),
|
|
381
|
-
travelers: participants.map((participant) => ({
|
|
382
|
-
id: participant.id,
|
|
383
|
-
participantType: participant.participantType,
|
|
384
|
-
firstName: participant.firstName,
|
|
385
|
-
lastName: participant.lastName,
|
|
386
|
-
isPrimary: participant.isPrimary,
|
|
387
|
-
})),
|
|
388
|
-
items: items.map((item) => ({
|
|
389
|
-
id: item.id,
|
|
390
|
-
title: item.title,
|
|
391
|
-
description: item.description ?? null,
|
|
392
|
-
itemType: item.itemType,
|
|
393
|
-
status: item.status,
|
|
394
|
-
serviceDate: normalizeDate(item.serviceDate),
|
|
395
|
-
startsAt: normalizeDateTime(item.startsAt),
|
|
396
|
-
endsAt: normalizeDateTime(item.endsAt),
|
|
397
|
-
quantity: item.quantity,
|
|
398
|
-
sellCurrency: item.sellCurrency,
|
|
399
|
-
unitSellAmountCents: item.unitSellAmountCents ?? null,
|
|
400
|
-
totalSellAmountCents: item.totalSellAmountCents ?? null,
|
|
401
|
-
costCurrency: item.costCurrency ?? null,
|
|
402
|
-
unitCostAmountCents: item.unitCostAmountCents ?? null,
|
|
403
|
-
totalCostAmountCents: item.totalCostAmountCents ?? null,
|
|
404
|
-
notes: item.notes ?? null,
|
|
405
|
-
productId: item.productId ?? null,
|
|
406
|
-
optionId: item.optionId ?? null,
|
|
407
|
-
optionUnitId: item.optionUnitId ?? null,
|
|
408
|
-
pricingCategoryId: item.pricingCategoryId ?? null,
|
|
409
|
-
travelerLinks: itemLinksByItemId.get(item.id) ?? [],
|
|
410
|
-
})),
|
|
411
|
-
documents: documents.map((document) => ({
|
|
412
|
-
id: document.id,
|
|
413
|
-
travelerId: document.travelerId ?? null,
|
|
414
|
-
type: document.type,
|
|
415
|
-
fileName: document.fileName,
|
|
416
|
-
fileUrl: document.fileUrl,
|
|
417
|
-
})),
|
|
418
|
-
fulfillments: fulfillments.map((fulfillment) => ({
|
|
419
|
-
id: fulfillment.id,
|
|
420
|
-
bookingItemId: fulfillment.bookingItemId ?? null,
|
|
421
|
-
travelerId: fulfillment.travelerId ?? null,
|
|
422
|
-
fulfillmentType: fulfillment.fulfillmentType,
|
|
423
|
-
deliveryChannel: fulfillment.deliveryChannel,
|
|
424
|
-
status: fulfillment.status,
|
|
425
|
-
artifactUrl: fulfillment.artifactUrl ?? null,
|
|
426
|
-
})),
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
function constantTimeEqualString(left, right) {
|
|
430
|
-
let result = left.length ^ right.length;
|
|
431
|
-
const length = Math.max(left.length, right.length);
|
|
432
|
-
for (let index = 0; index < length; index += 1) {
|
|
433
|
-
result |= (left.charCodeAt(index) || 0) ^ (right.charCodeAt(index) || 0);
|
|
434
|
-
}
|
|
435
|
-
return result === 0;
|
|
436
|
-
}
|
|
437
|
-
function buildUnitWarnings(unit, quantity, sessionPax) {
|
|
438
|
-
const warnings = [];
|
|
439
|
-
if (!unit) {
|
|
440
|
-
return warnings;
|
|
441
|
-
}
|
|
442
|
-
if (unit.minQuantity !== null && quantity < unit.minQuantity) {
|
|
443
|
-
warnings.push(`Selected quantity is below the minimum for ${unit.name}.`);
|
|
444
|
-
}
|
|
445
|
-
if (unit.maxQuantity !== null && quantity > unit.maxQuantity) {
|
|
446
|
-
warnings.push(`Selected quantity is above the maximum for ${unit.name}.`);
|
|
447
|
-
}
|
|
448
|
-
if (sessionPax && unit.occupancyMin !== null && quantity * unit.occupancyMin > sessionPax) {
|
|
449
|
-
warnings.push(`Selected ${unit.name} quantity exceeds the current traveler count.`);
|
|
450
|
-
}
|
|
451
|
-
if (sessionPax && unit.occupancyMax !== null && quantity * unit.occupancyMax < sessionPax) {
|
|
452
|
-
warnings.push(`Selected ${unit.name} quantity does not cover the current traveler count.`);
|
|
453
|
-
}
|
|
454
|
-
return warnings;
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Resolves the catalog-scoped pricing snapshot for a product (options → option
|
|
458
|
-
* price rules → per-unit price rules → tiers). The snapshot is the same data
|
|
459
|
-
* the storefront booking session uses to compute a total — exposing it as a
|
|
460
|
-
* standalone admin preview lets operator dialogs, tour-sheet exports, and
|
|
461
|
-
* reconciliation flows see the same numbers the customer would see, without
|
|
462
|
-
* creating a throwaway session.
|
|
463
|
-
*
|
|
464
|
-
* Returns `null` when the product isn't publicly visible or there's no active
|
|
465
|
-
* catalog / matching option (caller can decide whether to 404 or surface a
|
|
466
|
-
* "pricing unavailable for this selection" message).
|
|
467
|
-
*/
|
|
468
|
-
export async function resolveSessionPricingSnapshot(db, productId, input) {
|
|
469
|
-
const productConditions = [eq(productsRef.id, productId), eq(productsRef.status, "active")];
|
|
470
|
-
if (input.requirePublicProduct ?? true) {
|
|
471
|
-
productConditions.push(eq(productsRef.activated, true), eq(productsRef.visibility, "public"));
|
|
472
|
-
}
|
|
473
|
-
const [product] = await db
|
|
474
|
-
.select({
|
|
475
|
-
id: productsRef.id,
|
|
476
|
-
})
|
|
477
|
-
.from(productsRef)
|
|
478
|
-
.where(and(...productConditions))
|
|
479
|
-
.limit(1);
|
|
480
|
-
if (!product) {
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
const catalog = input.catalogId
|
|
484
|
-
? await db
|
|
485
|
-
.select({
|
|
486
|
-
id: priceCatalogsRef.id,
|
|
487
|
-
currencyCode: priceCatalogsRef.currencyCode,
|
|
488
|
-
})
|
|
489
|
-
.from(priceCatalogsRef)
|
|
490
|
-
.where(and(eq(priceCatalogsRef.id, input.catalogId), eq(priceCatalogsRef.catalogType, "public"), eq(priceCatalogsRef.active, true)))
|
|
491
|
-
.limit(1)
|
|
492
|
-
.then((rows) => rows[0] ?? null)
|
|
493
|
-
: await db
|
|
494
|
-
.select({
|
|
495
|
-
id: priceCatalogsRef.id,
|
|
496
|
-
currencyCode: priceCatalogsRef.currencyCode,
|
|
497
|
-
})
|
|
498
|
-
.from(priceCatalogsRef)
|
|
499
|
-
.where(and(eq(priceCatalogsRef.catalogType, "public"), eq(priceCatalogsRef.active, true)))
|
|
500
|
-
.orderBy(desc(priceCatalogsRef.isDefault), asc(priceCatalogsRef.name))
|
|
501
|
-
.limit(1)
|
|
502
|
-
.then((rows) => rows[0] ?? null);
|
|
503
|
-
if (!catalog) {
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
506
|
-
const optionConditions = [
|
|
507
|
-
eq(productOptionsRef.productId, productId),
|
|
508
|
-
eq(productOptionsRef.status, "active"),
|
|
509
|
-
];
|
|
510
|
-
if (input.optionId) {
|
|
511
|
-
optionConditions.push(eq(productOptionsRef.id, input.optionId));
|
|
512
|
-
}
|
|
513
|
-
const options = await db
|
|
514
|
-
.select({
|
|
515
|
-
id: productOptionsRef.id,
|
|
516
|
-
name: productOptionsRef.name,
|
|
517
|
-
isDefault: productOptionsRef.isDefault,
|
|
518
|
-
})
|
|
519
|
-
.from(productOptionsRef)
|
|
520
|
-
.where(and(...optionConditions))
|
|
521
|
-
.orderBy(desc(productOptionsRef.isDefault), asc(productOptionsRef.sortOrder));
|
|
522
|
-
if (options.length === 0) {
|
|
523
|
-
return null;
|
|
524
|
-
}
|
|
525
|
-
const optionIds = options.map((option) => option.id);
|
|
526
|
-
const [rules, unitPrices] = await Promise.all([
|
|
527
|
-
db
|
|
528
|
-
.select({
|
|
529
|
-
id: optionPriceRulesRef.id,
|
|
530
|
-
optionId: optionPriceRulesRef.optionId,
|
|
531
|
-
pricingMode: optionPriceRulesRef.pricingMode,
|
|
532
|
-
baseSellAmountCents: optionPriceRulesRef.baseSellAmountCents,
|
|
533
|
-
isDefault: optionPriceRulesRef.isDefault,
|
|
534
|
-
})
|
|
535
|
-
.from(optionPriceRulesRef)
|
|
536
|
-
.where(and(eq(optionPriceRulesRef.productId, productId), inArray(optionPriceRulesRef.optionId, optionIds), eq(optionPriceRulesRef.priceCatalogId, catalog.id), eq(optionPriceRulesRef.active, true)))
|
|
537
|
-
.orderBy(desc(optionPriceRulesRef.isDefault), asc(optionPriceRulesRef.name)),
|
|
538
|
-
db
|
|
539
|
-
.select({
|
|
540
|
-
id: optionUnitPriceRulesRef.id,
|
|
541
|
-
optionPriceRuleId: optionUnitPriceRulesRef.optionPriceRuleId,
|
|
542
|
-
optionId: optionUnitPriceRulesRef.optionId,
|
|
543
|
-
unitId: optionUnitPriceRulesRef.unitId,
|
|
544
|
-
unitName: optionUnitsRef.name,
|
|
545
|
-
unitType: optionUnitsRef.unitType,
|
|
546
|
-
occupancyMax: optionUnitsRef.occupancyMax,
|
|
547
|
-
pricingCategoryId: optionUnitPriceRulesRef.pricingCategoryId,
|
|
548
|
-
pricingMode: optionUnitPriceRulesRef.pricingMode,
|
|
549
|
-
sellAmountCents: optionUnitPriceRulesRef.sellAmountCents,
|
|
550
|
-
minQuantity: optionUnitPriceRulesRef.minQuantity,
|
|
551
|
-
maxQuantity: optionUnitPriceRulesRef.maxQuantity,
|
|
552
|
-
})
|
|
553
|
-
.from(optionUnitPriceRulesRef)
|
|
554
|
-
.innerJoin(optionUnitsRef, eq(optionUnitsRef.id, optionUnitPriceRulesRef.unitId))
|
|
555
|
-
.where(and(inArray(optionUnitPriceRulesRef.optionId, optionIds), eq(optionUnitPriceRulesRef.active, true)))
|
|
556
|
-
.orderBy(asc(optionUnitPriceRulesRef.sortOrder), asc(optionUnitPriceRulesRef.createdAt)),
|
|
557
|
-
]);
|
|
558
|
-
const pricingCategoryIds = Array.from(new Set(unitPrices.flatMap((row) => (row.pricingCategoryId ? [row.pricingCategoryId] : []))));
|
|
559
|
-
const [tiers, departureOverrides, pricingCategories] = await Promise.all([
|
|
560
|
-
unitPrices.length > 0
|
|
561
|
-
? db
|
|
562
|
-
.select({
|
|
563
|
-
id: optionUnitTiersRef.id,
|
|
564
|
-
optionUnitPriceRuleId: optionUnitTiersRef.optionUnitPriceRuleId,
|
|
565
|
-
minQuantity: optionUnitTiersRef.minQuantity,
|
|
566
|
-
maxQuantity: optionUnitTiersRef.maxQuantity,
|
|
567
|
-
sellAmountCents: optionUnitTiersRef.sellAmountCents,
|
|
568
|
-
sortOrder: optionUnitTiersRef.sortOrder,
|
|
569
|
-
})
|
|
570
|
-
.from(optionUnitTiersRef)
|
|
571
|
-
.where(and(inArray(optionUnitTiersRef.optionUnitPriceRuleId, unitPrices.map((row) => row.id)), eq(optionUnitTiersRef.active, true)))
|
|
572
|
-
.orderBy(asc(optionUnitTiersRef.sortOrder), asc(optionUnitTiersRef.minQuantity))
|
|
573
|
-
: Promise.resolve([]),
|
|
574
|
-
input.departureId
|
|
575
|
-
? db
|
|
576
|
-
.select({
|
|
577
|
-
optionUnitId: departurePriceOverridesRef.optionUnitId,
|
|
578
|
-
sellAmountCents: departurePriceOverridesRef.sellAmountCents,
|
|
579
|
-
})
|
|
580
|
-
.from(departurePriceOverridesRef)
|
|
581
|
-
.where(and(eq(departurePriceOverridesRef.departureId, input.departureId), eq(departurePriceOverridesRef.priceCatalogId, catalog.id), eq(departurePriceOverridesRef.active, true)))
|
|
582
|
-
: Promise.resolve([]),
|
|
583
|
-
pricingCategoryIds.length > 0
|
|
584
|
-
? db
|
|
585
|
-
.select({
|
|
586
|
-
id: pricingCategoriesRef.id,
|
|
587
|
-
name: pricingCategoriesRef.name,
|
|
588
|
-
code: pricingCategoriesRef.code,
|
|
589
|
-
categoryType: pricingCategoriesRef.categoryType,
|
|
590
|
-
minAge: pricingCategoriesRef.minAge,
|
|
591
|
-
maxAge: pricingCategoriesRef.maxAge,
|
|
592
|
-
metadata: pricingCategoriesRef.metadata,
|
|
593
|
-
sortOrder: pricingCategoriesRef.sortOrder,
|
|
594
|
-
})
|
|
595
|
-
.from(pricingCategoriesRef)
|
|
596
|
-
.where(and(inArray(pricingCategoriesRef.id, pricingCategoryIds), eq(pricingCategoriesRef.active, true)))
|
|
597
|
-
.orderBy(asc(pricingCategoriesRef.sortOrder), asc(pricingCategoriesRef.name))
|
|
598
|
-
: Promise.resolve([]),
|
|
599
|
-
]);
|
|
600
|
-
const tiersByUnitPriceId = new Map();
|
|
601
|
-
for (const tier of tiers) {
|
|
602
|
-
const existing = tiersByUnitPriceId.get(tier.optionUnitPriceRuleId) ?? [];
|
|
603
|
-
existing.push(tier);
|
|
604
|
-
tiersByUnitPriceId.set(tier.optionUnitPriceRuleId, existing);
|
|
605
|
-
}
|
|
606
|
-
const departureOverrideByUnitId = new Map(departureOverrides.map((row) => [row.optionUnitId, row]));
|
|
607
|
-
return {
|
|
608
|
-
catalog: catalog,
|
|
609
|
-
options: options,
|
|
610
|
-
rules: rules,
|
|
611
|
-
pricingCategories: pricingCategories,
|
|
612
|
-
unitPrices: unitPrices.map((row) => {
|
|
613
|
-
const override = departureOverrideByUnitId.get(row.unitId);
|
|
614
|
-
return {
|
|
615
|
-
...row,
|
|
616
|
-
sellAmountCents: override?.sellAmountCents ?? row.sellAmountCents,
|
|
617
|
-
tiers: override ? [] : (tiersByUnitPriceId.get(row.id) ?? []),
|
|
618
|
-
};
|
|
619
|
-
}),
|
|
620
|
-
};
|
|
621
|
-
}
|
|
622
|
-
async function buildSessionSnapshot(db, bookingId) {
|
|
623
|
-
const [booking, participants, items, allocations, itemParticipantLinks, state] = await Promise.all([
|
|
624
|
-
bookingsService.getBookingById(db, bookingId),
|
|
625
|
-
db
|
|
626
|
-
.select()
|
|
627
|
-
.from(bookingTravelers)
|
|
628
|
-
.where(eq(bookingTravelers.bookingId, bookingId))
|
|
629
|
-
.orderBy(asc(bookingTravelers.createdAt)),
|
|
630
|
-
db
|
|
631
|
-
.select()
|
|
632
|
-
.from(bookingItems)
|
|
633
|
-
.where(eq(bookingItems.bookingId, bookingId))
|
|
634
|
-
.orderBy(asc(bookingItems.createdAt)),
|
|
635
|
-
db
|
|
636
|
-
.select()
|
|
637
|
-
.from(bookingAllocations)
|
|
638
|
-
.where(eq(bookingAllocations.bookingId, bookingId))
|
|
639
|
-
.orderBy(asc(bookingAllocations.createdAt)),
|
|
640
|
-
db
|
|
641
|
-
.select({
|
|
642
|
-
id: bookingItemTravelers.id,
|
|
643
|
-
bookingItemId: bookingItemTravelers.bookingItemId,
|
|
644
|
-
travelerId: bookingItemTravelers.travelerId,
|
|
645
|
-
role: bookingItemTravelers.role,
|
|
646
|
-
isPrimary: bookingItemTravelers.isPrimary,
|
|
647
|
-
})
|
|
648
|
-
.from(bookingItemTravelers)
|
|
649
|
-
.innerJoin(bookingItems, eq(bookingItems.id, bookingItemTravelers.bookingItemId))
|
|
650
|
-
.where(eq(bookingItems.bookingId, bookingId))
|
|
651
|
-
.orderBy(asc(bookingItemTravelers.createdAt)),
|
|
652
|
-
getWizardSessionState(db, bookingId),
|
|
653
|
-
]);
|
|
654
|
-
if (!booking) {
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
const itemLinksByItemId = new Map();
|
|
658
|
-
for (const link of itemParticipantLinks) {
|
|
659
|
-
const existing = itemLinksByItemId.get(link.bookingItemId) ?? [];
|
|
660
|
-
existing.push({
|
|
661
|
-
id: link.id,
|
|
662
|
-
travelerId: link.travelerId,
|
|
663
|
-
role: link.role,
|
|
664
|
-
isPrimary: link.isPrimary,
|
|
665
|
-
});
|
|
666
|
-
itemLinksByItemId.set(link.bookingItemId, existing);
|
|
667
|
-
}
|
|
668
|
-
const hasTraveler = countTravelerParticipants(participants) > 0;
|
|
669
|
-
const hasTravelers = participants.length > 0;
|
|
670
|
-
const hasPrimaryTraveler = participants.some((participant) => participant.isPrimary);
|
|
671
|
-
const hasItems = items.length > 0;
|
|
672
|
-
const hasAllocations = allocations.length > 0;
|
|
673
|
-
return {
|
|
674
|
-
sessionId: booking.id,
|
|
675
|
-
bookingNumber: booking.bookingNumber,
|
|
676
|
-
status: booking.status,
|
|
677
|
-
externalBookingRef: booking.externalBookingRef ?? null,
|
|
678
|
-
communicationLanguage: booking.communicationLanguage ?? null,
|
|
679
|
-
sellCurrency: booking.sellCurrency,
|
|
680
|
-
sellAmountCents: booking.sellAmountCents ?? null,
|
|
681
|
-
startDate: normalizeDate(booking.startDate),
|
|
682
|
-
endDate: normalizeDate(booking.endDate),
|
|
683
|
-
pax: booking.pax ?? null,
|
|
684
|
-
holdExpiresAt: normalizeDateTime(booking.holdExpiresAt),
|
|
685
|
-
confirmedAt: normalizeDateTime(booking.confirmedAt),
|
|
686
|
-
expiredAt: normalizeDateTime(booking.expiredAt),
|
|
687
|
-
cancelledAt: normalizeDateTime(booking.cancelledAt),
|
|
688
|
-
completedAt: normalizeDateTime(booking.completedAt),
|
|
689
|
-
travelers: participants.map((participant) => ({
|
|
690
|
-
id: participant.id,
|
|
691
|
-
participantType: participant.participantType,
|
|
692
|
-
travelerCategory: participant.travelerCategory ?? null,
|
|
693
|
-
firstName: participant.firstName,
|
|
694
|
-
lastName: participant.lastName,
|
|
695
|
-
email: participant.email ?? null,
|
|
696
|
-
phone: participant.phone ?? null,
|
|
697
|
-
preferredLanguage: participant.preferredLanguage ?? null,
|
|
698
|
-
specialRequests: participant.specialRequests ?? null,
|
|
699
|
-
isPrimary: participant.isPrimary,
|
|
700
|
-
notes: participant.notes ?? null,
|
|
701
|
-
})),
|
|
702
|
-
items: items.map((item) => ({
|
|
703
|
-
id: item.id,
|
|
704
|
-
title: item.title,
|
|
705
|
-
description: item.description ?? null,
|
|
706
|
-
itemType: item.itemType,
|
|
707
|
-
status: item.status,
|
|
708
|
-
serviceDate: normalizeDate(item.serviceDate),
|
|
709
|
-
startsAt: normalizeDateTime(item.startsAt),
|
|
710
|
-
endsAt: normalizeDateTime(item.endsAt),
|
|
711
|
-
quantity: item.quantity,
|
|
712
|
-
sellCurrency: item.sellCurrency,
|
|
713
|
-
unitSellAmountCents: item.unitSellAmountCents ?? null,
|
|
714
|
-
totalSellAmountCents: item.totalSellAmountCents ?? null,
|
|
715
|
-
costCurrency: item.costCurrency ?? null,
|
|
716
|
-
unitCostAmountCents: item.unitCostAmountCents ?? null,
|
|
717
|
-
totalCostAmountCents: item.totalCostAmountCents ?? null,
|
|
718
|
-
notes: item.notes ?? null,
|
|
719
|
-
productId: item.productId ?? null,
|
|
720
|
-
optionId: item.optionId ?? null,
|
|
721
|
-
optionUnitId: item.optionUnitId ?? null,
|
|
722
|
-
pricingCategoryId: item.pricingCategoryId ?? null,
|
|
723
|
-
travelerLinks: itemLinksByItemId.get(item.id) ?? [],
|
|
724
|
-
})),
|
|
725
|
-
allocations: allocations.map((allocation) => ({
|
|
726
|
-
id: allocation.id,
|
|
727
|
-
bookingItemId: allocation.bookingItemId ?? null,
|
|
728
|
-
productId: allocation.productId ?? null,
|
|
729
|
-
optionId: allocation.optionId ?? null,
|
|
730
|
-
optionUnitId: allocation.optionUnitId ?? null,
|
|
731
|
-
pricingCategoryId: allocation.pricingCategoryId ?? null,
|
|
732
|
-
availabilitySlotId: allocation.availabilitySlotId ?? null,
|
|
733
|
-
quantity: allocation.quantity,
|
|
734
|
-
allocationType: allocation.allocationType,
|
|
735
|
-
status: allocation.status,
|
|
736
|
-
holdExpiresAt: normalizeDateTime(allocation.holdExpiresAt),
|
|
737
|
-
confirmedAt: normalizeDateTime(allocation.confirmedAt),
|
|
738
|
-
releasedAt: normalizeDateTime(allocation.releasedAt),
|
|
739
|
-
})),
|
|
740
|
-
checklist: {
|
|
741
|
-
hasTravelers,
|
|
742
|
-
hasPrimaryTraveler,
|
|
743
|
-
hasItems,
|
|
744
|
-
hasAllocations,
|
|
745
|
-
readyForConfirmation: booking.status === "on_hold" &&
|
|
746
|
-
hasTravelers &&
|
|
747
|
-
hasTraveler &&
|
|
748
|
-
hasPrimaryTraveler &&
|
|
749
|
-
hasItems &&
|
|
750
|
-
hasAllocations,
|
|
751
|
-
},
|
|
752
|
-
state,
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
export const publicBookingsService = {
|
|
756
|
-
async createSession(db, input, userId, resolvers = {}) {
|
|
757
|
-
const travelers = input.travelers ?? [];
|
|
758
|
-
const travelerCount = countTravelerParticipants(travelers);
|
|
759
|
-
const bookingNumber = await generateBookingNumber(db);
|
|
760
|
-
const result = await bookingsService.reserveBooking(db, {
|
|
761
|
-
bookingNumber,
|
|
762
|
-
sourceType: "direct",
|
|
763
|
-
externalBookingRef: input.externalBookingRef ?? null,
|
|
764
|
-
communicationLanguage: input.communicationLanguage ?? null,
|
|
765
|
-
sellCurrency: input.sellCurrency,
|
|
766
|
-
baseCurrency: input.baseCurrency ?? null,
|
|
767
|
-
sellAmountCents: input.sellAmountCents ?? null,
|
|
768
|
-
baseSellAmountCents: input.baseSellAmountCents ?? null,
|
|
769
|
-
costAmountCents: input.costAmountCents ?? null,
|
|
770
|
-
baseCostAmountCents: input.baseCostAmountCents ?? null,
|
|
771
|
-
marginPercent: input.marginPercent ?? null,
|
|
772
|
-
startDate: input.startDate ?? null,
|
|
773
|
-
endDate: input.endDate ?? null,
|
|
774
|
-
pax: input.pax ?? (travelerCount > 0 ? travelerCount : null),
|
|
775
|
-
holdMinutes: input.holdMinutes,
|
|
776
|
-
holdExpiresAt: input.holdExpiresAt ?? null,
|
|
777
|
-
items: input.items.map((item) => ({
|
|
778
|
-
...item,
|
|
779
|
-
sellCurrency: item.sellCurrency ?? input.sellCurrency,
|
|
780
|
-
costCurrency: item.costCurrency ?? null,
|
|
781
|
-
description: item.description ?? null,
|
|
782
|
-
notes: item.notes ?? null,
|
|
783
|
-
productId: item.productId ?? null,
|
|
784
|
-
optionId: item.optionId ?? null,
|
|
785
|
-
optionUnitId: item.optionUnitId ?? null,
|
|
786
|
-
pricingCategoryId: item.pricingCategoryId ?? null,
|
|
787
|
-
sourceSnapshotId: item.sourceSnapshotId ?? null,
|
|
788
|
-
sourceOfferId: null,
|
|
789
|
-
metadata: item.metadata ?? null,
|
|
790
|
-
})),
|
|
791
|
-
internalNotes: null,
|
|
792
|
-
}, userId);
|
|
793
|
-
if (!("booking" in result) || !result.booking) {
|
|
794
|
-
return result;
|
|
795
|
-
}
|
|
796
|
-
for (const participant of travelers) {
|
|
797
|
-
const personId = await safeResolveTravelerPerson(db, resolvers.resolveTravelerPerson, {
|
|
798
|
-
firstName: participant.firstName,
|
|
799
|
-
lastName: participant.lastName,
|
|
800
|
-
email: participant.email ?? null,
|
|
801
|
-
phone: participant.phone ?? null,
|
|
802
|
-
preferredLanguage: participant.preferredLanguage ?? null,
|
|
803
|
-
}, result.booking.id);
|
|
804
|
-
await bookingsService.createTravelerRecord(db, result.booking.id, {
|
|
805
|
-
participantType: participant.participantType,
|
|
806
|
-
travelerCategory: participant.travelerCategory ?? null,
|
|
807
|
-
firstName: participant.firstName,
|
|
808
|
-
lastName: participant.lastName,
|
|
809
|
-
email: participant.email ?? null,
|
|
810
|
-
phone: participant.phone ?? null,
|
|
811
|
-
preferredLanguage: participant.preferredLanguage ?? null,
|
|
812
|
-
specialRequests: participant.specialRequests ?? null,
|
|
813
|
-
isPrimary: participant.isPrimary,
|
|
814
|
-
notes: participant.notes ?? null,
|
|
815
|
-
personId,
|
|
816
|
-
}, userId);
|
|
817
|
-
}
|
|
818
|
-
const session = await buildSessionSnapshot(db, result.booking.id);
|
|
819
|
-
return session ? { status: "ok", session } : { status: "not_found" };
|
|
820
|
-
},
|
|
821
|
-
getSessionById(db, bookingId) {
|
|
822
|
-
return buildSessionSnapshot(db, bookingId);
|
|
823
|
-
},
|
|
824
|
-
async getSessionState(db, bookingId) {
|
|
825
|
-
const booking = await bookingsService.getBookingById(db, bookingId);
|
|
826
|
-
if (!booking) {
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
829
|
-
return getWizardSessionState(db, bookingId);
|
|
830
|
-
},
|
|
831
|
-
async updateSessionState(db, bookingId, input, resolvers = {}, userId) {
|
|
832
|
-
const booking = await bookingsService.getBookingById(db, bookingId);
|
|
833
|
-
if (!booking) {
|
|
834
|
-
return { status: "not_found" };
|
|
835
|
-
}
|
|
836
|
-
const state = await upsertWizardSessionState(db, bookingId, input);
|
|
837
|
-
if (!state) {
|
|
838
|
-
return { status: "not_found" };
|
|
839
|
-
}
|
|
840
|
-
const bookingContact = extractBookingContactFromStatePayload(state.payload);
|
|
841
|
-
if (bookingContact) {
|
|
842
|
-
// Resolve a CRM person from the billing snapshot when a resolver
|
|
843
|
-
// is wired — issue #961 calls out that storefront bookings used
|
|
844
|
-
// to land with `person_id = NULL`, leaving every customer outside
|
|
845
|
-
// the CRM until each operator wired their own bridge.
|
|
846
|
-
const personId = booking.personId
|
|
847
|
-
? booking.personId
|
|
848
|
-
: await safeResolveBillingPerson(db, resolvers.resolveBillingPerson, {
|
|
849
|
-
firstName: bookingContact.contactFirstName,
|
|
850
|
-
lastName: bookingContact.contactLastName,
|
|
851
|
-
email: bookingContact.contactEmail,
|
|
852
|
-
phone: bookingContact.contactPhone,
|
|
853
|
-
preferredLanguage: null,
|
|
854
|
-
}, bookingId);
|
|
855
|
-
await bookingsService.updateBooking(db, bookingId, {
|
|
856
|
-
...bookingContact,
|
|
857
|
-
...(personId && booking.personId !== personId ? { personId } : {}),
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
await syncTravelerRowsFromStatePayload(db, bookingId, state.payload, resolvers, userId);
|
|
861
|
-
return { status: "ok", state };
|
|
862
|
-
},
|
|
863
|
-
async updateSession(db, bookingId, input, userId, resolvers = {}) {
|
|
864
|
-
const booking = await bookingsService.getBookingById(db, bookingId);
|
|
865
|
-
if (!booking) {
|
|
866
|
-
return { status: "not_found" };
|
|
867
|
-
}
|
|
868
|
-
if (input.externalBookingRef !== undefined ||
|
|
869
|
-
input.communicationLanguage !== undefined ||
|
|
870
|
-
input.pax !== undefined) {
|
|
871
|
-
await bookingsService.updateBooking(db, bookingId, {
|
|
872
|
-
externalBookingRef: input.externalBookingRef,
|
|
873
|
-
communicationLanguage: input.communicationLanguage,
|
|
874
|
-
pax: input.pax,
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
const travelers = input.travelers;
|
|
878
|
-
const removedTravelerIds = input.removedTravelerIds ?? [];
|
|
879
|
-
for (const travelerId of removedTravelerIds) {
|
|
880
|
-
const participant = await bookingsService.getTravelerRecordById(db, bookingId, travelerId);
|
|
881
|
-
if (participant) {
|
|
882
|
-
await bookingsService.deleteTravelerRecord(db, participant.id);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
if (travelers) {
|
|
886
|
-
for (const participant of travelers) {
|
|
887
|
-
if (participant.id) {
|
|
888
|
-
const existing = await bookingsService.getTravelerRecordById(db, bookingId, participant.id);
|
|
889
|
-
if (!existing) {
|
|
890
|
-
return { status: "participant_not_found" };
|
|
891
|
-
}
|
|
892
|
-
await bookingsService.updateTravelerRecord(db, participant.id, {
|
|
893
|
-
participantType: participant.participantType,
|
|
894
|
-
travelerCategory: participant.travelerCategory ?? null,
|
|
895
|
-
firstName: participant.firstName,
|
|
896
|
-
lastName: participant.lastName,
|
|
897
|
-
email: participant.email ?? null,
|
|
898
|
-
phone: participant.phone ?? null,
|
|
899
|
-
preferredLanguage: participant.preferredLanguage ?? null,
|
|
900
|
-
specialRequests: participant.specialRequests ?? null,
|
|
901
|
-
isPrimary: participant.isPrimary,
|
|
902
|
-
notes: participant.notes ?? null,
|
|
903
|
-
});
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
const personId = await safeResolveTravelerPerson(db, resolvers.resolveTravelerPerson, {
|
|
907
|
-
firstName: participant.firstName,
|
|
908
|
-
lastName: participant.lastName,
|
|
909
|
-
email: participant.email ?? null,
|
|
910
|
-
phone: participant.phone ?? null,
|
|
911
|
-
preferredLanguage: participant.preferredLanguage ?? null,
|
|
912
|
-
}, bookingId);
|
|
913
|
-
await bookingsService.createTravelerRecord(db, bookingId, {
|
|
914
|
-
participantType: participant.participantType,
|
|
915
|
-
travelerCategory: participant.travelerCategory ?? null,
|
|
916
|
-
firstName: participant.firstName,
|
|
917
|
-
lastName: participant.lastName,
|
|
918
|
-
email: participant.email ?? null,
|
|
919
|
-
phone: participant.phone ?? null,
|
|
920
|
-
preferredLanguage: participant.preferredLanguage ?? null,
|
|
921
|
-
specialRequests: participant.specialRequests ?? null,
|
|
922
|
-
isPrimary: participant.isPrimary,
|
|
923
|
-
notes: participant.notes ?? null,
|
|
924
|
-
personId,
|
|
925
|
-
}, userId);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
if (input.holdMinutes !== undefined || input.holdExpiresAt !== undefined) {
|
|
929
|
-
const holdResult = await bookingsService.extendBookingHold(db, bookingId, {
|
|
930
|
-
holdMinutes: input.holdMinutes,
|
|
931
|
-
holdExpiresAt: input.holdExpiresAt,
|
|
932
|
-
}, userId);
|
|
933
|
-
if (holdResult.status !== "ok") {
|
|
934
|
-
return holdResult;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
if (input.pax === undefined && (travelers || removedTravelerIds.length > 0)) {
|
|
938
|
-
const participants = await db
|
|
939
|
-
.select({ participantType: bookingTravelers.participantType })
|
|
940
|
-
.from(bookingTravelers)
|
|
941
|
-
.where(eq(bookingTravelers.bookingId, bookingId));
|
|
942
|
-
const travelerCount = countTravelerParticipants(participants);
|
|
943
|
-
await bookingsService.updateBooking(db, bookingId, {
|
|
944
|
-
pax: travelerCount > 0 ? travelerCount : null,
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
const session = await buildSessionSnapshot(db, bookingId);
|
|
948
|
-
return session ? { status: "ok", session } : { status: "not_found" };
|
|
949
|
-
},
|
|
950
|
-
async repriceSession(db, bookingId, input) {
|
|
951
|
-
const [booking, items] = await Promise.all([
|
|
952
|
-
bookingsService.getBookingById(db, bookingId),
|
|
953
|
-
db
|
|
954
|
-
.select()
|
|
955
|
-
.from(bookingItems)
|
|
956
|
-
.where(eq(bookingItems.bookingId, bookingId))
|
|
957
|
-
.orderBy(asc(bookingItems.createdAt)),
|
|
958
|
-
]);
|
|
959
|
-
if (!booking) {
|
|
960
|
-
return { status: "not_found" };
|
|
961
|
-
}
|
|
962
|
-
const selectedItemIds = input.selections.map((selection) => selection.itemId);
|
|
963
|
-
const itemById = new Map(items.map((item) => [item.id, item]));
|
|
964
|
-
for (const selection of input.selections) {
|
|
965
|
-
if (!itemById.has(selection.itemId)) {
|
|
966
|
-
return { status: "invalid_selection" };
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
const requestedUnitIds = Array.from(new Set(input.selections
|
|
970
|
-
.map((selection) => selection.optionUnitId)
|
|
971
|
-
.filter((value) => Boolean(value))));
|
|
972
|
-
const requestedUnits = requestedUnitIds.length > 0
|
|
973
|
-
? await db.select().from(optionUnitsRef).where(inArray(optionUnitsRef.id, requestedUnitIds))
|
|
974
|
-
: [];
|
|
975
|
-
const requestedUnitById = new Map(requestedUnits.map((unit) => [unit.id, unit]));
|
|
976
|
-
const pricingWarnings = [];
|
|
977
|
-
const pricedItems = [];
|
|
978
|
-
let resolvedCatalogId = input.catalogId ?? null;
|
|
979
|
-
let resolvedCurrency = booking.sellCurrency;
|
|
980
|
-
for (const selection of input.selections) {
|
|
981
|
-
const item = itemById.get(selection.itemId);
|
|
982
|
-
if (!item?.productId) {
|
|
983
|
-
return { status: "invalid_selection" };
|
|
984
|
-
}
|
|
985
|
-
const optionId = selection.optionId === undefined
|
|
986
|
-
? (item.optionId ?? undefined)
|
|
987
|
-
: (selection.optionId ?? undefined);
|
|
988
|
-
const quantity = selection.quantity ?? item.quantity;
|
|
989
|
-
const pricingCategoryId = selection.pricingCategoryId === undefined
|
|
990
|
-
? (item.pricingCategoryId ?? null)
|
|
991
|
-
: (selection.pricingCategoryId ?? null);
|
|
992
|
-
const selectedUnitId = selection.optionUnitId === undefined
|
|
993
|
-
? (item.optionUnitId ?? null)
|
|
994
|
-
: (selection.optionUnitId ?? null);
|
|
995
|
-
const snapshot = await resolveSessionPricingSnapshot(db, item.productId, {
|
|
996
|
-
catalogId: input.catalogId,
|
|
997
|
-
departureId: item.availabilitySlotId ?? undefined,
|
|
998
|
-
optionId,
|
|
999
|
-
});
|
|
1000
|
-
if (!snapshot) {
|
|
1001
|
-
return { status: "pricing_unavailable" };
|
|
1002
|
-
}
|
|
1003
|
-
resolvedCatalogId = snapshot.catalog.id;
|
|
1004
|
-
resolvedCurrency = snapshot.catalog.currencyCode ?? booking.sellCurrency;
|
|
1005
|
-
const option = snapshot.options.find((candidate) => candidate.id === optionId) ??
|
|
1006
|
-
snapshot.options[0] ??
|
|
1007
|
-
null;
|
|
1008
|
-
if (!option) {
|
|
1009
|
-
return { status: "pricing_unavailable" };
|
|
1010
|
-
}
|
|
1011
|
-
const rule = snapshot.rules.find((candidate) => candidate.optionId === option.id && candidate.isDefault) ??
|
|
1012
|
-
snapshot.rules.find((candidate) => candidate.optionId === option.id) ??
|
|
1013
|
-
null;
|
|
1014
|
-
if (!rule) {
|
|
1015
|
-
return { status: "pricing_unavailable" };
|
|
1016
|
-
}
|
|
1017
|
-
const ruleUnitPrices = snapshot.unitPrices.filter((candidate) => candidate.optionPriceRuleId === rule.id);
|
|
1018
|
-
const unitPriceCandidates = ruleUnitPrices.filter((candidate) => {
|
|
1019
|
-
if (selectedUnitId && candidate.unitId !== selectedUnitId) {
|
|
1020
|
-
return false;
|
|
1021
|
-
}
|
|
1022
|
-
if (pricingCategoryId && candidate.pricingCategoryId !== pricingCategoryId) {
|
|
1023
|
-
return false;
|
|
1024
|
-
}
|
|
1025
|
-
if (candidate.minQuantity !== null && quantity < candidate.minQuantity) {
|
|
1026
|
-
return false;
|
|
1027
|
-
}
|
|
1028
|
-
if (candidate.maxQuantity !== null && quantity > candidate.maxQuantity) {
|
|
1029
|
-
return false;
|
|
1030
|
-
}
|
|
1031
|
-
return true;
|
|
1032
|
-
});
|
|
1033
|
-
const fallbackUnitPrice = !pricingCategoryId && !selectedUnitId
|
|
1034
|
-
? (ruleUnitPrices.find((candidate) => candidate.pricingCategoryId === null &&
|
|
1035
|
-
(candidate.minQuantity === null || quantity >= candidate.minQuantity) &&
|
|
1036
|
-
(candidate.maxQuantity === null || quantity <= candidate.maxQuantity)) ?? null)
|
|
1037
|
-
: null;
|
|
1038
|
-
const unitPrice = unitPriceCandidates[0] ?? fallbackUnitPrice;
|
|
1039
|
-
if ((selectedUnitId || ruleUnitPrices.length > 0) &&
|
|
1040
|
-
!unitPrice &&
|
|
1041
|
-
rule.pricingMode !== "per_booking") {
|
|
1042
|
-
return { status: "pricing_unavailable" };
|
|
1043
|
-
}
|
|
1044
|
-
const unit = selectedUnitId ? (requestedUnitById.get(selectedUnitId) ?? null) : null;
|
|
1045
|
-
const unitSellAmountCents = unitPrice
|
|
1046
|
-
? resolveTierAmount(unitPrice.tiers, quantity, unitPrice.sellAmountCents)
|
|
1047
|
-
: rule.baseSellAmountCents;
|
|
1048
|
-
const pricingMode = unitPrice?.pricingMode ?? rule.pricingMode;
|
|
1049
|
-
const totalSellAmountCents = computeLineTotal(pricingMode, unitSellAmountCents, quantity, rule.baseSellAmountCents);
|
|
1050
|
-
const warnings = buildUnitWarnings(unit, quantity, booking.pax ?? null);
|
|
1051
|
-
if (selectedUnitId && !unit) {
|
|
1052
|
-
warnings.push("Selected room/unit metadata is not available in the current catalog.");
|
|
1053
|
-
}
|
|
1054
|
-
if (pricingMode === "on_request") {
|
|
1055
|
-
warnings.push("Selected option requires manual pricing confirmation.");
|
|
1056
|
-
}
|
|
1057
|
-
pricingWarnings.push(...warnings);
|
|
1058
|
-
pricedItems.push({
|
|
1059
|
-
itemId: item.id,
|
|
1060
|
-
title: item.title,
|
|
1061
|
-
productId: item.productId ?? null,
|
|
1062
|
-
optionId: option.id,
|
|
1063
|
-
optionUnitId: selectedUnitId,
|
|
1064
|
-
optionUnitName: unit?.name ?? unitPrice?.unitName ?? null,
|
|
1065
|
-
optionUnitType: unit?.unitType ?? unitPrice?.unitType ?? null,
|
|
1066
|
-
pricingCategoryId,
|
|
1067
|
-
quantity,
|
|
1068
|
-
pricingMode,
|
|
1069
|
-
unitSellAmountCents,
|
|
1070
|
-
totalSellAmountCents,
|
|
1071
|
-
warnings,
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1074
|
-
const totalSellAmountCents = items.reduce((total, item) => {
|
|
1075
|
-
const repriced = pricedItems.find((candidate) => candidate.itemId === item.id);
|
|
1076
|
-
return total + (repriced?.totalSellAmountCents ?? item.totalSellAmountCents ?? 0);
|
|
1077
|
-
}, 0);
|
|
1078
|
-
let session = null;
|
|
1079
|
-
if (input.applyToSession) {
|
|
1080
|
-
const activeAllocations = selectedItemIds.length > 0
|
|
1081
|
-
? await db
|
|
1082
|
-
.select()
|
|
1083
|
-
.from(bookingAllocations)
|
|
1084
|
-
.where(and(eq(bookingAllocations.bookingId, bookingId), inArray(bookingAllocations.bookingItemId, selectedItemIds), or(eq(bookingAllocations.status, "held"), eq(bookingAllocations.status, "confirmed"))))
|
|
1085
|
-
: [];
|
|
1086
|
-
const activeAllocationsByItemId = new Map();
|
|
1087
|
-
for (const allocation of activeAllocations) {
|
|
1088
|
-
const existing = activeAllocationsByItemId.get(allocation.bookingItemId) ?? [];
|
|
1089
|
-
existing.push(allocation);
|
|
1090
|
-
activeAllocationsByItemId.set(allocation.bookingItemId, existing);
|
|
1091
|
-
}
|
|
1092
|
-
const quantityChangedWithActiveAllocation = pricedItems.some((pricedItem) => {
|
|
1093
|
-
const item = itemById.get(pricedItem.itemId);
|
|
1094
|
-
return Boolean(item &&
|
|
1095
|
-
item.quantity !== pricedItem.quantity &&
|
|
1096
|
-
(activeAllocationsByItemId.get(pricedItem.itemId)?.length ?? 0) > 0);
|
|
1097
|
-
});
|
|
1098
|
-
if (quantityChangedWithActiveAllocation) {
|
|
1099
|
-
return { status: "quantity_change_requires_reallocation" };
|
|
1100
|
-
}
|
|
1101
|
-
await db.transaction(async (tx) => {
|
|
1102
|
-
for (const pricedItem of pricedItems) {
|
|
1103
|
-
await tx
|
|
1104
|
-
.update(bookingItems)
|
|
1105
|
-
.set({
|
|
1106
|
-
optionId: pricedItem.optionId,
|
|
1107
|
-
optionUnitId: pricedItem.optionUnitId,
|
|
1108
|
-
pricingCategoryId: pricedItem.pricingCategoryId,
|
|
1109
|
-
quantity: pricedItem.quantity,
|
|
1110
|
-
sellCurrency: resolvedCurrency,
|
|
1111
|
-
unitSellAmountCents: pricedItem.unitSellAmountCents,
|
|
1112
|
-
totalSellAmountCents: pricedItem.totalSellAmountCents,
|
|
1113
|
-
updatedAt: new Date(),
|
|
1114
|
-
})
|
|
1115
|
-
.where(eq(bookingItems.id, pricedItem.itemId));
|
|
1116
|
-
await tx
|
|
1117
|
-
.update(bookingAllocations)
|
|
1118
|
-
.set({
|
|
1119
|
-
optionId: pricedItem.optionId,
|
|
1120
|
-
optionUnitId: pricedItem.optionUnitId,
|
|
1121
|
-
pricingCategoryId: pricedItem.pricingCategoryId,
|
|
1122
|
-
updatedAt: new Date(),
|
|
1123
|
-
})
|
|
1124
|
-
.where(and(eq(bookingAllocations.bookingId, bookingId), eq(bookingAllocations.bookingItemId, pricedItem.itemId), or(eq(bookingAllocations.status, "held"), eq(bookingAllocations.status, "confirmed"))));
|
|
1125
|
-
}
|
|
1126
|
-
await tx
|
|
1127
|
-
.update(bookings)
|
|
1128
|
-
.set({
|
|
1129
|
-
sellCurrency: resolvedCurrency,
|
|
1130
|
-
sellAmountCents: totalSellAmountCents,
|
|
1131
|
-
updatedAt: new Date(),
|
|
1132
|
-
})
|
|
1133
|
-
.where(eq(bookings.id, bookingId));
|
|
1134
|
-
});
|
|
1135
|
-
session = await buildSessionSnapshot(db, bookingId);
|
|
1136
|
-
}
|
|
1137
|
-
return {
|
|
1138
|
-
status: "ok",
|
|
1139
|
-
pricing: {
|
|
1140
|
-
sessionId: bookingId,
|
|
1141
|
-
catalogId: resolvedCatalogId,
|
|
1142
|
-
currencyCode: resolvedCurrency,
|
|
1143
|
-
totalSellAmountCents,
|
|
1144
|
-
items: pricedItems,
|
|
1145
|
-
warnings: Array.from(new Set(pricingWarnings)),
|
|
1146
|
-
appliedToSession: input.applyToSession,
|
|
1147
|
-
},
|
|
1148
|
-
session,
|
|
1149
|
-
};
|
|
1150
|
-
},
|
|
1151
|
-
async confirmSession(db, bookingId, input, userId) {
|
|
1152
|
-
const result = await bookingsService.confirmBooking(db, bookingId, input, userId);
|
|
1153
|
-
if (result.status !== "ok") {
|
|
1154
|
-
return result;
|
|
1155
|
-
}
|
|
1156
|
-
const session = await buildSessionSnapshot(db, bookingId);
|
|
1157
|
-
return session ? { status: "ok", session } : { status: "not_found" };
|
|
1158
|
-
},
|
|
1159
|
-
async expireSession(db, bookingId, input, userId, runtime = {}) {
|
|
1160
|
-
const result = await bookingsService.expireBooking(db, bookingId, input, userId, runtime);
|
|
1161
|
-
if (result.status !== "ok") {
|
|
1162
|
-
return result;
|
|
1163
|
-
}
|
|
1164
|
-
const session = await buildSessionSnapshot(db, bookingId);
|
|
1165
|
-
return session ? { status: "ok", session } : { status: "not_found" };
|
|
1166
|
-
},
|
|
1167
|
-
async getOverview(db, query) {
|
|
1168
|
-
return buildOverviewSnapshot(db, query);
|
|
1169
|
-
},
|
|
1170
|
-
async getOverviewByGuestAccess(db, query) {
|
|
1171
|
-
return buildOverviewSnapshot(db, query);
|
|
1172
|
-
},
|
|
1173
|
-
async getOverviewByLookup(db, query) {
|
|
1174
|
-
return buildOverviewSnapshot(db, query);
|
|
1175
|
-
},
|
|
1176
|
-
};
|
|
1
|
+
export * from "./service-public-core.js";
|