@aranova/tracking-react 0.16.2 → 0.17.1
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/README.md +27 -0
- package/dist/index.d.mts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.js +111 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +111 -45
- package/dist/index.mjs.map +1 -1
- package/dist/phone.js +52 -43
- package/dist/phone.js.map +1 -1
- package/dist/phone.mjs +52 -43
- package/dist/phone.mjs.map +1 -1
- package/dist/{sales-Bq7H-Vym.d.mts → sales-C3jFBx08.d.mts} +10 -0
- package/dist/{sales-Bq7H-Vym.d.ts → sales-C3jFBx08.d.ts} +10 -0
- package/dist/sales.d.mts +1 -1
- package/dist/sales.d.ts +1 -1
- package/dist/sales.js +7 -2
- package/dist/sales.js.map +1 -1
- package/dist/sales.mjs +7 -2
- package/dist/sales.mjs.map +1 -1
- package/package.json +1 -1
package/dist/phone.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../tracking-core/src/session.ts","../../tracking-core/src/events/page-view.ts","../../tracking-core/src/events/cta-click.ts","../../tracking-core/src/events/sdk-heartbeat.ts","../../tracking-core/src/events/form-start.ts","../../tracking-core/src/events/form-submit.ts","../../tracking-core/src/events/multi-page-session.ts","../../tracking-core/src/events/page-exit.ts","../../tracking-core/src/events/phone-click.ts","../../tracking-core/src/events/scroll-depth.ts","../../tracking-core/src/events/specific-page-visit.ts","../../tracking-core/src/events/time-on-site.ts","../../tracking-core/src/events/registry.ts","../../tracking-core/src/resources/sales/schema.ts","../../tracking-core/src/phone.ts","../../tracking-core/src/phone-field.ts"],"sourcesContent":["// Visitor + session identity for the tracking SDK.\n//\n// Visitor: persistent localStorage UUID, never expires until the user clears\n// browser storage. Used for cross-session correlation.\n//\n// Session: rolling 30-minute idle window. Regenerated when more than\n// SESSION_IDLE_MS has passed since the last event. Matches the behavior of\n// GA4, PostHog, Mixpanel, etc., so analytics is comparable.\n\nimport { clearLandingRecord } from \"./landing\";\n\n/**\n * localStorage key for the persistent visitor id.\n */\nexport const VISITOR_STORAGE_KEY = \"aranova_tracking_visitor\";\n\n/**\n * localStorage key for the rolling session id state.\n */\nexport const SESSION_STORAGE_KEY = \"aranova_tracking_session\";\n\n/**\n * Idle window before a new session id is created.\n */\nexport const SESSION_IDLE_MS = 30 * 60 * 1000;\n\n/**\n * Serialized session state stored in localStorage.\n */\nexport interface StoredSession {\n /** Client-generated session UUID. */\n id: string;\n /** Unix timestamp in milliseconds for the most recent event/session touch. */\n last_event_at: number;\n}\n\nfunction safeUuid(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\")\n return crypto.randomUUID();\n // Fallback for ancient browsers — not cryptographically perfect but unique enough.\n return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;\n}\n\nfunction readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Storage may be denied (private mode, blocked cookies, quota). Caller is\n // responsible for degrading gracefully.\n }\n}\n\n/**\n * Return the persistent visitor id for this browser profile.\n *\n * Creates and stores a new id when one does not already exist. During SSR,\n * returns an ephemeral id because browser storage is unavailable.\n */\nexport function getVisitorId(): string {\n if (typeof window === \"undefined\") return safeUuid();\n\n const existing = readLocalStorage(VISITOR_STORAGE_KEY);\n if (existing && existing.length > 0) return existing;\n\n const fresh = safeUuid();\n writeLocalStorage(VISITOR_STORAGE_KEY, fresh);\n return fresh;\n}\n\n/**\n * Result from `getOrRotateSessionId()`.\n */\nexport interface SessionIdResult {\n /** Current session id. */\n id: string;\n /** Whether this call created a new session. */\n isNew: boolean;\n}\n\n/**\n * Return the current session id, rotating it after the idle window expires.\n *\n * Also refreshes `last_event_at` for active sessions.\n */\nexport function getOrRotateSessionId(now: number = Date.now()): SessionIdResult {\n if (typeof window === \"undefined\") return { id: safeUuid(), isNew: true };\n\n const raw = readLocalStorage(SESSION_STORAGE_KEY);\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredSession>;\n if (typeof parsed.id === \"string\" && typeof parsed.last_event_at === \"number\") {\n if (now - parsed.last_event_at <= SESSION_IDLE_MS) {\n const refreshed: StoredSession = { id: parsed.id, last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(refreshed));\n return { id: parsed.id, isNew: false };\n }\n }\n } catch {\n // Fall through to a fresh session.\n }\n }\n\n const fresh: StoredSession = { id: safeUuid(), last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));\n return { id: fresh.id, isNew: true };\n}\n\n/**\n * Clear visitor and session identity from localStorage, including the\n * session-scoped landing record (a landing must never outlive its session).\n *\n * Intended for tests, debugging, and explicit user reset flows.\n */\nexport function resetTrackingIdentity(): void {\n clearLandingRecord();\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(VISITOR_STORAGE_KEY);\n window.localStorage.removeItem(SESSION_STORAGE_KEY);\n } catch {\n // ignored\n }\n}\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `page_view` event.\n *\n * The SDK emits this on initial load, SPA route changes, and bfcache restores.\n * Consumers do not call `trackEvent('page_view', ...)`; registering\n * `automatic: { page_view: {} }` enables the SDK-owned trigger.\n */\nexport const pageViewMetadataSchema = z\n .object({\n page: z\n .object({\n title: z.string().nullable(),\n path: z.string(),\n search: z.string(),\n hash: z.string(),\n })\n .strict(),\n referrer: z.string().nullable(),\n // `.nullable().optional()` — absent (undefined) OR explicit null OR a\n // real viewport object. Mirrors Pydantic's `_Viewport | None = None`\n // on the backend side so the drift test stays clean.\n viewport: z\n .object({\n w: z.number(),\n h: z.number(),\n })\n .strict()\n .nullable()\n .optional(),\n })\n .strict();\n\nexport type PageViewMetadata = z.infer<typeof pageViewMetadataSchema>;\n\n/**\n * Registration config for automatic `page_view`.\n *\n * `page_view` is required in every trigger registry and currently has no\n * options. Use `{ page_view: {} }`.\n */\nexport const pageViewConfigSchema = z.object({}).strict();\nexport type PageViewConfig = z.infer<typeof pageViewConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for a manually fired `cta_click` event.\n *\n * Use this for non-phone calls to action such as directions, appointment\n * buttons, downloads, or external booking links.\n */\nexport const ctaClickMetadataSchema = z\n .object({\n cta_name: z.string(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n section: z.string().nullable().optional(),\n destination_url: z.string().nullable().optional(),\n // Set by auto-capture (and available to manual callers): the link target\n // and a short element descriptor (tag#id) for tying clicks to specific UI.\n href: z.string().nullable().optional(),\n element: z.string().nullable().optional(),\n })\n .strict();\n\nexport type CtaClickMetadata = z.infer<typeof ctaClickMetadataSchema>;\n\n/**\n * Registration config for `cta_click`.\n *\n * The event stays manually fireable; `autoCapture` additionally attaches a\n * delegated click listener that fires it for any element matching `selector`\n * (default `[data-aranova-cta]`) — tag your CTAs, get analytics for free.\n */\nexport const ctaClickConfigSchema = z\n .object({\n autoCapture: z\n .object({\n selector: z.string().optional(),\n })\n .strict()\n .optional(),\n })\n .strict();\nexport type CtaClickConfig = z.infer<typeof ctaClickConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Registered trigger names reported by the SDK heartbeat.\n */\nexport const sdkHeartbeatTriggersSchema = z\n .object({\n automatic: z.array(z.string()),\n manual: z.array(z.string()),\n })\n .strict();\n\n/**\n * Metadata for the SDK-internal `sdk_heartbeat` event.\n *\n * The SDK fires this once per new session so the dashboard can show which SDK\n * version, install surface, and trigger registry a client site is running.\n * Consumers do not manually register or fire this event.\n */\nexport const sdkHeartbeatMetadataSchema = z\n .object({\n sdk_version: z.string(),\n package_name: z.string().nullable(),\n surface: z.enum([\"next\", \"react\", \"script\"]),\n triggers: sdkHeartbeatTriggersSchema,\n trigger_config: z.record(z.string(), z.record(z.string(), z.unknown())).nullable().optional(),\n configured_gtag_ids: z.record(z.string(), z.string()).nullable().optional(),\n })\n .strict();\n\nexport type SdkHeartbeatMetadata = z.infer<typeof sdkHeartbeatMetadataSchema>;\n\n/**\n * Internal registration config for `sdk_heartbeat`.\n *\n * This event has no consumer-facing options.\n */\nexport const sdkHeartbeatConfigSchema = z.object({}).strict();\nexport type SdkHeartbeatConfig = z.infer<typeof sdkHeartbeatConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `form_start` event.\n *\n * The SDK emits this once per form when the visitor first focuses a field.\n */\nexport const formStartMetadataSchema = z\n .object({\n form: z\n .object({\n id: z.string(),\n action: z.string().nullable(),\n })\n .strict(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type FormStartMetadata = z.infer<typeof formStartMetadataSchema>;\n\n/**\n * Registration config for automatic `form_start`.\n *\n * Use `selector` to narrow which forms can trigger the event. When omitted,\n * the SDK observes all `<form>` elements.\n */\nexport const formStartConfigSchema = z\n .object({\n selector: z.string().optional(),\n })\n .strict();\n\nexport type FormStartConfig = z.infer<typeof formStartConfigSchema>;\n","import { z } from \"zod\";\n\n// form_submit is manual — the SDK never auto-fires this. Consumer code calls\n// `tracking.trackEvent('form_submit', { form, page })` from their own submit\n// handler. Registering it enables the type-level permission; omitting it\n// turns manual calls into a compile error.\n//\n// The optional `fields` array captures submitted form field metadata and JSON\n// values. Consumers explicitly build the fields array themselves so they\n// control exactly what is sent.\n\n/**\n * JSON-serializable value accepted by `form_submit.fields[].value`.\n *\n * This intentionally excludes `undefined`, functions, symbols, `Date`\n * instances, and non-finite numbers. Values are stored in PostgreSQL JSONB, so\n * consumers should send only data that has a stable JSON representation.\n */\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\nexport const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>\n z.union([\n z.string(),\n z.number().finite(),\n z.boolean(),\n z.null(),\n z.array(jsonValueSchema),\n z.record(jsonValueSchema),\n ]),\n);\n\n/**\n * Metadata for a manually fired `form_submit` event.\n *\n * Register the event with `manual: { form_submit: {} }`, then call\n * `trackEvent('form_submit', metadata)` from the host site's submit handler.\n *\n * `fields` is optional. If present, each field value must be JSON-serializable\n * and should be explicitly allowlisted by the integration. Do not send names,\n * emails, visitor phone numbers, addresses, payment data, medical details,\n * passwords, file contents, or free-text messages.\n *\n * @example\n * ```ts\n * tracking.trackEvent('form_submit', {\n * form: {\n * id: 'lead-form',\n * action: '/api/lead',\n * fields: [\n * {\n * name: 'service_interest',\n * type: 'select',\n * label: 'Service interest',\n * value: 'teeth_whitening',\n * },\n * ],\n * },\n * page: { path: window.location.pathname },\n * });\n * ```\n */\nexport const formSubmitMetadataSchema = z\n .object({\n form: z\n .object({\n id: z.string(),\n action: z.string().nullable(),\n fields: z\n .array(\n z\n .object({\n name: z.string(),\n type: z.string(),\n label: z.string().nullable(),\n value: jsonValueSchema,\n })\n .strict(),\n )\n .optional(),\n })\n .strict(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type FormSubmitMetadata = z.infer<typeof formSubmitMetadataSchema>;\n\n/**\n * Registration config for `form_submit`.\n *\n * This event is manual-only and currently has no registration options. The\n * empty object enables typed `trackEvent('form_submit', ...)` calls.\n */\nexport const formSubmitConfigSchema = z.object({}).strict();\nexport type FormSubmitConfig = z.infer<typeof formSubmitConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `multi_page_session` event.\n *\n * Fired when the visitor reaches the configured distinct-page threshold in a\n * single tracking session.\n */\nexport const multiPageSessionMetadataSchema = z\n .object({\n page_count: z.number().int().min(2),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type MultiPageSessionMetadata = z.infer<typeof multiPageSessionMetadataSchema>;\n\n/**\n * Registration config for automatic `multi_page_session`.\n */\nexport const multiPageSessionConfigSchema = z\n .object({\n pageThreshold: z.number().int().min(2),\n })\n .strict();\n\nexport type MultiPageSessionConfig = z.infer<typeof multiPageSessionConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `page_exit` event.\n *\n * Fired when the user leaves a page (SPA navigation away, tab hidden, or\n * pagehide). `dwell_ms` is the ACTIVE (visible) time spent on the page segment\n * being closed — hidden time never counts, matching `time_on_site` semantics.\n * A page revisited after being hidden emits another `page_exit` for the next\n * visible segment, so summing `dwell_ms` per page/session yields total active\n * dwell without double counting.\n */\nexport const pageExitMetadataSchema = z\n .object({\n dwell_ms: z.number().int().min(0),\n // null = left without any scroll signal; floor is 0 so a valid 0% is never\n // rejected (a single bad field 422s the whole keepalive beacon batch).\n max_scroll_percent: z.number().int().min(0).max(100).nullable(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type PageExitMetadata = z.infer<typeof pageExitMetadataSchema>;\n\n/**\n * Registration config for automatic `page_exit`.\n *\n * SDK-internal: attached unconditionally (like `sdk_heartbeat`), so there are\n * no registration options.\n */\nexport const pageExitConfigSchema = z.object({}).strict();\nexport type PageExitConfig = z.infer<typeof pageExitConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for a manually fired `phone_click` event.\n *\n * `phone_number` should be the business phone number from the clicked `tel:`\n * link, not a visitor-entered phone number. `section` can distinguish header,\n * footer, hero, or contact-page links.\n */\nexport const phoneClickMetadataSchema = z\n .object({\n phone_number: z.string(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n section: z.string().nullable().optional(),\n })\n .strict();\n\nexport type PhoneClickMetadata = z.infer<typeof phoneClickMetadataSchema>;\n\n/**\n * Registration config for `phone_click`.\n *\n * This event is manual-only and currently has no registration options.\n */\nexport const phoneClickConfigSchema = z.object({}).strict();\nexport type PhoneClickConfig = z.infer<typeof phoneClickConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `scroll_depth` event.\n *\n * Fired once per configured threshold per page.\n */\nexport const scrollDepthMetadataSchema = z\n .object({\n depth_percent: z.number().int().min(1).max(100),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type ScrollDepthMetadata = z.infer<typeof scrollDepthMetadataSchema>;\n\n/**\n * Registration config for automatic `scroll_depth`.\n *\n * `thresholds` are integer percentages from 1 to 100.\n */\nexport const scrollDepthConfigSchema = z\n .object({\n thresholds: z.array(z.number().int().min(1).max(100)).min(1),\n })\n .strict();\n\nexport type ScrollDepthConfig = z.infer<typeof scrollDepthConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Canonical page intent names supported by `specific_page_visit`.\n */\nexport const SPECIFIC_PAGE_NAMES = [\n \"contact_page\",\n \"about_page\",\n \"services_page\",\n \"booking_page\",\n \"location_page\",\n \"pricing_page\",\n \"faq_page\",\n \"testimonials_page\",\n] as const;\n\nexport type SpecificPageName = (typeof SPECIFIC_PAGE_NAMES)[number];\n\nexport const specificPageNameSchema = z.enum(SPECIFIC_PAGE_NAMES);\n\n/**\n * Metadata for the automatic `specific_page_visit` event.\n *\n * The SDK emits this when the current pathname matches one of the configured\n * named page patterns.\n */\nexport const specificPageVisitMetadataSchema = z\n .object({\n page_name: specificPageNameSchema,\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type SpecificPageVisitMetadata = z.infer<typeof specificPageVisitMetadataSchema>;\n\n/**\n * Registration config for automatic `specific_page_visit`.\n *\n * Each page entry pairs a semantic `name` with a `RegExp` that matches the\n * pathname. Use this instead of hard-coding path regexes downstream.\n */\nexport const specificPageVisitConfigSchema = z\n .object({\n pages: z\n .array(\n z\n .object({\n name: specificPageNameSchema,\n pathPattern: z.custom<RegExp>((value) => value instanceof RegExp, {\n message: \"pathPattern must be a RegExp\",\n }),\n })\n .strict(),\n )\n .min(1),\n })\n .strict();\n\nexport type SpecificPageVisitConfig = z.infer<typeof specificPageVisitConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `time_on_site` event.\n *\n * The SDK starts a visibility-aware timer and fires once when visible\n * engagement crosses the configured threshold.\n */\nexport const timeOnSiteMetadataSchema = z\n .object({\n duration_ms: z.number().int().nonnegative(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type TimeOnSiteMetadata = z.infer<typeof timeOnSiteMetadataSchema>;\n\n/**\n * Registration config for automatic `time_on_site`.\n */\nexport const timeOnSiteConfigSchema = z\n .object({\n thresholdSeconds: z.number().int().positive(),\n })\n .strict();\nexport type TimeOnSiteConfig = z.infer<typeof timeOnSiteConfigSchema>;\n","import type { z } from \"zod\";\n\nimport {\n ctaClickConfigSchema,\n ctaClickMetadataSchema,\n type CtaClickConfig,\n type CtaClickMetadata,\n} from \"./cta-click\";\nimport {\n sdkHeartbeatConfigSchema,\n sdkHeartbeatMetadataSchema,\n type SdkHeartbeatConfig,\n type SdkHeartbeatMetadata,\n} from \"./sdk-heartbeat\";\nimport {\n formStartConfigSchema,\n formStartMetadataSchema,\n type FormStartConfig,\n type FormStartMetadata,\n} from \"./form-start\";\nimport {\n formSubmitConfigSchema,\n formSubmitMetadataSchema,\n type FormSubmitConfig,\n type FormSubmitMetadata,\n} from \"./form-submit\";\nimport {\n multiPageSessionConfigSchema,\n multiPageSessionMetadataSchema,\n type MultiPageSessionConfig,\n type MultiPageSessionMetadata,\n} from \"./multi-page-session\";\nimport {\n pageExitConfigSchema,\n pageExitMetadataSchema,\n type PageExitConfig,\n type PageExitMetadata,\n} from \"./page-exit\";\nimport {\n pageViewConfigSchema,\n pageViewMetadataSchema,\n type PageViewConfig,\n type PageViewMetadata,\n} from \"./page-view\";\nimport {\n phoneClickConfigSchema,\n phoneClickMetadataSchema,\n type PhoneClickConfig,\n type PhoneClickMetadata,\n} from \"./phone-click\";\nimport {\n scrollDepthConfigSchema,\n scrollDepthMetadataSchema,\n type ScrollDepthConfig,\n type ScrollDepthMetadata,\n} from \"./scroll-depth\";\nimport {\n specificPageVisitConfigSchema,\n specificPageVisitMetadataSchema,\n type SpecificPageVisitConfig,\n type SpecificPageVisitMetadata,\n} from \"./specific-page-visit\";\nimport {\n timeOnSiteConfigSchema,\n timeOnSiteMetadataSchema,\n type TimeOnSiteConfig,\n type TimeOnSiteMetadata,\n} from \"./time-on-site\";\n\n// Event kind — automatic events are fired by the SDK itself when their\n// client-side signal fires (page_view on navigation, time_on_site on timer,\n// etc). Manual events are only fireable via explicit consumer code.\nexport type EventKind = \"automatic\" | \"manual\";\n\n// The registry is the single client-side source of truth for \"what events\n// exist, what shape does their metadata take, and what kind are they\". The\n// backend mirror lives in apps/api/src/schemas/tracking_events.py and is\n// kept in sync via the drift test in apps/api/tests/test_event_schema_drift.py.\nexport const EVENT_REGISTRY = {\n // --- automatic triggers ---\n page_view: {\n kind: \"automatic\",\n metadataSchema: pageViewMetadataSchema,\n configSchema: pageViewConfigSchema,\n },\n time_on_site: {\n kind: \"automatic\",\n metadataSchema: timeOnSiteMetadataSchema,\n configSchema: timeOnSiteConfigSchema,\n },\n specific_page_visit: {\n kind: \"automatic\",\n metadataSchema: specificPageVisitMetadataSchema,\n configSchema: specificPageVisitConfigSchema,\n },\n scroll_depth: {\n kind: \"automatic\",\n metadataSchema: scrollDepthMetadataSchema,\n configSchema: scrollDepthConfigSchema,\n },\n multi_page_session: {\n kind: \"automatic\",\n metadataSchema: multiPageSessionMetadataSchema,\n configSchema: multiPageSessionConfigSchema,\n },\n form_start: {\n kind: \"automatic\",\n metadataSchema: formStartMetadataSchema,\n configSchema: formStartConfigSchema,\n },\n // --- SDK-internal automatic (not consumer-configurable) ---\n sdk_heartbeat: {\n kind: \"automatic\",\n metadataSchema: sdkHeartbeatMetadataSchema,\n configSchema: sdkHeartbeatConfigSchema,\n },\n page_exit: {\n kind: \"automatic\",\n metadataSchema: pageExitMetadataSchema,\n configSchema: pageExitConfigSchema,\n },\n // --- manual triggers ---\n form_submit: {\n kind: \"manual\",\n metadataSchema: formSubmitMetadataSchema,\n configSchema: formSubmitConfigSchema,\n },\n phone_click: {\n kind: \"manual\",\n metadataSchema: phoneClickMetadataSchema,\n configSchema: phoneClickConfigSchema,\n },\n cta_click: {\n kind: \"manual\",\n metadataSchema: ctaClickMetadataSchema,\n configSchema: ctaClickConfigSchema,\n },\n} as const;\n\n/**\n * Name of any event known to the tracking SDK.\n */\nexport type EventName = keyof typeof EVENT_REGISTRY;\n\n/**\n * Event names that are fired by the SDK when their configured signal occurs.\n *\n * Automatic events are not accepted by the typed `trackEvent()` API.\n */\nexport type AutomaticEventName = {\n [K in EventName]: (typeof EVENT_REGISTRY)[K][\"kind\"] extends \"automatic\" ? K : never;\n}[EventName];\n\n/**\n * Event names that consumer code can fire manually after registering them.\n */\nexport type ManualEventName = {\n [K in EventName]: (typeof EVENT_REGISTRY)[K][\"kind\"] extends \"manual\" ? K : never;\n}[EventName];\n\n// Map an event name to its explicit metadata / config TS type. We pull the\n// inferred types directly from the per-event files rather than deriving\n// via `z.infer<typeof EVENT_REGISTRY[K]['metadataSchema']>` so that error\n// messages name the event-specific type (PageViewMetadata, not an\n// anonymous zod inference).\ntype MetadataByName = {\n page_view: PageViewMetadata;\n time_on_site: TimeOnSiteMetadata;\n specific_page_visit: SpecificPageVisitMetadata;\n scroll_depth: ScrollDepthMetadata;\n multi_page_session: MultiPageSessionMetadata;\n form_start: FormStartMetadata;\n sdk_heartbeat: SdkHeartbeatMetadata;\n page_exit: PageExitMetadata;\n form_submit: FormSubmitMetadata;\n phone_click: PhoneClickMetadata;\n cta_click: CtaClickMetadata;\n};\n\ntype ConfigByName = {\n page_view: PageViewConfig;\n time_on_site: TimeOnSiteConfig;\n specific_page_visit: SpecificPageVisitConfig;\n scroll_depth: ScrollDepthConfig;\n multi_page_session: MultiPageSessionConfig;\n form_start: FormStartConfig;\n sdk_heartbeat: SdkHeartbeatConfig;\n page_exit: PageExitConfig;\n form_submit: FormSubmitConfig;\n phone_click: PhoneClickConfig;\n cta_click: CtaClickConfig;\n};\n\n/**\n * Metadata payload type for a specific tracking event.\n *\n * @example\n * ```ts\n * type SubmitMetadata = EventMetadata<'form_submit'>;\n * ```\n */\nexport type EventMetadata<K extends EventName> = MetadataByName[K];\n\n/**\n * Trigger registration config type for a specific tracking event.\n */\nexport type EventConfig<K extends EventName> = ConfigByName[K];\n\n// Runtime constant arrays for iteration at consumer / factory time.\n/**\n * Runtime list of automatic event names.\n */\nexport const ALL_AUTOMATIC_EVENT_NAMES: readonly AutomaticEventName[] = (\n Object.entries(EVENT_REGISTRY) as Array<[EventName, (typeof EVENT_REGISTRY)[EventName]]>\n)\n .filter(([, def]) => def.kind === \"automatic\")\n .map(([name]) => name) as AutomaticEventName[];\n\n/**\n * Runtime list of manual event names.\n */\nexport const ALL_MANUAL_EVENT_NAMES: readonly ManualEventName[] = (\n Object.entries(EVENT_REGISTRY) as Array<[EventName, (typeof EVENT_REGISTRY)[EventName]]>\n)\n .filter(([, def]) => def.kind === \"manual\")\n .map(([name]) => name) as ManualEventName[];\n\n/**\n * Trigger registry passed to `createTracking({ triggers })`.\n *\n * `automatic.page_view` is required because every install should capture page\n * views. Other automatic events are opt-in. Manual events must be registered\n * here before the typed client accepts `trackEvent()` calls for them.\n *\n * @example\n * ```ts\n * createTracking({\n * apiKey,\n * endpoint,\n * triggers: {\n * automatic: {\n * page_view: {},\n * time_on_site: { thresholdSeconds: 60 },\n * },\n * manual: {\n * form_submit: {},\n * phone_click: {},\n * },\n * },\n * });\n * ```\n */\nexport type TriggerRegistryConfig = {\n automatic: {\n page_view: EventConfig<\"page_view\">;\n } & Partial<{\n time_on_site: EventConfig<\"time_on_site\">;\n specific_page_visit: EventConfig<\"specific_page_visit\">;\n scroll_depth: EventConfig<\"scroll_depth\">;\n multi_page_session: EventConfig<\"multi_page_session\">;\n form_start: EventConfig<\"form_start\">;\n }>;\n manual?: Partial<{\n form_submit: EventConfig<\"form_submit\">;\n phone_click: EventConfig<\"phone_click\">;\n cta_click: EventConfig<\"cta_click\">;\n }>;\n};\n\n/**\n * Manual event names registered in a concrete trigger registry.\n *\n * Used by `TypedTrackingClient` so `trackEvent()` only accepts events the\n * consumer explicitly enabled.\n */\nexport type RegisteredManualEvents<TRegistry extends TriggerRegistryConfig> = Extract<\n keyof NonNullable<TRegistry[\"manual\"]>,\n ManualEventName\n>;\n\n/**\n * Automatic event names registered in a concrete trigger registry.\n */\nexport type RegisteredAutomaticEvents<TRegistry extends TriggerRegistryConfig> = Extract<\n keyof TRegistry[\"automatic\"],\n AutomaticEventName\n>;\n\n/**\n * Discriminated union of valid manual tracking calls for a registry.\n */\nexport type TrackableEvent<TRegistry extends TriggerRegistryConfig> = {\n [K in RegisteredManualEvents<TRegistry>]: {\n eventType: K;\n metadata: EventMetadata<K>;\n };\n}[RegisteredManualEvents<TRegistry>];\n\n// Runtime helper: look up a schema pair by name. Cast through `unknown`\n// because the registry is `as const` and TS loses the specific schema type\n// when indexing via a dynamic key.\nexport function getEventDefinition(name: EventName): {\n kind: EventKind;\n metadataSchema: z.ZodTypeAny;\n configSchema: z.ZodTypeAny;\n} {\n return EVENT_REGISTRY[name] as unknown as {\n kind: EventKind;\n metadataSchema: z.ZodTypeAny;\n configSchema: z.ZodTypeAny;\n };\n}\n","import { z } from \"zod\";\n\n/**\n * Sales / Conversions wire schemas — the client-side source of truth.\n *\n * `saleCreateSchema` is mirrored by `SaleCreateSchema` in\n * `apps/api/src/schemas/tracking_sales.py` and enforced by the backend drift\n * test (the `resources` section of `events.schema.json`). Keep them in lockstep.\n *\n * Money is **integer minor units (cents)**; `quantity` is a decimal string;\n * `currency` is the required `SupportedCurrency` enum.\n */\n\nexport const SUPPORTED_CURRENCIES = [\"USD\", \"CAD\"] as const;\nexport type SupportedCurrency = (typeof SUPPORTED_CURRENCIES)[number];\n\nconst TRACKING_ENVIRONMENTS = [\"production\", \"development\"] as const;\n\nconst currencySchema = z.enum(SUPPORTED_CURRENCIES);\nconst centsSchema = z.number().int().nonnegative();\nconst quantitySchema = z.string().regex(/^\\d+(\\.\\d{1,3})?$/);\n// Free-form JSON object — the sale's extensibility escape hatch. Mirrors the\n// Pydantic `dict[str, Any]` (`additionalProperties: true`) on the wire.\nconst metadataSchema = z.record(z.unknown());\n\nexport const saleItemSchema = z\n .object({\n external_item_id: z.string().nullable().optional(),\n name: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n quantity: quantitySchema,\n unit_price_cents: centsSchema,\n // Non-negativity validated on the wire — same contract as the other cents\n // fields — and backstopped by the DB CHECK.\n unit_cost_cents: centsSchema.nullable().optional(),\n })\n .strict();\n\n// One service covered by a sale, at its own price. `service` is the per-business\n// service key, validated against the taxonomy by the backend at write time.\nexport const saleServiceSchema = z\n .object({\n service: z.string(),\n amount_cents: centsSchema,\n })\n .strict();\n\n// Raw customer identity. Stored as-is on the backend for human display in\n// dashboards; hashing happens at writeback-upload time in the future Google\n// Ads reconciliation worker (see docs/tracking-package/conversion-writeback.md).\n// The SDK never hashes — pass values straight through.\nconst customerNameSchema = z.string().max(200);\nconst customerPhoneSchema = z.string().max(64);\nconst customerEmailSchema = z.string().max(320).email();\n\n// The singular `service` (priced by `amount_total_cents`) and the plural\n// `services` (each priced individually) are mutually exclusive. When `services`\n// is present, `amount_total_cents` is optional and the backend derives it as the\n// sum; otherwise it is required. Shared by create + update.\nfunction refineServiceXor(\n val: {\n service?: string | null;\n services?: ReadonlyArray<{ service: string; amount_cents: number }> | null;\n amount_total_cents?: number | null;\n },\n ctx: z.RefinementCtx,\n { requireAmount }: { requireAmount: boolean },\n): void {\n if (val.services != null) {\n if (val.service != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"pass either `service` or `services`, not both\",\n path: [\"services\"],\n });\n }\n if (val.services.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"`services` must not be empty\",\n path: [\"services\"],\n });\n }\n const keys = val.services.map((s) => s.service);\n if (new Set(keys).size !== keys.length) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"`services` must not list the same service more than once\",\n path: [\"services\"],\n });\n }\n if (val.amount_total_cents != null) {\n const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);\n if (val.amount_total_cents !== sum) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n \"amount_total_cents must equal the sum of the services amounts \" +\n \"(omit it to derive it automatically)\",\n path: [\"amount_total_cents\"],\n });\n }\n }\n } else if (requireAmount && val.amount_total_cents == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"amount_total_cents is required unless `services` is provided\",\n path: [\"amount_total_cents\"],\n });\n }\n}\n\nexport const saleCreateSchema = z\n .object({\n external_id: z.string().nullable().optional(),\n description: z.string().nullable().optional(),\n service: z.string().nullable().optional(),\n services: z.array(saleServiceSchema).nullable().optional(),\n currency: currencySchema,\n // Optional only because the plural `services` form derives it from the sum\n // (see refineServiceXor); the singular/serviceless path still requires it.\n amount_total_cents: centsSchema.nullable().optional(),\n occurred_at: z.string().datetime(),\n environment: z.enum(TRACKING_ENVIRONMENTS).default(\"production\"),\n items: z.array(saleItemSchema).default([]),\n metadata: metadataSchema.nullable().optional(),\n customer_name: customerNameSchema.nullable().optional(),\n customer_phone: customerPhoneSchema.nullable().optional(),\n customer_email: customerEmailSchema.nullable().optional(),\n })\n .strict()\n .superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));\n\nexport const saleUpdateSchema = z\n .object({\n description: z.string().nullable().optional(),\n service: z.string().nullable().optional(),\n services: z.array(saleServiceSchema).nullable().optional(),\n currency: currencySchema.optional(),\n amount_total_cents: centsSchema.optional(),\n occurred_at: z.string().datetime().optional(),\n items: z.array(saleItemSchema).optional(),\n metadata: metadataSchema.nullable().optional(),\n customer_name: customerNameSchema.nullable().optional(),\n customer_phone: customerPhoneSchema.nullable().optional(),\n customer_email: customerEmailSchema.nullable().optional(),\n })\n .strict()\n .superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: false }));\n\nexport type SaleItemInput = z.input<typeof saleItemSchema>;\nexport type SaleServiceInput = z.input<typeof saleServiceSchema>;\n// Use the INPUT type so fields with Zod defaults (environment, items) and\n// occurred_at are optional for callers — the sales client fills them in.\nexport type SaleInput = z.input<typeof saleCreateSchema>;\nexport type SaleUpdateInput = z.input<typeof saleUpdateSchema>;\n\n// ---------------------------------------------------------------------------\n// Read shapes (server responses; not validated on the client)\n// ---------------------------------------------------------------------------\n\nexport interface SaleItem {\n id: string;\n external_item_id: string | null;\n name: string | null;\n category: string | null;\n quantity: string;\n unit_price_cents: number;\n unit_cost_cents: number | null;\n}\n\nexport interface SaleService {\n id: string;\n service_id: string;\n service_key: string;\n service_label: string;\n amount_cents: number;\n}\n\nexport interface Sale {\n id: string;\n business_id: string;\n business_name?: string | null;\n external_id: string | null;\n currency: SupportedCurrency;\n amount_total_cents: number;\n description: string | null;\n // Singular service fields are populated only for single-service sales (kept\n // for backwards compatibility); `services` is always the full set.\n service_id: string | null;\n service_key?: string | null;\n service_label?: string | null;\n services: SaleService[];\n occurred_at: string;\n environment: (typeof TRACKING_ENVIRONMENTS)[number];\n metadata: Record<string, unknown> | null;\n customer_name: string | null;\n customer_phone: string | null;\n customer_email: string | null;\n created_at: string;\n updated_at: string;\n items: SaleItem[];\n}\n\nexport interface SaleListPage {\n items: Sale[];\n total: number;\n}\n\nexport interface SaleCursorPage {\n items: Sale[];\n next_cursor: string | null;\n /** Populated only when `want_total` was requested (a single indexed COUNT). */\n total_count?: number;\n has_more?: boolean;\n}\n\n/**\n * Sortable columns on the secret-key (keyset) and admin (offset) list endpoints.\n * `business_name` only applies to the cross-business admin list — it sorts the\n * joined `businesses.name` column.\n */\nexport type SaleSortField =\n | \"occurred_at\"\n | \"created_at\"\n | \"amount_total_cents\"\n | \"customer_name\"\n | \"business_name\";\n\nexport type SaleSortOrder = \"asc\" | \"desc\";\n\n/**\n * Comprehensive filter shape mirrored from the backend's `SaleQueryFilters`.\n *\n * `search` runs case-insensitively across `customer_name`, `customer_phone`,\n * `customer_email`, `description`, and `external_id` — the human-facing\n * columns. `service_id` is the resolved per-business service UUID (different\n * from the create-time `service` *key*).\n */\nexport interface SaleFilters {\n business_id?: string;\n external_id?: string;\n service_id?: string;\n /** E.164 phone — the canonical customer key; filters sales for one customer. */\n customer_phone?: string;\n currency?: SupportedCurrency;\n environment?: (typeof TRACKING_ENVIRONMENTS)[number];\n since?: string;\n until?: string;\n min_amount_cents?: number;\n max_amount_cents?: number;\n search?: string;\n}\n\n/**\n * Full query input for `SalesClient.list()` — filters + pagination.\n *\n * Pagination is **keyset (cursor)**: `next_cursor` returned by one page is\n * passed back as `cursor` on the next. `null` / undefined cursor = first page.\n *\n * Ordering on this endpoint is fixed at **`occurred_at DESC, id DESC`** — the\n * cursor encodes a position in that index, so a different sort would\n * invalidate cursors mid-pagination. For ad-hoc sorted reads use the\n * dashboard admin endpoint, which is offset-paginated.\n */\nexport interface SaleListQuery extends SaleFilters {\n limit?: number;\n cursor?: string | null;\n}\n\n/** Keyset-safe sort columns (NOT-NULL, indexed). */\nexport type SaleKeysetSortField = \"occurred_at\" | \"created_at\" | \"amount_total_cents\";\n\n/** v2 list query — adds server sort + opt-in total. */\nexport interface SaleListQueryV2 extends SaleListQuery {\n sort?: SaleKeysetSortField;\n order?: SaleSortOrder;\n /** Opt-in: a single indexed COUNT over the filtered set. */\n want_total?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Sales summary (aggregations). Read by `SalesClient.summary()`. Every monetary\n// figure is grouped **by currency** — USD and CAD cents are never summed into a\n// single total. Money stays integer cents; only `quantity` is a decimal string.\n// ---------------------------------------------------------------------------\n\nexport const TRACKING_RANGES = [\"24h\", \"7d\", \"30d\"] as const;\nexport type TrackingOverviewRange = (typeof TRACKING_RANGES)[number];\n\n/** Query input for `SalesClient.summary()` — filters + range + options. */\nexport interface SaleSummaryQuery extends SaleFilters {\n range: TrackingOverviewRange;\n /** Include the per-category line-item breakdown (extra join; default false). */\n include_categories?: boolean;\n /** Surface soft-deleted services individually (flagged \"(deleted)\") instead of\n * rolling them into a single \"Deleted services\" bucket. Default false. */\n include_deleted_services?: boolean;\n /** Cap on the by-service / by-category rows (1–50, default 10). */\n top_n?: number;\n}\n\nexport interface CurrencyRevenue {\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n average_order_value_cents: number;\n}\n\nexport interface SalesServiceBreakdown {\n service_id: string | null;\n service_key: string | null;\n /** \"Unassigned\" when the sale has no service. */\n service_label: string | null;\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n}\n\nexport interface SalesCategoryBreakdown {\n category: string | null;\n currency: SupportedCurrency;\n /** sum(unit_price_cents * quantity) — advisory, not authoritative. */\n revenue_cents: number;\n /** Decimal string. */\n quantity: string;\n}\n\nexport interface SalesTrendPoint {\n bucket_start: string;\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n}\n\nexport interface SaleSummary {\n range: TrackingOverviewRange;\n business_id: string | null;\n metrics: {\n sale_count: number;\n distinct_customers: number;\n by_currency: CurrencyRevenue[];\n };\n by_service: SalesServiceBreakdown[];\n by_currency: CurrencyRevenue[];\n by_category: SalesCategoryBreakdown[];\n series: SalesTrendPoint[];\n}\n\n// ---------------------------------------------------------------------------\n// v2 dashboard surface (additive, back-compat). Reads only → plain interfaces,\n// not drift-checked. Legacy `24h/7d/30d` callers get the same numbers; new\n// ranges/options layer on tz-correct calendar windows + comparisons.\n// ---------------------------------------------------------------------------\n\nexport const NAMED_RANGES = [\n \"today\",\n \"yesterday\",\n \"wtd\",\n \"mtd\",\n \"qtd\",\n \"ytd\",\n \"24h\",\n \"7d\",\n \"30d\",\n \"90d\",\n \"custom\",\n] as const;\n/** Superset of `TrackingOverviewRange` (`24h`/`7d`/`30d` stay valid). */\nexport type NamedRange = (typeof NAMED_RANGES)[number];\nexport type Granularity = \"hour\" | \"day\" | \"week\" | \"month\" | \"auto\";\nexport type CompareTo = \"previous_period\" | \"previous_year\" | \"none\";\n\nexport interface DistinctCustomersByCurrency {\n currency: SupportedCurrency;\n distinct_customers: number;\n}\n\nexport interface SummaryWindow {\n since: string;\n until: string;\n}\n\nexport interface SummaryCurrencyDelta {\n currency: SupportedCurrency;\n revenue_cents_delta: number;\n /** Ratio (0.12 = +12%); null when previous revenue was 0. */\n revenue_pct_delta: number | null;\n sale_count_delta: number;\n}\n\nexport interface SummaryDeltas {\n by_currency: SummaryCurrencyDelta[];\n sale_count_delta: number;\n distinct_customers_delta: number;\n}\n\nexport interface SaleSummaryPrevious {\n window: SummaryWindow;\n metrics: SaleSummaryV2[\"metrics\"];\n by_currency: CurrencyRevenue[];\n series: SalesTrendPoint[];\n}\n\n/** Query input for `SalesClient.summary()` v2 — widened range + tz/compare options. */\nexport interface SaleSummaryQueryV2 extends SaleFilters {\n range?: NamedRange;\n /** Required when `range === 'custom'`. */\n since?: string;\n until?: string;\n /** IANA tz, e.g. `America/Toronto`. Default = business tz. */\n timezone?: string;\n granularity?: Granularity;\n compare_to?: CompareTo;\n include_categories?: boolean;\n include_deleted_services?: boolean;\n top_n?: number;\n}\n\n/** Superset of `SaleSummary` (assignable to it). */\nexport interface SaleSummaryV2 extends Omit<SaleSummary, \"range\" | \"metrics\"> {\n range: NamedRange;\n metrics: SaleSummary[\"metrics\"] & {\n distinct_customers_by_currency: DistinctCustomersByCurrency[];\n };\n timezone: string | null;\n window: SummaryWindow | null;\n granularity: string | null;\n previous: SaleSummaryPrevious | null;\n deltas: SummaryDeltas | null;\n}\n\n// ---------------------------------------------------------------------------\n// Customers — phone-keyed (a customer IS their E.164 phone). `customer_id` is\n// the E.164 string. Money is per-currency, never summed.\n// ---------------------------------------------------------------------------\n\nexport type CustomerSegment = \"new\" | \"returning\" | \"repeat\" | \"lapsed\";\nexport type CustomerSortField = \"total_spent\" | \"last_purchase\" | \"purchases\" | \"first_purchase\";\n\nexport interface CustomerCurrencyTotal {\n currency: SupportedCurrency;\n revenue_cents: number;\n sale_count: number;\n average_order_value_cents: number;\n}\n\nexport interface CustomerSummary {\n /** The E.164 phone — the canonical customer id. */\n customer_id: string;\n display_name: string | null;\n email: string | null;\n phone: string;\n first_purchase_at: string;\n last_purchase_at: string;\n purchase_count: number;\n segment: CustomerSegment;\n totals: CustomerCurrencyTotal[];\n}\n\nexport type CustomerProfile = CustomerSummary;\n\nexport interface CustomerListQuery extends Omit<SaleFilters, \"external_id\"> {\n segment?: CustomerSegment;\n sort?: CustomerSortField;\n order?: SaleSortOrder;\n cursor?: string | null;\n limit?: number;\n want_total?: boolean;\n}\n\nexport interface CustomerListPage {\n items: CustomerSummary[];\n next_cursor: string | null;\n total_count: number | null;\n}\n\nexport interface CustomerGetOptions {\n include_sales?: boolean;\n limit?: number;\n cursor?: string | null;\n}\n\nexport interface CustomerGetResult {\n customer: CustomerProfile;\n /** Present only when `include_sales: true` (and a business-scoped key). */\n sales?: SaleCursorPage | null;\n}\n\nexport interface CustomerSummaryQuery extends SaleFilters {\n range?: NamedRange;\n since?: string;\n until?: string;\n timezone?: string;\n compare_to?: CompareTo;\n}\n\nexport interface CustomerSegmentCount {\n segment: CustomerSegment;\n count: number;\n}\n\nexport interface CustomerCurrencyDelta {\n currency: SupportedCurrency;\n revenue_cents_delta: number;\n /** Ratio (0.12 = +12%); null when previous revenue was 0. */\n revenue_pct_delta: number | null;\n}\n\nexport interface CustomerKpisDeltas {\n total_customers_delta: number;\n new_customers_delta: number;\n returning_customers_delta: number;\n repeat_rate_delta: number;\n ltv_by_currency: CustomerCurrencyDelta[];\n}\n\nexport interface CustomerKpisPrevious {\n window_since: string;\n window_until: string;\n total_customers: number;\n new_customers: number;\n returning_customers: number;\n repeat_rate: number;\n ltv_by_currency: CustomerCurrencyTotal[];\n}\n\nexport interface CustomerKpis {\n range: NamedRange;\n timezone: string;\n window_since: string;\n window_until: string;\n total_customers: number;\n new_customers: number;\n returning_customers: number;\n repeat_rate: number;\n by_segment: CustomerSegmentCount[];\n ltv_by_currency: CustomerCurrencyTotal[];\n /** Present only when `compare_to` is set. */\n previous?: CustomerKpisPrevious | null;\n deltas?: CustomerKpisDeltas | null;\n}\n\n// ---------------------------------------------------------------------------\n// Business config — supersets `fetchServices()`.\n// ---------------------------------------------------------------------------\n\nexport interface BusinessConfigService {\n key: string;\n label: string;\n archived: boolean;\n}\n\nexport interface BusinessConfigFeatures {\n customers: boolean;\n comparisons: boolean;\n retention: boolean;\n}\n\nexport interface BusinessConfig {\n business_id: string;\n display_name: string;\n timezone: string;\n primary_currency: SupportedCurrency;\n currencies: SupportedCurrency[];\n default_phone_country: string | null;\n services: BusinessConfigService[];\n features: BusinessConfigFeatures;\n}\n","// Framework-agnostic phone-number utilities — isomorphic (browser + Node/RSC),\n// zero React. Bundled `libphonenumber-js` (standard metadata) so clients add no\n// dependency. The transmitted value is ALWAYS E.164; display is the only knob.\n\nimport { AsYouType, parsePhoneNumberFromString, type CountryCode } from \"libphonenumber-js\";\n\nexport type { CountryCode };\n\n/**\n * How a phone number is shown in the UI. The transmitted value is always E.164\n * and is deliberately NOT part of this — only the display format is configurable.\n * A function form covers the long tail (`(parsed) => string`).\n */\nexport type PhoneDisplayFormat =\n | \"national\"\n | \"international\"\n | \"e164\"\n | ((parsed: ParsedPhone) => string);\n\nexport interface ParsedPhone {\n /** E.164 (`\"+14165550199\"`) or `null` when the input isn't a valid number. This is what gets transmitted. */\n e164: string | null;\n /** National display form (`\"(416) 555-0199\"`); empty string when unparseable. */\n national: string;\n /** International display form (`\"+1 416 555 0199\"`); empty string when unparseable. */\n international: string;\n /** ISO-3166 country resolved by libphonenumber, or `null`. */\n country: CountryCode | null;\n isValid: boolean;\n}\n\n/** Region assumed for numbers typed without a country code. */\nexport const DEFAULT_PHONE_COUNTRY: CountryCode = \"CA\";\n\n/** Parse a raw/display string into every representation at once (one parse → display + wire never drift). */\nexport function parsePhone(raw: string, country?: CountryCode): ParsedPhone {\n const region = country ?? DEFAULT_PHONE_COUNTRY;\n const parsed = parsePhoneNumberFromString(raw ?? \"\", region);\n if (!parsed) {\n return { e164: null, national: \"\", international: \"\", country: region, isValid: false };\n }\n const isValid = parsed.isValid();\n return {\n // E.164 is only surfaced for a *valid* number — a possible-but-invalid input\n // (e.g. too few digits) still parses but must not be transmitted.\n e164: isValid ? parsed.number : null,\n national: parsed.formatNational(),\n international: parsed.formatInternational(),\n country: parsed.country ?? region,\n isValid,\n };\n}\n\n/** Normalize any raw/display value to E.164, or `null` if it isn't a valid number. */\nexport function toE164(raw: string, country?: CountryCode): string | null {\n return parsePhone(raw, country).e164;\n}\n\n/** Format a value for display. Defaults to `'national'`. Never affects the wire value. */\nexport function formatPhone(\n value: string,\n format: PhoneDisplayFormat = \"national\",\n country?: CountryCode,\n): string {\n const parsed = parsePhone(value, country);\n if (typeof format === \"function\") return format(parsed);\n switch (format) {\n case \"international\":\n return parsed.international || value;\n case \"e164\":\n return parsed.e164 ?? value;\n case \"national\":\n default:\n return parsed.national || value;\n }\n}\n\n/** Live, incremental formatting for an `<input>` as the user types (`AsYouType`). */\nexport function formatPhoneAsTyped(raw: string, country?: CountryCode): string {\n return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? \"\");\n}\n","// Layer 0 (init config) + Layer 3 (tracking integration) for phone fields.\n// Kept in core so both the React and Next packages share identical types, and\n// the `phoneField` helper stays isomorphic.\n\nimport type { JsonValue } from \"./events/form-submit\";\nimport { toE164, type CountryCode, type PhoneDisplayFormat } from \"./phone\";\n\n/**\n * Init-time phone config (`createTracking({ phone })`). The transmitted value is\n * ALWAYS E.164 and is deliberately not configurable here — only display is.\n */\nexport interface PhoneConfig {\n /** Region assumed for numbers typed without a country code. Default `'CA'`. */\n defaultCountry?: CountryCode;\n /** How the input DISPLAYS to the user. Default `'national'`. Does not affect the wire. */\n display?: PhoneDisplayFormat;\n}\n\n/** A single tracked form field destined for `form_submit.fields[]`. */\nexport interface TrackedField {\n name: string;\n type: string;\n value: JsonValue;\n label?: string | null;\n}\n\n/**\n * Build a tracked field whose wire value is ALWAYS E.164. The client keeps its\n * own display value for UI/email; this puts `+E.164` on the wire (or `null` when\n * the input isn't a valid number).\n */\nexport function phoneField(name: string, raw: string, country?: CountryCode): TrackedField {\n return { name, type: \"phone\", value: toE164(raw, country) };\n}\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z","z"]}
|
|
1
|
+
{"version":3,"sources":["../../tracking-core/src/session.ts","../../tracking-core/src/events/page-view.ts","../../tracking-core/src/events/cta-click.ts","../../tracking-core/src/events/sdk-heartbeat.ts","../../tracking-core/src/events/form-start.ts","../../tracking-core/src/events/form-submit.ts","../../tracking-core/src/events/multi-page-session.ts","../../tracking-core/src/events/page-exit.ts","../../tracking-core/src/events/phone-click.ts","../../tracking-core/src/events/scroll-depth.ts","../../tracking-core/src/events/specific-page-visit.ts","../../tracking-core/src/events/time-on-site.ts","../../tracking-core/src/events/registry.ts","../../tracking-core/src/phone.ts","../../tracking-core/src/resources/sales/schema.ts","../../tracking-core/src/phone-field.ts"],"sourcesContent":["// Visitor + session identity for the tracking SDK.\n//\n// Visitor: persistent localStorage UUID, never expires until the user clears\n// browser storage. Used for cross-session correlation.\n//\n// Session: rolling 30-minute idle window. Regenerated when more than\n// SESSION_IDLE_MS has passed since the last event. Matches the behavior of\n// GA4, PostHog, Mixpanel, etc., so analytics is comparable.\n\nimport { clearLandingRecord } from \"./landing\";\n\n/**\n * localStorage key for the persistent visitor id.\n */\nexport const VISITOR_STORAGE_KEY = \"aranova_tracking_visitor\";\n\n/**\n * localStorage key for the rolling session id state.\n */\nexport const SESSION_STORAGE_KEY = \"aranova_tracking_session\";\n\n/**\n * Idle window before a new session id is created.\n */\nexport const SESSION_IDLE_MS = 30 * 60 * 1000;\n\n/**\n * Serialized session state stored in localStorage.\n */\nexport interface StoredSession {\n /** Client-generated session UUID. */\n id: string;\n /** Unix timestamp in milliseconds for the most recent event/session touch. */\n last_event_at: number;\n}\n\nfunction safeUuid(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\")\n return crypto.randomUUID();\n // Fallback for ancient browsers — not cryptographically perfect but unique enough.\n return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}-${Math.random().toString(16).slice(2)}`;\n}\n\nfunction readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Storage may be denied (private mode, blocked cookies, quota). Caller is\n // responsible for degrading gracefully.\n }\n}\n\n/**\n * Return the persistent visitor id for this browser profile.\n *\n * Creates and stores a new id when one does not already exist. During SSR,\n * returns an ephemeral id because browser storage is unavailable.\n */\nexport function getVisitorId(): string {\n if (typeof window === \"undefined\") return safeUuid();\n\n const existing = readLocalStorage(VISITOR_STORAGE_KEY);\n if (existing && existing.length > 0) return existing;\n\n const fresh = safeUuid();\n writeLocalStorage(VISITOR_STORAGE_KEY, fresh);\n return fresh;\n}\n\n/**\n * Result from `getOrRotateSessionId()`.\n */\nexport interface SessionIdResult {\n /** Current session id. */\n id: string;\n /** Whether this call created a new session. */\n isNew: boolean;\n}\n\n/**\n * Return the current session id, rotating it after the idle window expires.\n *\n * Also refreshes `last_event_at` for active sessions.\n */\nexport function getOrRotateSessionId(now: number = Date.now()): SessionIdResult {\n if (typeof window === \"undefined\") return { id: safeUuid(), isNew: true };\n\n const raw = readLocalStorage(SESSION_STORAGE_KEY);\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredSession>;\n if (typeof parsed.id === \"string\" && typeof parsed.last_event_at === \"number\") {\n if (now - parsed.last_event_at <= SESSION_IDLE_MS) {\n const refreshed: StoredSession = { id: parsed.id, last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(refreshed));\n return { id: parsed.id, isNew: false };\n }\n }\n } catch {\n // Fall through to a fresh session.\n }\n }\n\n const fresh: StoredSession = { id: safeUuid(), last_event_at: now };\n writeLocalStorage(SESSION_STORAGE_KEY, JSON.stringify(fresh));\n return { id: fresh.id, isNew: true };\n}\n\n/**\n * Clear visitor and session identity from localStorage, including the\n * session-scoped landing record (a landing must never outlive its session).\n *\n * Intended for tests, debugging, and explicit user reset flows.\n */\nexport function resetTrackingIdentity(): void {\n clearLandingRecord();\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(VISITOR_STORAGE_KEY);\n window.localStorage.removeItem(SESSION_STORAGE_KEY);\n } catch {\n // ignored\n }\n}\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `page_view` event.\n *\n * The SDK emits this on initial load, SPA route changes, and bfcache restores.\n * Consumers do not call `trackEvent('page_view', ...)`; registering\n * `automatic: { page_view: {} }` enables the SDK-owned trigger.\n */\nexport const pageViewMetadataSchema = z\n .object({\n page: z\n .object({\n title: z.string().nullable(),\n path: z.string(),\n search: z.string(),\n hash: z.string(),\n })\n .strict(),\n referrer: z.string().nullable(),\n // `.nullable().optional()` — absent (undefined) OR explicit null OR a\n // real viewport object. Mirrors Pydantic's `_Viewport | None = None`\n // on the backend side so the drift test stays clean.\n viewport: z\n .object({\n w: z.number(),\n h: z.number(),\n })\n .strict()\n .nullable()\n .optional(),\n })\n .strict();\n\nexport type PageViewMetadata = z.infer<typeof pageViewMetadataSchema>;\n\n/**\n * Registration config for automatic `page_view`.\n *\n * `page_view` is required in every trigger registry and currently has no\n * options. Use `{ page_view: {} }`.\n */\nexport const pageViewConfigSchema = z.object({}).strict();\nexport type PageViewConfig = z.infer<typeof pageViewConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for a manually fired `cta_click` event.\n *\n * Use this for non-phone calls to action such as directions, appointment\n * buttons, downloads, or external booking links.\n */\nexport const ctaClickMetadataSchema = z\n .object({\n cta_name: z.string(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n section: z.string().nullable().optional(),\n destination_url: z.string().nullable().optional(),\n // Set by auto-capture (and available to manual callers): the link target\n // and a short element descriptor (tag#id) for tying clicks to specific UI.\n href: z.string().nullable().optional(),\n element: z.string().nullable().optional(),\n })\n .strict();\n\nexport type CtaClickMetadata = z.infer<typeof ctaClickMetadataSchema>;\n\n/**\n * Registration config for `cta_click`.\n *\n * The event stays manually fireable; `autoCapture` additionally attaches a\n * delegated click listener that fires it for any element matching `selector`\n * (default `[data-aranova-cta]`) — tag your CTAs, get analytics for free.\n */\nexport const ctaClickConfigSchema = z\n .object({\n autoCapture: z\n .object({\n selector: z.string().optional(),\n })\n .strict()\n .optional(),\n })\n .strict();\nexport type CtaClickConfig = z.infer<typeof ctaClickConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Registered trigger names reported by the SDK heartbeat.\n */\nexport const sdkHeartbeatTriggersSchema = z\n .object({\n automatic: z.array(z.string()),\n manual: z.array(z.string()),\n })\n .strict();\n\n/**\n * Metadata for the SDK-internal `sdk_heartbeat` event.\n *\n * The SDK fires this once per new session so the dashboard can show which SDK\n * version, install surface, and trigger registry a client site is running.\n * Consumers do not manually register or fire this event.\n */\nexport const sdkHeartbeatMetadataSchema = z\n .object({\n sdk_version: z.string(),\n package_name: z.string().nullable(),\n surface: z.enum([\"next\", \"react\", \"script\"]),\n triggers: sdkHeartbeatTriggersSchema,\n trigger_config: z.record(z.string(), z.record(z.string(), z.unknown())).nullable().optional(),\n configured_gtag_ids: z.record(z.string(), z.string()).nullable().optional(),\n })\n .strict();\n\nexport type SdkHeartbeatMetadata = z.infer<typeof sdkHeartbeatMetadataSchema>;\n\n/**\n * Internal registration config for `sdk_heartbeat`.\n *\n * This event has no consumer-facing options.\n */\nexport const sdkHeartbeatConfigSchema = z.object({}).strict();\nexport type SdkHeartbeatConfig = z.infer<typeof sdkHeartbeatConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `form_start` event.\n *\n * The SDK emits this once per form when the visitor first focuses a field.\n */\nexport const formStartMetadataSchema = z\n .object({\n form: z\n .object({\n id: z.string(),\n action: z.string().nullable(),\n })\n .strict(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type FormStartMetadata = z.infer<typeof formStartMetadataSchema>;\n\n/**\n * Registration config for automatic `form_start`.\n *\n * Use `selector` to narrow which forms can trigger the event. When omitted,\n * the SDK observes all `<form>` elements.\n */\nexport const formStartConfigSchema = z\n .object({\n selector: z.string().optional(),\n })\n .strict();\n\nexport type FormStartConfig = z.infer<typeof formStartConfigSchema>;\n","import { z } from \"zod\";\n\n// form_submit is manual — the SDK never auto-fires this. Consumer code calls\n// `tracking.trackEvent('form_submit', { form, page })` from their own submit\n// handler. Registering it enables the type-level permission; omitting it\n// turns manual calls into a compile error.\n//\n// The optional `fields` array captures submitted form field metadata and JSON\n// values. Consumers explicitly build the fields array themselves so they\n// control exactly what is sent.\n\n/**\n * JSON-serializable value accepted by `form_submit.fields[].value`.\n *\n * This intentionally excludes `undefined`, functions, symbols, `Date`\n * instances, and non-finite numbers. Values are stored in PostgreSQL JSONB, so\n * consumers should send only data that has a stable JSON representation.\n */\nexport type JsonValue =\n | string\n | number\n | boolean\n | null\n | JsonValue[]\n | { [key: string]: JsonValue };\n\nexport const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() =>\n z.union([\n z.string(),\n z.number().finite(),\n z.boolean(),\n z.null(),\n z.array(jsonValueSchema),\n z.record(jsonValueSchema),\n ]),\n);\n\n/**\n * Metadata for a manually fired `form_submit` event.\n *\n * Register the event with `manual: { form_submit: {} }`, then call\n * `trackEvent('form_submit', metadata)` from the host site's submit handler.\n *\n * `fields` is optional. If present, each field value must be JSON-serializable\n * and should be explicitly allowlisted by the integration. Do not send names,\n * emails, visitor phone numbers, addresses, payment data, medical details,\n * passwords, file contents, or free-text messages.\n *\n * @example\n * ```ts\n * tracking.trackEvent('form_submit', {\n * form: {\n * id: 'lead-form',\n * action: '/api/lead',\n * fields: [\n * {\n * name: 'service_interest',\n * type: 'select',\n * label: 'Service interest',\n * value: 'teeth_whitening',\n * },\n * ],\n * },\n * page: { path: window.location.pathname },\n * });\n * ```\n */\nexport const formSubmitMetadataSchema = z\n .object({\n form: z\n .object({\n id: z.string(),\n action: z.string().nullable(),\n fields: z\n .array(\n z\n .object({\n name: z.string(),\n type: z.string(),\n label: z.string().nullable(),\n value: jsonValueSchema,\n })\n .strict(),\n )\n .optional(),\n })\n .strict(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type FormSubmitMetadata = z.infer<typeof formSubmitMetadataSchema>;\n\n/**\n * Registration config for `form_submit`.\n *\n * This event is manual-only and currently has no registration options. The\n * empty object enables typed `trackEvent('form_submit', ...)` calls.\n */\nexport const formSubmitConfigSchema = z.object({}).strict();\nexport type FormSubmitConfig = z.infer<typeof formSubmitConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `multi_page_session` event.\n *\n * Fired when the visitor reaches the configured distinct-page threshold in a\n * single tracking session.\n */\nexport const multiPageSessionMetadataSchema = z\n .object({\n page_count: z.number().int().min(2),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type MultiPageSessionMetadata = z.infer<typeof multiPageSessionMetadataSchema>;\n\n/**\n * Registration config for automatic `multi_page_session`.\n */\nexport const multiPageSessionConfigSchema = z\n .object({\n pageThreshold: z.number().int().min(2),\n })\n .strict();\n\nexport type MultiPageSessionConfig = z.infer<typeof multiPageSessionConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `page_exit` event.\n *\n * Fired when the user leaves a page (SPA navigation away, tab hidden, or\n * pagehide). `dwell_ms` is the ACTIVE (visible) time spent on the page segment\n * being closed — hidden time never counts, matching `time_on_site` semantics.\n * A page revisited after being hidden emits another `page_exit` for the next\n * visible segment, so summing `dwell_ms` per page/session yields total active\n * dwell without double counting.\n */\nexport const pageExitMetadataSchema = z\n .object({\n dwell_ms: z.number().int().min(0),\n // null = left without any scroll signal; floor is 0 so a valid 0% is never\n // rejected (a single bad field 422s the whole keepalive beacon batch).\n max_scroll_percent: z.number().int().min(0).max(100).nullable(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type PageExitMetadata = z.infer<typeof pageExitMetadataSchema>;\n\n/**\n * Registration config for automatic `page_exit`.\n *\n * SDK-internal: attached unconditionally (like `sdk_heartbeat`), so there are\n * no registration options.\n */\nexport const pageExitConfigSchema = z.object({}).strict();\nexport type PageExitConfig = z.infer<typeof pageExitConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for a manually fired `phone_click` event.\n *\n * `phone_number` should be the business phone number from the clicked `tel:`\n * link, not a visitor-entered phone number. `section` can distinguish header,\n * footer, hero, or contact-page links.\n */\nexport const phoneClickMetadataSchema = z\n .object({\n phone_number: z.string(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n section: z.string().nullable().optional(),\n })\n .strict();\n\nexport type PhoneClickMetadata = z.infer<typeof phoneClickMetadataSchema>;\n\n/**\n * Registration config for `phone_click`.\n *\n * The event stays manually fireable; `autoCapture` additionally attaches a\n * delegated click listener that fires it for any `tel:` link matching\n * `selector` (default `a[href^=\"tel:\"]`) — link your phone number, get the\n * analytics event (and, for a linked phone-click goal, the conversion) for free.\n */\nexport const phoneClickConfigSchema = z\n .object({\n autoCapture: z\n .object({\n selector: z.string().optional(),\n })\n .strict()\n .optional(),\n })\n .strict();\nexport type PhoneClickConfig = z.infer<typeof phoneClickConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `scroll_depth` event.\n *\n * Fired once per configured threshold per page.\n */\nexport const scrollDepthMetadataSchema = z\n .object({\n depth_percent: z.number().int().min(1).max(100),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type ScrollDepthMetadata = z.infer<typeof scrollDepthMetadataSchema>;\n\n/**\n * Registration config for automatic `scroll_depth`.\n *\n * `thresholds` are integer percentages from 1 to 100.\n */\nexport const scrollDepthConfigSchema = z\n .object({\n thresholds: z.array(z.number().int().min(1).max(100)).min(1),\n })\n .strict();\n\nexport type ScrollDepthConfig = z.infer<typeof scrollDepthConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Canonical page intent names supported by `specific_page_visit`.\n */\nexport const SPECIFIC_PAGE_NAMES = [\n \"contact_page\",\n \"about_page\",\n \"services_page\",\n \"booking_page\",\n \"location_page\",\n \"pricing_page\",\n \"faq_page\",\n \"testimonials_page\",\n] as const;\n\nexport type SpecificPageName = (typeof SPECIFIC_PAGE_NAMES)[number];\n\nexport const specificPageNameSchema = z.enum(SPECIFIC_PAGE_NAMES);\n\n/**\n * Metadata for the automatic `specific_page_visit` event.\n *\n * The SDK emits this when the current pathname matches one of the configured\n * named page patterns.\n */\nexport const specificPageVisitMetadataSchema = z\n .object({\n page_name: specificPageNameSchema,\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type SpecificPageVisitMetadata = z.infer<typeof specificPageVisitMetadataSchema>;\n\n/**\n * Registration config for automatic `specific_page_visit`.\n *\n * Each page entry pairs a semantic `name` with a `RegExp` that matches the\n * pathname. Use this instead of hard-coding path regexes downstream.\n */\nexport const specificPageVisitConfigSchema = z\n .object({\n pages: z\n .array(\n z\n .object({\n name: specificPageNameSchema,\n pathPattern: z.custom<RegExp>((value) => value instanceof RegExp, {\n message: \"pathPattern must be a RegExp\",\n }),\n })\n .strict(),\n )\n .min(1),\n })\n .strict();\n\nexport type SpecificPageVisitConfig = z.infer<typeof specificPageVisitConfigSchema>;\n","import { z } from \"zod\";\n\n/**\n * Metadata for the automatic `time_on_site` event.\n *\n * The SDK starts a visibility-aware timer and fires once when visible\n * engagement crosses the configured threshold.\n */\nexport const timeOnSiteMetadataSchema = z\n .object({\n duration_ms: z.number().int().nonnegative(),\n page: z\n .object({\n path: z.string(),\n })\n .strict(),\n })\n .strict();\n\nexport type TimeOnSiteMetadata = z.infer<typeof timeOnSiteMetadataSchema>;\n\n/**\n * Registration config for automatic `time_on_site`.\n */\nexport const timeOnSiteConfigSchema = z\n .object({\n thresholdSeconds: z.number().int().positive(),\n })\n .strict();\nexport type TimeOnSiteConfig = z.infer<typeof timeOnSiteConfigSchema>;\n","import type { z } from \"zod\";\n\nimport {\n ctaClickConfigSchema,\n ctaClickMetadataSchema,\n type CtaClickConfig,\n type CtaClickMetadata,\n} from \"./cta-click\";\nimport {\n sdkHeartbeatConfigSchema,\n sdkHeartbeatMetadataSchema,\n type SdkHeartbeatConfig,\n type SdkHeartbeatMetadata,\n} from \"./sdk-heartbeat\";\nimport {\n formStartConfigSchema,\n formStartMetadataSchema,\n type FormStartConfig,\n type FormStartMetadata,\n} from \"./form-start\";\nimport {\n formSubmitConfigSchema,\n formSubmitMetadataSchema,\n type FormSubmitConfig,\n type FormSubmitMetadata,\n} from \"./form-submit\";\nimport {\n multiPageSessionConfigSchema,\n multiPageSessionMetadataSchema,\n type MultiPageSessionConfig,\n type MultiPageSessionMetadata,\n} from \"./multi-page-session\";\nimport {\n pageExitConfigSchema,\n pageExitMetadataSchema,\n type PageExitConfig,\n type PageExitMetadata,\n} from \"./page-exit\";\nimport {\n pageViewConfigSchema,\n pageViewMetadataSchema,\n type PageViewConfig,\n type PageViewMetadata,\n} from \"./page-view\";\nimport {\n phoneClickConfigSchema,\n phoneClickMetadataSchema,\n type PhoneClickConfig,\n type PhoneClickMetadata,\n} from \"./phone-click\";\nimport {\n scrollDepthConfigSchema,\n scrollDepthMetadataSchema,\n type ScrollDepthConfig,\n type ScrollDepthMetadata,\n} from \"./scroll-depth\";\nimport {\n specificPageVisitConfigSchema,\n specificPageVisitMetadataSchema,\n type SpecificPageVisitConfig,\n type SpecificPageVisitMetadata,\n} from \"./specific-page-visit\";\nimport {\n timeOnSiteConfigSchema,\n timeOnSiteMetadataSchema,\n type TimeOnSiteConfig,\n type TimeOnSiteMetadata,\n} from \"./time-on-site\";\n\n// Event kind — automatic events are fired by the SDK itself when their\n// client-side signal fires (page_view on navigation, time_on_site on timer,\n// etc). Manual events are only fireable via explicit consumer code.\nexport type EventKind = \"automatic\" | \"manual\";\n\n// The registry is the single client-side source of truth for \"what events\n// exist, what shape does their metadata take, and what kind are they\". The\n// backend mirror lives in apps/api/src/schemas/tracking_events.py and is\n// kept in sync via the drift test in apps/api/tests/test_event_schema_drift.py.\nexport const EVENT_REGISTRY = {\n // --- automatic triggers ---\n page_view: {\n kind: \"automatic\",\n metadataSchema: pageViewMetadataSchema,\n configSchema: pageViewConfigSchema,\n },\n time_on_site: {\n kind: \"automatic\",\n metadataSchema: timeOnSiteMetadataSchema,\n configSchema: timeOnSiteConfigSchema,\n },\n specific_page_visit: {\n kind: \"automatic\",\n metadataSchema: specificPageVisitMetadataSchema,\n configSchema: specificPageVisitConfigSchema,\n },\n scroll_depth: {\n kind: \"automatic\",\n metadataSchema: scrollDepthMetadataSchema,\n configSchema: scrollDepthConfigSchema,\n },\n multi_page_session: {\n kind: \"automatic\",\n metadataSchema: multiPageSessionMetadataSchema,\n configSchema: multiPageSessionConfigSchema,\n },\n form_start: {\n kind: \"automatic\",\n metadataSchema: formStartMetadataSchema,\n configSchema: formStartConfigSchema,\n },\n // --- SDK-internal automatic (not consumer-configurable) ---\n sdk_heartbeat: {\n kind: \"automatic\",\n metadataSchema: sdkHeartbeatMetadataSchema,\n configSchema: sdkHeartbeatConfigSchema,\n },\n page_exit: {\n kind: \"automatic\",\n metadataSchema: pageExitMetadataSchema,\n configSchema: pageExitConfigSchema,\n },\n // --- manual triggers ---\n form_submit: {\n kind: \"manual\",\n metadataSchema: formSubmitMetadataSchema,\n configSchema: formSubmitConfigSchema,\n },\n phone_click: {\n kind: \"manual\",\n metadataSchema: phoneClickMetadataSchema,\n configSchema: phoneClickConfigSchema,\n },\n cta_click: {\n kind: \"manual\",\n metadataSchema: ctaClickMetadataSchema,\n configSchema: ctaClickConfigSchema,\n },\n} as const;\n\n/**\n * Name of any event known to the tracking SDK.\n */\nexport type EventName = keyof typeof EVENT_REGISTRY;\n\n/**\n * Event names that are fired by the SDK when their configured signal occurs.\n *\n * Automatic events are not accepted by the typed `trackEvent()` API.\n */\nexport type AutomaticEventName = {\n [K in EventName]: (typeof EVENT_REGISTRY)[K][\"kind\"] extends \"automatic\" ? K : never;\n}[EventName];\n\n/**\n * Event names that consumer code can fire manually after registering them.\n */\nexport type ManualEventName = {\n [K in EventName]: (typeof EVENT_REGISTRY)[K][\"kind\"] extends \"manual\" ? K : never;\n}[EventName];\n\n// Map an event name to its explicit metadata / config TS type. We pull the\n// inferred types directly from the per-event files rather than deriving\n// via `z.infer<typeof EVENT_REGISTRY[K]['metadataSchema']>` so that error\n// messages name the event-specific type (PageViewMetadata, not an\n// anonymous zod inference).\ntype MetadataByName = {\n page_view: PageViewMetadata;\n time_on_site: TimeOnSiteMetadata;\n specific_page_visit: SpecificPageVisitMetadata;\n scroll_depth: ScrollDepthMetadata;\n multi_page_session: MultiPageSessionMetadata;\n form_start: FormStartMetadata;\n sdk_heartbeat: SdkHeartbeatMetadata;\n page_exit: PageExitMetadata;\n form_submit: FormSubmitMetadata;\n phone_click: PhoneClickMetadata;\n cta_click: CtaClickMetadata;\n};\n\ntype ConfigByName = {\n page_view: PageViewConfig;\n time_on_site: TimeOnSiteConfig;\n specific_page_visit: SpecificPageVisitConfig;\n scroll_depth: ScrollDepthConfig;\n multi_page_session: MultiPageSessionConfig;\n form_start: FormStartConfig;\n sdk_heartbeat: SdkHeartbeatConfig;\n page_exit: PageExitConfig;\n form_submit: FormSubmitConfig;\n phone_click: PhoneClickConfig;\n cta_click: CtaClickConfig;\n};\n\n/**\n * Metadata payload type for a specific tracking event.\n *\n * @example\n * ```ts\n * type SubmitMetadata = EventMetadata<'form_submit'>;\n * ```\n */\nexport type EventMetadata<K extends EventName> = MetadataByName[K];\n\n/**\n * Trigger registration config type for a specific tracking event.\n */\nexport type EventConfig<K extends EventName> = ConfigByName[K];\n\n// Runtime constant arrays for iteration at consumer / factory time.\n/**\n * Runtime list of automatic event names.\n */\nexport const ALL_AUTOMATIC_EVENT_NAMES: readonly AutomaticEventName[] = (\n Object.entries(EVENT_REGISTRY) as Array<[EventName, (typeof EVENT_REGISTRY)[EventName]]>\n)\n .filter(([, def]) => def.kind === \"automatic\")\n .map(([name]) => name) as AutomaticEventName[];\n\n/**\n * Runtime list of manual event names.\n */\nexport const ALL_MANUAL_EVENT_NAMES: readonly ManualEventName[] = (\n Object.entries(EVENT_REGISTRY) as Array<[EventName, (typeof EVENT_REGISTRY)[EventName]]>\n)\n .filter(([, def]) => def.kind === \"manual\")\n .map(([name]) => name) as ManualEventName[];\n\n/**\n * Trigger registry passed to `createTracking({ triggers })`.\n *\n * `automatic.page_view` is required because every install should capture page\n * views. Other automatic events are opt-in. Manual events must be registered\n * here before the typed client accepts `trackEvent()` calls for them.\n *\n * @example\n * ```ts\n * createTracking({\n * apiKey,\n * endpoint,\n * triggers: {\n * automatic: {\n * page_view: {},\n * time_on_site: { thresholdSeconds: 60 },\n * },\n * manual: {\n * form_submit: {},\n * phone_click: {},\n * },\n * },\n * });\n * ```\n */\nexport type TriggerRegistryConfig = {\n automatic: {\n page_view: EventConfig<\"page_view\">;\n } & Partial<{\n time_on_site: EventConfig<\"time_on_site\">;\n specific_page_visit: EventConfig<\"specific_page_visit\">;\n scroll_depth: EventConfig<\"scroll_depth\">;\n multi_page_session: EventConfig<\"multi_page_session\">;\n form_start: EventConfig<\"form_start\">;\n }>;\n manual?: Partial<{\n form_submit: EventConfig<\"form_submit\">;\n phone_click: EventConfig<\"phone_click\">;\n cta_click: EventConfig<\"cta_click\">;\n }>;\n};\n\n/**\n * Manual event names registered in a concrete trigger registry.\n *\n * Used by `TypedTrackingClient` so `trackEvent()` only accepts events the\n * consumer explicitly enabled.\n */\nexport type RegisteredManualEvents<TRegistry extends TriggerRegistryConfig> = Extract<\n keyof NonNullable<TRegistry[\"manual\"]>,\n ManualEventName\n>;\n\n/**\n * Automatic event names registered in a concrete trigger registry.\n */\nexport type RegisteredAutomaticEvents<TRegistry extends TriggerRegistryConfig> = Extract<\n keyof TRegistry[\"automatic\"],\n AutomaticEventName\n>;\n\n/**\n * Discriminated union of valid manual tracking calls for a registry.\n */\nexport type TrackableEvent<TRegistry extends TriggerRegistryConfig> = {\n [K in RegisteredManualEvents<TRegistry>]: {\n eventType: K;\n metadata: EventMetadata<K>;\n };\n}[RegisteredManualEvents<TRegistry>];\n\n// Runtime helper: look up a schema pair by name. Cast through `unknown`\n// because the registry is `as const` and TS loses the specific schema type\n// when indexing via a dynamic key.\nexport function getEventDefinition(name: EventName): {\n kind: EventKind;\n metadataSchema: z.ZodTypeAny;\n configSchema: z.ZodTypeAny;\n} {\n return EVENT_REGISTRY[name] as unknown as {\n kind: EventKind;\n metadataSchema: z.ZodTypeAny;\n configSchema: z.ZodTypeAny;\n };\n}\n","// Framework-agnostic phone-number utilities — isomorphic (browser + Node/RSC),\n// zero React. Bundled `libphonenumber-js` (standard metadata) so clients add no\n// dependency. The transmitted value is ALWAYS E.164; display is the only knob.\n\nimport { AsYouType, parsePhoneNumberFromString, type CountryCode } from \"libphonenumber-js\";\n\nexport type { CountryCode };\n\n/**\n * How a phone number is shown in the UI. The transmitted value is always E.164\n * and is deliberately NOT part of this — only the display format is configurable.\n * A function form covers the long tail (`(parsed) => string`).\n */\nexport type PhoneDisplayFormat =\n | \"national\"\n | \"international\"\n | \"e164\"\n | ((parsed: ParsedPhone) => string);\n\nexport interface ParsedPhone {\n /** E.164 (`\"+14165550199\"`) or `null` when the input isn't a valid number. This is what gets transmitted. */\n e164: string | null;\n /** National display form (`\"(416) 555-0199\"`); empty string when unparseable. */\n national: string;\n /** International display form (`\"+1 416 555 0199\"`); empty string when unparseable. */\n international: string;\n /** ISO-3166 country resolved by libphonenumber, or `null`. */\n country: CountryCode | null;\n isValid: boolean;\n}\n\n/** Region assumed for numbers typed without a country code. */\nexport const DEFAULT_PHONE_COUNTRY: CountryCode = \"CA\";\n\n/** Parse a raw/display string into every representation at once (one parse → display + wire never drift). */\nexport function parsePhone(raw: string, country?: CountryCode): ParsedPhone {\n const region = country ?? DEFAULT_PHONE_COUNTRY;\n const parsed = parsePhoneNumberFromString(raw ?? \"\", region);\n if (!parsed) {\n return { e164: null, national: \"\", international: \"\", country: region, isValid: false };\n }\n const isValid = parsed.isValid();\n return {\n // E.164 is only surfaced for a *valid* number — a possible-but-invalid input\n // (e.g. too few digits) still parses but must not be transmitted.\n e164: isValid ? parsed.number : null,\n national: parsed.formatNational(),\n international: parsed.formatInternational(),\n country: parsed.country ?? region,\n isValid,\n };\n}\n\n/** Normalize any raw/display value to E.164, or `null` if it isn't a valid number. */\nexport function toE164(raw: string, country?: CountryCode): string | null {\n return parsePhone(raw, country).e164;\n}\n\n/** Format a value for display. Defaults to `'national'`. Never affects the wire value. */\nexport function formatPhone(\n value: string,\n format: PhoneDisplayFormat = \"national\",\n country?: CountryCode,\n): string {\n const parsed = parsePhone(value, country);\n if (typeof format === \"function\") return format(parsed);\n switch (format) {\n case \"international\":\n return parsed.international || value;\n case \"e164\":\n return parsed.e164 ?? value;\n case \"national\":\n default:\n return parsed.national || value;\n }\n}\n\n/** Live, incremental formatting for an `<input>` as the user types (`AsYouType`). */\nexport function formatPhoneAsTyped(raw: string, country?: CountryCode): string {\n return new AsYouType(country ?? DEFAULT_PHONE_COUNTRY).input(raw ?? \"\");\n}\n","import { z } from \"zod\";\n\n/**\n * Sales / Conversions wire schemas — the client-side source of truth.\n *\n * `saleCreateSchema` is mirrored by `SaleCreateSchema` in\n * `apps/api/src/schemas/tracking_sales.py` and enforced by the backend drift\n * test (the `resources` section of `events.schema.json`). Keep them in lockstep.\n *\n * Money is **integer minor units (cents)**; `quantity` is a decimal string;\n * `currency` is the required `SupportedCurrency` enum.\n */\n\nexport const SUPPORTED_CURRENCIES = [\"USD\", \"CAD\"] as const;\nexport type SupportedCurrency = (typeof SUPPORTED_CURRENCIES)[number];\n\nconst TRACKING_ENVIRONMENTS = [\"production\", \"development\"] as const;\n\nconst currencySchema = z.enum(SUPPORTED_CURRENCIES);\nconst centsSchema = z.number().int().nonnegative();\nconst quantitySchema = z.string().regex(/^\\d+(\\.\\d{1,3})?$/);\n// Free-form JSON object — the sale's extensibility escape hatch. Mirrors the\n// Pydantic `dict[str, Any]` (`additionalProperties: true`) on the wire.\nconst metadataSchema = z.record(z.unknown());\n\nexport const saleItemSchema = z\n .object({\n external_item_id: z.string().nullable().optional(),\n name: z.string().nullable().optional(),\n category: z.string().nullable().optional(),\n quantity: quantitySchema,\n unit_price_cents: centsSchema,\n // Non-negativity validated on the wire — same contract as the other cents\n // fields — and backstopped by the DB CHECK.\n unit_cost_cents: centsSchema.nullable().optional(),\n })\n .strict();\n\n// One service covered by a sale, at its own price. `service` is the per-business\n// service key, validated against the taxonomy by the backend at write time.\nexport const saleServiceSchema = z\n .object({\n service: z.string(),\n amount_cents: centsSchema,\n })\n .strict();\n\n// Raw customer identity. Stored as-is on the backend for human display in\n// dashboards; hashing happens at writeback-upload time in the future Google\n// Ads reconciliation worker (see docs/tracking-package/conversion-writeback.md).\n// The SDK never hashes — pass values straight through.\nconst customerNameSchema = z.string().max(200);\nconst customerPhoneSchema = z.string().max(64);\nconst customerEmailSchema = z.string().max(320).email();\n\n// The singular `service` (priced by `amount_total_cents`) and the plural\n// `services` (each priced individually) are mutually exclusive. When `services`\n// is present, `amount_total_cents` is optional and the backend derives it as the\n// sum; otherwise it is required. Shared by create + update.\nfunction refineServiceXor(\n val: {\n service?: string | null;\n services?: ReadonlyArray<{ service: string; amount_cents: number }> | null;\n amount_total_cents?: number | null;\n },\n ctx: z.RefinementCtx,\n { requireAmount }: { requireAmount: boolean },\n): void {\n if (val.services != null) {\n if (val.service != null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"pass either `service` or `services`, not both\",\n path: [\"services\"],\n });\n }\n if (val.services.length === 0) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"`services` must not be empty\",\n path: [\"services\"],\n });\n }\n const keys = val.services.map((s) => s.service);\n if (new Set(keys).size !== keys.length) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"`services` must not list the same service more than once\",\n path: [\"services\"],\n });\n }\n if (val.amount_total_cents != null) {\n const sum = val.services.reduce((acc, s) => acc + s.amount_cents, 0);\n if (val.amount_total_cents !== sum) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message:\n \"amount_total_cents must equal the sum of the services amounts \" +\n \"(omit it to derive it automatically)\",\n path: [\"amount_total_cents\"],\n });\n }\n }\n } else if (requireAmount && val.amount_total_cents == null) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: \"amount_total_cents is required unless `services` is provided\",\n path: [\"amount_total_cents\"],\n });\n }\n}\n\nexport const saleCreateSchema = z\n .object({\n external_id: z.string().nullable().optional(),\n description: z.string().nullable().optional(),\n service: z.string().nullable().optional(),\n services: z.array(saleServiceSchema).nullable().optional(),\n currency: currencySchema,\n // Optional only because the plural `services` form derives it from the sum\n // (see refineServiceXor); the singular/serviceless path still requires it.\n amount_total_cents: centsSchema.nullable().optional(),\n occurred_at: z.string().datetime(),\n environment: z.enum(TRACKING_ENVIRONMENTS).default(\"production\"),\n items: z.array(saleItemSchema).default([]),\n metadata: metadataSchema.nullable().optional(),\n customer_name: customerNameSchema.nullable().optional(),\n customer_phone: customerPhoneSchema.nullable().optional(),\n customer_email: customerEmailSchema.nullable().optional(),\n // CASL consent attestation: the customer agreed to receive SMS. Recorded\n // with a timestamp server-side; every SMS send path gates on it.\n sms_consent: z.boolean().default(false),\n })\n .strict()\n .superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));\n\nexport const saleUpdateSchema = z\n .object({\n description: z.string().nullable().optional(),\n service: z.string().nullable().optional(),\n services: z.array(saleServiceSchema).nullable().optional(),\n currency: currencySchema.optional(),\n amount_total_cents: centsSchema.optional(),\n occurred_at: z.string().datetime().optional(),\n items: z.array(saleItemSchema).optional(),\n metadata: metadataSchema.nullable().optional(),\n customer_name: customerNameSchema.nullable().optional(),\n customer_phone: customerPhoneSchema.nullable().optional(),\n customer_email: customerEmailSchema.nullable().optional(),\n // NOT NULL server-side: omit to leave unchanged (explicit null is rejected).\n sms_consent: z.boolean().optional(),\n })\n .strict()\n .superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: false }));\n\nexport type SaleItemInput = z.input<typeof saleItemSchema>;\nexport type SaleServiceInput = z.input<typeof saleServiceSchema>;\n// Use the INPUT type so fields with Zod defaults (environment, items) and\n// occurred_at are optional for callers — the sales client fills them in.\nexport type SaleInput = z.input<typeof saleCreateSchema>;\nexport type SaleUpdateInput = z.input<typeof saleUpdateSchema>;\n\n// ---------------------------------------------------------------------------\n// Read shapes (server responses; not validated on the client)\n// ---------------------------------------------------------------------------\n\nexport interface SaleItem {\n id: string;\n external_item_id: string | null;\n name: string | null;\n category: string | null;\n quantity: string;\n unit_price_cents: number;\n unit_cost_cents: number | null;\n}\n\nexport interface SaleService {\n id: string;\n service_id: string;\n service_key: string;\n service_label: string;\n amount_cents: number;\n}\n\nexport interface Sale {\n id: string;\n business_id: string;\n business_name?: string | null;\n external_id: string | null;\n currency: SupportedCurrency;\n amount_total_cents: number;\n description: string | null;\n // Singular service fields are populated only for single-service sales (kept\n // for backwards compatibility); `services` is always the full set.\n service_id: string | null;\n service_key?: string | null;\n service_label?: string | null;\n services: SaleService[];\n occurred_at: string;\n environment: (typeof TRACKING_ENVIRONMENTS)[number];\n metadata: Record<string, unknown> | null;\n customer_name: string | null;\n customer_phone: string | null;\n customer_email: string | null;\n created_at: string;\n updated_at: string;\n items: SaleItem[];\n}\n\nexport interface SaleListPage {\n items: Sale[];\n total: number;\n}\n\nexport interface SaleCursorPage {\n items: Sale[];\n next_cursor: string | null;\n /** Populated only when `want_total` was requested (a single indexed COUNT). */\n total_count?: number;\n has_more?: boolean;\n}\n\n/**\n * Sortable columns on the secret-key (keyset) and admin (offset) list endpoints.\n * `business_name` only applies to the cross-business admin list — it sorts the\n * joined `businesses.name` column.\n */\nexport type SaleSortField =\n | \"occurred_at\"\n | \"created_at\"\n | \"amount_total_cents\"\n | \"customer_name\"\n | \"business_name\";\n\nexport type SaleSortOrder = \"asc\" | \"desc\";\n\n/**\n * Comprehensive filter shape mirrored from the backend's `SaleQueryFilters`.\n *\n * `search` runs case-insensitively across `customer_name`, `customer_phone`,\n * `customer_email`, `description`, and `external_id` — the human-facing\n * columns. `service_id` is the resolved per-business service UUID (different\n * from the create-time `service` *key*).\n */\nexport interface SaleFilters {\n business_id?: string;\n external_id?: string;\n service_id?: string;\n /** E.164 phone — the canonical customer key; filters sales for one customer. */\n customer_phone?: string;\n currency?: SupportedCurrency;\n environment?: (typeof TRACKING_ENVIRONMENTS)[number];\n since?: string;\n until?: string;\n min_amount_cents?: number;\n max_amount_cents?: number;\n search?: string;\n}\n\n/**\n * Full query input for `SalesClient.list()` — filters + pagination.\n *\n * Pagination is **keyset (cursor)**: `next_cursor` returned by one page is\n * passed back as `cursor` on the next. `null` / undefined cursor = first page.\n *\n * Ordering on this endpoint is fixed at **`occurred_at DESC, id DESC`** — the\n * cursor encodes a position in that index, so a different sort would\n * invalidate cursors mid-pagination. For ad-hoc sorted reads use the\n * dashboard admin endpoint, which is offset-paginated.\n */\nexport interface SaleListQuery extends SaleFilters {\n limit?: number;\n cursor?: string | null;\n}\n\n/** Keyset-safe sort columns (NOT-NULL, indexed). */\nexport type SaleKeysetSortField = \"occurred_at\" | \"created_at\" | \"amount_total_cents\";\n\n/** v2 list query — adds server sort + opt-in total. */\nexport interface SaleListQueryV2 extends SaleListQuery {\n sort?: SaleKeysetSortField;\n order?: SaleSortOrder;\n /** Opt-in: a single indexed COUNT over the filtered set. */\n want_total?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Sales summary (aggregations). Read by `SalesClient.summary()`. Every monetary\n// figure is grouped **by currency** — USD and CAD cents are never summed into a\n// single total. Money stays integer cents; only `quantity` is a decimal string.\n// ---------------------------------------------------------------------------\n\nexport const TRACKING_RANGES = [\"24h\", \"7d\", \"30d\"] as const;\nexport type TrackingOverviewRange = (typeof TRACKING_RANGES)[number];\n\n/** Query input for `SalesClient.summary()` — filters + range + options. */\nexport interface SaleSummaryQuery extends SaleFilters {\n range: TrackingOverviewRange;\n /** Include the per-category line-item breakdown (extra join; default false). */\n include_categories?: boolean;\n /** Surface soft-deleted services individually (flagged \"(deleted)\") instead of\n * rolling them into a single \"Deleted services\" bucket. Default false. */\n include_deleted_services?: boolean;\n /** Cap on the by-service / by-category rows (1–50, default 10). */\n top_n?: number;\n}\n\nexport interface CurrencyRevenue {\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n average_order_value_cents: number;\n}\n\nexport interface SalesServiceBreakdown {\n service_id: string | null;\n service_key: string | null;\n /** \"Unassigned\" when the sale has no service. */\n service_label: string | null;\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n}\n\nexport interface SalesCategoryBreakdown {\n category: string | null;\n currency: SupportedCurrency;\n /** sum(unit_price_cents * quantity) — advisory, not authoritative. */\n revenue_cents: number;\n /** Decimal string. */\n quantity: string;\n}\n\nexport interface SalesTrendPoint {\n bucket_start: string;\n currency: SupportedCurrency;\n sale_count: number;\n revenue_cents: number;\n}\n\nexport interface SaleSummary {\n range: TrackingOverviewRange;\n business_id: string | null;\n metrics: {\n sale_count: number;\n distinct_customers: number;\n by_currency: CurrencyRevenue[];\n };\n by_service: SalesServiceBreakdown[];\n by_currency: CurrencyRevenue[];\n by_category: SalesCategoryBreakdown[];\n series: SalesTrendPoint[];\n}\n\n// ---------------------------------------------------------------------------\n// v2 dashboard surface (additive, back-compat). Reads only → plain interfaces,\n// not drift-checked. Legacy `24h/7d/30d` callers get the same numbers; new\n// ranges/options layer on tz-correct calendar windows + comparisons.\n// ---------------------------------------------------------------------------\n\nexport const NAMED_RANGES = [\n \"today\",\n \"yesterday\",\n \"wtd\",\n \"mtd\",\n \"qtd\",\n \"ytd\",\n \"24h\",\n \"7d\",\n \"30d\",\n \"90d\",\n \"custom\",\n] as const;\n/** Superset of `TrackingOverviewRange` (`24h`/`7d`/`30d` stay valid). */\nexport type NamedRange = (typeof NAMED_RANGES)[number];\nexport type Granularity = \"hour\" | \"day\" | \"week\" | \"month\" | \"auto\";\nexport type CompareTo = \"previous_period\" | \"previous_year\" | \"none\";\n\nexport interface DistinctCustomersByCurrency {\n currency: SupportedCurrency;\n distinct_customers: number;\n}\n\nexport interface SummaryWindow {\n since: string;\n until: string;\n}\n\nexport interface SummaryCurrencyDelta {\n currency: SupportedCurrency;\n revenue_cents_delta: number;\n /** Ratio (0.12 = +12%); null when previous revenue was 0. */\n revenue_pct_delta: number | null;\n sale_count_delta: number;\n}\n\nexport interface SummaryDeltas {\n by_currency: SummaryCurrencyDelta[];\n sale_count_delta: number;\n distinct_customers_delta: number;\n}\n\nexport interface SaleSummaryPrevious {\n window: SummaryWindow;\n metrics: SaleSummaryV2[\"metrics\"];\n by_currency: CurrencyRevenue[];\n series: SalesTrendPoint[];\n}\n\n/** Query input for `SalesClient.summary()` v2 — widened range + tz/compare options. */\nexport interface SaleSummaryQueryV2 extends SaleFilters {\n range?: NamedRange;\n /** Required when `range === 'custom'`. */\n since?: string;\n until?: string;\n /** IANA tz, e.g. `America/Toronto`. Default = business tz. */\n timezone?: string;\n granularity?: Granularity;\n compare_to?: CompareTo;\n include_categories?: boolean;\n include_deleted_services?: boolean;\n top_n?: number;\n}\n\n/** Superset of `SaleSummary` (assignable to it). */\nexport interface SaleSummaryV2 extends Omit<SaleSummary, \"range\" | \"metrics\"> {\n range: NamedRange;\n metrics: SaleSummary[\"metrics\"] & {\n distinct_customers_by_currency: DistinctCustomersByCurrency[];\n };\n timezone: string | null;\n window: SummaryWindow | null;\n granularity: string | null;\n previous: SaleSummaryPrevious | null;\n deltas: SummaryDeltas | null;\n}\n\n// ---------------------------------------------------------------------------\n// Customers — phone-keyed (a customer IS their E.164 phone). `customer_id` is\n// the E.164 string. Money is per-currency, never summed.\n// ---------------------------------------------------------------------------\n\nexport type CustomerSegment = \"new\" | \"returning\" | \"repeat\" | \"lapsed\";\nexport type CustomerSortField = \"total_spent\" | \"last_purchase\" | \"purchases\" | \"first_purchase\";\n\nexport interface CustomerCurrencyTotal {\n currency: SupportedCurrency;\n revenue_cents: number;\n sale_count: number;\n average_order_value_cents: number;\n}\n\nexport interface CustomerSummary {\n /** The E.164 phone — the canonical customer id. */\n customer_id: string;\n display_name: string | null;\n email: string | null;\n phone: string;\n first_purchase_at: string;\n last_purchase_at: string;\n purchase_count: number;\n segment: CustomerSegment;\n totals: CustomerCurrencyTotal[];\n}\n\nexport type CustomerProfile = CustomerSummary;\n\nexport interface CustomerListQuery extends Omit<SaleFilters, \"external_id\"> {\n segment?: CustomerSegment;\n sort?: CustomerSortField;\n order?: SaleSortOrder;\n cursor?: string | null;\n limit?: number;\n want_total?: boolean;\n}\n\nexport interface CustomerListPage {\n items: CustomerSummary[];\n next_cursor: string | null;\n total_count: number | null;\n}\n\nexport interface CustomerGetOptions {\n include_sales?: boolean;\n limit?: number;\n cursor?: string | null;\n}\n\nexport interface CustomerGetResult {\n customer: CustomerProfile;\n /** Present only when `include_sales: true` (and a business-scoped key). */\n sales?: SaleCursorPage | null;\n}\n\nexport interface CustomerSummaryQuery extends SaleFilters {\n range?: NamedRange;\n since?: string;\n until?: string;\n timezone?: string;\n compare_to?: CompareTo;\n}\n\nexport interface CustomerSegmentCount {\n segment: CustomerSegment;\n count: number;\n}\n\nexport interface CustomerCurrencyDelta {\n currency: SupportedCurrency;\n revenue_cents_delta: number;\n /** Ratio (0.12 = +12%); null when previous revenue was 0. */\n revenue_pct_delta: number | null;\n}\n\nexport interface CustomerKpisDeltas {\n total_customers_delta: number;\n new_customers_delta: number;\n returning_customers_delta: number;\n repeat_rate_delta: number;\n ltv_by_currency: CustomerCurrencyDelta[];\n}\n\nexport interface CustomerKpisPrevious {\n window_since: string;\n window_until: string;\n total_customers: number;\n new_customers: number;\n returning_customers: number;\n repeat_rate: number;\n ltv_by_currency: CustomerCurrencyTotal[];\n}\n\nexport interface CustomerKpis {\n range: NamedRange;\n timezone: string;\n window_since: string;\n window_until: string;\n total_customers: number;\n new_customers: number;\n returning_customers: number;\n repeat_rate: number;\n by_segment: CustomerSegmentCount[];\n ltv_by_currency: CustomerCurrencyTotal[];\n /** Present only when `compare_to` is set. */\n previous?: CustomerKpisPrevious | null;\n deltas?: CustomerKpisDeltas | null;\n}\n\n// ---------------------------------------------------------------------------\n// Business config — supersets `fetchServices()`.\n// ---------------------------------------------------------------------------\n\nexport interface BusinessConfigService {\n key: string;\n label: string;\n archived: boolean;\n}\n\nexport interface BusinessConfigFeatures {\n customers: boolean;\n comparisons: boolean;\n retention: boolean;\n}\n\nexport interface BusinessConfig {\n business_id: string;\n display_name: string;\n timezone: string;\n primary_currency: SupportedCurrency;\n currencies: SupportedCurrency[];\n default_phone_country: string | null;\n services: BusinessConfigService[];\n features: BusinessConfigFeatures;\n}\n","// Layer 0 (init config) + Layer 3 (tracking integration) for phone fields.\n// Kept in core so both the React and Next packages share identical types, and\n// the `phoneField` helper stays isomorphic.\n\nimport type { JsonValue } from \"./events/form-submit\";\nimport { toE164, type CountryCode, type PhoneDisplayFormat } from \"./phone\";\n\n/**\n * Init-time phone config (`createTracking({ phone })`). The transmitted value is\n * ALWAYS E.164 and is deliberately not configurable here — only display is.\n */\nexport interface PhoneConfig {\n /** Region assumed for numbers typed without a country code. Default `'CA'`. */\n defaultCountry?: CountryCode;\n /** How the input DISPLAYS to the user. Default `'national'`. Does not affect the wire. */\n display?: PhoneDisplayFormat;\n}\n\n/** A single tracked form field destined for `form_submit.fields[]`. */\nexport interface TrackedField {\n name: string;\n type: string;\n value: JsonValue;\n label?: string | null;\n}\n\n/**\n * Build a tracked field whose wire value is ALWAYS E.164. The client keeps its\n * own display value for UI/email; this puts `+E.164` on the wire (or `null` when\n * the input isn't a valid number).\n */\nexport function phoneField(name: string, raw: string, country?: CountryCode): TrackedField {\n return { name, type: \"phone\", value: toE164(raw, country) };\n}\n"],"mappings":"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","names":["z","z","z","z","z","z","z","z","z","z","z"]}
|
|
@@ -167,6 +167,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
167
167
|
customer_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
168
168
|
customer_phone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
169
169
|
customer_email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
170
|
+
sms_consent: z.ZodDefault<z.ZodBoolean>;
|
|
170
171
|
}, "strict", z.ZodTypeAny, {
|
|
171
172
|
currency: "USD" | "CAD";
|
|
172
173
|
environment: "production" | "development";
|
|
@@ -179,6 +180,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
179
180
|
category?: string | null | undefined;
|
|
180
181
|
unit_cost_cents?: number | null | undefined;
|
|
181
182
|
}[];
|
|
183
|
+
sms_consent: boolean;
|
|
182
184
|
metadata?: Record<string, unknown> | null | undefined;
|
|
183
185
|
services?: {
|
|
184
186
|
service: string;
|
|
@@ -215,6 +217,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
215
217
|
customer_name?: string | null | undefined;
|
|
216
218
|
customer_phone?: string | null | undefined;
|
|
217
219
|
customer_email?: string | null | undefined;
|
|
220
|
+
sms_consent?: boolean | undefined;
|
|
218
221
|
}>, {
|
|
219
222
|
currency: "USD" | "CAD";
|
|
220
223
|
environment: "production" | "development";
|
|
@@ -227,6 +230,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
227
230
|
category?: string | null | undefined;
|
|
228
231
|
unit_cost_cents?: number | null | undefined;
|
|
229
232
|
}[];
|
|
233
|
+
sms_consent: boolean;
|
|
230
234
|
metadata?: Record<string, unknown> | null | undefined;
|
|
231
235
|
services?: {
|
|
232
236
|
service: string;
|
|
@@ -263,6 +267,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
263
267
|
customer_name?: string | null | undefined;
|
|
264
268
|
customer_phone?: string | null | undefined;
|
|
265
269
|
customer_email?: string | null | undefined;
|
|
270
|
+
sms_consent?: boolean | undefined;
|
|
266
271
|
}>;
|
|
267
272
|
declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
268
273
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
@@ -306,6 +311,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
306
311
|
customer_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
307
312
|
customer_phone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
308
313
|
customer_email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
314
|
+
sms_consent: z.ZodOptional<z.ZodBoolean>;
|
|
309
315
|
}, "strict", z.ZodTypeAny, {
|
|
310
316
|
currency?: "USD" | "CAD" | undefined;
|
|
311
317
|
metadata?: Record<string, unknown> | null | undefined;
|
|
@@ -328,6 +334,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
328
334
|
customer_name?: string | null | undefined;
|
|
329
335
|
customer_phone?: string | null | undefined;
|
|
330
336
|
customer_email?: string | null | undefined;
|
|
337
|
+
sms_consent?: boolean | undefined;
|
|
331
338
|
}, {
|
|
332
339
|
currency?: "USD" | "CAD" | undefined;
|
|
333
340
|
metadata?: Record<string, unknown> | null | undefined;
|
|
@@ -350,6 +357,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
350
357
|
customer_name?: string | null | undefined;
|
|
351
358
|
customer_phone?: string | null | undefined;
|
|
352
359
|
customer_email?: string | null | undefined;
|
|
360
|
+
sms_consent?: boolean | undefined;
|
|
353
361
|
}>, {
|
|
354
362
|
currency?: "USD" | "CAD" | undefined;
|
|
355
363
|
metadata?: Record<string, unknown> | null | undefined;
|
|
@@ -372,6 +380,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
372
380
|
customer_name?: string | null | undefined;
|
|
373
381
|
customer_phone?: string | null | undefined;
|
|
374
382
|
customer_email?: string | null | undefined;
|
|
383
|
+
sms_consent?: boolean | undefined;
|
|
375
384
|
}, {
|
|
376
385
|
currency?: "USD" | "CAD" | undefined;
|
|
377
386
|
metadata?: Record<string, unknown> | null | undefined;
|
|
@@ -394,6 +403,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
394
403
|
customer_name?: string | null | undefined;
|
|
395
404
|
customer_phone?: string | null | undefined;
|
|
396
405
|
customer_email?: string | null | undefined;
|
|
406
|
+
sms_consent?: boolean | undefined;
|
|
397
407
|
}>;
|
|
398
408
|
type SaleItemInput = z.input<typeof saleItemSchema>;
|
|
399
409
|
type SaleServiceInput = z.input<typeof saleServiceSchema>;
|
|
@@ -167,6 +167,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
167
167
|
customer_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
168
168
|
customer_phone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
169
169
|
customer_email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
170
|
+
sms_consent: z.ZodDefault<z.ZodBoolean>;
|
|
170
171
|
}, "strict", z.ZodTypeAny, {
|
|
171
172
|
currency: "USD" | "CAD";
|
|
172
173
|
environment: "production" | "development";
|
|
@@ -179,6 +180,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
179
180
|
category?: string | null | undefined;
|
|
180
181
|
unit_cost_cents?: number | null | undefined;
|
|
181
182
|
}[];
|
|
183
|
+
sms_consent: boolean;
|
|
182
184
|
metadata?: Record<string, unknown> | null | undefined;
|
|
183
185
|
services?: {
|
|
184
186
|
service: string;
|
|
@@ -215,6 +217,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
215
217
|
customer_name?: string | null | undefined;
|
|
216
218
|
customer_phone?: string | null | undefined;
|
|
217
219
|
customer_email?: string | null | undefined;
|
|
220
|
+
sms_consent?: boolean | undefined;
|
|
218
221
|
}>, {
|
|
219
222
|
currency: "USD" | "CAD";
|
|
220
223
|
environment: "production" | "development";
|
|
@@ -227,6 +230,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
227
230
|
category?: string | null | undefined;
|
|
228
231
|
unit_cost_cents?: number | null | undefined;
|
|
229
232
|
}[];
|
|
233
|
+
sms_consent: boolean;
|
|
230
234
|
metadata?: Record<string, unknown> | null | undefined;
|
|
231
235
|
services?: {
|
|
232
236
|
service: string;
|
|
@@ -263,6 +267,7 @@ declare const saleCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
263
267
|
customer_name?: string | null | undefined;
|
|
264
268
|
customer_phone?: string | null | undefined;
|
|
265
269
|
customer_email?: string | null | undefined;
|
|
270
|
+
sms_consent?: boolean | undefined;
|
|
266
271
|
}>;
|
|
267
272
|
declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
268
273
|
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
@@ -306,6 +311,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
306
311
|
customer_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
307
312
|
customer_phone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
308
313
|
customer_email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
314
|
+
sms_consent: z.ZodOptional<z.ZodBoolean>;
|
|
309
315
|
}, "strict", z.ZodTypeAny, {
|
|
310
316
|
currency?: "USD" | "CAD" | undefined;
|
|
311
317
|
metadata?: Record<string, unknown> | null | undefined;
|
|
@@ -328,6 +334,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
328
334
|
customer_name?: string | null | undefined;
|
|
329
335
|
customer_phone?: string | null | undefined;
|
|
330
336
|
customer_email?: string | null | undefined;
|
|
337
|
+
sms_consent?: boolean | undefined;
|
|
331
338
|
}, {
|
|
332
339
|
currency?: "USD" | "CAD" | undefined;
|
|
333
340
|
metadata?: Record<string, unknown> | null | undefined;
|
|
@@ -350,6 +357,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
350
357
|
customer_name?: string | null | undefined;
|
|
351
358
|
customer_phone?: string | null | undefined;
|
|
352
359
|
customer_email?: string | null | undefined;
|
|
360
|
+
sms_consent?: boolean | undefined;
|
|
353
361
|
}>, {
|
|
354
362
|
currency?: "USD" | "CAD" | undefined;
|
|
355
363
|
metadata?: Record<string, unknown> | null | undefined;
|
|
@@ -372,6 +380,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
372
380
|
customer_name?: string | null | undefined;
|
|
373
381
|
customer_phone?: string | null | undefined;
|
|
374
382
|
customer_email?: string | null | undefined;
|
|
383
|
+
sms_consent?: boolean | undefined;
|
|
375
384
|
}, {
|
|
376
385
|
currency?: "USD" | "CAD" | undefined;
|
|
377
386
|
metadata?: Record<string, unknown> | null | undefined;
|
|
@@ -394,6 +403,7 @@ declare const saleUpdateSchema: z.ZodEffects<z.ZodObject<{
|
|
|
394
403
|
customer_name?: string | null | undefined;
|
|
395
404
|
customer_phone?: string | null | undefined;
|
|
396
405
|
customer_email?: string | null | undefined;
|
|
406
|
+
sms_consent?: boolean | undefined;
|
|
397
407
|
}>;
|
|
398
408
|
type SaleItemInput = z.input<typeof saleItemSchema>;
|
|
399
409
|
type SaleServiceInput = z.input<typeof saleServiceSchema>;
|
package/dist/sales.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { A as AranovaApiError, B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, e as CurrencyRevenue, f as CustomerCurrencyDelta, g as CustomerCurrencyTotal, h as CustomerGetOptions, i as CustomerGetResult, j as CustomerKpis, k as CustomerKpisDeltas, l as CustomerKpisPrevious, m as CustomerListPage, n as CustomerListQuery, o as CustomerProfile, p as CustomerSegment, q as CustomerSegmentCount, r as CustomerSortField, s as CustomerSummary, t as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, u as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, v as Sale, w as SaleCursorPage, x as SaleFilters, y as SaleInput, z as SaleItem, E as SaleItemInput, F as SaleKeysetSortField, H as SaleListPage, I as SaleListQuery, J as SaleListQueryV2, K as SaleService, L as SaleServiceInput, M as SaleSortField, O as SaleSortOrder, Q as SaleSummary, R as SaleSummaryPrevious, T as SaleSummaryQuery, U as SaleSummaryQueryV2, V as SaleSummaryV2, W as SaleUpdateInput, X as SalesBusinessClient, Y as SalesCategoryBreakdown, Z as SalesClient, _ as SalesClientConfig, $ as SalesCustomersClient, a0 as SalesServiceBreakdown, a1 as SalesTransportConfig, a2 as SalesTrendPoint, a3 as SummaryCurrencyDelta, a4 as SummaryDeltas, a5 as SummaryWindow, a6 as SupportedCurrency, a7 as TRACKING_RANGES, a8 as TrackingOverviewRange, a9 as createSalesClient, aa as fetchServices, ab as formatDateInTz, ac as formatMoney, ad as fromMinor, af as saleCreateSchema, ag as saleItemSchema, ah as saleServiceSchema, ai as saleUpdateSchema, aj as salesRequest, ak as toMinor } from './sales-
|
|
1
|
+
export { A as AranovaApiError, B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, e as CurrencyRevenue, f as CustomerCurrencyDelta, g as CustomerCurrencyTotal, h as CustomerGetOptions, i as CustomerGetResult, j as CustomerKpis, k as CustomerKpisDeltas, l as CustomerKpisPrevious, m as CustomerListPage, n as CustomerListQuery, o as CustomerProfile, p as CustomerSegment, q as CustomerSegmentCount, r as CustomerSortField, s as CustomerSummary, t as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, u as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, v as Sale, w as SaleCursorPage, x as SaleFilters, y as SaleInput, z as SaleItem, E as SaleItemInput, F as SaleKeysetSortField, H as SaleListPage, I as SaleListQuery, J as SaleListQueryV2, K as SaleService, L as SaleServiceInput, M as SaleSortField, O as SaleSortOrder, Q as SaleSummary, R as SaleSummaryPrevious, T as SaleSummaryQuery, U as SaleSummaryQueryV2, V as SaleSummaryV2, W as SaleUpdateInput, X as SalesBusinessClient, Y as SalesCategoryBreakdown, Z as SalesClient, _ as SalesClientConfig, $ as SalesCustomersClient, a0 as SalesServiceBreakdown, a1 as SalesTransportConfig, a2 as SalesTrendPoint, a3 as SummaryCurrencyDelta, a4 as SummaryDeltas, a5 as SummaryWindow, a6 as SupportedCurrency, a7 as TRACKING_RANGES, a8 as TrackingOverviewRange, a9 as createSalesClient, aa as fetchServices, ab as formatDateInTz, ac as formatMoney, ad as fromMinor, af as saleCreateSchema, ag as saleItemSchema, ah as saleServiceSchema, ai as saleUpdateSchema, aj as salesRequest, ak as toMinor } from './sales-C3jFBx08.mjs';
|
|
2
2
|
import 'zod';
|
package/dist/sales.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { A as AranovaApiError, B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, e as CurrencyRevenue, f as CustomerCurrencyDelta, g as CustomerCurrencyTotal, h as CustomerGetOptions, i as CustomerGetResult, j as CustomerKpis, k as CustomerKpisDeltas, l as CustomerKpisPrevious, m as CustomerListPage, n as CustomerListQuery, o as CustomerProfile, p as CustomerSegment, q as CustomerSegmentCount, r as CustomerSortField, s as CustomerSummary, t as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, u as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, v as Sale, w as SaleCursorPage, x as SaleFilters, y as SaleInput, z as SaleItem, E as SaleItemInput, F as SaleKeysetSortField, H as SaleListPage, I as SaleListQuery, J as SaleListQueryV2, K as SaleService, L as SaleServiceInput, M as SaleSortField, O as SaleSortOrder, Q as SaleSummary, R as SaleSummaryPrevious, T as SaleSummaryQuery, U as SaleSummaryQueryV2, V as SaleSummaryV2, W as SaleUpdateInput, X as SalesBusinessClient, Y as SalesCategoryBreakdown, Z as SalesClient, _ as SalesClientConfig, $ as SalesCustomersClient, a0 as SalesServiceBreakdown, a1 as SalesTransportConfig, a2 as SalesTrendPoint, a3 as SummaryCurrencyDelta, a4 as SummaryDeltas, a5 as SummaryWindow, a6 as SupportedCurrency, a7 as TRACKING_RANGES, a8 as TrackingOverviewRange, a9 as createSalesClient, aa as fetchServices, ab as formatDateInTz, ac as formatMoney, ad as fromMinor, af as saleCreateSchema, ag as saleItemSchema, ah as saleServiceSchema, ai as saleUpdateSchema, aj as salesRequest, ak as toMinor } from './sales-
|
|
1
|
+
export { A as AranovaApiError, B as BusinessConfig, a as BusinessConfigFeatures, b as BusinessConfigService, c as CompareTo, e as CurrencyRevenue, f as CustomerCurrencyDelta, g as CustomerCurrencyTotal, h as CustomerGetOptions, i as CustomerGetResult, j as CustomerKpis, k as CustomerKpisDeltas, l as CustomerKpisPrevious, m as CustomerListPage, n as CustomerListQuery, o as CustomerProfile, p as CustomerSegment, q as CustomerSegmentCount, r as CustomerSortField, s as CustomerSummary, t as CustomerSummaryQuery, D as DistinctCustomersByCurrency, G as Granularity, N as NAMED_RANGES, u as NamedRange, P as PublicServiceItem, S as SUPPORTED_CURRENCIES, v as Sale, w as SaleCursorPage, x as SaleFilters, y as SaleInput, z as SaleItem, E as SaleItemInput, F as SaleKeysetSortField, H as SaleListPage, I as SaleListQuery, J as SaleListQueryV2, K as SaleService, L as SaleServiceInput, M as SaleSortField, O as SaleSortOrder, Q as SaleSummary, R as SaleSummaryPrevious, T as SaleSummaryQuery, U as SaleSummaryQueryV2, V as SaleSummaryV2, W as SaleUpdateInput, X as SalesBusinessClient, Y as SalesCategoryBreakdown, Z as SalesClient, _ as SalesClientConfig, $ as SalesCustomersClient, a0 as SalesServiceBreakdown, a1 as SalesTransportConfig, a2 as SalesTrendPoint, a3 as SummaryCurrencyDelta, a4 as SummaryDeltas, a5 as SummaryWindow, a6 as SupportedCurrency, a7 as TRACKING_RANGES, a8 as TrackingOverviewRange, a9 as createSalesClient, aa as fetchServices, ab as formatDateInTz, ac as formatMoney, ad as fromMinor, af as saleCreateSchema, ag as saleItemSchema, ah as saleServiceSchema, ai as saleUpdateSchema, aj as salesRequest, ak as toMinor } from './sales-C3jFBx08.js';
|
|
2
2
|
import 'zod';
|
package/dist/sales.js
CHANGED
|
@@ -476,7 +476,10 @@ var saleCreateSchema = import_zod2.z.object({
|
|
|
476
476
|
metadata: metadataSchema.nullable().optional(),
|
|
477
477
|
customer_name: customerNameSchema.nullable().optional(),
|
|
478
478
|
customer_phone: customerPhoneSchema.nullable().optional(),
|
|
479
|
-
customer_email: customerEmailSchema.nullable().optional()
|
|
479
|
+
customer_email: customerEmailSchema.nullable().optional(),
|
|
480
|
+
// CASL consent attestation: the customer agreed to receive SMS. Recorded
|
|
481
|
+
// with a timestamp server-side; every SMS send path gates on it.
|
|
482
|
+
sms_consent: import_zod2.z.boolean().default(false)
|
|
480
483
|
}).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: true }));
|
|
481
484
|
var saleUpdateSchema = import_zod2.z.object({
|
|
482
485
|
description: import_zod2.z.string().nullable().optional(),
|
|
@@ -489,7 +492,9 @@ var saleUpdateSchema = import_zod2.z.object({
|
|
|
489
492
|
metadata: metadataSchema.nullable().optional(),
|
|
490
493
|
customer_name: customerNameSchema.nullable().optional(),
|
|
491
494
|
customer_phone: customerPhoneSchema.nullable().optional(),
|
|
492
|
-
customer_email: customerEmailSchema.nullable().optional()
|
|
495
|
+
customer_email: customerEmailSchema.nullable().optional(),
|
|
496
|
+
// NOT NULL server-side: omit to leave unchanged (explicit null is rejected).
|
|
497
|
+
sms_consent: import_zod2.z.boolean().optional()
|
|
493
498
|
}).strict().superRefine((val, ctx) => refineServiceXor(val, ctx, { requireAmount: false }));
|
|
494
499
|
var TRACKING_RANGES = ["24h", "7d", "30d"];
|
|
495
500
|
var NAMED_RANGES = [
|