@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.
Files changed (66) hide show
  1. package/README.md +2 -2
  2. package/dist/index.d.ts +8 -8
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +5 -5
  5. package/dist/pii.d.ts +10 -9
  6. package/dist/pii.d.ts.map +1 -1
  7. package/dist/pii.js +33 -33
  8. package/dist/products-ref.d.ts +93 -1
  9. package/dist/products-ref.d.ts.map +1 -1
  10. package/dist/products-ref.js +12 -1
  11. package/dist/routes-groups.d.ts +25 -5
  12. package/dist/routes-groups.d.ts.map +1 -1
  13. package/dist/routes-groups.js +3 -3
  14. package/dist/routes-public.d.ts +19 -21
  15. package/dist/routes-public.d.ts.map +1 -1
  16. package/dist/routes-public.js +1 -1
  17. package/dist/routes-shared.d.ts +3 -2
  18. package/dist/routes-shared.d.ts.map +1 -1
  19. package/dist/routes.d.ts +283 -188
  20. package/dist/routes.d.ts.map +1 -1
  21. package/dist/routes.js +89 -102
  22. package/dist/schema/travel-details.d.ts +27 -27
  23. package/dist/schema/travel-details.d.ts.map +1 -1
  24. package/dist/schema/travel-details.js +19 -14
  25. package/dist/schema-core.d.ts +194 -305
  26. package/dist/schema-core.d.ts.map +1 -1
  27. package/dist/schema-core.js +19 -10
  28. package/dist/schema-items.d.ts +15 -15
  29. package/dist/schema-items.d.ts.map +1 -1
  30. package/dist/schema-items.js +12 -12
  31. package/dist/schema-operations.d.ts +1 -1
  32. package/dist/schema-operations.js +3 -3
  33. package/dist/schema-relations.d.ts +26 -9
  34. package/dist/schema-relations.d.ts.map +1 -1
  35. package/dist/schema-relations.js +36 -21
  36. package/dist/schema-shared.d.ts +3 -2
  37. package/dist/schema-shared.d.ts.map +1 -1
  38. package/dist/schema-shared.js +4 -5
  39. package/dist/schema-staff.d.ts +267 -0
  40. package/dist/schema-staff.d.ts.map +1 -0
  41. package/dist/schema-staff.js +31 -0
  42. package/dist/schema.d.ts +1 -0
  43. package/dist/schema.d.ts.map +1 -1
  44. package/dist/schema.js +1 -0
  45. package/dist/service-groups.d.ts +3 -7
  46. package/dist/service-groups.d.ts.map +1 -1
  47. package/dist/service-groups.js +6 -10
  48. package/dist/service-public.d.ts +102 -55
  49. package/dist/service-public.d.ts.map +1 -1
  50. package/dist/service-public.js +119 -54
  51. package/dist/service.d.ts +327 -104
  52. package/dist/service.d.ts.map +1 -1
  53. package/dist/service.js +530 -130
  54. package/dist/transactions-ref.d.ts +930 -66
  55. package/dist/transactions-ref.d.ts.map +1 -1
  56. package/dist/transactions-ref.js +56 -2
  57. package/dist/validation-public.d.ts +29 -69
  58. package/dist/validation-public.d.ts.map +1 -1
  59. package/dist/validation-public.js +21 -20
  60. package/dist/validation-shared.d.ts +4 -5
  61. package/dist/validation-shared.d.ts.map +1 -1
  62. package/dist/validation-shared.js +2 -10
  63. package/dist/validation.d.ts +248 -44
  64. package/dist/validation.d.ts.map +1 -1
  65. package/dist/validation.js +103 -28
  66. 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, bookingItemParticipants, bookingItems, bookingNotes, bookingParticipants, bookingRedemptionEvents, bookingSupplierStatuses, bookings, } from "./schema.js";
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 toPassengerResponse(participant) {
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
- isLeadPassenger: participant.isPrimary,
40
+ isPrimary: participant.isPrimary,
41
+ notes: participant.notes,
37
42
  createdAt: participant.createdAt,
38
43
  updatedAt: participant.updatedAt,
39
44
  };
40
45
  }
