{"version":3,"sources":["../../src/schemas/meeting-booking-schema.ts"],"names":["z"],"mappings":"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","file":"meeting-booking-schema.cjs","sourcesContent":["import { z } from 'zod';\n\n/**\n * Meeting-booking wire contracts + validation factory.\n *\n * SERVER-SAFE tsup entry (no \"use client\" banner) with its OWN per-file\n * subpath (`./schemas/meeting-booking-schema`) — same pattern and reasons as\n * `schemas/contact-schema`: used by BOTH the lib's `<HubSpotMeetingScheduler>`\n * (client-side validation) AND the host's server-side booking route, which\n * REBUILDS the schema from the link's own fetched metadata and never trusts a\n * client-shaped instance. zod is an optional peer quarantined to per-subpath\n * verticals — do NOT re-export this module through any broad barrel.\n *\n * Every cross-boundary type for the scheduling feature lives HERE (the lib\n * owns the contract; hosts import this subpath directly, type-only where\n * possible).\n */\n\n// ---------------------------------------------------------------------------\n// Wire types\n// ---------------------------------------------------------------------------\n\n/**\n * Sanitized availability payload served by the host proxy\n * (`GET {apiBaseUrl}/api/meetings/availability?meeting=<id>&monthOffset=<n>`).\n *\n * NO timezone field on purpose: slot starts are absolute epoch-ms instants\n * (verified timezone-independent against the live API), upstream fetches are\n * UTC-pinned, and ALL zone rendering happens client-side. Slots must be\n * whitelist-copied from HubSpot's `linkAvailability` ONLY — never derived\n * from busy-time data.\n */\nexport interface MeetingAvailability {\n  meetingId: string;\n  monthOffset: number;\n  hasMore: boolean;\n  /** Offered durations in ms — HubSpot's native unit for the booking POST. */\n  durationsMs: number[];\n  /** Bookable slot start times (epoch ms), keyed by duration in ms. */\n  slotsByDurationMs: Record<string, number[]>;\n  formFields: MeetingFormField[];\n  /** Verbatim whitelist-copy of HubSpot's `legalConsentOptions` when consent is enabled; null when disabled. */\n  legalConsent: MeetingLegalConsent | null;\n  /**\n   * Who the visitor is meeting — whitelisted DISPLAY projection the host DAL\n   * builds from its own people data (e.g. a profiles table matched\n   * server-side). NEVER carries emails or busy-time data; optional so\n   * existing hosts stay wire-compatible.\n   */\n  hosts?: MeetingHost[];\n}\n\n/** Display-only host identity for the scheduler's context panel. */\nexport interface MeetingHost {\n  name: string;\n  avatarUrl: string | null;\n  /** Job title / role line under the name (null → omitted). */\n  title: string | null;\n}\n\n/**\n * One scheduling link on the DIRECTORY wire (`GET /api/meetings`) — the\n * host-DAL whitelist projection consumed by `MeetingSchedulerDirectory` and\n * host pages. Never carries organizer emails/busy-time data.\n */\nexport interface SchedulingLink {\n  id: string;\n  /** The link's public HubSpot booking URL — escape-hatch target only. */\n  link: string;\n  /** HubSpot slug path — the row's in-app destination is `<basePath>/<slug>`. */\n  slug: string;\n  /** Audience group key (slugified audience label; `\"other\"` in scope=all). */\n  purpose: string;\n  title: string;\n  description: string | null;\n  kind: 'personal' | 'team';\n  /** Display-only minutes projection (booking stays ms end-to-end). */\n  durationsMinutes: number[];\n  hosts: MeetingHost[];\n  /** Earliest bookable slot (epoch ms) from the current-month payload. */\n  nextAvailableMs: number | null;\n}\n\nexport interface SchedulingLinksPayload {\n  purposes: Array<{ purpose: string; label: string; links: SchedulingLink[] }>;\n  fetchedAt: string;\n}\n\nexport interface MeetingFormField {\n  name: string;\n  label: string;\n  type: string;\n  required: boolean;\n  options?: string[];\n}\n\n/**\n * HubSpot's consent copy, rendered VERBATIM by the widget (GDPR surface —\n * never edited, never summarized). Responses are keyed by\n * `communicationTypeId`.\n */\nexport interface MeetingLegalConsent {\n  processingConsentText: string;\n  processingConsentCheckboxLabel: string | null;\n  communicationConsentText: string | null;\n  communicationConsentCheckboxes: Array<{\n    communicationTypeId: string;\n    label: string;\n    required: boolean;\n  }>;\n  privacyPolicyText: string | null;\n  isLegitimateInterest: boolean;\n}\n\n/**\n * Whitelisted booking result returned by the host proxy — the THIRD HubSpot\n * payload that reaches a browser, so it gets the same whitelist-copy\n * treatment as the two GETs. Nothing organizer-derived.\n */\nexport interface BookingConfirmation {\n  meetingId: string;\n  title: string;\n  startTimeMs: number;\n  durationMs: number;\n}\n\n/**\n * Typed domain errors the booking route emits; the widget keys its recovery\n * UI off these. `SLOT_TAKEN` → refetch-and-recover; `TEMPORARILY_UNAVAILABLE`\n * → retry affordance; `MEETING_UNAVAILABLE` → daily ceiling exhausted\n * (escape hatch, not a retry timer); `LINK_GONE` → link deleted upstream;\n * `INVALID_EMAIL` → HubSpot rejected the attendee address (its\n * MeetingsBookingCreatedError.INVALID_EMAIL class — fake/unreachable\n * mailboxes), so the widget tells the visitor to fix the email instead of\n * blaming the slot or their other details.\n */\nexport const MEETING_BOOKING_ERROR_CODES = [\n  'SLOT_TAKEN',\n  'VALIDATION',\n  'INVALID_EMAIL',\n  'LINK_GONE',\n  'TEMPORARILY_UNAVAILABLE',\n  'MEETING_UNAVAILABLE',\n] as const;\n\n/**\n * Derived from the runtime array ABOVE — the array is the single source of\n * truth. The booking hook validates server codes against it at runtime, so a\n * code added only to a hand-written type would be silently coerced to\n * TEMPORARILY_UNAVAILABLE by every deployed widget (exactly how the\n * INVALID_EMAIL rollout mis-rendered on 2026-08-27: type extended, runtime\n * allowlist stale — an array annotation is not exhaustiveness-checked).\n */\nexport type MeetingBookingErrorCode = (typeof MEETING_BOOKING_ERROR_CODES)[number];\n\n// ---------------------------------------------------------------------------\n// Field-type vocabulary — ONE set drives the renderer AND the validator\n// ---------------------------------------------------------------------------\n\n/**\n * THE registry of HubSpot question types the native form supports — one entry\n * per `fieldType`, and the ONLY place a type is declared. Everything else\n * derives from it: `SupportedFormFieldType` is its key union,\n * `SUPPORTED_FORM_FIELD_TYPES` its keys, `FORM_FIELD_TYPES_WITH_OPTIONS` the\n * entries that carry `options`, and `makeBookingSchema` maps each answer\n * through the entry's validator. The widget's control table\n * (`booking-form.tsx`) is a `Record` over the same key union, so a type added\n * here without a control is a COMPILE error, not a silent gap.\n *\n * Fail-closed: a `fieldType` with no entry makes the link \"not natively\n * bookable\" and the card falls back to the HubSpot escape hatch.\n *\n * Every string type rides the wire as a STRING (HubSpot's book endpoint takes\n * `{ name, value: string }`); `checkbox` is the one boolean. `number` is a\n * Number property validated as a decimal literal — what `<input type=\"number\">`\n * emits and what the property stores.\n */\nexport interface FormFieldTypeSpec {\n  /** The answer's wire shape, which also decides how required/optional wraps it. */\n  kind: 'string' | 'boolean';\n  /** Whether HubSpot publishes `options` for the type (the pickers). */\n  hasOptions: boolean;\n  /**\n   * The validator for ONE answer. String types EXTEND `base`, which already\n   * carries the required-ness (`min(1)` when required) — so \"X is required\" is\n   * the first issue reported for an empty answer, ahead of the type's own rule.\n   * Boolean types ignore it.\n   */\n  validator: (field: MeetingFormField, base: z.ZodString) => z.ZodTypeAny;\n  /** The control's placeholder, derived from the field (the mock's \"Enter Company Name\"). */\n  placeholder?: (field: MeetingFormField) => string;\n}\n\nconst optionValidator: FormFieldTypeSpec['validator'] = (field, base) =>\n  base.refine(v => !v || (field.options ?? []).includes(v), {\n    message: `Please choose a valid option for ${field.label}`,\n  });\n\n/** The wire shape of a number answer — what the form canonicalises TO and the validator checks. */\nexport const DECIMAL_LITERAL_RE = /^-?\\d+(\\.\\d+)?$/;\n\nexport const FORM_FIELD_TYPES = {\n  text: {\n    kind: 'string',\n    hasOptions: false,\n    validator: (field, base) => base.max(1000, { message: `${field.label} is too long` }),\n    placeholder: field => `Enter ${field.label}`,\n  },\n  textarea: {\n    kind: 'string',\n    hasOptions: false,\n    validator: (field, base) => base.max(5000, { message: `${field.label} is too long` }),\n    placeholder: field => `Enter text${field.required ? '' : ' (optional)'}`,\n  },\n  number: {\n    kind: 'string',\n    hasOptions: false,\n    // Canonical decimal literal — the control normalises what the browser\n    // accepts (`1e3`, `007`) to this before it is validated or sent.\n    validator: (field, base) =>\n      base\n        .max(32, { message: `${field.label} is too long` })\n        .regex(DECIMAL_LITERAL_RE, { message: `${field.label} must be a number` }),\n  },\n  select: { kind: 'string', hasOptions: true, validator: optionValidator },\n  radio: { kind: 'string', hasOptions: true, validator: optionValidator },\n  checkbox: { kind: 'boolean', hasOptions: false, validator: () => z.boolean() },\n} as const satisfies Record<string, FormFieldTypeSpec>;\n\nexport type SupportedFormFieldType = keyof typeof FORM_FIELD_TYPES;\n\nexport const SUPPORTED_FORM_FIELD_TYPES = Object.keys(FORM_FIELD_TYPES) as readonly SupportedFormFieldType[];\n\n/** The types whose `options` the host must forward (select, radio). */\nexport const FORM_FIELD_TYPES_WITH_OPTIONS: readonly SupportedFormFieldType[] = SUPPORTED_FORM_FIELD_TYPES.filter(\n  type => FORM_FIELD_TYPES[type].hasOptions,\n);\n\n/** Whether a raw HubSpot field type is one whose answers come from declared options. */\nexport function formFieldTypeHasOptions(type: string): boolean {\n  return (FORM_FIELD_TYPES_WITH_OPTIONS as readonly string[]).includes(type);\n}\n\nexport function isSupportedFormField(field: MeetingFormField): field is SupportedMeetingFormField {\n  return Object.prototype.hasOwnProperty.call(FORM_FIELD_TYPES, field.type);\n}\n\n/** A declared question whose `type` is in the registry. */\nexport type SupportedMeetingFormField = MeetingFormField & { type: SupportedFormFieldType };\n\n/**\n * The scheduler's fixed identity fields. HubSpot's book endpoint takes them\n * TOP-LEVEL (`firstName`, `lastName`, `email`), its own booking page hardcodes\n * them, and the link's `formFields` never lists them — so they are data HERE,\n * rendered by the widget through the SAME control path as every declared\n * question, rather than three hand-written blocks. `inputType`/`autoComplete`\n * are the browser hints a text control takes; the wire and the validator do\n * not see them.\n */\nexport interface BuiltInBookingField extends MeetingFormField {\n  type: 'text';\n  required: true;\n  inputType?: 'email';\n  autoComplete: string;\n  /** Own placeholder. Omitted, the control derives one from the label (the registry's rule). */\n  placeholder?: string;\n  /** The wire's own required/format message — kept verbatim from the schema it replaced. */\n  requiredMessage: string;\n}\n\nexport const BUILT_IN_BOOKING_FIELDS = [\n  {\n    name: 'firstName',\n    label: 'First Name',\n    type: 'text',\n    required: true,\n    autoComplete: 'given-name',\n    requiredMessage: 'First name is required',\n  },\n  {\n    name: 'lastName',\n    label: 'Last Name',\n    type: 'text',\n    required: true,\n    autoComplete: 'family-name',\n    requiredMessage: 'Last name is required',\n  },\n  {\n    name: 'email',\n    label: 'Email',\n    type: 'text',\n    required: true,\n    inputType: 'email',\n    autoComplete: 'email',\n    placeholder: 'username@mail.com',\n    requiredMessage: 'Please enter a valid email address',\n  },\n] as const satisfies readonly BuiltInBookingField[];\n\n/** `'firstName' | 'lastName' | 'email'` — derived from the array, never restated. */\nexport type BuiltInBookingFieldName = (typeof BUILT_IN_BOOKING_FIELDS)[number]['name'];\n\n/** The registry entry for a supported type, widened to the spec so optional\n *  members (`placeholder`) are readable without narrowing on the union. */\nexport function fieldTypeSpec(type: SupportedFormFieldType): FormFieldTypeSpec {\n  return FORM_FIELD_TYPES[type];\n}\n\n/** The wire validator for one identity field, from ITS declaration above. */\nfunction identityValidator(name: BuiltInBookingFieldName) {\n  const field: BuiltInBookingField | undefined = BUILT_IN_BOOKING_FIELDS.find(f => f.name === name);\n  if (!field) throw new Error(`Unknown built-in booking field: ${name}`);\n  // The field's own message FIRST: an empty value must fail `min`/`email` with\n  // the wire's copy, not a length cap it cannot have hit.\n  const tooLong = { message: `${field.label} is too long` };\n  return field.inputType === 'email'\n    ? z.string().email({ message: field.requiredMessage }).max(255, tooLong)\n    : z.string().min(1, { message: field.requiredMessage }).max(255, tooLong);\n}\n\n// ---------------------------------------------------------------------------\n// Validators (single home — client widget and server rebuild both use these)\n// ---------------------------------------------------------------------------\n\nconst IANA_TZ_RE = /^(?:UTC|[A-Za-z_]+(?:\\/[A-Za-z0-9_+-]+)+)$/;\n\n/**\n * IANA timezone check. Shape prefilter, then the authoritative resolution\n * test: `Intl.DateTimeFormat` throws on unknown zones. NOT\n * `Intl.supportedValuesOf('timeZone')` — that list excludes `'UTC'` itself\n * (verified in Node), which is a legitimate booking zone. Reject, never coerce.\n */\nexport function isValidIanaTimezone(tz: string): boolean {\n  if (!IANA_TZ_RE.test(tz)) return false;\n  try {\n    new Intl.DateTimeFormat('en-US', { timeZone: tz });\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n/** BCP-47 locale shape check via `Intl.getCanonicalLocales`. Reject, never coerce. */\nexport function isValidBcp47Locale(locale: string): boolean {\n  try {\n    return Intl.getCanonicalLocales(locale).length > 0;\n  } catch {\n    return false;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Schema factory\n// ---------------------------------------------------------------------------\n\n/**\n * Build the booking-form schema for ONE link's declared questions + consent.\n *\n * A factory (not a static schema) because per-link required questions cannot\n * be expressed statically. The widget builds it from the availability payload\n * it rendered; the server REBUILDS it from the link's own fetched metadata —\n * required-consent enforcement flows from this rebuild, not a parallel check.\n *\n * Deliberately NOT `.strict()`: the humanity-signal fields\n * (`HUMANITY_SIGNAL_KEYS` from `utils/humanity-signals`) ride alongside in\n * the same POST body (read raw by the host's bot gate BEFORE parsing) and are\n * stripped server-side before anything reaches HubSpot. zod's default\n * unknown-key stripping means the parsed output never contains them.\n *\n * `timezone`/`locale` are POST-only presentation fields (the invite renders\n * in the visitor's local time) — this schema is the ONLY place a\n * client-supplied zone is accepted; the availability path is UTC-pinned.\n */\nfunction buildBookingSchema<TStart extends z.ZodTypeAny, TDuration extends z.ZodTypeAny, TZone extends z.ZodTypeAny>(\n  formFields: MeetingFormField[],\n  legalConsent: MeetingLegalConsent | null,\n  // Passed IN rather than switched on a boolean: a `deferSlot: boolean`\n  // parameter cannot be narrowed at type level, so every ternary inside would\n  // infer a union and widen `startTimeMs`/`durationMs` to `number | null |\n  // undefined` for BOTH schemas — including the server's wire type.\n  slot: { startTimeMs: TStart; durationMs: TDuration; timezone: TZone },\n) {\n  const answers: Record<string, z.ZodTypeAny> = {};\n  for (const field of formFields) {\n    if (!isSupportedFormField(field)) continue; // unsupported types are fail-closed at render time\n    const spec: FormFieldTypeSpec = FORM_FIELD_TYPES[field.type];\n    // required/optional wrapping follows the answer's wire KIND, not its type:\n    // a boolean is true-or-absent, a string is non-empty-or-empty. For strings\n    // the required check is the FIRST rule on the chain, so an empty answer\n    // reports \"is required\" rather than the type's own message.\n    let validator: z.ZodTypeAny;\n    if (spec.kind === 'boolean') {\n      validator = field.required ? z.literal(true, { message: `${field.label} is required` }) : z.boolean().optional();\n    } else {\n      const base = field.required ? z.string().min(1, { message: `${field.label} is required` }) : z.string();\n      validator = spec.validator(field, base);\n      if (!field.required) validator = validator.optional().or(z.literal(''));\n    }\n    answers[field.name] = validator;\n  }\n\n  const requiredConsentIds = (legalConsent?.communicationConsentCheckboxes ?? [])\n    .filter(c => c.required)\n    .map(c => c.communicationTypeId);\n\n  // A required answer cannot be enforced by an `.optional()` parent object —\n  // omitting the `formFields` key entirely would skip every per-question\n  // rule. When the link declares at least one required supported question,\n  // the object itself is required.\n  const hasRequiredAnswers = formFields.some(f => isSupportedFormField(f) && f.required);\n  const answersObject = z.object(answers);\n\n  return (\n    z\n      .object({\n        meetingId: z.string().min(1),\n        startTimeMs: slot.startTimeMs,\n        durationMs: slot.durationMs,\n        // The identity trio's rules come from BUILT_IN_BOOKING_FIELDS — the\n        // keys stay literal so the payload type keeps its named properties.\n        firstName: identityValidator('firstName'),\n        lastName: identityValidator('lastName'),\n        email: identityValidator('email'),\n        timezone: slot.timezone,\n        locale: z.string().refine(isValidBcp47Locale, { message: 'Invalid locale' }).optional(),\n        // Plain `.optional()` (no `.default()`) so zod's input and output types\n        // match — react-hook-form's zodResolver needs them identical, and the\n        // server handles `undefined` explicitly anyway. REQUIRED when the link\n        // declares required questions (see `hasRequiredAnswers` above).\n        formFields: hasRequiredAnswers ? answersObject : answersObject.optional(),\n        legalConsentResponses: z\n          .array(z.object({ communicationTypeId: z.string(), consented: z.boolean() }))\n          .optional(),\n      })\n      // Object-level rule: `.refine` on an `.optional()` field is skipped when\n      // the field is absent — required consents must reject even on a payload\n      // that omits the array entirely.\n      .superRefine((data, ctx) => {\n        const responses = data.legalConsentResponses ?? [];\n        const ok = requiredConsentIds.every(id =>\n          responses.some(r => r.communicationTypeId === id && r.consented === true),\n        );\n        if (!ok) {\n          ctx.addIssue({\n            code: z.ZodIssueCode.custom,\n            path: ['legalConsentResponses'],\n            message: 'Required consent checkboxes must be accepted',\n          });\n        }\n      })\n  );\n}\n\n/** The slot's wire validators — ONE declaration for the strict and the deferred schema. */\nconst SLOT_INSTANT = z.number().int().positive();\nconst TIMEZONE = z.string().refine(isValidIanaTimezone, { message: 'Invalid timezone' });\n\n/**\n * The STRICT schema — the wire contract. The server rebuilds it from the link's\n * own fetched metadata and validates every booking against it; a slot and a\n * duration are not optional on anything that reaches HubSpot.\n */\nexport function makeBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n  return buildBookingSchema(formFields, legalConsent, {\n    startTimeMs: SLOT_INSTANT,\n    durationMs: SLOT_INSTANT,\n    timezone: TIMEZONE,\n  });\n}\n\n/**\n * The DEFERRED schema — same fields, with the slot/duration/timezone triple\n * relaxed. `flow=\"details-first\"` collects answers BEFORE a slot exists, so the\n * form validates against this and the parent re-attaches the authoritative\n * values at POST time.\n *\n * Two named exports over one private builder rather than an overload or an\n * options flag: `MeetingBookingPayload` is `z.infer<ReturnType<...>>`, and\n * either of those alternatives would widen `startTimeMs`/`durationMs` to\n * `number | undefined` for EVERY consumer — including the server's\n * `durationsMs.includes(data.durationMs)`.\n */\nexport function makeDeferredBookingSchema(formFields: MeetingFormField[], legalConsent: MeetingLegalConsent | null) {\n  // `.nullish()`, not `.optional()`: the widget passes these straight into\n  // `defaultValues`, and details-first has `timezone === null` on the server\n  // render and the first client render (zone resolution is post-hydration).\n  // Accepting only `undefined` would reject a field with no rendered control\n  // and no FieldWrapper — the submit button silently dead, type-check clean.\n  return buildBookingSchema(formFields, legalConsent, {\n    startTimeMs: SLOT_INSTANT.nullish(),\n    durationMs: SLOT_INSTANT.nullish(),\n    timezone: TIMEZONE.nullish(),\n  });\n}\n\n/** The wire payload. Pinned to the STRICT builder — see above. */\nexport type MeetingBookingPayload = z.infer<ReturnType<typeof makeBookingSchema>>;\n\n/**\n * The form's value type, used as the `useForm` generic in BOTH flows: a relaxed\n * resolver is not assignable to `Resolver<MeetingBookingPayload>`, and mixing\n * the two survives only on TS's bivariant method-parameter check.\n */\nexport type BookingFormValues = z.infer<ReturnType<typeof makeDeferredBookingSchema>>;\n"]}