@voyantjs/bookings 0.6.8 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/pii.d.ts +10 -9
- package/dist/pii.d.ts.map +1 -1
- package/dist/pii.js +33 -33
- package/dist/products-ref.d.ts +93 -1
- package/dist/products-ref.d.ts.map +1 -1
- package/dist/products-ref.js +12 -1
- package/dist/routes-groups.d.ts +25 -5
- package/dist/routes-groups.d.ts.map +1 -1
- package/dist/routes-groups.js +3 -3
- package/dist/routes-public.d.ts +19 -21
- package/dist/routes-public.d.ts.map +1 -1
- package/dist/routes-public.js +1 -1
- package/dist/routes-shared.d.ts +3 -2
- package/dist/routes-shared.d.ts.map +1 -1
- package/dist/routes.d.ts +283 -188
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +89 -102
- package/dist/schema/travel-details.d.ts +27 -27
- package/dist/schema/travel-details.d.ts.map +1 -1
- package/dist/schema/travel-details.js +19 -14
- package/dist/schema-core.d.ts +194 -305
- package/dist/schema-core.d.ts.map +1 -1
- package/dist/schema-core.js +19 -10
- package/dist/schema-items.d.ts +15 -15
- package/dist/schema-items.d.ts.map +1 -1
- package/dist/schema-items.js +12 -12
- package/dist/schema-operations.d.ts +1 -1
- package/dist/schema-operations.js +3 -3
- package/dist/schema-relations.d.ts +26 -9
- package/dist/schema-relations.d.ts.map +1 -1
- package/dist/schema-relations.js +36 -21
- package/dist/schema-shared.d.ts +3 -2
- package/dist/schema-shared.d.ts.map +1 -1
- package/dist/schema-shared.js +4 -5
- package/dist/schema-staff.d.ts +267 -0
- package/dist/schema-staff.d.ts.map +1 -0
- package/dist/schema-staff.js +31 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +1 -0
- package/dist/service-groups.d.ts +3 -7
- package/dist/service-groups.d.ts.map +1 -1
- package/dist/service-groups.js +6 -10
- package/dist/service-public.d.ts +102 -55
- package/dist/service-public.d.ts.map +1 -1
- package/dist/service-public.js +119 -54
- package/dist/service.d.ts +327 -104
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +530 -130
- package/dist/transactions-ref.d.ts +930 -66
- package/dist/transactions-ref.d.ts.map +1 -1
- package/dist/transactions-ref.js +56 -2
- package/dist/validation-public.d.ts +29 -69
- package/dist/validation-public.d.ts.map +1 -1
- package/dist/validation-public.js +21 -20
- package/dist/validation-shared.d.ts +4 -5
- package/dist/validation-shared.d.ts.map +1 -1
- package/dist/validation-shared.js +2 -10
- package/dist/validation.d.ts +248 -44
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +103 -28
- package/package.json +6 -6
package/dist/service.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { and, asc, desc, eq, ilike, inArray, lte, ne, or, sql } from "drizzle-orm";
|
|
1
|
+
import { and, asc, desc, eq, exists, ilike, inArray, lte, ne, or, sql } from "drizzle-orm";
|
|
2
2
|
import { availabilitySlotsRef } from "./availability-ref.js";
|
|
3
|
-
import { bookingItemProductDetailsRef, bookingProductDetailsRef, optionUnitsRef, productDayServicesRef, productDaysRef, productOptionsRef, productsRef, productTicketSettingsRef, } from "./products-ref.js";
|
|
4
|
-
import { bookingActivityLog, bookingAllocations, bookingDocuments, bookingFulfillments,
|
|
3
|
+
import { bookingItemProductDetailsRef, bookingProductDetailsRef, optionUnitsRef, productDayServicesRef, productDaysRef, productItinerariesRef, productOptionsRef, productsRef, productTicketSettingsRef, } from "./products-ref.js";
|
|
4
|
+
import { bookingActivityLog, bookingAllocations, bookingDocuments, bookingFulfillments, bookingItems, bookingItemTravelers, bookingNotes, bookingRedemptionEvents, bookingStaffAssignments, bookingSupplierStatuses, bookings, bookingTravelers, } from "./schema.js";
|
|
5
5
|
import { cleanupGroupOnBookingCancelled } from "./service-groups.js";
|
|
6
|
-
import { bookingTransactionDetailsRef, offerItemParticipantsRef, offerItemsRef, offerParticipantsRef, offersRef, orderItemParticipantsRef, orderItemsRef, orderParticipantsRef, ordersRef, } from "./transactions-ref.js";
|
|
6
|
+
import { bookingTransactionDetailsRef, offerItemParticipantsRef, offerItemsRef, offerParticipantsRef, offerStaffAssignmentsRef, offersRef, orderItemParticipantsRef, orderItemsRef, orderParticipantsRef, orderStaffAssignmentsRef, ordersRef, } from "./transactions-ref.js";
|
|
7
7
|
const travelerParticipantTypes = ["traveler", "occupant"];
|
|
8
8
|
class BookingServiceError extends Error {
|
|
9
9
|
code;
|
|
@@ -24,47 +24,31 @@ function toDateValueOrNull(value) {
|
|
|
24
24
|
return null;
|
|
25
25
|
return value instanceof Date ? value : new Date(value);
|
|
26
26
|
}
|
|
27
|
-
function
|
|
27
|
+
function toTravelerResponse(participant) {
|
|
28
28
|
return {
|
|
29
29
|
id: participant.id,
|
|
30
30
|
bookingId: participant.bookingId,
|
|
31
|
+
participantType: participant.participantType,
|
|
32
|
+
travelerCategory: participant.travelerCategory,
|
|
31
33
|
firstName: participant.firstName,
|
|
32
34
|
lastName: participant.lastName,
|
|
33
35
|
email: participant.email,
|
|
34
36
|
phone: participant.phone,
|
|
37
|
+
preferredLanguage: participant.preferredLanguage,
|
|
38
|
+
accessibilityNeeds: participant.accessibilityNeeds,
|
|
35
39
|
specialRequests: participant.specialRequests,
|
|
36
|
-
|
|
40
|
+
isPrimary: participant.isPrimary,
|
|
41
|
+
notes: participant.notes,
|
|
37
42
|
createdAt: participant.createdAt,
|
|
38
43
|
updatedAt: participant.updatedAt,
|
|
39
44
|
};
|
|
40
45
|
}
|
|
41
|
-
function
|
|
42
|
-
return {
|
|
43
|
-
participantType: "traveler",
|
|
44
|
-
firstName: data.firstName,
|
|
45
|
-
lastName: data.lastName,
|
|
46
|
-
email: data.email ?? null,
|
|
47
|
-
phone: data.phone ?? null,
|
|
48
|
-
specialRequests: data.specialRequests ?? null,
|
|
49
|
-
isPrimary: data.isLeadPassenger ?? false,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
function toUpdateParticipantFromPassenger(data) {
|
|
53
|
-
return {
|
|
54
|
-
firstName: data.firstName,
|
|
55
|
-
lastName: data.lastName,
|
|
56
|
-
email: data.email ?? null,
|
|
57
|
-
phone: data.phone ?? null,
|
|
58
|
-
specialRequests: data.specialRequests ?? null,
|
|
59
|
-
isPrimary: data.isLeadPassenger ?? undefined,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
async function ensureParticipantFlags(db, bookingId, participantId, data) {
|
|
46
|
+
async function ensureParticipantFlags(db, bookingId, travelerId, data) {
|
|
63
47
|
if (data.isPrimary) {
|
|
64
48
|
await db
|
|
65
|
-
.update(
|
|
49
|
+
.update(bookingTravelers)
|
|
66
50
|
.set({ isPrimary: false, updatedAt: new Date() })
|
|
67
|
-
.where(and(eq(
|
|
51
|
+
.where(and(eq(bookingTravelers.bookingId, bookingId), ne(bookingTravelers.id, travelerId)));
|
|
68
52
|
}
|
|
69
53
|
}
|
|
70
54
|
async function ensureBookingScopedLinks(db, bookingId, data) {
|
|
@@ -78,18 +62,24 @@ async function ensureBookingScopedLinks(db, bookingId, data) {
|
|
|
78
62
|
return { ok: false, reason: "booking_item_not_found" };
|
|
79
63
|
}
|
|
80
64
|
}
|
|
81
|
-
if (data.
|
|
82
|
-
const [
|
|
83
|
-
.select({ id:
|
|
84
|
-
.from(
|
|
85
|
-
.where(and(eq(
|
|
65
|
+
if (data.travelerId) {
|
|
66
|
+
const [traveler] = await db
|
|
67
|
+
.select({ id: bookingTravelers.id })
|
|
68
|
+
.from(bookingTravelers)
|
|
69
|
+
.where(and(eq(bookingTravelers.id, data.travelerId), eq(bookingTravelers.bookingId, bookingId)))
|
|
86
70
|
.limit(1);
|
|
87
|
-
if (!
|
|
88
|
-
return { ok: false, reason: "
|
|
71
|
+
if (!traveler) {
|
|
72
|
+
return { ok: false, reason: "traveler_not_found" };
|
|
89
73
|
}
|
|
90
74
|
}
|
|
91
75
|
return { ok: true };
|
|
92
76
|
}
|
|
77
|
+
function isStaffParticipantType(participantType) {
|
|
78
|
+
return participantType === "staff";
|
|
79
|
+
}
|
|
80
|
+
function toStaffAssignmentRole(role) {
|
|
81
|
+
return role === "service_assignee" ? "service_assignee" : "other";
|
|
82
|
+
}
|
|
93
83
|
function deriveBookingDateRange(items) {
|
|
94
84
|
const dates = items
|
|
95
85
|
.flatMap((item) => [item.serviceDate, item.startsAt?.toISOString().slice(0, 10) ?? null])
|
|
@@ -112,6 +102,23 @@ function deriveBookingPax(participants, items) {
|
|
|
112
102
|
function getTransactionItemParticipantItemId(link) {
|
|
113
103
|
return "offerItemId" in link ? link.offerItemId : link.orderItemId;
|
|
114
104
|
}
|
|
105
|
+
function toStaffReservationParticipant(assignment, suffix) {
|
|
106
|
+
return {
|
|
107
|
+
id: `staff:${suffix}:${assignment.id}`,
|
|
108
|
+
personId: assignment.personId,
|
|
109
|
+
participantType: "staff",
|
|
110
|
+
travelerCategory: null,
|
|
111
|
+
firstName: assignment.firstName,
|
|
112
|
+
lastName: assignment.lastName,
|
|
113
|
+
email: assignment.email,
|
|
114
|
+
phone: assignment.phone,
|
|
115
|
+
preferredLanguage: assignment.preferredLanguage,
|
|
116
|
+
isPrimary: assignment.isPrimary,
|
|
117
|
+
notes: assignment.notes,
|
|
118
|
+
createdAt: new Date(),
|
|
119
|
+
updatedAt: new Date(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
115
122
|
function mapDeliveryFormatToFulfillment(format) {
|
|
116
123
|
switch (format) {
|
|
117
124
|
case "pdf":
|
|
@@ -160,10 +167,15 @@ async function getConvertProductData(db, data) {
|
|
|
160
167
|
.limit(1);
|
|
161
168
|
option = defaultOption ?? null;
|
|
162
169
|
}
|
|
170
|
+
// product_days is keyed by itinerary_id (products re-parented days onto
|
|
171
|
+
// product_itineraries); getConvertProductData joins through the itinerary
|
|
172
|
+
// ref so the per-product day lookup still works for converts that want to
|
|
173
|
+
// seed booking supplier statuses from the product's day services.
|
|
163
174
|
const days = await db
|
|
164
|
-
.select()
|
|
175
|
+
.select({ id: productDaysRef.id, dayNumber: productDaysRef.dayNumber })
|
|
165
176
|
.from(productDaysRef)
|
|
166
|
-
.
|
|
177
|
+
.innerJoin(productItinerariesRef, eq(productDaysRef.itineraryId, productItinerariesRef.id))
|
|
178
|
+
.where(eq(productItinerariesRef.productId, product.id))
|
|
167
179
|
.orderBy(asc(productDaysRef.dayNumber));
|
|
168
180
|
const dayServices = days.length
|
|
169
181
|
? await db
|
|
@@ -177,7 +189,9 @@ async function getConvertProductData(db, data) {
|
|
|
177
189
|
.where(sql `${productDayServicesRef.dayId} IN (
|
|
178
190
|
SELECT ${productDaysRef.id}
|
|
179
191
|
FROM ${productDaysRef}
|
|
180
|
-
|
|
192
|
+
INNER JOIN ${productItinerariesRef}
|
|
193
|
+
ON ${productDaysRef.itineraryId} = ${productItinerariesRef.id}
|
|
194
|
+
WHERE ${productItinerariesRef.productId} = ${product.id}
|
|
181
195
|
)`)
|
|
182
196
|
.orderBy(asc(productDayServicesRef.sortOrder), asc(productDayServicesRef.id))
|
|
183
197
|
: [];
|
|
@@ -188,6 +202,27 @@ async function getConvertProductData(db, data) {
|
|
|
188
202
|
.from(optionUnitsRef)
|
|
189
203
|
.where(eq(optionUnitsRef.optionId, option.id))
|
|
190
204
|
.orderBy(asc(optionUnitsRef.sortOrder), asc(optionUnitsRef.createdAt));
|
|
205
|
+
let slot = null;
|
|
206
|
+
if (data.slotId) {
|
|
207
|
+
const [selectedSlot] = await db
|
|
208
|
+
.select()
|
|
209
|
+
.from(availabilitySlotsRef)
|
|
210
|
+
.where(and(eq(availabilitySlotsRef.id, data.slotId), eq(availabilitySlotsRef.productId, product.id)))
|
|
211
|
+
.limit(1);
|
|
212
|
+
if (!selectedSlot) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
if (option && selectedSlot.optionId && selectedSlot.optionId !== option.id) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
slot = {
|
|
219
|
+
id: selectedSlot.id,
|
|
220
|
+
dateLocal: selectedSlot.dateLocal,
|
|
221
|
+
startsAt: selectedSlot.startsAt,
|
|
222
|
+
endsAt: selectedSlot.endsAt,
|
|
223
|
+
timezone: selectedSlot.timezone,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
191
226
|
return {
|
|
192
227
|
product: {
|
|
193
228
|
id: product.id,
|
|
@@ -202,6 +237,7 @@ async function getConvertProductData(db, data) {
|
|
|
202
237
|
pax: product.pax,
|
|
203
238
|
},
|
|
204
239
|
option: option ? { id: option.id, name: option.name } : null,
|
|
240
|
+
slot,
|
|
205
241
|
dayServices,
|
|
206
242
|
units: units.map((unit) => ({
|
|
207
243
|
id: unit.id,
|
|
@@ -318,6 +354,16 @@ async function reserveBookingFromTransactionSource(db, source, data, userId) {
|
|
|
318
354
|
personId: source.personId,
|
|
319
355
|
organizationId: source.organizationId,
|
|
320
356
|
sourceType: data.sourceType,
|
|
357
|
+
contactFirstName: data.contactFirstName ?? source.contactFirstName,
|
|
358
|
+
contactLastName: data.contactLastName ?? source.contactLastName,
|
|
359
|
+
contactEmail: data.contactEmail ?? source.contactEmail,
|
|
360
|
+
contactPhone: data.contactPhone ?? source.contactPhone,
|
|
361
|
+
contactPreferredLanguage: data.contactPreferredLanguage ?? source.contactPreferredLanguage,
|
|
362
|
+
contactCountry: data.contactCountry ?? source.contactCountry,
|
|
363
|
+
contactRegion: data.contactRegion ?? source.contactRegion,
|
|
364
|
+
contactCity: data.contactCity ?? source.contactCity,
|
|
365
|
+
contactAddressLine1: data.contactAddressLine1 ?? source.contactAddressLine1,
|
|
366
|
+
contactPostalCode: data.contactPostalCode ?? source.contactPostalCode,
|
|
321
367
|
sellCurrency: source.currency,
|
|
322
368
|
baseCurrency: source.baseCurrency,
|
|
323
369
|
sellAmountCents: source.totalAmountCents,
|
|
@@ -333,10 +379,15 @@ async function reserveBookingFromTransactionSource(db, source, data, userId) {
|
|
|
333
379
|
throw new BookingServiceError("booking_create_failed");
|
|
334
380
|
}
|
|
335
381
|
const participantMap = new Map();
|
|
382
|
+
const staffParticipantMap = new Map();
|
|
336
383
|
if (data.includeParticipants) {
|
|
337
384
|
for (const participant of source.participants) {
|
|
385
|
+
if (isStaffParticipantType(participant.participantType)) {
|
|
386
|
+
staffParticipantMap.set(participant.id, participant);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
338
389
|
const [createdParticipant] = await tx
|
|
339
|
-
.insert(
|
|
390
|
+
.insert(bookingTravelers)
|
|
340
391
|
.values({
|
|
341
392
|
bookingId: booking.id,
|
|
342
393
|
personId: participant.personId ?? null,
|
|
@@ -437,17 +488,78 @@ async function reserveBookingFromTransactionSource(db, source, data, userId) {
|
|
|
437
488
|
continue;
|
|
438
489
|
}
|
|
439
490
|
const bookingItemId = bookingItemMap.get(sourceItemId);
|
|
440
|
-
const
|
|
441
|
-
if (!bookingItemId || !
|
|
491
|
+
const travelerId = participantMap.get(link.travelerId);
|
|
492
|
+
if (!bookingItemId || !travelerId) {
|
|
442
493
|
continue;
|
|
443
494
|
}
|
|
444
|
-
await tx.insert(
|
|
495
|
+
await tx.insert(bookingItemTravelers).values({
|
|
445
496
|
bookingItemId,
|
|
446
|
-
|
|
497
|
+
travelerId,
|
|
447
498
|
role: link.role,
|
|
448
499
|
isPrimary: link.isPrimary,
|
|
449
500
|
});
|
|
450
501
|
}
|
|
502
|
+
if (staffParticipantMap.size > 0) {
|
|
503
|
+
const linkedStaffAssignments = [];
|
|
504
|
+
const linkedStaffParticipantIds = new Set();
|
|
505
|
+
for (const link of source.itemParticipants) {
|
|
506
|
+
const staffParticipant = staffParticipantMap.get(link.travelerId);
|
|
507
|
+
if (!staffParticipant) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const sourceItemId = getTransactionItemParticipantItemId(link);
|
|
511
|
+
if (!sourceItemId) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const bookingItemId = bookingItemMap.get(sourceItemId);
|
|
515
|
+
if (!bookingItemId) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
linkedStaffParticipantIds.add(staffParticipant.id);
|
|
519
|
+
linkedStaffAssignments.push({
|
|
520
|
+
bookingId: booking.id,
|
|
521
|
+
bookingItemId,
|
|
522
|
+
personId: staffParticipant.personId ?? null,
|
|
523
|
+
role: toStaffAssignmentRole(link.role),
|
|
524
|
+
firstName: staffParticipant.firstName,
|
|
525
|
+
lastName: staffParticipant.lastName,
|
|
526
|
+
email: staffParticipant.email ?? null,
|
|
527
|
+
phone: staffParticipant.phone ?? null,
|
|
528
|
+
preferredLanguage: staffParticipant.preferredLanguage ?? null,
|
|
529
|
+
isPrimary: link.isPrimary || staffParticipant.isPrimary,
|
|
530
|
+
notes: staffParticipant.notes ?? null,
|
|
531
|
+
metadata: {
|
|
532
|
+
sourceParticipantId: staffParticipant.id,
|
|
533
|
+
sourceItemId,
|
|
534
|
+
sourceRole: link.role,
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
for (const staffParticipant of staffParticipantMap.values()) {
|
|
539
|
+
if (linkedStaffParticipantIds.has(staffParticipant.id)) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
linkedStaffAssignments.push({
|
|
543
|
+
bookingId: booking.id,
|
|
544
|
+
bookingItemId: null,
|
|
545
|
+
personId: staffParticipant.personId ?? null,
|
|
546
|
+
role: "service_assignee",
|
|
547
|
+
firstName: staffParticipant.firstName,
|
|
548
|
+
lastName: staffParticipant.lastName,
|
|
549
|
+
email: staffParticipant.email ?? null,
|
|
550
|
+
phone: staffParticipant.phone ?? null,
|
|
551
|
+
preferredLanguage: staffParticipant.preferredLanguage ?? null,
|
|
552
|
+
isPrimary: staffParticipant.isPrimary,
|
|
553
|
+
notes: staffParticipant.notes ?? null,
|
|
554
|
+
metadata: {
|
|
555
|
+
sourceParticipantId: staffParticipant.id,
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (linkedStaffAssignments.length > 0) {
|
|
560
|
+
await tx.insert(bookingStaffAssignments).values(linkedStaffAssignments);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
451
563
|
await tx
|
|
452
564
|
.insert(bookingTransactionDetailsRef)
|
|
453
565
|
.values({
|
|
@@ -616,13 +728,13 @@ async function autoIssueFulfillmentsForBooking(db, bookingId, userId) {
|
|
|
616
728
|
const settingsByProductId = new Map(settings.map((setting) => [setting.productId, setting]));
|
|
617
729
|
const travelerParticipants = await db
|
|
618
730
|
.select()
|
|
619
|
-
.from(
|
|
620
|
-
.where(and(eq(
|
|
621
|
-
.orderBy(desc(
|
|
731
|
+
.from(bookingTravelers)
|
|
732
|
+
.where(and(eq(bookingTravelers.bookingId, bookingId), or(eq(bookingTravelers.participantType, "traveler"), eq(bookingTravelers.participantType, "occupant"))))
|
|
733
|
+
.orderBy(desc(bookingTravelers.isPrimary), asc(bookingTravelers.createdAt));
|
|
622
734
|
const participantLinks = await db
|
|
623
735
|
.select()
|
|
624
|
-
.from(
|
|
625
|
-
.where(sql `${
|
|
736
|
+
.from(bookingItemTravelers)
|
|
737
|
+
.where(sql `${bookingItemTravelers.bookingItemId} IN (
|
|
626
738
|
SELECT ${bookingItems.id}
|
|
627
739
|
FROM ${bookingItems}
|
|
628
740
|
WHERE ${bookingItems.bookingId} = ${bookingId}
|
|
@@ -660,7 +772,7 @@ async function autoIssueFulfillmentsForBooking(db, bookingId, userId) {
|
|
|
660
772
|
fulfillmentsToInsert.push({
|
|
661
773
|
bookingId,
|
|
662
774
|
bookingItemId: item.id,
|
|
663
|
-
|
|
775
|
+
travelerId: null,
|
|
664
776
|
fulfillmentType: delivery.fulfillmentType,
|
|
665
777
|
deliveryChannel: delivery.deliveryChannel,
|
|
666
778
|
status: "issued",
|
|
@@ -673,7 +785,7 @@ async function autoIssueFulfillmentsForBooking(db, bookingId, userId) {
|
|
|
673
785
|
fulfillmentsToInsert.push({
|
|
674
786
|
bookingId,
|
|
675
787
|
bookingItemId: item.id,
|
|
676
|
-
|
|
788
|
+
travelerId: null,
|
|
677
789
|
fulfillmentType: delivery.fulfillmentType,
|
|
678
790
|
deliveryChannel: delivery.deliveryChannel,
|
|
679
791
|
status: "issued",
|
|
@@ -684,20 +796,20 @@ async function autoIssueFulfillmentsForBooking(db, bookingId, userId) {
|
|
|
684
796
|
}
|
|
685
797
|
const linkedParticipants = participantLinksByItemId
|
|
686
798
|
.get(item.id)
|
|
687
|
-
?.map((link) => travelerParticipants.find((participant) => participant.id === link.
|
|
799
|
+
?.map((link) => travelerParticipants.find((participant) => participant.id === link.travelerId))
|
|
688
800
|
.filter((participant) => Boolean(participant)) ?? [];
|
|
689
801
|
const participantsForItem = linkedParticipants.length > 0 ? linkedParticipants : travelerParticipants;
|
|
690
802
|
for (const participant of participantsForItem) {
|
|
691
803
|
fulfillmentsToInsert.push({
|
|
692
804
|
bookingId,
|
|
693
805
|
bookingItemId: item.id,
|
|
694
|
-
|
|
806
|
+
travelerId: participant.id,
|
|
695
807
|
fulfillmentType: delivery.fulfillmentType,
|
|
696
808
|
deliveryChannel: delivery.deliveryChannel,
|
|
697
809
|
status: "issued",
|
|
698
810
|
payload: {
|
|
699
811
|
...payloadBase,
|
|
700
|
-
|
|
812
|
+
travelerId: participant.id,
|
|
701
813
|
scope: "participant",
|
|
702
814
|
},
|
|
703
815
|
issuedAt: now,
|
|
@@ -716,7 +828,92 @@ async function autoIssueFulfillmentsForBooking(db, bookingId, userId) {
|
|
|
716
828
|
metadata: { count: fulfillmentsToInsert.length },
|
|
717
829
|
});
|
|
718
830
|
}
|
|
831
|
+
/**
|
|
832
|
+
* Booking statuses that count as "active" for aggregate purposes (matches the
|
|
833
|
+
* slot-unit-availability counting rules — cancelled and expired drop out).
|
|
834
|
+
*/
|
|
835
|
+
const AGGREGATE_ACTIVE_STATUSES = [
|
|
836
|
+
"draft",
|
|
837
|
+
"on_hold",
|
|
838
|
+
"confirmed",
|
|
839
|
+
"in_progress",
|
|
840
|
+
"completed",
|
|
841
|
+
];
|
|
719
842
|
export const bookingsService = {
|
|
843
|
+
/**
|
|
844
|
+
* Pre-aggregated dashboard numbers for the admin bookings surface. Replaces
|
|
845
|
+
* the pattern of fetching a large `listBookings` page and deriving KPIs
|
|
846
|
+
* client-side — which broke past the page limit and disagreed across apps
|
|
847
|
+
* on which statuses count.
|
|
848
|
+
*
|
|
849
|
+
* All ranges are UTC-based.
|
|
850
|
+
*/
|
|
851
|
+
async getBookingAggregates(db, options = {}) {
|
|
852
|
+
const fromDate = options.from ? new Date(options.from) : undefined;
|
|
853
|
+
const toDate = options.to ? new Date(options.to) : undefined;
|
|
854
|
+
const rangeConditions = [];
|
|
855
|
+
if (fromDate)
|
|
856
|
+
rangeConditions.push(sql `${bookings.createdAt} >= ${fromDate}`);
|
|
857
|
+
if (toDate)
|
|
858
|
+
rangeConditions.push(sql `${bookings.createdAt} < ${toDate}`);
|
|
859
|
+
const rangeWhere = rangeConditions.length ? and(...rangeConditions) : undefined;
|
|
860
|
+
const [totalRow] = await db
|
|
861
|
+
.select({ count: sql `count(*)::int` })
|
|
862
|
+
.from(bookings)
|
|
863
|
+
.where(rangeWhere);
|
|
864
|
+
const statusRows = await db
|
|
865
|
+
.select({
|
|
866
|
+
status: bookings.status,
|
|
867
|
+
count: sql `count(*)::int`,
|
|
868
|
+
})
|
|
869
|
+
.from(bookings)
|
|
870
|
+
.where(rangeWhere)
|
|
871
|
+
.groupBy(bookings.status);
|
|
872
|
+
const countsByStatusMap = new Map(statusRows.map((row) => [row.status, row.count]));
|
|
873
|
+
const monthlyCountsRows = await db
|
|
874
|
+
.select({
|
|
875
|
+
yearMonth: sql `to_char(${bookings.createdAt} at time zone 'UTC', 'YYYY-MM')`,
|
|
876
|
+
count: sql `count(*)::int`,
|
|
877
|
+
})
|
|
878
|
+
.from(bookings)
|
|
879
|
+
.where(rangeWhere)
|
|
880
|
+
.groupBy(sql `to_char(${bookings.createdAt} at time zone 'UTC', 'YYYY-MM')`)
|
|
881
|
+
.orderBy(sql `to_char(${bookings.createdAt} at time zone 'UTC', 'YYYY-MM')`);
|
|
882
|
+
const monthlyRevenueRows = await db
|
|
883
|
+
.select({
|
|
884
|
+
yearMonth: sql `to_char(${bookings.createdAt} at time zone 'UTC', 'YYYY-MM')`,
|
|
885
|
+
currency: bookings.sellCurrency,
|
|
886
|
+
sellAmountCents: sql `coalesce(sum(${bookings.sellAmountCents}), 0)::bigint`,
|
|
887
|
+
})
|
|
888
|
+
.from(bookings)
|
|
889
|
+
.where(and(...(rangeConditions.length ? rangeConditions : []), sql `${bookings.sellAmountCents} IS NOT NULL`, inArray(bookings.status, [...AGGREGATE_ACTIVE_STATUSES])))
|
|
890
|
+
.groupBy(sql `to_char(${bookings.createdAt} at time zone 'UTC', 'YYYY-MM')`, bookings.sellCurrency)
|
|
891
|
+
.orderBy(sql `to_char(${bookings.createdAt} at time zone 'UTC', 'YYYY-MM')`, bookings.sellCurrency);
|
|
892
|
+
const todayUtc = new Date();
|
|
893
|
+
todayUtc.setUTCHours(0, 0, 0, 0);
|
|
894
|
+
const todayDateString = todayUtc.toISOString().slice(0, 10);
|
|
895
|
+
const [upcomingRow] = await db
|
|
896
|
+
.select({ count: sql `count(*)::int` })
|
|
897
|
+
.from(bookings)
|
|
898
|
+
.where(and(inArray(bookings.status, [...AGGREGATE_ACTIVE_STATUSES]), sql `${bookings.startDate} >= ${todayDateString}`));
|
|
899
|
+
return {
|
|
900
|
+
total: totalRow?.count ?? 0,
|
|
901
|
+
countsByStatus: AGGREGATE_ACTIVE_STATUSES.concat(["expired", "cancelled"]).map((status) => ({
|
|
902
|
+
status,
|
|
903
|
+
count: countsByStatusMap.get(status) ?? 0,
|
|
904
|
+
})),
|
|
905
|
+
monthlyCounts: monthlyCountsRows.map((row) => ({
|
|
906
|
+
yearMonth: row.yearMonth,
|
|
907
|
+
count: row.count,
|
|
908
|
+
})),
|
|
909
|
+
monthlyRevenue: monthlyRevenueRows.map((row) => ({
|
|
910
|
+
yearMonth: row.yearMonth,
|
|
911
|
+
currency: row.currency,
|
|
912
|
+
sellAmountCents: Number(row.sellAmountCents),
|
|
913
|
+
})),
|
|
914
|
+
upcomingDepartures: upcomingRow?.count ?? 0,
|
|
915
|
+
};
|
|
916
|
+
},
|
|
720
917
|
async listBookings(db, query) {
|
|
721
918
|
const conditions = [];
|
|
722
919
|
if (query.status) {
|
|
@@ -726,6 +923,25 @@ export const bookingsService = {
|
|
|
726
923
|
const term = `%${query.search}%`;
|
|
727
924
|
conditions.push(or(ilike(bookings.bookingNumber, term), ilike(bookings.internalNotes, term)));
|
|
728
925
|
}
|
|
926
|
+
if (query.personId) {
|
|
927
|
+
conditions.push(eq(bookings.personId, query.personId));
|
|
928
|
+
}
|
|
929
|
+
if (query.organizationId) {
|
|
930
|
+
conditions.push(eq(bookings.organizationId, query.organizationId));
|
|
931
|
+
}
|
|
932
|
+
if (query.productId || query.optionId) {
|
|
933
|
+
const itemConditions = [eq(bookingItems.bookingId, bookings.id)];
|
|
934
|
+
if (query.productId) {
|
|
935
|
+
itemConditions.push(eq(bookingItems.productId, query.productId));
|
|
936
|
+
}
|
|
937
|
+
if (query.optionId) {
|
|
938
|
+
itemConditions.push(eq(bookingItems.optionId, query.optionId));
|
|
939
|
+
}
|
|
940
|
+
conditions.push(exists(db
|
|
941
|
+
.select({ one: sql `1` })
|
|
942
|
+
.from(bookingItems)
|
|
943
|
+
.where(and(...itemConditions))));
|
|
944
|
+
}
|
|
729
945
|
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
|
730
946
|
const [rows, countResult] = await Promise.all([
|
|
731
947
|
db
|
|
@@ -745,7 +961,16 @@ export const bookingsService = {
|
|
|
745
961
|
};
|
|
746
962
|
},
|
|
747
963
|
async convertProductToBooking(db, data, productData, userId) {
|
|
748
|
-
const { product, option, dayServices, units } = productData;
|
|
964
|
+
const { product, option, slot, dayServices, units } = productData;
|
|
965
|
+
// Slot dates win over product dates so scheduled/recurring products don't
|
|
966
|
+
// land with null dates. endsAt is a timestamp; fall back to the slot's
|
|
967
|
+
// dateLocal when the slot has no explicit end timestamp.
|
|
968
|
+
const startDate = slot?.dateLocal ?? product.startDate;
|
|
969
|
+
const endDate = slot
|
|
970
|
+
? slot.endsAt
|
|
971
|
+
? slot.endsAt.toISOString().slice(0, 10)
|
|
972
|
+
: slot.dateLocal
|
|
973
|
+
: product.endDate;
|
|
749
974
|
const [booking] = await db
|
|
750
975
|
.insert(bookings)
|
|
751
976
|
.values({
|
|
@@ -757,8 +982,8 @@ export const bookingsService = {
|
|
|
757
982
|
sellAmountCents: product.sellAmountCents,
|
|
758
983
|
costAmountCents: product.costAmountCents,
|
|
759
984
|
marginPercent: product.marginPercent,
|
|
760
|
-
startDate
|
|
761
|
-
endDate
|
|
985
|
+
startDate,
|
|
986
|
+
endDate,
|
|
762
987
|
pax: product.pax,
|
|
763
988
|
internalNotes: data.internalNotes ?? null,
|
|
764
989
|
})
|
|
@@ -782,6 +1007,14 @@ export const bookingsService = {
|
|
|
782
1007
|
: selectedUnits.length === 1
|
|
783
1008
|
? selectedUnits
|
|
784
1009
|
: [];
|
|
1010
|
+
const slotFields = slot
|
|
1011
|
+
? {
|
|
1012
|
+
serviceDate: slot.dateLocal,
|
|
1013
|
+
startsAt: slot.startsAt,
|
|
1014
|
+
endsAt: slot.endsAt,
|
|
1015
|
+
metadata: { availabilitySlotId: slot.id },
|
|
1016
|
+
}
|
|
1017
|
+
: { metadata: null };
|
|
785
1018
|
const itemRows = unitsToSeed.length > 0
|
|
786
1019
|
? unitsToSeed.map((unit, index) => {
|
|
787
1020
|
const quantity = unit.unitType === "person" && product.pax
|
|
@@ -814,6 +1047,7 @@ export const bookingsService = {
|
|
|
814
1047
|
productId: product.id,
|
|
815
1048
|
optionId: option?.id ?? null,
|
|
816
1049
|
optionUnitId: unit.id,
|
|
1050
|
+
...slotFields,
|
|
817
1051
|
};
|
|
818
1052
|
})
|
|
819
1053
|
: [
|
|
@@ -833,6 +1067,7 @@ export const bookingsService = {
|
|
|
833
1067
|
productId: product.id,
|
|
834
1068
|
optionId: option?.id ?? null,
|
|
835
1069
|
optionUnitId: null,
|
|
1070
|
+
...slotFields,
|
|
836
1071
|
},
|
|
837
1072
|
];
|
|
838
1073
|
const insertedItems = await db.insert(bookingItems).values(itemRows).returning();
|
|
@@ -865,7 +1100,12 @@ export const bookingsService = {
|
|
|
865
1100
|
actorId: userId ?? "system",
|
|
866
1101
|
activityType: "booking_converted",
|
|
867
1102
|
description: `Booking converted from product "${product.name}"`,
|
|
868
|
-
metadata: {
|
|
1103
|
+
metadata: {
|
|
1104
|
+
productId: product.id,
|
|
1105
|
+
productName: product.name,
|
|
1106
|
+
optionId: option?.id ?? null,
|
|
1107
|
+
slotId: slot?.id ?? null,
|
|
1108
|
+
},
|
|
869
1109
|
});
|
|
870
1110
|
return booking;
|
|
871
1111
|
},
|
|
@@ -892,7 +1132,7 @@ export const bookingsService = {
|
|
|
892
1132
|
if (!offer) {
|
|
893
1133
|
return { status: "not_found" };
|
|
894
1134
|
}
|
|
895
|
-
const [participants, items, itemParticipants] = await Promise.all([
|
|
1135
|
+
const [participants, items, itemParticipants, staffAssignments] = await Promise.all([
|
|
896
1136
|
db
|
|
897
1137
|
.select()
|
|
898
1138
|
.from(offerParticipantsRef)
|
|
@@ -912,7 +1152,31 @@ export const bookingsService = {
|
|
|
912
1152
|
WHERE ${offerItemsRef.offerId} = ${offerId}
|
|
913
1153
|
)`)
|
|
914
1154
|
.orderBy(asc(offerItemParticipantsRef.createdAt)),
|
|
1155
|
+
db
|
|
1156
|
+
.select()
|
|
1157
|
+
.from(offerStaffAssignmentsRef)
|
|
1158
|
+
.where(eq(offerStaffAssignmentsRef.offerId, offerId))
|
|
1159
|
+
.orderBy(asc(offerStaffAssignmentsRef.createdAt)),
|
|
915
1160
|
]);
|
|
1161
|
+
const reservationParticipants = [...participants];
|
|
1162
|
+
const reservationItemParticipants = itemParticipants.map((link) => ({
|
|
1163
|
+
travelerId: link.travelerId,
|
|
1164
|
+
role: link.role,
|
|
1165
|
+
isPrimary: link.isPrimary,
|
|
1166
|
+
offerItemId: link.offerItemId,
|
|
1167
|
+
}));
|
|
1168
|
+
for (const assignment of staffAssignments) {
|
|
1169
|
+
const participant = toStaffReservationParticipant(assignment, "offer");
|
|
1170
|
+
reservationParticipants.push(participant);
|
|
1171
|
+
if (assignment.offerItemId) {
|
|
1172
|
+
reservationItemParticipants.push({
|
|
1173
|
+
travelerId: participant.id,
|
|
1174
|
+
role: assignment.role,
|
|
1175
|
+
isPrimary: assignment.isPrimary,
|
|
1176
|
+
offerItemId: assignment.offerItemId,
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
916
1180
|
return reserveBookingFromTransactionSource(db, {
|
|
917
1181
|
kind: "offer",
|
|
918
1182
|
sourceId: offerId,
|
|
@@ -920,14 +1184,24 @@ export const bookingsService = {
|
|
|
920
1184
|
orderId: null,
|
|
921
1185
|
personId: offer.personId ?? null,
|
|
922
1186
|
organizationId: offer.organizationId ?? null,
|
|
1187
|
+
contactFirstName: offer.contactFirstName ?? null,
|
|
1188
|
+
contactLastName: offer.contactLastName ?? null,
|
|
1189
|
+
contactEmail: offer.contactEmail ?? null,
|
|
1190
|
+
contactPhone: offer.contactPhone ?? null,
|
|
1191
|
+
contactPreferredLanguage: offer.contactPreferredLanguage ?? null,
|
|
1192
|
+
contactCountry: offer.contactCountry ?? null,
|
|
1193
|
+
contactRegion: offer.contactRegion ?? null,
|
|
1194
|
+
contactCity: offer.contactCity ?? null,
|
|
1195
|
+
contactAddressLine1: offer.contactAddressLine1 ?? null,
|
|
1196
|
+
contactPostalCode: offer.contactPostalCode ?? null,
|
|
923
1197
|
currency: offer.currency,
|
|
924
1198
|
baseCurrency: offer.baseCurrency ?? null,
|
|
925
1199
|
totalAmountCents: offer.totalAmountCents ?? null,
|
|
926
1200
|
costAmountCents: offer.costAmountCents ?? null,
|
|
927
1201
|
notes: offer.notes ?? null,
|
|
928
|
-
participants,
|
|
1202
|
+
participants: reservationParticipants,
|
|
929
1203
|
items,
|
|
930
|
-
itemParticipants,
|
|
1204
|
+
itemParticipants: reservationItemParticipants,
|
|
931
1205
|
}, data, userId);
|
|
932
1206
|
},
|
|
933
1207
|
async reserveBookingFromOrder(db, orderId, data, userId) {
|
|
@@ -935,7 +1209,7 @@ export const bookingsService = {
|
|
|
935
1209
|
if (!order) {
|
|
936
1210
|
return { status: "not_found" };
|
|
937
1211
|
}
|
|
938
|
-
const [participants, items, itemParticipants] = await Promise.all([
|
|
1212
|
+
const [participants, items, itemParticipants, staffAssignments] = await Promise.all([
|
|
939
1213
|
db
|
|
940
1214
|
.select()
|
|
941
1215
|
.from(orderParticipantsRef)
|
|
@@ -955,7 +1229,31 @@ export const bookingsService = {
|
|
|
955
1229
|
WHERE ${orderItemsRef.orderId} = ${orderId}
|
|
956
1230
|
)`)
|
|
957
1231
|
.orderBy(asc(orderItemParticipantsRef.createdAt)),
|
|
1232
|
+
db
|
|
1233
|
+
.select()
|
|
1234
|
+
.from(orderStaffAssignmentsRef)
|
|
1235
|
+
.where(eq(orderStaffAssignmentsRef.orderId, orderId))
|
|
1236
|
+
.orderBy(asc(orderStaffAssignmentsRef.createdAt)),
|
|
958
1237
|
]);
|
|
1238
|
+
const reservationParticipants = [...participants];
|
|
1239
|
+
const reservationItemParticipants = itemParticipants.map((link) => ({
|
|
1240
|
+
travelerId: link.travelerId,
|
|
1241
|
+
role: link.role,
|
|
1242
|
+
isPrimary: link.isPrimary,
|
|
1243
|
+
orderItemId: link.orderItemId,
|
|
1244
|
+
}));
|
|
1245
|
+
for (const assignment of staffAssignments) {
|
|
1246
|
+
const participant = toStaffReservationParticipant(assignment, "order");
|
|
1247
|
+
reservationParticipants.push(participant);
|
|
1248
|
+
if (assignment.orderItemId) {
|
|
1249
|
+
reservationItemParticipants.push({
|
|
1250
|
+
travelerId: participant.id,
|
|
1251
|
+
role: assignment.role,
|
|
1252
|
+
isPrimary: assignment.isPrimary,
|
|
1253
|
+
orderItemId: assignment.orderItemId,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
959
1257
|
return reserveBookingFromTransactionSource(db, {
|
|
960
1258
|
kind: "order",
|
|
961
1259
|
sourceId: orderId,
|
|
@@ -963,14 +1261,24 @@ export const bookingsService = {
|
|
|
963
1261
|
orderId: order.id,
|
|
964
1262
|
personId: order.personId ?? null,
|
|
965
1263
|
organizationId: order.organizationId ?? null,
|
|
1264
|
+
contactFirstName: order.contactFirstName ?? null,
|
|
1265
|
+
contactLastName: order.contactLastName ?? null,
|
|
1266
|
+
contactEmail: order.contactEmail ?? null,
|
|
1267
|
+
contactPhone: order.contactPhone ?? null,
|
|
1268
|
+
contactPreferredLanguage: order.contactPreferredLanguage ?? null,
|
|
1269
|
+
contactCountry: order.contactCountry ?? null,
|
|
1270
|
+
contactRegion: order.contactRegion ?? null,
|
|
1271
|
+
contactCity: order.contactCity ?? null,
|
|
1272
|
+
contactAddressLine1: order.contactAddressLine1 ?? null,
|
|
1273
|
+
contactPostalCode: order.contactPostalCode ?? null,
|
|
966
1274
|
currency: order.currency,
|
|
967
1275
|
baseCurrency: order.baseCurrency ?? null,
|
|
968
1276
|
totalAmountCents: order.totalAmountCents ?? null,
|
|
969
1277
|
costAmountCents: order.costAmountCents ?? null,
|
|
970
1278
|
notes: order.notes ?? null,
|
|
971
|
-
participants,
|
|
1279
|
+
participants: reservationParticipants,
|
|
972
1280
|
items,
|
|
973
|
-
itemParticipants,
|
|
1281
|
+
itemParticipants: reservationItemParticipants,
|
|
974
1282
|
}, data, userId);
|
|
975
1283
|
},
|
|
976
1284
|
async reserveBooking(db, data, userId) {
|
|
@@ -987,6 +1295,16 @@ export const bookingsService = {
|
|
|
987
1295
|
sourceType: data.sourceType,
|
|
988
1296
|
externalBookingRef: data.externalBookingRef ?? null,
|
|
989
1297
|
communicationLanguage: data.communicationLanguage ?? null,
|
|
1298
|
+
contactFirstName: data.contactFirstName ?? null,
|
|
1299
|
+
contactLastName: data.contactLastName ?? null,
|
|
1300
|
+
contactEmail: data.contactEmail ?? null,
|
|
1301
|
+
contactPhone: data.contactPhone ?? null,
|
|
1302
|
+
contactPreferredLanguage: data.contactPreferredLanguage ?? null,
|
|
1303
|
+
contactCountry: data.contactCountry ?? null,
|
|
1304
|
+
contactRegion: data.contactRegion ?? null,
|
|
1305
|
+
contactCity: data.contactCity ?? null,
|
|
1306
|
+
contactAddressLine1: data.contactAddressLine1 ?? null,
|
|
1307
|
+
contactPostalCode: data.contactPostalCode ?? null,
|
|
990
1308
|
sellCurrency: data.sellCurrency,
|
|
991
1309
|
baseCurrency: data.baseCurrency ?? null,
|
|
992
1310
|
sellAmountCents: data.sellAmountCents ?? null,
|
|
@@ -1097,6 +1415,16 @@ export const bookingsService = {
|
|
|
1097
1415
|
.insert(bookings)
|
|
1098
1416
|
.values({
|
|
1099
1417
|
...data,
|
|
1418
|
+
contactFirstName: data.contactFirstName ?? null,
|
|
1419
|
+
contactLastName: data.contactLastName ?? null,
|
|
1420
|
+
contactEmail: data.contactEmail ?? null,
|
|
1421
|
+
contactPhone: data.contactPhone ?? null,
|
|
1422
|
+
contactPreferredLanguage: data.contactPreferredLanguage ?? null,
|
|
1423
|
+
contactCountry: data.contactCountry ?? null,
|
|
1424
|
+
contactRegion: data.contactRegion ?? null,
|
|
1425
|
+
contactCity: data.contactCity ?? null,
|
|
1426
|
+
contactAddressLine1: data.contactAddressLine1 ?? null,
|
|
1427
|
+
contactPostalCode: data.contactPostalCode ?? null,
|
|
1100
1428
|
holdExpiresAt: toTimestamp(data.holdExpiresAt),
|
|
1101
1429
|
confirmedAt: toTimestamp(data.confirmedAt),
|
|
1102
1430
|
expiredAt: toTimestamp(data.expiredAt),
|
|
@@ -1122,6 +1450,18 @@ export const bookingsService = {
|
|
|
1122
1450
|
.update(bookings)
|
|
1123
1451
|
.set({
|
|
1124
1452
|
...data,
|
|
1453
|
+
contactFirstName: data.contactFirstName === undefined ? undefined : (data.contactFirstName ?? null),
|
|
1454
|
+
contactLastName: data.contactLastName === undefined ? undefined : (data.contactLastName ?? null),
|
|
1455
|
+
contactEmail: data.contactEmail === undefined ? undefined : (data.contactEmail ?? null),
|
|
1456
|
+
contactPhone: data.contactPhone === undefined ? undefined : (data.contactPhone ?? null),
|
|
1457
|
+
contactPreferredLanguage: data.contactPreferredLanguage === undefined
|
|
1458
|
+
? undefined
|
|
1459
|
+
: (data.contactPreferredLanguage ?? null),
|
|
1460
|
+
contactCountry: data.contactCountry === undefined ? undefined : (data.contactCountry ?? null),
|
|
1461
|
+
contactRegion: data.contactRegion === undefined ? undefined : (data.contactRegion ?? null),
|
|
1462
|
+
contactCity: data.contactCity === undefined ? undefined : (data.contactCity ?? null),
|
|
1463
|
+
contactAddressLine1: data.contactAddressLine1 === undefined ? undefined : (data.contactAddressLine1 ?? null),
|
|
1464
|
+
contactPostalCode: data.contactPostalCode === undefined ? undefined : (data.contactPostalCode ?? null),
|
|
1125
1465
|
holdExpiresAt: data.holdExpiresAt === undefined ? undefined : toTimestamp(data.holdExpiresAt),
|
|
1126
1466
|
confirmedAt: data.confirmedAt === undefined ? undefined : toTimestamp(data.confirmedAt),
|
|
1127
1467
|
expiredAt: data.expiredAt === undefined ? undefined : toTimestamp(data.expiredAt),
|
|
@@ -1141,7 +1481,7 @@ export const bookingsService = {
|
|
|
1141
1481
|
.returning({ id: bookings.id });
|
|
1142
1482
|
return row ?? null;
|
|
1143
1483
|
},
|
|
1144
|
-
async updateBookingStatus(db, id, data, userId) {
|
|
1484
|
+
async updateBookingStatus(db, id, data, userId, runtime = {}) {
|
|
1145
1485
|
const [current] = await db
|
|
1146
1486
|
.select({ id: bookings.id, status: bookings.status })
|
|
1147
1487
|
.from(bookings)
|
|
@@ -1151,13 +1491,13 @@ export const bookingsService = {
|
|
|
1151
1491
|
return { status: "not_found" };
|
|
1152
1492
|
}
|
|
1153
1493
|
if (current.status === "on_hold" && data.status === "confirmed") {
|
|
1154
|
-
return bookingsService.confirmBooking(db, id, { note: data.note }, userId);
|
|
1494
|
+
return bookingsService.confirmBooking(db, id, { note: data.note }, userId, runtime);
|
|
1155
1495
|
}
|
|
1156
1496
|
if (current.status === "on_hold" && data.status === "expired") {
|
|
1157
|
-
return bookingsService.expireBooking(db, id, { note: data.note }, userId);
|
|
1497
|
+
return bookingsService.expireBooking(db, id, { note: data.note }, userId, runtime);
|
|
1158
1498
|
}
|
|
1159
1499
|
if (data.status === "cancelled") {
|
|
1160
|
-
return bookingsService.cancelBooking(db, id, { note: data.note }, userId);
|
|
1500
|
+
return bookingsService.cancelBooking(db, id, { note: data.note }, userId, runtime);
|
|
1161
1501
|
}
|
|
1162
1502
|
if (data.status === "on_hold") {
|
|
1163
1503
|
return { status: "invalid_transition" };
|
|
@@ -1193,9 +1533,9 @@ export const bookingsService = {
|
|
|
1193
1533
|
}
|
|
1194
1534
|
return { status: "ok", booking: row ?? null };
|
|
1195
1535
|
},
|
|
1196
|
-
async confirmBooking(db, id, data, userId) {
|
|
1536
|
+
async confirmBooking(db, id, data, userId, runtime = {}) {
|
|
1197
1537
|
try {
|
|
1198
|
-
|
|
1538
|
+
const result = await db.transaction(async (tx) => {
|
|
1199
1539
|
const rows = await tx.execute(sql `SELECT id, booking_number, status, hold_expires_at
|
|
1200
1540
|
FROM ${bookings}
|
|
1201
1541
|
WHERE ${bookings.id} = ${id}
|
|
@@ -1249,6 +1589,17 @@ export const bookingsService = {
|
|
|
1249
1589
|
}
|
|
1250
1590
|
return { status: "ok", booking: row ?? null };
|
|
1251
1591
|
});
|
|
1592
|
+
// Emit AFTER the transaction commits so subscribers can't observe a
|
|
1593
|
+
// confirmed state that might still roll back. `emit` is fire-and-forget
|
|
1594
|
+
// per the EventBus contract — subscriber errors are logged, not rethrown.
|
|
1595
|
+
if (result.status === "ok" && result.booking) {
|
|
1596
|
+
await runtime.eventBus?.emit("booking.confirmed", {
|
|
1597
|
+
bookingId: result.booking.id,
|
|
1598
|
+
bookingNumber: result.booking.bookingNumber,
|
|
1599
|
+
actorId: userId ?? null,
|
|
1600
|
+
}, { category: "domain", source: "service" });
|
|
1601
|
+
}
|
|
1602
|
+
return result;
|
|
1252
1603
|
}
|
|
1253
1604
|
catch (error) {
|
|
1254
1605
|
if (error instanceof BookingServiceError) {
|
|
@@ -1307,9 +1658,9 @@ export const bookingsService = {
|
|
|
1307
1658
|
throw error;
|
|
1308
1659
|
}
|
|
1309
1660
|
},
|
|
1310
|
-
async expireBooking(db, id, data, userId) {
|
|
1661
|
+
async expireBooking(db, id, data, userId, runtime = {}) {
|
|
1311
1662
|
try {
|
|
1312
|
-
|
|
1663
|
+
const result = await db.transaction(async (tx) => {
|
|
1313
1664
|
const rows = await tx.execute(sql `SELECT id, status, hold_expires_at
|
|
1314
1665
|
FROM ${bookings}
|
|
1315
1666
|
WHERE ${bookings.id} = ${id}
|
|
@@ -1366,6 +1717,15 @@ export const bookingsService = {
|
|
|
1366
1717
|
}
|
|
1367
1718
|
return { status: "ok", booking: row ?? null };
|
|
1368
1719
|
});
|
|
1720
|
+
if (result.status === "ok" && result.booking) {
|
|
1721
|
+
await runtime.eventBus?.emit("booking.expired", {
|
|
1722
|
+
bookingId: result.booking.id,
|
|
1723
|
+
bookingNumber: result.booking.bookingNumber,
|
|
1724
|
+
cause: runtime.cause ?? "route",
|
|
1725
|
+
actorId: userId ?? null,
|
|
1726
|
+
}, { category: "domain", source: "service" });
|
|
1727
|
+
}
|
|
1728
|
+
return result;
|
|
1369
1729
|
}
|
|
1370
1730
|
catch (error) {
|
|
1371
1731
|
if (error instanceof BookingServiceError) {
|
|
@@ -1374,7 +1734,7 @@ export const bookingsService = {
|
|
|
1374
1734
|
throw error;
|
|
1375
1735
|
}
|
|
1376
1736
|
},
|
|
1377
|
-
async expireStaleBookings(db, data, userId) {
|
|
1737
|
+
async expireStaleBookings(db, data, userId, runtime = {}) {
|
|
1378
1738
|
const cutoff = data.before ? new Date(data.before) : new Date();
|
|
1379
1739
|
const staleBookings = await db
|
|
1380
1740
|
.select({ id: bookings.id })
|
|
@@ -1383,7 +1743,7 @@ export const bookingsService = {
|
|
|
1383
1743
|
.orderBy(asc(bookings.holdExpiresAt), asc(bookings.createdAt));
|
|
1384
1744
|
const expiredIds = [];
|
|
1385
1745
|
for (const booking of staleBookings) {
|
|
1386
|
-
const result = await this.expireBooking(db, booking.id, { note: data.note ?? "Hold expired by sweep" }, userId);
|
|
1746
|
+
const result = await this.expireBooking(db, booking.id, { note: data.note ?? "Hold expired by sweep" }, userId, { ...runtime, cause: "sweep" });
|
|
1387
1747
|
if ("booking" in result && result.booking) {
|
|
1388
1748
|
expiredIds.push(result.booking.id);
|
|
1389
1749
|
}
|
|
@@ -1394,9 +1754,9 @@ export const bookingsService = {
|
|
|
1394
1754
|
cutoff,
|
|
1395
1755
|
};
|
|
1396
1756
|
},
|
|
1397
|
-
async cancelBooking(db, id, data, userId) {
|
|
1757
|
+
async cancelBooking(db, id, data, userId, runtime = {}) {
|
|
1398
1758
|
try {
|
|
1399
|
-
|
|
1759
|
+
const result = await db.transaction(async (tx) => {
|
|
1400
1760
|
const rows = await tx.execute(sql `SELECT id, status
|
|
1401
1761
|
FROM ${bookings}
|
|
1402
1762
|
WHERE ${bookings.id} = ${id}
|
|
@@ -1408,6 +1768,7 @@ export const bookingsService = {
|
|
|
1408
1768
|
if (!["draft", "on_hold", "confirmed", "in_progress"].includes(booking.status)) {
|
|
1409
1769
|
throw new BookingServiceError("invalid_transition");
|
|
1410
1770
|
}
|
|
1771
|
+
const previousStatus = booking.status;
|
|
1411
1772
|
const allocations = await tx
|
|
1412
1773
|
.select()
|
|
1413
1774
|
.from(bookingAllocations)
|
|
@@ -1457,8 +1818,17 @@ export const bookingsService = {
|
|
|
1457
1818
|
}
|
|
1458
1819
|
// Clean up any booking-group membership (dissolve if ≤1 active members remain).
|
|
1459
1820
|
await cleanupGroupOnBookingCancelled(tx, id);
|
|
1460
|
-
return { status: "ok", booking: row ?? null };
|
|
1821
|
+
return { status: "ok", booking: row ?? null, previousStatus };
|
|
1461
1822
|
});
|
|
1823
|
+
if (result.status === "ok" && result.booking) {
|
|
1824
|
+
await runtime.eventBus?.emit("booking.cancelled", {
|
|
1825
|
+
bookingId: result.booking.id,
|
|
1826
|
+
bookingNumber: result.booking.bookingNumber,
|
|
1827
|
+
previousStatus: result.previousStatus,
|
|
1828
|
+
actorId: userId ?? null,
|
|
1829
|
+
}, { category: "domain", source: "service" });
|
|
1830
|
+
}
|
|
1831
|
+
return { status: result.status, booking: result.booking };
|
|
1462
1832
|
}
|
|
1463
1833
|
catch (error) {
|
|
1464
1834
|
if (error instanceof BookingServiceError) {
|
|
@@ -1467,22 +1837,22 @@ export const bookingsService = {
|
|
|
1467
1837
|
throw error;
|
|
1468
1838
|
}
|
|
1469
1839
|
},
|
|
1470
|
-
|
|
1840
|
+
listTravelerRecords(db, bookingId) {
|
|
1471
1841
|
return db
|
|
1472
1842
|
.select()
|
|
1473
|
-
.from(
|
|
1474
|
-
.where(eq(
|
|
1475
|
-
.orderBy(desc(
|
|
1843
|
+
.from(bookingTravelers)
|
|
1844
|
+
.where(eq(bookingTravelers.bookingId, bookingId))
|
|
1845
|
+
.orderBy(desc(bookingTravelers.isPrimary), asc(bookingTravelers.createdAt));
|
|
1476
1846
|
},
|
|
1477
|
-
async
|
|
1847
|
+
async getTravelerRecordById(db, bookingId, travelerId) {
|
|
1478
1848
|
const [row] = await db
|
|
1479
1849
|
.select()
|
|
1480
|
-
.from(
|
|
1481
|
-
.where(and(eq(
|
|
1850
|
+
.from(bookingTravelers)
|
|
1851
|
+
.where(and(eq(bookingTravelers.id, travelerId), eq(bookingTravelers.bookingId, bookingId)))
|
|
1482
1852
|
.limit(1);
|
|
1483
1853
|
return row ?? null;
|
|
1484
1854
|
},
|
|
1485
|
-
async
|
|
1855
|
+
async createTravelerRecord(db, bookingId, data, userId) {
|
|
1486
1856
|
const [booking] = await db
|
|
1487
1857
|
.select({ id: bookings.id })
|
|
1488
1858
|
.from(bookings)
|
|
@@ -1492,7 +1862,7 @@ export const bookingsService = {
|
|
|
1492
1862
|
return null;
|
|
1493
1863
|
}
|
|
1494
1864
|
const [row] = await db
|
|
1495
|
-
.insert(
|
|
1865
|
+
.insert(bookingTravelers)
|
|
1496
1866
|
.values({
|
|
1497
1867
|
bookingId,
|
|
1498
1868
|
personId: data.personId ?? null,
|
|
@@ -1518,15 +1888,15 @@ export const bookingsService = {
|
|
|
1518
1888
|
actorId: userId ?? "system",
|
|
1519
1889
|
activityType: "passenger_update",
|
|
1520
1890
|
description: `Participant ${data.firstName} ${data.lastName} added`,
|
|
1521
|
-
metadata: {
|
|
1891
|
+
metadata: { travelerId: row.id, participantType: data.participantType },
|
|
1522
1892
|
});
|
|
1523
1893
|
return row;
|
|
1524
1894
|
},
|
|
1525
|
-
async
|
|
1895
|
+
async updateTravelerRecord(db, travelerId, data) {
|
|
1526
1896
|
const [row] = await db
|
|
1527
|
-
.update(
|
|
1897
|
+
.update(bookingTravelers)
|
|
1528
1898
|
.set({ ...data, updatedAt: new Date() })
|
|
1529
|
-
.where(eq(
|
|
1899
|
+
.where(eq(bookingTravelers.id, travelerId))
|
|
1530
1900
|
.returning();
|
|
1531
1901
|
if (!row) {
|
|
1532
1902
|
return null;
|
|
@@ -1534,31 +1904,54 @@ export const bookingsService = {
|
|
|
1534
1904
|
await ensureParticipantFlags(db, row.bookingId, row.id, data);
|
|
1535
1905
|
return row;
|
|
1536
1906
|
},
|
|
1537
|
-
async
|
|
1907
|
+
async deleteTravelerRecord(db, travelerId) {
|
|
1538
1908
|
const [row] = await db
|
|
1539
|
-
.delete(
|
|
1540
|
-
.where(eq(
|
|
1541
|
-
.returning({ id:
|
|
1909
|
+
.delete(bookingTravelers)
|
|
1910
|
+
.where(eq(bookingTravelers.id, travelerId))
|
|
1911
|
+
.returning({ id: bookingTravelers.id });
|
|
1542
1912
|
return row ?? null;
|
|
1543
1913
|
},
|
|
1544
|
-
|
|
1914
|
+
listTravelers(db, bookingId) {
|
|
1545
1915
|
return db
|
|
1546
1916
|
.select()
|
|
1547
|
-
.from(
|
|
1548
|
-
.where(and(eq(
|
|
1549
|
-
.orderBy(asc(
|
|
1550
|
-
.then((rows) => rows.map(
|
|
1917
|
+
.from(bookingTravelers)
|
|
1918
|
+
.where(and(eq(bookingTravelers.bookingId, bookingId), or(...travelerParticipantTypes.map((type) => eq(bookingTravelers.participantType, type)))))
|
|
1919
|
+
.orderBy(asc(bookingTravelers.createdAt))
|
|
1920
|
+
.then((rows) => rows.map(toTravelerResponse));
|
|
1551
1921
|
},
|
|
1552
|
-
async
|
|
1553
|
-
const row = await this.
|
|
1554
|
-
|
|
1922
|
+
async createTraveler(db, bookingId, data, userId) {
|
|
1923
|
+
const row = await this.createTravelerRecord(db, bookingId, {
|
|
1924
|
+
participantType: "traveler",
|
|
1925
|
+
travelerCategory: data.travelerCategory ?? null,
|
|
1926
|
+
firstName: data.firstName,
|
|
1927
|
+
lastName: data.lastName,
|
|
1928
|
+
email: data.email ?? null,
|
|
1929
|
+
phone: data.phone ?? null,
|
|
1930
|
+
preferredLanguage: data.preferredLanguage ?? null,
|
|
1931
|
+
accessibilityNeeds: data.accessibilityNeeds ?? null,
|
|
1932
|
+
specialRequests: data.specialRequests ?? null,
|
|
1933
|
+
isPrimary: data.isPrimary ?? false,
|
|
1934
|
+
notes: data.notes ?? null,
|
|
1935
|
+
}, userId);
|
|
1936
|
+
return row ? toTravelerResponse(row) : null;
|
|
1555
1937
|
},
|
|
1556
|
-
async
|
|
1557
|
-
const row = await this.
|
|
1558
|
-
|
|
1938
|
+
async updateTraveler(db, travelerId, data) {
|
|
1939
|
+
const row = await this.updateTravelerRecord(db, travelerId, {
|
|
1940
|
+
firstName: data.firstName,
|
|
1941
|
+
lastName: data.lastName,
|
|
1942
|
+
email: data.email ?? null,
|
|
1943
|
+
phone: data.phone ?? null,
|
|
1944
|
+
preferredLanguage: data.preferredLanguage ?? null,
|
|
1945
|
+
accessibilityNeeds: data.accessibilityNeeds ?? null,
|
|
1946
|
+
specialRequests: data.specialRequests ?? null,
|
|
1947
|
+
travelerCategory: data.travelerCategory ?? null,
|
|
1948
|
+
isPrimary: data.isPrimary ?? undefined,
|
|
1949
|
+
notes: data.notes ?? null,
|
|
1950
|
+
});
|
|
1951
|
+
return row ? toTravelerResponse(row) : null;
|
|
1559
1952
|
},
|
|
1560
|
-
async
|
|
1561
|
-
return this.
|
|
1953
|
+
async deleteTraveler(db, travelerId) {
|
|
1954
|
+
return this.deleteTravelerRecord(db, travelerId);
|
|
1562
1955
|
},
|
|
1563
1956
|
listItems(db, bookingId) {
|
|
1564
1957
|
return db
|
|
@@ -1639,9 +2032,9 @@ export const bookingsService = {
|
|
|
1639
2032
|
listItemParticipants(db, itemId) {
|
|
1640
2033
|
return db
|
|
1641
2034
|
.select()
|
|
1642
|
-
.from(
|
|
1643
|
-
.where(eq(
|
|
1644
|
-
.orderBy(desc(
|
|
2035
|
+
.from(bookingItemTravelers)
|
|
2036
|
+
.where(eq(bookingItemTravelers.bookingItemId, itemId))
|
|
2037
|
+
.orderBy(desc(bookingItemTravelers.isPrimary), asc(bookingItemTravelers.createdAt));
|
|
1645
2038
|
},
|
|
1646
2039
|
async addItemParticipant(db, itemId, data) {
|
|
1647
2040
|
const [item] = await db
|
|
@@ -1652,25 +2045,25 @@ export const bookingsService = {
|
|
|
1652
2045
|
if (!item) {
|
|
1653
2046
|
return null;
|
|
1654
2047
|
}
|
|
1655
|
-
const [
|
|
1656
|
-
.select({ id:
|
|
1657
|
-
.from(
|
|
1658
|
-
.where(eq(
|
|
2048
|
+
const [traveler] = await db
|
|
2049
|
+
.select({ id: bookingTravelers.id })
|
|
2050
|
+
.from(bookingTravelers)
|
|
2051
|
+
.where(eq(bookingTravelers.id, data.travelerId))
|
|
1659
2052
|
.limit(1);
|
|
1660
|
-
if (!
|
|
2053
|
+
if (!traveler) {
|
|
1661
2054
|
return null;
|
|
1662
2055
|
}
|
|
1663
2056
|
if (data.isPrimary) {
|
|
1664
2057
|
await db
|
|
1665
|
-
.update(
|
|
2058
|
+
.update(bookingItemTravelers)
|
|
1666
2059
|
.set({ isPrimary: false })
|
|
1667
|
-
.where(eq(
|
|
2060
|
+
.where(eq(bookingItemTravelers.bookingItemId, itemId));
|
|
1668
2061
|
}
|
|
1669
2062
|
const [row] = await db
|
|
1670
|
-
.insert(
|
|
2063
|
+
.insert(bookingItemTravelers)
|
|
1671
2064
|
.values({
|
|
1672
2065
|
bookingItemId: itemId,
|
|
1673
|
-
|
|
2066
|
+
travelerId: data.travelerId,
|
|
1674
2067
|
role: data.role,
|
|
1675
2068
|
isPrimary: data.isPrimary ?? false,
|
|
1676
2069
|
})
|
|
@@ -1679,9 +2072,9 @@ export const bookingsService = {
|
|
|
1679
2072
|
},
|
|
1680
2073
|
async removeItemParticipant(db, linkId) {
|
|
1681
2074
|
const [row] = await db
|
|
1682
|
-
.delete(
|
|
1683
|
-
.where(eq(
|
|
1684
|
-
.returning({ id:
|
|
2075
|
+
.delete(bookingItemTravelers)
|
|
2076
|
+
.where(eq(bookingItemTravelers.id, linkId))
|
|
2077
|
+
.returning({ id: bookingItemTravelers.id });
|
|
1685
2078
|
return row ?? null;
|
|
1686
2079
|
},
|
|
1687
2080
|
listSupplierStatuses(db, bookingId) {
|
|
@@ -1789,7 +2182,7 @@ export const bookingsService = {
|
|
|
1789
2182
|
.values({
|
|
1790
2183
|
bookingId,
|
|
1791
2184
|
bookingItemId: data.bookingItemId ?? null,
|
|
1792
|
-
|
|
2185
|
+
travelerId: data.travelerId ?? null,
|
|
1793
2186
|
fulfillmentType: data.fulfillmentType,
|
|
1794
2187
|
deliveryChannel: data.deliveryChannel,
|
|
1795
2188
|
status,
|
|
@@ -1807,7 +2200,7 @@ export const bookingsService = {
|
|
|
1807
2200
|
metadata: {
|
|
1808
2201
|
fulfillmentId: row?.id ?? null,
|
|
1809
2202
|
bookingItemId: data.bookingItemId ?? null,
|
|
1810
|
-
|
|
2203
|
+
travelerId: data.travelerId ?? null,
|
|
1811
2204
|
status,
|
|
1812
2205
|
},
|
|
1813
2206
|
});
|
|
@@ -1831,7 +2224,7 @@ export const bookingsService = {
|
|
|
1831
2224
|
.update(bookingFulfillments)
|
|
1832
2225
|
.set({
|
|
1833
2226
|
bookingItemId: data.bookingItemId === undefined ? undefined : (data.bookingItemId ?? null),
|
|
1834
|
-
|
|
2227
|
+
travelerId: data.travelerId === undefined ? undefined : (data.travelerId ?? null),
|
|
1835
2228
|
fulfillmentType: data.fulfillmentType,
|
|
1836
2229
|
deliveryChannel: data.deliveryChannel,
|
|
1837
2230
|
status: nextStatus,
|
|
@@ -1860,7 +2253,7 @@ export const bookingsService = {
|
|
|
1860
2253
|
metadata: {
|
|
1861
2254
|
fulfillmentId,
|
|
1862
2255
|
bookingItemId: row.bookingItemId,
|
|
1863
|
-
|
|
2256
|
+
travelerId: row.travelerId,
|
|
1864
2257
|
status: row.status,
|
|
1865
2258
|
},
|
|
1866
2259
|
});
|
|
@@ -1897,7 +2290,7 @@ export const bookingsService = {
|
|
|
1897
2290
|
.values({
|
|
1898
2291
|
bookingId,
|
|
1899
2292
|
bookingItemId: data.bookingItemId ?? null,
|
|
1900
|
-
|
|
2293
|
+
travelerId: data.travelerId ?? null,
|
|
1901
2294
|
redeemedAt,
|
|
1902
2295
|
redeemedBy: data.redeemedBy ?? userId ?? null,
|
|
1903
2296
|
location: data.location ?? null,
|
|
@@ -1938,7 +2331,7 @@ export const bookingsService = {
|
|
|
1938
2331
|
metadata: {
|
|
1939
2332
|
redemptionEventId: event?.id ?? null,
|
|
1940
2333
|
bookingItemId: data.bookingItemId ?? null,
|
|
1941
|
-
|
|
2334
|
+
travelerId: data.travelerId ?? null,
|
|
1942
2335
|
redeemedAt: redeemedAt.toISOString(),
|
|
1943
2336
|
method: data.method,
|
|
1944
2337
|
},
|
|
@@ -1986,6 +2379,13 @@ export const bookingsService = {
|
|
|
1986
2379
|
});
|
|
1987
2380
|
return row;
|
|
1988
2381
|
},
|
|
2382
|
+
async deleteNote(db, noteId) {
|
|
2383
|
+
const [row] = await db
|
|
2384
|
+
.delete(bookingNotes)
|
|
2385
|
+
.where(eq(bookingNotes.id, noteId))
|
|
2386
|
+
.returning({ id: bookingNotes.id });
|
|
2387
|
+
return row ?? null;
|
|
2388
|
+
},
|
|
1989
2389
|
listDocuments(db, bookingId) {
|
|
1990
2390
|
return db
|
|
1991
2391
|
.select()
|
|
@@ -2006,7 +2406,7 @@ export const bookingsService = {
|
|
|
2006
2406
|
.insert(bookingDocuments)
|
|
2007
2407
|
.values({
|
|
2008
2408
|
bookingId,
|
|
2009
|
-
|
|
2409
|
+
travelerId: data.travelerId ?? null,
|
|
2010
2410
|
type: data.type,
|
|
2011
2411
|
fileName: data.fileName,
|
|
2012
2412
|
fileUrl: data.fileUrl,
|