@aranova/tracking-react 1.2.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/sales.js CHANGED
@@ -32,6 +32,7 @@ var sales_exports = {};
32
32
  __export(sales_exports, {
33
33
  AranovaApiError: () => AranovaApiError,
34
34
  NAMED_RANGES: () => NAMED_RANGES,
35
+ SALE_SOURCES: () => SALE_SOURCES,
35
36
  SUPPORTED_CURRENCIES: () => SUPPORTED_CURRENCIES,
36
37
  TRACKING_RANGES: () => TRACKING_RANGES,
37
38
  createSalesClient: () => createSalesClient,
@@ -40,6 +41,7 @@ __export(sales_exports, {
40
41
  formatDateInTz: () => formatDateInTz,
41
42
  formatMoney: () => formatMoney,
42
43
  fromMinor: () => fromMinor,
44
+ newBookingAttemptKey: () => newBookingAttemptKey,
43
45
  saleCreateSchema: () => saleCreateSchema,
44
46
  saleItemSchema: () => saleItemSchema,
45
47
  saleServiceSchema: () => saleServiceSchema,
@@ -221,12 +223,106 @@ function fireConversionWithConsent(input, options) {
221
223
  }
222
224
  }
223
225
 
224
- // ../tracking-core/src/ingest.ts
225
- var API_KEY_HEADER = "X-Aranova-Api-Key";
226
- var SDK_VERSION_HEADER = "X-Aranova-Sdk-Version";
227
- var SDK_PACKAGE_HEADER = "X-Aranova-Sdk-Package";
228
- var SDK_SURFACE_HEADER = "X-Aranova-Sdk-Surface";
229
- var SDK_ENVIRONMENT_HEADER = "X-Aranova-Sdk-Environment";
226
+ // ../../contracts/autoleap-booking-links.json
227
+ var autoleap_booking_links_default = {
228
+ hosts: ["myalp.io"]
229
+ };
230
+
231
+ // ../tracking-core/src/integrations-public.ts
232
+ var HANDOFF_HOSTS = new Set(autoleap_booking_links_default.hosts);
233
+ function getBookingIntegration(integrations) {
234
+ const booking = integrations?.autoleap?.booking;
235
+ if (!booking) return null;
236
+ const rules = booking.rules ? camelRules(booking.rules) : null;
237
+ return {
238
+ provider: "autoleap",
239
+ enabled: booking.enabled,
240
+ fallbackUrl: booking.fallback_url,
241
+ acceptsBookings: booking.accepts_bookings && rules !== null && booking.config_version !== null,
242
+ timezone: booking.timezone,
243
+ configVersion: booking.config_version,
244
+ rules,
245
+ options: booking.options.map((option) => ({ key: option.key, label: option.label })),
246
+ mode: booking.mode ?? "live"
247
+ };
248
+ }
249
+ function camelRules(rules) {
250
+ return {
251
+ minDaysAhead: rules.min_days_ahead,
252
+ maxDaysAhead: rules.max_days_ahead,
253
+ hours: rules.hours,
254
+ closedDates: rules.closed_dates,
255
+ specialHours: rules.special_hours,
256
+ vehicleRequired: rules.vehicle_required,
257
+ dropOffOptions: rules.drop_off_options
258
+ };
259
+ }
260
+ function buildBookingHandoffUrl(fallbackUrl, prefill = {}) {
261
+ let url;
262
+ try {
263
+ url = new URL(fallbackUrl);
264
+ } catch {
265
+ return null;
266
+ }
267
+ if (url.protocol !== "https:" || !HANDOFF_HOSTS.has(url.hostname) || url.username || url.password || url.port) {
268
+ return null;
269
+ }
270
+ const [firstName, lastName] = prefill.firstName?.trim() ? [prefill.firstName, prefill.lastName] : splitFullName(prefill.name);
271
+ const values = [
272
+ ["firstName", firstName?.trim()],
273
+ ["lastName", lastName?.trim()],
274
+ ["phone", prefill.phone ? nationalDigits(prefill.phone) : ""],
275
+ ["email", prefill.email?.trim()]
276
+ ];
277
+ for (const [key, value] of values) {
278
+ if (value) url.searchParams.set(key, value);
279
+ }
280
+ return url.toString();
281
+ }
282
+ function splitFullName(name) {
283
+ const [first = "", ...rest] = (name ?? "").trim().split(/\s+/);
284
+ return [first, rest.join(" ")];
285
+ }
286
+ function nationalDigits(phone) {
287
+ const parsed = parsePhone(phone);
288
+ return (parsed.isValid ? parsed.national : phone).replace(/\D/g, "");
289
+ }
290
+
291
+ // ../tracking-core/src/uuid.ts
292
+ function uuidV4() {
293
+ const cryptoApi = globalThis.crypto;
294
+ if (typeof cryptoApi?.randomUUID === "function") return cryptoApi.randomUUID();
295
+ const bytes = new Uint8Array(16);
296
+ if (typeof cryptoApi?.getRandomValues === "function") cryptoApi.getRandomValues(bytes);
297
+ else for (let i = 0; i < 16; i += 1) bytes[i] = Math.floor(Math.random() * 256);
298
+ bytes[6] = bytes[6] & 15 | 64;
299
+ bytes[8] = bytes[8] & 63 | 128;
300
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
301
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
302
+ }
303
+
304
+ // ../tracking-core/src/session.ts
305
+ var SESSION_STORAGE_KEY = "aranova_tracking_session";
306
+ var SESSION_IDLE_MS = 30 * 60 * 1e3;
307
+ function readLocalStorage(key) {
308
+ try {
309
+ return window.localStorage.getItem(key);
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+ function peekSessionId(now = Date.now()) {
315
+ if (typeof window === "undefined") return null;
316
+ const raw = readLocalStorage(SESSION_STORAGE_KEY);
317
+ if (!raw) return null;
318
+ try {
319
+ const parsed = JSON.parse(raw);
320
+ if (typeof parsed.id !== "string" || typeof parsed.last_event_at !== "number") return null;
321
+ return now - parsed.last_event_at <= SESSION_IDLE_MS ? parsed.id : null;
322
+ } catch {
323
+ return null;
324
+ }
325
+ }
230
326
 
231
327
  // ../tracking-core/src/resources/http/errors.ts
232
328
  var AranovaApiError = class extends Error {
@@ -236,9 +332,66 @@ var AranovaApiError = class extends Error {
236
332
  this.status = options.status;
237
333
  this.code = options.code;
238
334
  this.requestId = options.requestId;
335
+ this.context = options.context;
239
336
  }
240
337
  };
241
338
 
339
+ // ../tracking-core/src/resources/intl/date.ts
340
+ function formatDateInTz(iso3, timeZone, opts, locale) {
341
+ const date = new Date(iso3);
342
+ if (Number.isNaN(date.getTime())) return iso3;
343
+ return new Intl.DateTimeFormat(locale, {
344
+ year: "numeric",
345
+ month: "short",
346
+ day: "2-digit",
347
+ hour: "2-digit",
348
+ minute: "2-digit",
349
+ ...opts,
350
+ timeZone
351
+ }).format(date);
352
+ }
353
+
354
+ // ../tracking-core/src/resources/sales/money.ts
355
+ var MINOR_UNIT_EXPONENT = {
356
+ USD: 2,
357
+ CAD: 2
358
+ };
359
+ function exponentFor(currency) {
360
+ return MINOR_UNIT_EXPONENT[currency] ?? 2;
361
+ }
362
+ function toMinor(amount, currency) {
363
+ return Math.round(amount * 10 ** exponentFor(currency));
364
+ }
365
+ function fromMinor(cents, currency) {
366
+ return cents / 10 ** exponentFor(currency);
367
+ }
368
+ function formatMoney(cents, currency, locale) {
369
+ return new Intl.NumberFormat(locale, { style: "currency", currency }).format(
370
+ fromMinor(cents, currency)
371
+ );
372
+ }
373
+
374
+ // ../tracking-core/src/resources/tracking-config-runtime.ts
375
+ var DEFAULT_CDN_BASE_URL = "https://demos.aranova.io";
376
+ function trackingConfigKey(ref) {
377
+ if (ref.siteId) return `tracking-config/v1/sites/${ref.siteId}-${ref.environment}.json`;
378
+ return `tracking-config/v1/${ref.businessId}-${ref.environment}.json`;
379
+ }
380
+ function resolveTrackingConfigUrl(ref) {
381
+ if (ref.cdnUrl) return ref.cdnUrl;
382
+ const configured = ref.cdnBaseUrl?.trim();
383
+ const base = (configured || DEFAULT_CDN_BASE_URL).replace(/\/+$/, "");
384
+ return `${base}/${trackingConfigKey(ref)}`;
385
+ }
386
+ var RETRY_MAX_MS = 5 * 6e4;
387
+
388
+ // ../tracking-core/src/ingest.ts
389
+ var API_KEY_HEADER = "X-Aranova-Api-Key";
390
+ var SDK_VERSION_HEADER = "X-Aranova-Sdk-Version";
391
+ var SDK_PACKAGE_HEADER = "X-Aranova-Sdk-Package";
392
+ var SDK_SURFACE_HEADER = "X-Aranova-Sdk-Surface";
393
+ var SDK_ENVIRONMENT_HEADER = "X-Aranova-Sdk-Environment";
394
+
242
395
  // ../tracking-core/src/resources/http/request.ts
243
396
  function identityHeaders(config) {
244
397
  const headers = { [API_KEY_HEADER]: config.apiKey };
@@ -262,19 +415,24 @@ async function apiRequest(config, method, path, body, extraHeaders) {
262
415
  if (!response.ok) {
263
416
  let detail;
264
417
  let code;
418
+ let context;
265
419
  try {
266
420
  const parsed = await response.json();
267
421
  if (parsed && typeof parsed === "object") {
268
422
  const record3 = parsed;
269
423
  if (typeof record3.detail === "string") detail = record3.detail;
270
424
  if (typeof record3.code === "string") code = record3.code;
425
+ if (record3.context && typeof record3.context === "object") {
426
+ context = record3.context;
427
+ }
271
428
  }
272
429
  } catch {
273
430
  }
274
431
  throw new AranovaApiError(detail ?? response.statusText ?? "Request failed", {
275
432
  status: response.status,
276
433
  code,
277
- requestId: response.headers.get("x-request-id") ?? void 0
434
+ requestId: response.headers.get("x-request-id") ?? void 0,
435
+ context
278
436
  });
279
437
  }
280
438
  if (response.status === 204) return void 0;
@@ -284,39 +442,212 @@ async function apiRequest(config, method, path, body, extraHeaders) {
284
442
  // ../tracking-core/src/resources/sales/transport.ts
285
443
  var salesRequest = apiRequest;
286
444
 
287
- // ../tracking-core/src/resources/intl/date.ts
288
- function formatDateInTz(iso3, timeZone, opts, locale) {
289
- const date = new Date(iso3);
290
- if (Number.isNaN(date.getTime())) return iso3;
291
- return new Intl.DateTimeFormat(locale, {
292
- year: "numeric",
293
- month: "short",
294
- day: "2-digit",
295
- hour: "2-digit",
296
- minute: "2-digit",
297
- ...opts,
298
- timeZone
299
- }).format(date);
300
- }
301
-
302
- // ../tracking-core/src/resources/sales/money.ts
303
- var MINOR_UNIT_EXPONENT = {
304
- USD: 2,
305
- CAD: 2
445
+ // ../tracking-core/src/resources/sales/bookings.ts
446
+ var BOOKING_SIMULATE_HEADER = "X-Aranova-Simulate";
447
+ var BOOKING_ERROR_CODES = {
448
+ unavailable: "booking_unavailable",
449
+ stale: "booking_config_stale",
450
+ rejected: "booking_rejected"
306
451
  };
307
- function exponentFor(currency) {
308
- return MINOR_UNIT_EXPONENT[currency] ?? 2;
452
+ var POLL_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
453
+ var INVALID_DETAILS = "Please check your details and try again.";
454
+ var newBookingAttemptKey = uuidV4;
455
+ function sleep(ms) {
456
+ return new Promise((resolve) => {
457
+ setTimeout(resolve, ms);
458
+ });
309
459
  }
310
- function toMinor(amount, currency) {
311
- return Math.round(amount * 10 ** exponentFor(currency));
460
+ function outcomeOf(body) {
461
+ const mode = body.mode ?? "live";
462
+ if (body.status === "created")
463
+ return { status: "created", id: body.id, requestCode: body.request_code, mode };
464
+ if (body.status === "pending") return { status: "pending", id: body.id, mode };
465
+ return { status: "failed", id: body.id, fallbackUrl: body.fallback_url, mode };
312
466
  }
313
- function fromMinor(cents, currency) {
314
- return cents / 10 ** exponentFor(currency);
467
+ function formSubmitPayload(input, formId, labels) {
468
+ const vehicle = input.vehicle ? `${input.vehicle.year} ${input.vehicle.make} ${input.vehicle.model}` : "";
469
+ const field = (name, type, label, value) => ({
470
+ name,
471
+ type,
472
+ label,
473
+ value
474
+ });
475
+ return {
476
+ form: {
477
+ id: formId,
478
+ action: "/tracking/bookings",
479
+ fields: [
480
+ field("first_name", "text", "First name", input.firstName),
481
+ field("last_name", "text", "Last name", input.lastName),
482
+ // E.164 on the wire: it keys the customer and enhanced-conversion matching.
483
+ field("phone", "tel", "Phone", toE164(input.phone) ?? input.phone),
484
+ field("email", "email", "Email", input.email ?? ""),
485
+ field(
486
+ "service",
487
+ "select",
488
+ "Service",
489
+ input.services.map((key) => labels.get(key) ?? key).join(", ")
490
+ ),
491
+ field("vehicle", "text", "Vehicle", vehicle),
492
+ field(
493
+ "start",
494
+ "text",
495
+ "Requested time",
496
+ `${input.startDate} ${String(input.startHour).padStart(2, "0")}:00`
497
+ ),
498
+ field("note", "textarea", "Notes", input.notes ?? "")
499
+ ]
500
+ },
501
+ page: { path: typeof window === "undefined" ? "/" : window.location.pathname }
502
+ };
315
503
  }
316
- function formatMoney(cents, currency, locale) {
317
- return new Intl.NumberFormat(locale, { style: "currency", currency }).format(
318
- fromMinor(cents, currency)
319
- );
504
+ function runtimeOf(firing) {
505
+ return firing && "integrations" in firing ? firing : null;
506
+ }
507
+ function isLocalUrl(url) {
508
+ try {
509
+ return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname);
510
+ } catch {
511
+ return false;
512
+ }
513
+ }
514
+ function bookingSetupMismatch(endpoint, runtime) {
515
+ if (!runtime || !isLocalUrl(endpoint)) return null;
516
+ const configUrl = resolveTrackingConfigUrl(runtime.ref);
517
+ if (isLocalUrl(configUrl)) return null;
518
+ return `[aranova] Bookings go to the local API (${endpoint}) but the runtime config comes from ${configUrl}. Set NEXT_PUBLIC_ARANOVA_CDN_BASE_URL to the local bucket (http://localhost:9100/aranova-demos) so both come from one backend.`;
519
+ }
520
+ function createSalesBookingsClient(config, firing) {
521
+ const submittedForms = /* @__PURE__ */ new Set();
522
+ const staleAttempts = /* @__PURE__ */ new Set();
523
+ let isSetupChecked = false;
524
+ function warnOnSetupMismatch(runtime) {
525
+ if (isSetupChecked) return;
526
+ isSetupChecked = true;
527
+ const warning = bookingSetupMismatch(config.endpoint, runtime);
528
+ if (warning) console.warn(warning);
529
+ }
530
+ function handoff(input) {
531
+ const booking = getBookingIntegration(runtimeOf(firing)?.integrations());
532
+ if (!booking) return null;
533
+ return buildBookingHandoffUrl(booking.fallbackUrl, {
534
+ firstName: input.firstName,
535
+ lastName: input.lastName,
536
+ phone: input.phone,
537
+ email: input.email
538
+ });
539
+ }
540
+ function failedBeforeSend(input) {
541
+ const mode = getBookingIntegration(runtimeOf(firing)?.integrations())?.mode ?? "live";
542
+ return { status: "failed", id: null, fallbackUrl: handoff(input), mode };
543
+ }
544
+ function withHandoff(outcome, input) {
545
+ if (outcome.status === "failed" && !outcome.fallbackUrl) {
546
+ return { ...outcome, fallbackUrl: handoff(input) };
547
+ }
548
+ return outcome;
549
+ }
550
+ async function poll(id, mode) {
551
+ let outcome = { status: "pending", id, mode };
552
+ for (const delay of POLL_DELAYS_MS) {
553
+ await sleep(delay);
554
+ try {
555
+ const path = `/bookings/${encodeURIComponent(id)}`;
556
+ outcome = outcomeOf(await salesRequest(config, "GET", path));
557
+ } catch {
558
+ continue;
559
+ }
560
+ if (outcome.status !== "pending") return outcome;
561
+ }
562
+ return outcome;
563
+ }
564
+ async function rejected(error, input, attemptKey, runtime) {
565
+ if (!(error instanceof AranovaApiError)) {
566
+ return failedBeforeSend(input);
567
+ }
568
+ if (error.code === BOOKING_ERROR_CODES.stale) {
569
+ if (staleAttempts.has(attemptKey)) return failedBeforeSend(input);
570
+ staleAttempts.add(attemptKey);
571
+ await runtime?.revalidate();
572
+ return { status: "stale", message: error.message };
573
+ }
574
+ if (error.code === BOOKING_ERROR_CODES.rejected) {
575
+ const reasons = error.context?.reasons;
576
+ return {
577
+ status: "invalid",
578
+ message: error.message,
579
+ reasons: Array.isArray(reasons) ? reasons.map(String) : []
580
+ };
581
+ }
582
+ if (error.code === "validation_error") {
583
+ return { status: "invalid", message: INVALID_DETAILS, reasons: [] };
584
+ }
585
+ return failedBeforeSend(input);
586
+ }
587
+ return {
588
+ async submit(input, { attemptKey, formSubmit, simulate }) {
589
+ registerCapability("booking_integration");
590
+ const runtime = runtimeOf(firing);
591
+ warnOnSetupMismatch(runtime);
592
+ const booking = getBookingIntegration(runtime?.integrations());
593
+ if (!booking?.acceptsBookings || booking.configVersion === null) {
594
+ return failedBeforeSend(input);
595
+ }
596
+ if (formSubmit && !submittedForms.has(attemptKey)) {
597
+ submittedForms.add(attemptKey);
598
+ const labels = new Map(booking.options.map((option) => [option.key, option.label]));
599
+ formSubmit.tracking.trackEvent(
600
+ "form_submit",
601
+ formSubmitPayload(input, formSubmit.formId, labels)
602
+ );
603
+ }
604
+ const body = {
605
+ idempotency_key: attemptKey,
606
+ // The tracking client's own visit, so the booking and its form_submit share one session.
607
+ session_id: formSubmit?.tracking.getSessionId() ?? peekSessionId() ?? `no-session-${attemptKey}`,
608
+ config_version: booking.configVersion,
609
+ start_date: input.startDate,
610
+ start_hour: input.startHour,
611
+ first_name: input.firstName.trim(),
612
+ last_name: input.lastName.trim(),
613
+ phone: input.phone,
614
+ email: input.email?.trim() || null,
615
+ vehicle: input.vehicle ?? null,
616
+ drop_off: input.dropOff ?? null,
617
+ services: input.services,
618
+ notes: input.notes?.trim() || null,
619
+ marketing_sms_consent: input.marketingSmsConsent === true,
620
+ page_path: typeof window === "undefined" ? null : window.location.pathname
621
+ };
622
+ let response;
623
+ try {
624
+ const headers = simulate ? { [BOOKING_SIMULATE_HEADER]: simulate } : void 0;
625
+ response = await salesRequest(
626
+ config,
627
+ "POST",
628
+ "/bookings",
629
+ body,
630
+ headers
631
+ );
632
+ } catch (error) {
633
+ return rejected(error, input, attemptKey, runtime);
634
+ }
635
+ const outcome = outcomeOf(response);
636
+ return withHandoff(
637
+ outcome.status === "pending" ? await poll(outcome.id, outcome.mode) : outcome,
638
+ input
639
+ );
640
+ },
641
+ async status(id) {
642
+ return outcomeOf(
643
+ await salesRequest(
644
+ config,
645
+ "GET",
646
+ `/bookings/${encodeURIComponent(id)}`
647
+ )
648
+ );
649
+ }
650
+ };
320
651
  }
321
652
 
322
653
  // ../tracking-core/src/resources/sales/client.ts
@@ -493,7 +824,8 @@ function createSalesClient(config) {
493
824
  async config() {
494
825
  return salesRequest(config, "GET", "/business/config");
495
826
  }
496
- }
827
+ },
828
+ bookings: createSalesBookingsClient(config, config.firing)
497
829
  };
498
830
  }
499
831
 
@@ -502,6 +834,12 @@ var z2 = __toESM(require("zod/mini"));
502
834
 
503
835
  // ../tracking-core/src/resources/sales/generated/zod.gen.ts
504
836
  var z = __toESM(require("zod/mini"));
837
+ var zAutoLeapSaleFacts = z.object({
838
+ first_order: z.boolean(),
839
+ fleet_customer: z.boolean(),
840
+ invoice_code: z.string(),
841
+ invoice_id: z.string()
842
+ });
505
843
  var zErrorCode = z.enum([
506
844
  "missing_credentials",
507
845
  "invalid_api_key",
@@ -529,6 +867,9 @@ var zErrorCode = z.enum([
529
867
  "booking_not_reschedulable",
530
868
  "manage_token_invalid",
531
869
  "google_error",
870
+ "booking_unavailable",
871
+ "booking_config_stale",
872
+ "booking_rejected",
532
873
  "calendar_not_owned",
533
874
  "calendar_is_sandbox",
534
875
  "sandbox_exists",
@@ -570,6 +911,7 @@ var zSaleServiceInputSchema = z.strictObject({
570
911
  amount_cents: z.int().check(z.gte(0)),
571
912
  service: z.string()
572
913
  });
914
+ var zSaleSource = z.enum(["api", "admin", "autoleap"]);
573
915
  var zSmsAutomationState = z.enum([
574
916
  "needs_check",
575
917
  "failed",
@@ -601,7 +943,8 @@ var zSmsWithheldReason = z.enum([
601
943
  "number_not_active",
602
944
  "a2p_registration_required",
603
945
  "alert_recipient_cannot_receive_sms",
604
- "line_type_unresolved"
946
+ "line_type_unresolved",
947
+ "sale_source_sends_own_messages"
605
948
  ]);
606
949
  var zSaleSmsStatusSchema = z.object({
607
950
  can_send_now: z._default(z.boolean(), false),
@@ -710,6 +1053,8 @@ var zSaleEntitySchema = z.object({
710
1053
  sms_automation: z.nullable(zSaleSmsStatusSchema),
711
1054
  sms_consent: z._default(z.boolean(), false),
712
1055
  sms_consent_granted_at: z.nullable(z.iso.datetime({ offset: true })),
1056
+ source: zSaleSource,
1057
+ source_facts: z.nullable(zAutoLeapSaleFacts),
713
1058
  updated_at: z.iso.datetime({ offset: true }),
714
1059
  writeback: z.nullable(zSaleWritebackStatusSchema)
715
1060
  });
@@ -719,6 +1064,7 @@ var saleItemSchema = zSaleItemInputSchema;
719
1064
  var saleServiceSchema = zSaleServiceInputSchema;
720
1065
  var currencySchema = zSupportedCurrency;
721
1066
  var SUPPORTED_CURRENCIES = currencySchema.options;
1067
+ var SALE_SOURCES = zSaleSource.options;
722
1068
  var centsSchema = z2.int().check(z2.gte(0));
723
1069
  var metadataSchema = z2.record(z2.string(), z2.unknown());
724
1070
  var customerNameSchema = z2.string().check(z2.maxLength(200));
@@ -799,6 +1145,7 @@ async function fetchServices(config) {
799
1145
  0 && (module.exports = {
800
1146
  AranovaApiError,
801
1147
  NAMED_RANGES,
1148
+ SALE_SOURCES,
802
1149
  SUPPORTED_CURRENCIES,
803
1150
  TRACKING_RANGES,
804
1151
  createSalesClient,
@@ -807,6 +1154,7 @@ async function fetchServices(config) {
807
1154
  formatDateInTz,
808
1155
  formatMoney,
809
1156
  fromMinor,
1157
+ newBookingAttemptKey,
810
1158
  saleCreateSchema,
811
1159
  saleItemSchema,
812
1160
  saleServiceSchema,