@@ -83,7 +83,13 @@ const DEFAULT_CANCEL_NOTE = "Canceled";
8383/** Default customer message attached to a charge. */
8484const DEFAULT_CUSTOMER_MESSAGE = "Charge initiated via API" ;
8585
86- const BOOKING_ID_PREFIX = "b_" ;
86+ // Booking/order ids come in two forms: a lowercase db id with `_`
87+ // (`b_abc123` / `o_abc123`) and an uppercase display id with `-`
88+ // (`B-ABC123` / `O-ABC123`). Bookings are prefixed `b`, orders `o`.
89+ const BOOKING_DB_ID_REGEX = / ^ b _ [ a - z 0 - 9 ] + $ / ;
90+ const BOOKING_DISPLAY_ID_REGEX = / ^ B - [ A - Z 0 - 9 ] + $ / ;
91+ const ORDER_DB_ID_REGEX = / ^ o _ [ a - z 0 - 9 ] + $ / ;
92+ const ORDER_DISPLAY_ID_REGEX = / ^ O - [ A - Z 0 - 9 ] + $ / ;
8793const PAYMENT_SOURCE_PREFIX = "ps_" ;
8894const PAYMENT_ID_PREFIX = "pmt_" ;
8995const ALLOWED_PAYMENT_SOURCE_IDS = [ "cash/cash" , "custom/other" , "custom/voucher" ] ;
@@ -126,7 +132,10 @@ export interface AddAddonInput {
126132
127133const ERROR_ADDON_OPTION_ID_REQUIRED = "addonOptionId is required" ;
128134const ERROR_QUANTITY_INVALID = "quantity must be a positive integer string" ;
129- const ERROR_BOOKING_ID_REQUIRED = "bookingId is required" ;
135+ const ERROR_INVALID_BOOKING_ID =
136+ "bookingId is required and must be a valid booking id, e.g. 'b_abc123' or 'B-ABC123'" ;
137+ const ERROR_INVALID_ORDER_ID =
138+ "orderId is required and must be a valid order id, e.g. 'o_abc123' or 'O-ABC123'" ;
130139const ERROR_BOOKING_NOT_FOUND = "Booking not found" ;
131140const ERROR_MULTIPLE_BOOKINGS_FOUND =
132141 "Expected exactly one booking for the provided bookingId" ;
@@ -166,6 +175,7 @@ export class BookingService {
166175 bookingId : string ,
167176 options : BookingReadOptions = { } ,
168177 ) : Promise < Booking | null > {
178+ assertBookingId ( bookingId ) ;
169179 const includeGuests = options . includeGuests ?? false ;
170180 const includePriceBreakdown = options . includePriceBreakdown ?? false ;
171181
@@ -225,6 +235,7 @@ export class BookingService {
225235
226236 /** Returns the guests on a booking (primary guest included). */
227237 async getGuests ( bookingId : string ) : Promise < Guest [ ] > {
238+ assertBookingId ( bookingId ) ;
228239 const body : GraphQLBody < BookingGuestsResponse > =
229240 await this . client . request < BookingGuestsResponse > (
230241 SALES_ENDPOINT ,
@@ -240,6 +251,7 @@ export class BookingService {
240251
241252 /** Returns the payments on file for a booking, or null when not found. */
242253 async getPaymentsOnFile ( bookingId : string ) : Promise < BookingPaymentsOnFile | null > {
254+ assertBookingId ( bookingId ) ;
243255 const normalized = normalizeBookingId ( bookingId ) ;
244256 const body : GraphQLBody < BookingPaymentsOnFileResponse > =
245257 await this . client . request < BookingPaymentsOnFileResponse > (
@@ -259,6 +271,7 @@ export class BookingService {
259271 note : string ,
260272 mode : NoteMode = "append" ,
261273 ) : Promise < Booking | null > {
274+ assertBookingId ( bookingId ) ;
262275 const normalized = normalizeBookingId ( bookingId ) ;
263276 const booking = await this . getById ( normalized ) ;
264277 if ( ! booking ) {
@@ -284,6 +297,7 @@ export class BookingService {
284297 bookingId : string ,
285298 checkedIn : boolean ,
286299 ) : Promise < Booking | null > {
300+ assertBookingId ( bookingId ) ;
287301 const normalized = normalizeBookingId ( bookingId ) ;
288302 const checkedInAt = checkedIn ? new Date ( ) . toISOString ( ) : null ;
289303
@@ -299,6 +313,7 @@ export class BookingService {
299313 bookingId : string ,
300314 notes : string = DEFAULT_CANCEL_NOTE ,
301315 ) : Promise < CancelBookingResult > {
316+ assertBookingId ( bookingId ) ;
302317 const body : GraphQLBody < CancelBookingResponse > =
303318 await this . client . request < CancelBookingResponse > (
304319 SALES_ENDPOINT ,
@@ -337,12 +352,13 @@ export class BookingService {
337352 * @throws {Error } when `paymentSourceId` is missing or not a `ps_…` id / one
338353 * of `cash/cash`, `custom/other`, `custom/voucher`; when `amount` is not a
339354 * valid number; when `currency` is not a 3-letter uppercase code; when
340- * `idempotencyKey` is empty; when `bookingId` does not resolve to a `b_…` id;
341- * when the booking or payment source is not found; or when the charge fails.
355+ * `idempotencyKey` is empty; when `bookingId` is not a valid booking id
356+ * (`b_…`/`B-…`); when the booking or payment source is not found; or when the
357+ * charge fails.
342358 */
343359 async makePayment ( input : MakePaymentInput ) : Promise < MakePaymentResult > {
360+ this . validatePaymentInput ( input ) ;
344361 const normalized = normalizeBookingId ( input . bookingId ) ;
345- this . validatePaymentInput ( input , normalized ) ;
346362
347363 const onFile = await this . getPaymentsOnFile ( normalized ) ;
348364 if ( ! onFile ) {
@@ -392,8 +408,8 @@ export class BookingService {
392408 * payments-on-file, then applies the refund.
393409 */
394410 async refund ( input : RefundInput ) : Promise < RefundResult > {
411+ this . validateRefundInput ( input ) ;
395412 const normalized = normalizeBookingId ( input . bookingId ) ;
396- this . validateRefundInput ( input , normalized ) ;
397413
398414 const onFile = await this . getPaymentsOnFile ( normalized ) ;
399415 if ( ! onFile ) {
@@ -438,9 +454,7 @@ export class BookingService {
438454
439455 /** Creates an invoice link for a booking's order. */
440456 async createInvoiceLink ( bookingId : string ) : Promise < InvoiceLinkResult > {
441- if ( ! bookingId || bookingId . trim ( ) . length === 0 ) {
442- throw new Error ( "bookingId is required" ) ;
443- }
457+ assertBookingId ( bookingId ) ;
444458 const normalized = normalizeBookingId ( bookingId ) ;
445459
446460 const booking = await this . getById ( normalized ) ;
@@ -465,6 +479,7 @@ export class BookingService {
465479
466480 /** Returns the add-ons on a booking, grouped by add-on item (clean model). */
467481 async listAddons ( bookingId : string ) : Promise < BookingAddons > {
482+ assertBookingId ( bookingId ) ;
468483 const { items, displayId, orderId, normalizedBookingId } =
469484 await this . fetchBookingSale ( bookingId ) ;
470485
@@ -508,6 +523,7 @@ export class BookingService {
508523 bookingId : string ,
509524 input : AddAddonInput ,
510525 ) : Promise < BookingAddonsMutationResult > {
526+ assertBookingId ( bookingId ) ;
511527 const addonOptionId = ( input ?. addonOptionId || input ?. addonId || "" ) . trim ( ) ;
512528 if ( ! addonOptionId ) {
513529 throw new Error ( ERROR_ADDON_OPTION_ID_REQUIRED ) ;
@@ -583,6 +599,7 @@ export class BookingService {
583599 bookingId : string ,
584600 input : AddAddonInput ,
585601 ) : Promise < BookingAddonsMutationResult > {
602+ assertBookingId ( bookingId ) ;
586603 const addonOptionId = ( input ?. addonOptionId || input ?. addonId || "" ) . trim ( ) ;
587604 if ( ! addonOptionId ) {
588605 throw new Error ( ERROR_ADDON_OPTION_ID_REQUIRED ) ;
@@ -627,16 +644,11 @@ export class BookingService {
627644 orderId : string ;
628645 normalizedBookingId : string ;
629646 } > {
630- const searchString = ( bookingId || "" ) . trim ( ) ;
631- if ( ! searchString ) {
632- throw new Error ( ERROR_BOOKING_ID_REQUIRED ) ;
633- }
634-
635647 const body : GraphQLBody < SalesAddonsResponse > =
636648 await this . client . request < SalesAddonsResponse > (
637649 SALES_ENDPOINT ,
638650 SALES_ADDONS_QUERY ,
639- buildSalesAddonsVariables ( searchString ) ,
651+ buildSalesAddonsVariables ( bookingId ) ,
640652 ) ;
641653
642654 const edges = body . data ?. sales ?. edges ?? [ ] ;
@@ -864,7 +876,7 @@ export class BookingService {
864876 return matched . productId ;
865877 }
866878
867- private validatePaymentInput ( input : MakePaymentInput , normalizedBookingId : string ) : void {
879+ private validatePaymentInput ( input : MakePaymentInput ) : void {
868880 if (
869881 ! input . paymentSourceId ||
870882 ( ! input . paymentSourceId . startsWith ( PAYMENT_SOURCE_PREFIX ) &&
@@ -878,17 +890,17 @@ export class BookingService {
878890 assertAmount ( input . amount ) ;
879891 assertCurrency ( input . currency ) ;
880892 assertIdempotencyKey ( input . idempotencyKey ) ;
881- assertBookingId ( normalizedBookingId ) ;
893+ assertBookingId ( input . bookingId ) ;
882894 }
883895
884- private validateRefundInput ( input : RefundInput , normalizedBookingId : string ) : void {
896+ private validateRefundInput ( input : RefundInput ) : void {
885897 if ( ! input . paymentId || ! input . paymentId . startsWith ( PAYMENT_ID_PREFIX ) ) {
886898 throw new Error ( "paymentId is required and must start with 'pmt_'" ) ;
887899 }
888900 assertAmount ( input . amount ) ;
889901 assertCurrency ( input . currency ) ;
890902 assertIdempotencyKey ( input . idempotencyKey ) ;
891- assertBookingId ( normalizedBookingId ) ;
903+ assertBookingId ( input . bookingId ) ;
892904 }
893905
894906 private async fetchPaginated (
@@ -951,9 +963,17 @@ function assertIdempotencyKey(idempotencyKey: string): void {
951963 }
952964}
953965
954- function assertBookingId ( normalizedBookingId : string ) : void {
955- if ( ! normalizedBookingId . startsWith ( BOOKING_ID_PREFIX ) ) {
956- throw new Error ( "bookingId is required and must start with 'b_' or 'B-'" ) ;
966+ /** Throws unless `bookingId` is a valid booking db id (`b_…`) or display id (`B-…`). */
967+ function assertBookingId ( bookingId : string ) : void {
968+ if ( ! ( BOOKING_DB_ID_REGEX . test ( bookingId ) || BOOKING_DISPLAY_ID_REGEX . test ( bookingId ) ) ) {
969+ throw new Error ( ERROR_INVALID_BOOKING_ID ) ;
970+ }
971+ }
972+
973+ /** Throws unless `orderId` is a valid order db id (`o_…`) or display id (`O-…`). */
974+ function assertOrderId ( orderId : string ) : void {
975+ if ( ! ( ORDER_DB_ID_REGEX . test ( orderId ) || ORDER_DISPLAY_ID_REGEX . test ( orderId ) ) ) {
976+ throw new Error ( ERROR_INVALID_ORDER_ID ) ;
957977 }
958978}
959979
@@ -972,6 +992,9 @@ function validateCreateInput(input: CreateBookingInput): void {
972992 if ( input . markAsPaid && ! input . idempotencyKey ) {
973993 throw new Error ( "idempotencyKey is required when markAsPaid is set" ) ;
974994 }
995+ if ( input . parentOrderId ) {
996+ assertOrderId ( input . parentOrderId ) ;
997+ }
975998}
976999
9771000function parseQuantity ( value : unknown ) : number | null {
0 commit comments