@voyantjs/bookings 0.96.0 → 0.97.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.
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAA"}
@@ -1,632 +1 @@
1
- import { typeIdSchemas } from "@voyantjs/db/lib/typeid";
2
- import { z } from "zod";
3
- import { bookingTravelerBedPreferenceSchema, travelerAllocationMapSchema, } from "./schema/travel-details.js";
4
- import { bookingAllocationStatusSchema, bookingAllocationTypeSchema, bookingDocumentTypeSchema, bookingFulfillmentDeliveryChannelSchema, bookingFulfillmentStatusSchema, bookingFulfillmentTypeSchema, bookingItemParticipantRoleSchema, bookingItemStatusSchema, bookingItemTypeSchema, bookingParticipantTypeSchema, bookingRedemptionMethodSchema, bookingSourceTypeSchema, bookingStatusSchema, bookingTravelerCategorySchema, supplierConfirmationStatusSchema, } from "./validation-shared.js";
5
- // ---------- bookings ----------
6
- const bookingDepositRuleSchema = z.object({
7
- kind: z.enum(["none", "percent", "fixed_cents"]),
8
- percent: z.number().min(0).max(100).optional(),
9
- amountCents: z.number().int().min(0).optional(),
10
- });
11
- const bookingCustomerPaymentPolicySchema = z.object({
12
- deposit: bookingDepositRuleSchema,
13
- minDaysBeforeDepartureForDeposit: z.number().int().min(0),
14
- balanceDueDaysBeforeDeparture: z.number().int().min(0),
15
- balanceDueMinDaysFromNow: z.number().int().min(0),
16
- });
17
- export const bookingPriceOverrideSchema = z.object({
18
- isManual: z.literal(true),
19
- originalAmountCents: z.number().int().min(0).nullable(),
20
- overriddenAmountCents: z.number().int().min(0),
21
- currency: z.string().min(3).max(3),
22
- reason: z.string().trim().min(1).max(1000),
23
- overriddenBy: z.string().min(1),
24
- overriddenAt: z.string().datetime(),
25
- });
26
- const bookingBillingPersonIdSchema = typeIdSchemas.person.optional().nullable();
27
- const bookingBillingOrganizationIdSchema = typeIdSchemas.organization.optional().nullable();
28
- function validateExclusiveBillingParty(value, ctx) {
29
- if (!value.personId || !value.organizationId)
30
- return;
31
- ctx.addIssue({
32
- code: z.ZodIssueCode.custom,
33
- path: ["organizationId"],
34
- message: "Billing party must be either personId or organizationId, not both",
35
- });
36
- }
37
- const bookingCoreSchema = z.object({
38
- bookingNumber: z.string().min(1).max(50),
39
- status: bookingStatusSchema.default("draft"),
40
- personId: bookingBillingPersonIdSchema,
41
- organizationId: bookingBillingOrganizationIdSchema,
42
- sourceType: bookingSourceTypeSchema.default("manual"),
43
- externalBookingRef: z.string().optional().nullable(),
44
- communicationLanguage: z.string().max(35).optional().nullable(),
45
- contactFirstName: z.string().max(255).optional().nullable(),
46
- contactLastName: z.string().max(255).optional().nullable(),
47
- contactPartyType: z.enum(["individual", "company"]).optional().nullable(),
48
- contactTaxId: z.string().max(100).optional().nullable(),
49
- contactEmail: z.string().email().optional().nullable(),
50
- contactPhone: z.string().max(50).optional().nullable(),
51
- contactPreferredLanguage: z.string().max(35).optional().nullable(),
52
- contactCountry: z.string().max(100).optional().nullable(),
53
- contactRegion: z.string().max(255).optional().nullable(),
54
- contactCity: z.string().max(255).optional().nullable(),
55
- contactAddressLine1: z.string().max(255).optional().nullable(),
56
- contactAddressLine2: z.string().max(255).optional().nullable(),
57
- contactPostalCode: z.string().max(50).optional().nullable(),
58
- sellCurrency: z.string().min(3).max(3),
59
- baseCurrency: z.string().min(3).max(3).optional().nullable(),
60
- sellAmountCents: z.number().int().min(0).optional().nullable(),
61
- baseSellAmountCents: z.number().int().min(0).optional().nullable(),
62
- costAmountCents: z.number().int().min(0).optional().nullable(),
63
- baseCostAmountCents: z.number().int().min(0).optional().nullable(),
64
- marginPercent: z.number().int().optional().nullable(),
65
- startDate: z.string().optional().nullable(),
66
- endDate: z.string().optional().nullable(),
67
- pax: z.number().int().positive().optional().nullable(),
68
- internalNotes: z.string().optional().nullable(),
69
- customerPaymentPolicy: bookingCustomerPaymentPolicySchema.optional().nullable(),
70
- priceOverride: bookingPriceOverrideSchema.optional().nullable(),
71
- holdExpiresAt: z.string().datetime().optional().nullable(),
72
- confirmedAt: z.string().datetime().optional().nullable(),
73
- expiredAt: z.string().datetime().optional().nullable(),
74
- cancelledAt: z.string().datetime().optional().nullable(),
75
- completedAt: z.string().datetime().optional().nullable(),
76
- redeemedAt: z.string().datetime().optional().nullable(),
77
- });
78
- export const insertBookingSchema = bookingCoreSchema.superRefine(validateExclusiveBillingParty);
79
- export const updateBookingSchema = bookingCoreSchema
80
- .partial()
81
- .superRefine(validateExclusiveBillingParty);
82
- export const createBookingSchema = bookingCoreSchema
83
- .extend({
84
- sourceType: z.enum(["manual", "internal"]).default("manual"),
85
- })
86
- .refine((value) => value.status !== "on_hold", {
87
- message: "Use the reservation flow to create on-hold bookings",
88
- path: ["status"],
89
- })
90
- .refine((value) => value.holdExpiresAt == null, {
91
- message: "Use the reservation flow to manage booking hold expiry",
92
- path: ["holdExpiresAt"],
93
- })
94
- .superRefine(validateExclusiveBillingParty);
95
- export const bookingListSortFieldSchema = z.enum([
96
- "bookingNumber",
97
- "status",
98
- "sellAmount",
99
- "pax",
100
- "startDate",
101
- "endDate",
102
- "createdAt",
103
- ]);
104
- export const bookingListSortDirSchema = z.enum(["asc", "desc"]);
105
- export const bookingListQuerySchema = z.object({
106
- status: bookingStatusSchema.optional(),
107
- /**
108
- * Statuses to omit from the result. Lets the operator list page hide
109
- * noise (draft + expired by default) without forcing a separate
110
- * endpoint. The wire format is a comma-separated string (e.g.
111
- * `?excludeStatuses=draft,expired`) — query parsing collapses
112
- * repeated keys, so a list has to ride on a single param. The
113
- * preprocess hook splits + trims; the union then validates each
114
- * entry against the enum.
115
- */
116
- excludeStatuses: z.preprocess((value) => {
117
- if (typeof value !== "string" || !value.includes(","))
118
- return value;
119
- return value
120
- .split(",")
121
- .map((part) => part.trim())
122
- .filter(Boolean);
123
- }, z.union([bookingStatusSchema, z.array(bookingStatusSchema)]).optional()),
124
- search: z.string().optional(),
125
- productId: z.string().optional(),
126
- optionId: z.string().optional(),
127
- /**
128
- * Filter to bookings whose items reference this availability slot
129
- * (post-0026, items carry `availability_slot_id` directly). Scoped
130
- * to a specific departure so the operator can answer "who's on this
131
- * 28-May 09:00 sailing?" from the list page.
132
- */
133
- availabilitySlotId: z.string().optional(),
134
- supplierId: z.string().optional(),
135
- productCategoryId: z.string().optional(),
136
- personId: z.string().optional(),
137
- organizationId: z.string().optional(),
138
- dateFrom: z.string().optional(),
139
- dateTo: z.string().optional(),
140
- paxMin: z.coerce.number().int().min(0).optional(),
141
- paxMax: z.coerce.number().int().min(0).optional(),
142
- sortBy: bookingListSortFieldSchema.default("createdAt"),
143
- sortDir: bookingListSortDirSchema.default("desc"),
144
- limit: z.coerce.number().int().min(1).max(100).default(50),
145
- offset: z.coerce.number().int().min(0).default(0),
146
- });
147
- export const bookingAggregatesQuerySchema = z.object({
148
- from: z.string().datetime().optional(),
149
- to: z.string().datetime().optional(),
150
- /**
151
- * Cap on the number of upcoming-departure rows returned alongside
152
- * the count. The dashboard uses 8; we allow up to 20 so adjacent
153
- * dashboards / digests can share the endpoint.
154
- */
155
- upcomingLimit: z.coerce.number().int().min(0).max(20).default(8),
156
- });
157
- export const sharingGroupsForSlotQuerySchema = z.object({
158
- slotId: z.string().min(1),
159
- });
160
- export const convertProductSchema = z
161
- .object({
162
- productId: z.string().min(1),
163
- optionId: z.string().optional().nullable(),
164
- slotId: z.string().optional().nullable(),
165
- bookingNumber: z.string().min(1).max(50),
166
- personId: bookingBillingPersonIdSchema,
167
- organizationId: bookingBillingOrganizationIdSchema,
168
- pax: z.number().int().positive().optional().nullable(),
169
- internalNotes: z.string().optional().nullable(),
170
- /**
171
- * Override the seed `sellAmountCents` on the new booking + line item.
172
- * When unset, the converter uses `product.sellAmountCents` as before.
173
- * Used by the catalog booking-engine when promotional offers are
174
- * applied to the quote — the discounted base flows through here so
175
- * the booking row's payable amount reflects the customer-shown total
176
- * (per docs/architecture/promotions-architecture.md §7.1).
177
- */
178
- sellAmountCentsOverride: z.number().int().min(0).optional().nullable(),
179
- /**
180
- * Catalog-resolved preview total shown to the operator. Unlike
181
- * `sellAmountCentsOverride`, this is not a promotion adjustment; it lets
182
- * the create flow seed the booking total from the pricing preview even
183
- * when the legacy product row has no static price.
184
- */
185
- catalogSellAmountCents: z.number().int().min(0).optional().nullable(),
186
- /**
187
- * Operator-confirmed booking total. If it differs from the catalog preview
188
- * (or there was no catalog preview), `priceOverrideReason` is required and
189
- * the service stamps an audit payload onto `bookings.price_override`.
190
- */
191
- confirmedSellAmountCents: z.number().int().min(0).optional().nullable(),
192
- priceOverrideReason: z.string().trim().min(1).max(1000).optional().nullable(),
193
- /**
194
- * Initial status to insert with — defaults to `draft`. Lets the booking-
195
- * create flow commit straight to `confirmed` or `awaiting_payment` in
196
- * one transaction instead of doing a `create-then-flip` dance that's
197
- * vulnerable to a window where the second request can't see the just-
198
- * committed booking row. When set to `confirmed`, the caller is
199
- * responsible for emitting the matching `booking.confirmed` event.
200
- */
201
- initialStatus: bookingStatusSchema.optional(),
202
- /**
203
- * Billing-contact snapshot. Captures who the operator was billing
204
- * at create time so the booking detail page renders the right
205
- * payer even if the linked CRM person/organization changes later
206
- * (or is hard-deleted). All fields optional — the create flow can
207
- * pass a partial snapshot when only some details are known.
208
- */
209
- contactFirstName: z.string().max(255).optional().nullable(),
210
- contactLastName: z.string().max(255).optional().nullable(),
211
- contactPartyType: z.enum(["individual", "company"]).optional().nullable(),
212
- contactTaxId: z.string().max(100).optional().nullable(),
213
- contactEmail: z.string().max(255).optional().nullable(),
214
- contactPhone: z.string().max(50).optional().nullable(),
215
- contactPreferredLanguage: z.string().max(35).optional().nullable(),
216
- contactCountry: z.string().max(2).optional().nullable(),
217
- contactRegion: z.string().max(100).optional().nullable(),
218
- contactCity: z.string().max(100).optional().nullable(),
219
- contactAddressLine1: z.string().max(500).optional().nullable(),
220
- contactAddressLine2: z.string().max(500).optional().nullable(),
221
- contactPostalCode: z.string().max(20).optional().nullable(),
222
- itemLines: z
223
- .array(z.object({
224
- /**
225
- * Stable client-side key (e.g. `unit:optu_adult`). Stamped
226
- * into the inserted booking_item's
227
- * `metadata.bookingCreateLineKey` so the orchestrator can
228
- * link items to travelers via `booking_item_travelers`.
229
- * See voyantjs/voyant#1267.
230
- */
231
- clientLineKey: z.string().min(1).max(255).optional().nullable(),
232
- optionId: z.string().min(1).optional().nullable(),
233
- optionUnitId: z.string().min(1),
234
- quantity: z.number().int().min(1),
235
- title: z.string().min(1).max(255).optional().nullable(),
236
- description: z.string().max(5000).optional().nullable(),
237
- unitSellAmountCents: z.number().int().min(0).optional().nullable(),
238
- totalSellAmountCents: z.number().int().min(0).optional().nullable(),
239
- travelerKeys: z.array(z.string().min(1).max(255)).optional().nullable(),
240
- travelerIndexes: z.array(z.number().int().min(0)).optional().nullable(),
241
- }))
242
- .optional(),
243
- })
244
- .superRefine((value, ctx) => {
245
- validateExclusiveBillingParty(value, ctx);
246
- if (value.confirmedSellAmountCents == null)
247
- return;
248
- if (value.catalogSellAmountCents === value.confirmedSellAmountCents)
249
- return;
250
- if (value.priceOverrideReason)
251
- return;
252
- ctx.addIssue({
253
- code: z.ZodIssueCode.custom,
254
- path: ["priceOverrideReason"],
255
- message: "A price override reason is required when the confirmed total differs from catalog pricing",
256
- });
257
- });
258
- /**
259
- * Admin pricing-preview request. Mirrors the storefront pricing session
260
- * resolver input so the operator dialog sees the same numbers the customer
261
- * would see for the same product + option + catalog.
262
- */
263
- export const pricingPreviewSchema = z.object({
264
- productId: z.string().min(1),
265
- optionId: z.string().optional().nullable(),
266
- catalogId: z.string().optional().nullable(),
267
- });
268
- export const reserveBookingItemSchema = z.object({
269
- title: z.string().min(1).max(255),
270
- description: z.string().optional().nullable(),
271
- itemType: bookingItemTypeSchema.default("unit"),
272
- quantity: z.number().int().positive().default(1),
273
- sellCurrency: z.string().min(3).max(3).optional().nullable(),
274
- unitSellAmountCents: z.number().int().min(0).optional().nullable(),
275
- totalSellAmountCents: z.number().int().min(0).optional().nullable(),
276
- costCurrency: z.string().min(3).max(3).optional().nullable(),
277
- unitCostAmountCents: z.number().int().min(0).optional().nullable(),
278
- totalCostAmountCents: z.number().int().min(0).optional().nullable(),
279
- notes: z.string().optional().nullable(),
280
- productId: z.string().optional().nullable(),
281
- optionId: z.string().optional().nullable(),
282
- optionUnitId: z.string().optional().nullable(),
283
- pricingCategoryId: z.string().optional().nullable(),
284
- productNameSnapshot: z.string().optional().nullable(),
285
- optionNameSnapshot: z.string().optional().nullable(),
286
- unitNameSnapshot: z.string().optional().nullable(),
287
- departureLabelSnapshot: z.string().optional().nullable(),
288
- sourceSnapshotId: z.string().optional().nullable(),
289
- sourceOfferId: z.string().optional().nullable(),
290
- availabilitySlotId: z.string().min(1),
291
- allocationType: bookingAllocationTypeSchema.default("unit"),
292
- metadata: z.record(z.string(), z.unknown()).optional().nullable(),
293
- });
294
- export const reserveBookingSchema = bookingCoreSchema
295
- .omit({
296
- status: true,
297
- holdExpiresAt: true,
298
- confirmedAt: true,
299
- expiredAt: true,
300
- cancelledAt: true,
301
- completedAt: true,
302
- redeemedAt: true,
303
- })
304
- .extend({
305
- holdMinutes: z
306
- .number()
307
- .int()
308
- .positive()
309
- .max(24 * 60)
310
- .optional(),
311
- holdExpiresAt: z.string().datetime().optional().nullable(),
312
- items: z.array(reserveBookingItemSchema).min(1),
313
- })
314
- .superRefine(validateExclusiveBillingParty);
315
- export const extendBookingHoldSchema = z
316
- .object({
317
- holdMinutes: z
318
- .number()
319
- .int()
320
- .positive()
321
- .max(24 * 60)
322
- .optional(),
323
- holdExpiresAt: z.string().datetime().optional().nullable(),
324
- })
325
- .refine((value) => value.holdMinutes !== undefined || value.holdExpiresAt !== undefined, {
326
- message: "holdMinutes or holdExpiresAt is required",
327
- });
328
- export const confirmBookingSchema = z.object({
329
- note: z.string().optional().nullable(),
330
- /**
331
- * When true, downstream subscribers that send customer-facing
332
- * notifications (e.g. notifications module's `autoConfirmAndDispatch`)
333
- * skip dispatching for this confirmation. Lets the operator confirm a
334
- * booking internally without auto-sending a confirmation email.
335
- */
336
- suppressNotifications: z.boolean().optional(),
337
- });
338
- export const cancelBookingSchema = z.object({
339
- note: z.string().optional().nullable(),
340
- });
341
- export const expireBookingSchema = z.object({
342
- note: z.string().optional().nullable(),
343
- });
344
- export const expireStaleBookingsSchema = z.object({
345
- before: z.string().datetime().optional().nullable(),
346
- note: z.string().optional().nullable(),
347
- });
348
- export const startBookingSchema = z.object({
349
- note: z.string().optional().nullable(),
350
- });
351
- export const completeBookingSchema = z.object({
352
- note: z.string().optional().nullable(),
353
- });
354
- /**
355
- * Admin-only override: skips the transition graph. `reason` is required —
356
- * the operator has to explain why they're bypassing lifecycle laws. Use the
357
- * verb-specific endpoints (/confirm, /cancel, /start, /complete, /expire) for
358
- * normal state changes; this is for data-correction and exceptional cases.
359
- * Confirmed overrides emit `booking.confirmed` by default for create-dialog
360
- * compatibility; pass `suppressLifecycleEvents` for pure data correction.
361
- */
362
- export const overrideBookingStatusSchema = z.object({
363
- status: bookingStatusSchema,
364
- reason: z.string().min(1).max(2000),
365
- note: z.string().optional().nullable(),
366
- /**
367
- * Same notification opt-out as `confirmBookingSchema.suppressNotifications`.
368
- * Only applies when the override path emits `booking.confirmed`.
369
- */
370
- suppressNotifications: z.boolean().optional(),
371
- /**
372
- * When true, skip verb-specific lifecycle events such as
373
- * `booking.confirmed`. The audit event `booking.status_overridden` still
374
- * emits either way.
375
- */
376
- suppressLifecycleEvents: z.boolean().optional(),
377
- });
378
- export const reserveBookingFromTransactionSchema = bookingCoreSchema
379
- .pick({
380
- bookingNumber: true,
381
- sourceType: true,
382
- contactFirstName: true,
383
- contactLastName: true,
384
- contactPartyType: true,
385
- contactTaxId: true,
386
- contactEmail: true,
387
- contactPhone: true,
388
- contactPreferredLanguage: true,
389
- contactCountry: true,
390
- contactRegion: true,
391
- contactCity: true,
392
- contactAddressLine1: true,
393
- contactAddressLine2: true,
394
- contactPostalCode: true,
395
- internalNotes: true,
396
- })
397
- .extend({
398
- sourceType: bookingSourceTypeSchema.default("internal"),
399
- holdMinutes: z
400
- .number()
401
- .int()
402
- .positive()
403
- .max(24 * 60)
404
- .optional(),
405
- holdExpiresAt: z.string().datetime().optional().nullable(),
406
- note: z.string().optional().nullable(),
407
- includeParticipants: z.boolean().default(true),
408
- });
409
- // ---------- traveler records ----------
410
- const travelerRecordCoreSchema = z.object({
411
- personId: z.string().optional().nullable(),
412
- participantType: bookingParticipantTypeSchema.default("traveler"),
413
- travelerCategory: bookingTravelerCategorySchema.optional().nullable(),
414
- firstName: z.string().min(1).max(255),
415
- lastName: z.string().min(1).max(255),
416
- email: z.string().email().optional().nullable(),
417
- phone: z.string().max(50).optional().nullable(),
418
- preferredLanguage: z.string().max(35).optional().nullable(),
419
- specialRequests: z.string().optional().nullable(),
420
- isPrimary: z.boolean().default(false),
421
- notes: z.string().optional().nullable(),
422
- });
423
- // ---------- travelers ----------
424
- const travelerCoreSchema = z.object({
425
- firstName: z.string().min(1).max(255),
426
- lastName: z.string().min(1).max(255),
427
- email: z.string().email().optional().nullable(),
428
- phone: z.string().max(50).optional().nullable(),
429
- preferredLanguage: z.string().max(35).optional().nullable(),
430
- specialRequests: z.string().optional().nullable(),
431
- travelerCategory: bookingTravelerCategorySchema.optional().nullable(),
432
- isPrimary: z.boolean().optional().nullable(),
433
- notes: z.string().optional().nullable(),
434
- });
435
- export const insertTravelerSchema = travelerCoreSchema;
436
- export const updateTravelerSchema = travelerCoreSchema.partial();
437
- export const insertTravelerRecordSchema = travelerRecordCoreSchema;
438
- export const updateTravelerRecordSchema = travelerRecordCoreSchema.partial();
439
- // ---------- traveler travel details ----------
440
- export const upsertTravelerTravelDetailsSchema = z.object({
441
- nationality: z.string().max(100).optional().nullable(),
442
- documentType: z
443
- .enum(["passport", "id_card", "driver_license", "visa", "other"])
444
- .optional()
445
- .nullable(),
446
- documentNumber: z.string().max(255).optional().nullable(),
447
- documentExpiry: z.string().optional().nullable(),
448
- documentIssuingCountry: z.string().max(255).optional().nullable(),
449
- documentIssuingAuthority: z.string().max(255).optional().nullable(),
450
- /** Provenance pointer to the seeding `crm.person_documents` row. */
451
- documentPersonDocumentId: z.string().optional().nullable(),
452
- dateOfBirth: z.string().optional().nullable(),
453
- dietaryRequirements: z.string().optional().nullable(),
454
- accessibilityNeeds: z.string().optional().nullable(),
455
- isLeadTraveler: z.boolean().optional().nullable(),
456
- sharingGroupId: z.string().max(255).optional().nullable(),
457
- roomTypeId: z.string().max(255).optional().nullable(),
458
- bedPreference: bookingTravelerBedPreferenceSchema.optional().nullable(),
459
- allocations: travelerAllocationMapSchema.optional(),
460
- });
461
- // Flat shape combining plaintext traveler columns + encrypted travel-details
462
- // fields, matching the pre-0.10 `createTravelerRecord` ergonomics. Migration
463
- // boundary helper — see `bookingsService.createTravelerWithTravelDetails`.
464
- export const createTravelerWithTravelDetailsSchema = travelerRecordCoreSchema.extend(upsertTravelerTravelDetailsSchema.shape);
465
- export const updateTravelerWithTravelDetailsSchema = createTravelerWithTravelDetailsSchema.partial();
466
- // ---------- booking items ----------
467
- const bookingItemCoreSchema = z.object({
468
- title: z.string().min(1).max(255),
469
- description: z.string().optional().nullable(),
470
- itemType: bookingItemTypeSchema.default("unit"),
471
- status: bookingItemStatusSchema.default("draft"),
472
- serviceDate: z.string().optional().nullable(),
473
- startsAt: z.string().optional().nullable(),
474
- endsAt: z.string().optional().nullable(),
475
- quantity: z.number().int().positive().default(1),
476
- sellCurrency: z.string().min(3).max(3),
477
- unitSellAmountCents: z.number().int().optional().nullable(),
478
- totalSellAmountCents: z.number().int().optional().nullable(),
479
- costCurrency: z.string().min(3).max(3).optional().nullable(),
480
- unitCostAmountCents: z.number().int().optional().nullable(),
481
- totalCostAmountCents: z.number().int().optional().nullable(),
482
- notes: z.string().optional().nullable(),
483
- productId: z.string().optional().nullable(),
484
- optionId: z.string().optional().nullable(),
485
- optionUnitId: z.string().optional().nullable(),
486
- pricingCategoryId: z.string().optional().nullable(),
487
- availabilitySlotId: z.string().optional().nullable(),
488
- // Catalog-snapshot overrides for catalog-less deployments (OTA case)
489
- // or when the caller wants to write a specific historical label. If
490
- // omitted, the service looks the values up from the catalog refs
491
- // using the foreign IDs.
492
- productNameSnapshot: z.string().optional().nullable(),
493
- optionNameSnapshot: z.string().optional().nullable(),
494
- unitNameSnapshot: z.string().optional().nullable(),
495
- departureLabelSnapshot: z.string().optional().nullable(),
496
- sourceSnapshotId: z.string().optional().nullable(),
497
- sourceOfferId: z.string().optional().nullable(),
498
- metadata: z.record(z.string(), z.unknown()).optional().nullable(),
499
- });
500
- export const insertBookingItemSchema = bookingItemCoreSchema;
501
- export const updateBookingItemSchema = bookingItemCoreSchema.partial();
502
- export const insertBookingAllocationSchema = z.object({
503
- bookingItemId: z.string().min(1),
504
- productId: z.string().optional().nullable(),
505
- optionId: z.string().optional().nullable(),
506
- optionUnitId: z.string().optional().nullable(),
507
- pricingCategoryId: z.string().optional().nullable(),
508
- availabilitySlotId: z.string().optional().nullable(),
509
- quantity: z.number().int().positive().default(1),
510
- allocationType: bookingAllocationTypeSchema.default("unit"),
511
- status: bookingAllocationStatusSchema.default("held"),
512
- holdExpiresAt: z.string().datetime().optional().nullable(),
513
- confirmedAt: z.string().datetime().optional().nullable(),
514
- releasedAt: z.string().datetime().optional().nullable(),
515
- metadata: z.record(z.string(), z.unknown()).optional().nullable(),
516
- });
517
- export const updateBookingAllocationSchema = insertBookingAllocationSchema.partial();
518
- // ---------- booking fulfillments ----------
519
- const bookingFulfillmentInputSchema = z.object({
520
- bookingItemId: z.string().optional().nullable(),
521
- travelerId: z.string().optional().nullable(),
522
- fulfillmentType: bookingFulfillmentTypeSchema,
523
- deliveryChannel: bookingFulfillmentDeliveryChannelSchema,
524
- status: bookingFulfillmentStatusSchema.default("issued"),
525
- artifactUrl: z.string().url().optional().nullable(),
526
- payload: z.record(z.string(), z.unknown()).optional().nullable(),
527
- issuedAt: z.string().datetime().optional().nullable(),
528
- revokedAt: z.string().datetime().optional().nullable(),
529
- });
530
- export const insertBookingFulfillmentSchema = bookingFulfillmentInputSchema.transform(({ travelerId, ...rest }) => ({
531
- ...rest,
532
- travelerId: travelerId ?? null,
533
- }));
534
- export const updateBookingFulfillmentSchema = bookingFulfillmentInputSchema
535
- .partial()
536
- .transform(({ travelerId, ...rest }) => ({
537
- ...rest,
538
- travelerId: travelerId !== undefined ? (travelerId ?? null) : undefined,
539
- }));
540
- // ---------- booking redemption events ----------
541
- export const recordBookingRedemptionSchema = z
542
- .object({
543
- bookingItemId: z.string().optional().nullable(),
544
- travelerId: z.string().optional().nullable(),
545
- redeemedAt: z.string().datetime().optional().nullable(),
546
- redeemedBy: z.string().max(255).optional().nullable(),
547
- location: z.string().max(500).optional().nullable(),
548
- method: bookingRedemptionMethodSchema.default("manual"),
549
- metadata: z.record(z.string(), z.unknown()).optional().nullable(),
550
- })
551
- .transform(({ travelerId, ...rest }) => ({
552
- ...rest,
553
- travelerId: travelerId ?? null,
554
- }));
555
- // ---------- booking item participants ----------
556
- export const insertBookingItemTravelerSchema = z
557
- .object({
558
- travelerId: z.string().min(1).optional(),
559
- role: bookingItemParticipantRoleSchema.default("traveler"),
560
- isPrimary: z.boolean().default(false),
561
- })
562
- .refine((value) => Boolean(value.travelerId), {
563
- message: "travelerId is required",
564
- path: ["travelerId"],
565
- })
566
- .transform(({ travelerId, ...rest }) => ({
567
- ...rest,
568
- travelerId: travelerId,
569
- }));
570
- export const insertBookingItemParticipantSchema = insertBookingItemTravelerSchema;
571
- // ---------- supplier statuses ----------
572
- const supplierStatusCoreSchema = z.object({
573
- supplierServiceId: z.string().optional().nullable(),
574
- serviceName: z.string().min(1).max(255),
575
- status: supplierConfirmationStatusSchema.default("pending"),
576
- supplierReference: z.string().max(255).optional().nullable(),
577
- costCurrency: z.string().min(3).max(3),
578
- costAmountCents: z.number().int().min(0),
579
- notes: z.string().optional().nullable(),
580
- });
581
- export const insertSupplierStatusSchema = supplierStatusCoreSchema;
582
- export const updateSupplierStatusSchema = supplierStatusCoreSchema.partial().extend({
583
- confirmedAt: z.string().optional().nullable(),
584
- });
585
- // ---------- notes ----------
586
- export const insertBookingNoteSchema = z.object({
587
- content: z.string().min(1).max(10000),
588
- });
589
- export const updateBookingNoteSchema = z.object({
590
- content: z.string().min(1).max(10000),
591
- });
592
- // ---------- documents ----------
593
- export const insertBookingDocumentSchema = z
594
- .object({
595
- travelerId: z.string().optional().nullable(),
596
- type: bookingDocumentTypeSchema,
597
- fileName: z.string().min(1).max(500),
598
- fileUrl: z.string().url(),
599
- expiresAt: z.string().optional().nullable(),
600
- notes: z.string().optional().nullable(),
601
- })
602
- .transform(({ travelerId, ...rest }) => ({
603
- ...rest,
604
- travelerId: travelerId ?? null,
605
- }));
606
- export const insertBookingTravelerDocumentSchema = insertBookingDocumentSchema;
607
- // ---------- booking groups ----------
608
- export const bookingGroupKindSchema = z.enum(["shared_room", "cruise_party", "other"]);
609
- export const bookingGroupMemberRoleSchema = z.enum(["primary", "shared"]);
610
- const bookingGroupCoreSchema = z.object({
611
- kind: bookingGroupKindSchema.default("shared_room"),
612
- label: z.string().min(1).max(500),
613
- primaryBookingId: z.string().optional().nullable(),
614
- productId: z.string().optional().nullable(),
615
- optionUnitId: z.string().optional().nullable(),
616
- metadata: z.record(z.string(), z.unknown()).optional().nullable(),
617
- });
618
- export const insertBookingGroupSchema = bookingGroupCoreSchema;
619
- export const updateBookingGroupSchema = bookingGroupCoreSchema.partial();
620
- export const addBookingGroupMemberSchema = z.object({
621
- bookingId: z.string().min(1),
622
- role: bookingGroupMemberRoleSchema.default("shared"),
623
- });
624
- export const bookingGroupListQuerySchema = z.object({
625
- kind: bookingGroupKindSchema.optional(),
626
- productId: z.string().optional(),
627
- optionUnitId: z.string().optional(),
628
- limit: z.coerce.number().int().min(1).max(200).default(50),
629
- offset: z.coerce.number().int().min(0).default(0),
630
- });
631
- export * from "./validation-public.js";
632
- export * from "./validation-shared.js";
1
+ export * from "@voyantjs/bookings-contracts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/bookings",
3
- "version": "0.96.0",
3
+ "version": "0.97.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -84,15 +84,16 @@
84
84
  "drizzle-orm": "^0.45.2",
85
85
  "hono": "^4.12.10",
86
86
  "zod": "^4.3.6",
87
- "@voyantjs/action-ledger": "0.96.0",
88
- "@voyantjs/core": "0.96.0",
89
- "@voyantjs/db": "0.96.0",
90
- "@voyantjs/hono": "0.96.0",
91
- "@voyantjs/utils": "0.96.0"
87
+ "@voyantjs/action-ledger": "0.97.0",
88
+ "@voyantjs/bookings-contracts": "0.97.0",
89
+ "@voyantjs/core": "0.97.0",
90
+ "@voyantjs/db": "0.97.0",
91
+ "@voyantjs/hono": "0.97.0",
92
+ "@voyantjs/utils": "0.97.0"
92
93
  },
93
94
  "devDependencies": {
94
95
  "typescript": "^6.0.2",
95
- "@voyantjs/products": "0.96.0",
96
+ "@voyantjs/products": "0.97.0",
96
97
  "@voyantjs/voyant-typescript-config": "0.1.0"
97
98
  },
98
99
  "files": [