41
- function toCreateParticipantFromPassenger(data) {
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(bookingParticipants)
49
+ .update(bookingTravelers)
66
50
  .set({ isPrimary: false, updatedAt: new Date() })
67
- .where(and(eq(bookingParticipants.bookingId, bookingId), ne(bookingParticipants.id, participantId)));
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.participantId) {
82
- const [participant] = await db
83
- .select({ id: bookingParticipants.id })
84
- .from(bookingParticipants)
85
- .where(and(eq(bookingParticipants.id, data.participantId), eq(bookingParticipants.bookingId, bookingId)))
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 (!participant) {
88
- return { ok: false, reason: "participant_not_found" };
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
- .where(eq(productDaysRef.productId, product.id))
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
- WHERE ${productDaysRef.productId} = ${product.id}
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(bookingParticipants)
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 participantId = participantMap.get(link.participantId);
441
- if (!bookingItemId || !participantId) {
491
+ const travelerId = participantMap.get(link.travelerId);
492
+ if (!bookingItemId || !travelerId) {
442
493
  continue;
443
494
  }
444
- await tx.insert(bookingItemParticipants).values({
495
+ await tx.insert(bookingItemTravelers).values({
445
496
  bookingItemId,
446
- participantId,
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(bookingParticipants)
620
- .where(and(eq(bookingParticipants.bookingId, bookingId), or(eq(bookingParticipants.participantType, "traveler"), eq(bookingParticipants.participantType, "occupant"))))
621
- .orderBy(desc(bookingParticipants.isPrimary), asc(bookingParticipants.createdAt));
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(bookingItemParticipants)
625
- .where(sql `${bookingItemParticipants.bookingItemId} IN (
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
- participantId: null,
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
- participantId: null,
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.participantId))
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
- participantId: participant.id,
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
- participantId: participant.id,
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: product.startDate,
761
- endDate: product.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: { productId: product.id, productName: product.name, optionId: option?.id ?? null },
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
- return await db.transaction(async (tx) => {
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
- return await db.transaction(async (tx) => {
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
- return await db.transaction(async (tx) => {
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
- listParticipants(db, bookingId) {
1840
+ listTravelerRecords(db, bookingId) {
1471
1841
  return db
1472
1842
  .select()
1473
- .from(bookingParticipants)
1474
- .where(eq(bookingParticipants.bookingId, bookingId))
1475
- .orderBy(desc(bookingParticipants.isPrimary), asc(bookingParticipants.createdAt));
1843
+ .from(bookingTravelers)
1844
+ .where(eq(bookingTravelers.bookingId, bookingId))
1845
+ .orderBy(desc(bookingTravelers.isPrimary), asc(bookingTravelers.createdAt));
1476
1846
  },
1477
- async getParticipantById(db, bookingId, participantId) {
1847
+ async getTravelerRecordById(db, bookingId, travelerId) {
1478
1848
  const [row] = await db
1479
1849
  .select()
1480
- .from(bookingParticipants)
1481
- .where(and(eq(bookingParticipants.id, participantId), eq(bookingParticipants.bookingId, bookingId)))
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 createParticipant(db, bookingId, data, userId) {
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(bookingParticipants)
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: { participantId: row.id, participantType: data.participantType },
1891
+ metadata: { travelerId: row.id, participantType: data.participantType },
1522
1892
  });
1523
1893
  return row;
1524
1894
  },
1525
- async updateParticipant(db, participantId, data) {
1895
+ async updateTravelerRecord(db, travelerId, data) {
1526
1896
  const [row] = await db
1527
- .update(bookingParticipants)
1897
+ .update(bookingTravelers)
1528
1898
  .set({ ...data, updatedAt: new Date() })
1529
- .where(eq(bookingParticipants.id, participantId))
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 deleteParticipant(db, participantId) {
1907
+ async deleteTravelerRecord(db, travelerId) {
1538
1908
  const [row] = await db
1539
- .delete(bookingParticipants)
1540
- .where(eq(bookingParticipants.id, participantId))
1541
- .returning({ id: bookingParticipants.id });
1909
+ .delete(bookingTravelers)
1910
+ .where(eq(bookingTravelers.id, travelerId))
1911
+ .returning({ id: bookingTravelers.id });
1542
1912
  return row ?? null;
1543
1913
  },
1544
- listPassengers(db, bookingId) {
1914
+ listTravelers(db, bookingId) {
1545
1915
  return db
1546
1916
  .select()
1547
- .from(bookingParticipants)
1548
- .where(and(eq(bookingParticipants.bookingId, bookingId), or(...travelerParticipantTypes.map((type) => eq(bookingParticipants.participantType, type)))))
1549
- .orderBy(asc(bookingParticipants.createdAt))
1550
- .then((rows) => rows.map(toPassengerResponse));
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 createPassenger(db, bookingId, data, userId) {
1553
- const row = await this.createParticipant(db, bookingId, toCreateParticipantFromPassenger(data), userId);
1554
- return row ? toPassengerResponse(row) : null;
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 updatePassenger(db, passengerId, data) {
1557
- const row = await this.updateParticipant(db, passengerId, toUpdateParticipantFromPassenger(data));
1558
- return row ? toPassengerResponse(row) : null;
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 deletePassenger(db, passengerId) {
1561
- return this.deleteParticipant(db, passengerId);
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(bookingItemParticipants)
1643
- .where(eq(bookingItemParticipants.bookingItemId, itemId))
1644
- .orderBy(desc(bookingItemParticipants.isPrimary), asc(bookingItemParticipants.createdAt));
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 [participant] = await db
1656
- .select({ id: bookingParticipants.id })
1657
- .from(bookingParticipants)
1658
- .where(eq(bookingParticipants.id, data.participantId))
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 (!participant) {
2053
+ if (!traveler) {
1661
2054
  return null;
1662
2055
  }
1663
2056
  if (data.isPrimary) {
1664
2057
  await db
1665
- .update(bookingItemParticipants)
2058
+ .update(bookingItemTravelers)
1666
2059
  .set({ isPrimary: false })
1667
- .where(eq(bookingItemParticipants.bookingItemId, itemId));
2060
+ .where(eq(bookingItemTravelers.bookingItemId, itemId));
1668
2061
  }
1669
2062
  const [row] = await db
1670
- .insert(bookingItemParticipants)
2063
+ .insert(bookingItemTravelers)
1671
2064
  .values({
1672
2065
  bookingItemId: itemId,
1673
- participantId: data.participantId,
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(bookingItemParticipants)
1683
- .where(eq(bookingItemParticipants.id, linkId))
1684
- .returning({ id: bookingItemParticipants.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
- participantId: data.participantId ?? null,
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
- participantId: data.participantId ?? null,
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
- participantId: data.participantId === undefined ? undefined : (data.participantId ?? null),
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
- participantId: row.participantId,
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
- participantId: data.participantId ?? null,
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
- participantId: data.participantId ?? null,
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
- participantId: data.participantId ?? data.passengerId ?? null,
2409
+ travelerId: data.travelerId ?? null,
2010
2410
  type: data.type,
2011
2411
  fileName: data.fileName,
2012
2412
  fileUrl: data.fileUrl,