@aglyn/plugins-bookings 1.0.0-beta.143

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.
Files changed (63) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +7 -0
  3. package/package.json +53 -0
  4. package/src/index.d.ts +19 -0
  5. package/src/index.js +20 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/book-meeting-action.d.ts +91 -0
  8. package/src/lib/components/book-meeting-action.js +296 -0
  9. package/src/lib/components/book-meeting-action.js.map +1 -0
  10. package/src/lib/components/booking.d.ts +33 -0
  11. package/src/lib/components/booking.js +463 -0
  12. package/src/lib/components/booking.js.map +1 -0
  13. package/src/lib/components/bookings-console-page.d.ts +12 -0
  14. package/src/lib/components/bookings-console-page.js +743 -0
  15. package/src/lib/components/bookings-console-page.js.map +1 -0
  16. package/src/lib/constants/bundle-common.d.ts +26 -0
  17. package/src/lib/constants/bundle-common.js +26 -0
  18. package/src/lib/constants/bundle-common.js.map +1 -0
  19. package/src/lib/model/booking-purchase-analytics.d.ts +129 -0
  20. package/src/lib/model/booking-purchase-analytics.js +197 -0
  21. package/src/lib/model/booking-purchase-analytics.js.map +1 -0
  22. package/src/lib/model/bookings.d.ts +141 -0
  23. package/src/lib/model/bookings.js +124 -0
  24. package/src/lib/model/bookings.js.map +1 -0
  25. package/src/lib/model/index.d.ts +22 -0
  26. package/src/lib/model/index.js +22 -0
  27. package/src/lib/model/index.js.map +1 -0
  28. package/src/lib/plugin-config.d.ts +24 -0
  29. package/src/lib/plugin-config.js +51 -0
  30. package/src/lib/plugin-config.js.map +1 -0
  31. package/src/lib/plugin.d.ts +22 -0
  32. package/src/lib/plugin.js +70 -0
  33. package/src/lib/plugin.js.map +1 -0
  34. package/src/lib/server/billing-webhook.d.ts +23 -0
  35. package/src/lib/server/billing-webhook.js +345 -0
  36. package/src/lib/server/billing-webhook.js.map +1 -0
  37. package/src/lib/server/booking-analytics.d.ts +68 -0
  38. package/src/lib/server/booking-analytics.js +115 -0
  39. package/src/lib/server/booking-analytics.js.map +1 -0
  40. package/src/lib/server/booking-crm.d.ts +39 -0
  41. package/src/lib/server/booking-crm.js +67 -0
  42. package/src/lib/server/booking-crm.js.map +1 -0
  43. package/src/lib/server/booking-figures.d.ts +32 -0
  44. package/src/lib/server/booking-figures.js +155 -0
  45. package/src/lib/server/booking-figures.js.map +1 -0
  46. package/src/lib/server/refund.d.ts +65 -0
  47. package/src/lib/server/refund.js +292 -0
  48. package/src/lib/server/refund.js.map +1 -0
  49. package/src/lib/server.d.ts +63 -0
  50. package/src/lib/server.js +968 -0
  51. package/src/lib/server.js.map +1 -0
  52. package/src/lib/site.d.ts +27 -0
  53. package/src/lib/site.js +50 -0
  54. package/src/lib/site.js.map +1 -0
  55. package/src/lib/testing/stand-in-tax-profile.d.ts +29 -0
  56. package/src/lib/testing/stand-in-tax-profile.js +64 -0
  57. package/src/lib/testing/stand-in-tax-profile.js.map +1 -0
  58. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  59. package/src/lib/utils/generate-preset-id.js +25 -0
  60. package/src/lib/utils/generate-preset-id.js.map +1 -0
  61. package/src/lib/utils/use-booking-purchase-event.d.ts +65 -0
  62. package/src/lib/utils/use-booking-purchase-event.js +147 -0
  63. package/src/lib/utils/use-booking-purchase-event.js.map +1 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type CrmBookingRef } from '@aglyn/aglyn/app-utils/crm-booking';
18
+ /**
19
+ * Bookings v1 (AGL-159): services with weekly availability windows and
20
+ * pure, timezone-explicit slot computation. All times are minutes since
21
+ * midnight in the HOST's timezone; instants are epoch milliseconds. No
22
+ * I/O here — collision checks against stored bookings happen in the
23
+ * booking API over the values these helpers produce.
24
+ */
25
+ /** `hosts/{hostId}/services/{id}` doc. */
26
+ export interface HostBookingService {
27
+ name: string;
28
+ durationMinutes: number;
29
+ description?: string;
30
+ /** Optional price; 0/absent means free. */
31
+ priceUsd?: number;
32
+ /**
33
+ * Weekly availability: `windows[weekday]` (0 = Sunday … 6 = Saturday)
34
+ * lists open intervals in minutes since midnight, host-local.
35
+ */
36
+ windows?: Partial<Record<number, Array<{
37
+ start: number;
38
+ end: number;
39
+ }>>>;
40
+ /** IANA timezone the windows are defined in, e.g. "America/Chicago". */
41
+ timezone?: string;
42
+ /**
43
+ * Create a follow-up task on the CRM record when this service is booked
44
+ * (AGL-2660) — due one business day after the slot, on whoever holds the
45
+ * relationship. Off unless the service says so.
46
+ */
47
+ crmFollowUpTask?: boolean;
48
+ /**
49
+ * File the booking as a `meeting` on the CRM record's timeline (AGL-2660).
50
+ * Absent means ON: a booking is a meeting the record has, and the service
51
+ * has to opt out of saying so.
52
+ */
53
+ crmMeetingActivity?: boolean;
54
+ }
55
+ /** Booked interval as epoch-ms instants. */
56
+ export interface BookedInterval {
57
+ startsAtMs: number;
58
+ endsAtMs: number;
59
+ }
60
+ export declare const BOOKING_MIN_DURATION_MINUTES = 5;
61
+ export declare const BOOKING_MAX_DURATION_MINUTES: number;
62
+ /** Slot enumeration horizon; the console/API never look further out. */
63
+ export declare const BOOKING_MAX_DAYS_AHEAD = 60;
64
+ /**
65
+ * The band a booking has to fall in to earn its 24-hour reminder, in hours
66
+ * from now (AGL-160, scheduled in AGL-2431).
67
+ *
68
+ * IN THE MODEL, not beside the scan that uses them, so the console card can
69
+ * apply the identical window without importing server code into a client
70
+ * bundle. That sharing is the point rather than a convenience: a card that
71
+ * drew its own boundary would report a queue depth the job does not act on,
72
+ * and a merchant checking whether reminders are working would be reading a
73
+ * number produced by different arithmetic than the sender's.
74
+ */
75
+ export declare const REMINDER_WINDOW_START_HOURS = 23;
76
+ export declare const REMINDER_WINDOW_END_HOURS = 25;
77
+ /**
78
+ * Whether `booking` is one this pass would mail — the three tests the scan
79
+ * applies, in one place both halves call.
80
+ *
81
+ * `nowMs` is a parameter and not `Date.now()` so the card and the job can be
82
+ * asked the same question about the same instant, and so a test can pin a
83
+ * booking exactly on either edge of the band.
84
+ */
85
+ export declare function isBookingReminderDue(booking: {
86
+ status?: string;
87
+ email?: string;
88
+ reminderSentAt?: unknown;
89
+ startsAtMs?: number;
90
+ }, nowMs: number): boolean;
91
+ export interface BookingSlot {
92
+ startsAtMs: number;
93
+ endsAtMs: number;
94
+ }
95
+ /**
96
+ * Enumerates open slots for a service between two instants: windows are
97
+ * walked in the service timezone, quantized to the service duration, and
98
+ * intervals overlapping `booked` (or starting before `fromMs`) drop out.
99
+ * Bounded: at most `limit` slots, never beyond BOOKING_MAX_DAYS_AHEAD.
100
+ */
101
+ export declare function computeOpenSlots(service: HostBookingService, fromMs: number, toMs: number, booked?: BookedInterval[], limit?: number): BookingSlot[];
102
+ /** True when the exact slot is open for the service (booking API check). */
103
+ export declare function isSlotOpen(service: HostBookingService, startsAtMs: number, booked?: BookedInterval[]): boolean;
104
+ /**
105
+ * The page on the site that holds the Booking block, as the plugin's
106
+ * `bookingPath` setting stores it (AGL-2660). The site root by default: a
107
+ * site that put the block on its home page needs no setting, and a site
108
+ * that put it elsewhere says where once, per site.
109
+ */
110
+ export declare const BOOKING_PATH_DEFAULT = "/";
111
+ /** `/book`, `book/`, ` /book?x ` → `/book`; anything empty → the root. */
112
+ export declare function normalizeBookingPath(raw: unknown): string;
113
+ export interface BookingLinkInput {
114
+ /** The host's public naming — its custom domain or its subdomain. */
115
+ site: {
116
+ cname?: string | null;
117
+ subdomain?: string | null;
118
+ } | null | undefined;
119
+ /** The service the link opens the widget on. */
120
+ service: {
121
+ id: string;
122
+ };
123
+ /** The site's `bookingPath` setting; the root when unset. */
124
+ path?: string | null;
125
+ /** The CRM record the link is dropped from, when there is one. */
126
+ crmRef?: CrmBookingRef | null;
127
+ }
128
+ /**
129
+ * The public URL a visitor books a service at (AGL-2660), or `null` for a
130
+ * site with no public origin yet.
131
+ *
132
+ * PER SERVICE, not per member: the model carries no staff or per-member
133
+ * notion — a service has one calendar — so there is one link per service
134
+ * and nothing narrower. The Bookings plugin renders no route of its own
135
+ * either; the widget is a block the site owner placed on a page, so the
136
+ * link is that page (the `bookingPath` setting) with the service
137
+ * preselected and, when dropped from a CRM record, the record carried
138
+ * along so the booking can be attributed even when the booker uses a
139
+ * different address.
140
+ */
141
+ export declare function bookingLinkFor(input: BookingLinkInput): string | null;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ // Leaf paths, not the barrel: this model is read by the client bundle and
17
+ // the `/server` entry alike, and either barrel would drag the other's
18
+ // surface into a bundle that has no use for it.
19
+ import { BOOKING_SERVICE_PARAM, CRM_BOOKING_REF_PARAM, formatCrmBookingRef } from "@aglyn/aglyn/app-utils/crm-booking";
20
+ import { hostPublicOrigin } from "@aglyn/aglyn/app-utils/host-naming";
21
+ // The leaf entry: the package root extends the Firestore SDK's `Timestamp`,
22
+ // and this model is read on published pages.
23
+ import { zonedDateTime } from "@aglyn/shared-util-timestamp/zoned-time";
24
+ export const BOOKING_MIN_DURATION_MINUTES = 5;
25
+ export const BOOKING_MAX_DURATION_MINUTES = 8 * 60;
26
+ /** Slot enumeration horizon; the console/API never look further out. */ export const BOOKING_MAX_DAYS_AHEAD = 60;
27
+ /**
28
+ * The band a booking has to fall in to earn its 24-hour reminder, in hours
29
+ * from now (AGL-160, scheduled in AGL-2431).
30
+ *
31
+ * IN THE MODEL, not beside the scan that uses them, so the console card can
32
+ * apply the identical window without importing server code into a client
33
+ * bundle. That sharing is the point rather than a convenience: a card that
34
+ * drew its own boundary would report a queue depth the job does not act on,
35
+ * and a merchant checking whether reminders are working would be reading a
36
+ * number produced by different arithmetic than the sender's.
37
+ */ export const REMINDER_WINDOW_START_HOURS = 23;
38
+ export const REMINDER_WINDOW_END_HOURS = 25;
39
+ /**
40
+ * Whether `booking` is one this pass would mail — the three tests the scan
41
+ * applies, in one place both halves call.
42
+ *
43
+ * `nowMs` is a parameter and not `Date.now()` so the card and the job can be
44
+ * asked the same question about the same instant, and so a test can pin a
45
+ * booking exactly on either edge of the band.
46
+ */ export function isBookingReminderDue(booking, nowMs) {
47
+ var _booking_startsAtMs;
48
+ const startsAtMs = Number((_booking_startsAtMs = booking.startsAtMs) != null ? _booking_startsAtMs : 0);
49
+ return booking.status !== 'canceled' && !booking.reminderSentAt && Boolean(booking.email) && startsAtMs >= nowMs + REMINDER_WINDOW_START_HOURS * 60 * 60 * 1000 && startsAtMs <= nowMs + REMINDER_WINDOW_END_HOURS * 60 * 60 * 1000;
50
+ }
51
+ /**
52
+ * Enumerates open slots for a service between two instants: windows are
53
+ * walked in the service timezone, quantized to the service duration, and
54
+ * intervals overlapping `booked` (or starting before `fromMs`) drop out.
55
+ * Bounded: at most `limit` slots, never beyond BOOKING_MAX_DAYS_AHEAD.
56
+ */ export function computeOpenSlots(service, fromMs, toMs, booked = [], limit = 200) {
57
+ const timezone = service.timezone || 'UTC';
58
+ const duration = Math.min(Math.max(Math.round(service.durationMinutes || 0), BOOKING_MIN_DURATION_MINUTES), BOOKING_MAX_DURATION_MINUTES);
59
+ const durationMs = duration * 60000;
60
+ const horizonMs = Math.min(toMs, fromMs + BOOKING_MAX_DAYS_AHEAD * 24 * 60 * 60000);
61
+ const slots = [];
62
+ // Walk in 15-minute steps and keep instants that start a window-aligned
63
+ // slot — O(minutes/15) and immune to DST arithmetic because weekday and
64
+ // minutes come from Intl per instant.
65
+ const stepMs = 15 * 60000;
66
+ const alignedFrom = Math.ceil(fromMs / stepMs) * stepMs;
67
+ for(let atMs = alignedFrom; atMs + durationMs <= horizonMs && slots.length < limit; atMs += stepMs){
68
+ var _ref;
69
+ var _service_windows;
70
+ const { weekday, hour, minute } = zonedDateTime(atMs, timezone);
71
+ const minutes = hour * 60 + minute;
72
+ const windows = (_ref = (_service_windows = service.windows) == null ? void 0 : _service_windows[weekday]) != null ? _ref : [];
73
+ const fitsWindow = windows.some((window)=>minutes >= window.start && minutes + duration <= window.end);
74
+ if (!fitsWindow) continue;
75
+ const endMs = atMs + durationMs;
76
+ const collides = booked.some((interval)=>atMs < interval.endsAtMs && endMs > interval.startsAtMs);
77
+ if (collides) continue;
78
+ slots.push({
79
+ startsAtMs: atMs,
80
+ endsAtMs: endMs
81
+ });
82
+ }
83
+ return slots;
84
+ }
85
+ /** True when the exact slot is open for the service (booking API check). */ export function isSlotOpen(service, startsAtMs, booked = []) {
86
+ const slots = computeOpenSlots(service, startsAtMs, startsAtMs + BOOKING_MAX_DURATION_MINUTES * 60000, booked, 1);
87
+ return slots.length > 0 && slots[0].startsAtMs === startsAtMs;
88
+ }
89
+ /**
90
+ * The page on the site that holds the Booking block, as the plugin's
91
+ * `bookingPath` setting stores it (AGL-2660). The site root by default: a
92
+ * site that put the block on its home page needs no setting, and a site
93
+ * that put it elsewhere says where once, per site.
94
+ */ export const BOOKING_PATH_DEFAULT = '/';
95
+ /** `/book`, `book/`, ` /book?x ` → `/book`; anything empty → the root. */ export function normalizeBookingPath(raw) {
96
+ const text = String(raw != null ? raw : '').trim().split(/[?#]/)[0];
97
+ if (!text) return BOOKING_PATH_DEFAULT;
98
+ const leading = text.startsWith('/') ? text : `/${text}`;
99
+ const trimmed = leading.replace(/\/+$/, '');
100
+ return trimmed || BOOKING_PATH_DEFAULT;
101
+ }
102
+ /**
103
+ * The public URL a visitor books a service at (AGL-2660), or `null` for a
104
+ * site with no public origin yet.
105
+ *
106
+ * PER SERVICE, not per member: the model carries no staff or per-member
107
+ * notion — a service has one calendar — so there is one link per service
108
+ * and nothing narrower. The Bookings plugin renders no route of its own
109
+ * either; the widget is a block the site owner placed on a page, so the
110
+ * link is that page (the `bookingPath` setting) with the service
111
+ * preselected and, when dropped from a CRM record, the record carried
112
+ * along so the booking can be attributed even when the booker uses a
113
+ * different address.
114
+ */ export function bookingLinkFor(input) {
115
+ const origin = hostPublicOrigin(input.site);
116
+ if (!origin) return null;
117
+ const query = new URLSearchParams({
118
+ [BOOKING_SERVICE_PARAM]: input.service.id
119
+ });
120
+ if (input.crmRef) query.set(CRM_BOOKING_REF_PARAM, formatCrmBookingRef(input.crmRef));
121
+ return `${origin}${normalizeBookingPath(input.path)}?${query.toString()}`;
122
+ }
123
+
124
+ //# sourceMappingURL=bookings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/bookings.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Leaf paths, not the barrel: this model is read by the client bundle and\n// the `/server` entry alike, and either barrel would drag the other's\n// surface into a bundle that has no use for it.\nimport {\n BOOKING_SERVICE_PARAM,\n CRM_BOOKING_REF_PARAM,\n type CrmBookingRef,\n formatCrmBookingRef,\n} from '@aglyn/aglyn/app-utils/crm-booking'\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\n// The leaf entry: the package root extends the Firestore SDK's `Timestamp`,\n// and this model is read on published pages.\nimport { zonedDateTime } from '@aglyn/shared-util-timestamp/zoned-time'\n\n/**\n * Bookings v1 (AGL-159): services with weekly availability windows and\n * pure, timezone-explicit slot computation. All times are minutes since\n * midnight in the HOST's timezone; instants are epoch milliseconds. No\n * I/O here — collision checks against stored bookings happen in the\n * booking API over the values these helpers produce.\n */\n\n/** `hosts/{hostId}/services/{id}` doc. */\nexport interface HostBookingService {\n name: string\n durationMinutes: number\n description?: string\n /** Optional price; 0/absent means free. */\n priceUsd?: number\n /**\n * Weekly availability: `windows[weekday]` (0 = Sunday … 6 = Saturday)\n * lists open intervals in minutes since midnight, host-local.\n */\n windows?: Partial<Record<number, Array<{ start: number; end: number }>>>\n /** IANA timezone the windows are defined in, e.g. \"America/Chicago\". */\n timezone?: string\n /**\n * Create a follow-up task on the CRM record when this service is booked\n * (AGL-2660) — due one business day after the slot, on whoever holds the\n * relationship. Off unless the service says so.\n */\n crmFollowUpTask?: boolean\n /**\n * File the booking as a `meeting` on the CRM record's timeline (AGL-2660).\n * Absent means ON: a booking is a meeting the record has, and the service\n * has to opt out of saying so.\n */\n crmMeetingActivity?: boolean\n}\n\n/** Booked interval as epoch-ms instants. */\nexport interface BookedInterval {\n startsAtMs: number\n endsAtMs: number\n}\n\nexport const BOOKING_MIN_DURATION_MINUTES = 5\nexport const BOOKING_MAX_DURATION_MINUTES = 8 * 60\n/** Slot enumeration horizon; the console/API never look further out. */\nexport const BOOKING_MAX_DAYS_AHEAD = 60\n\n/**\n * The band a booking has to fall in to earn its 24-hour reminder, in hours\n * from now (AGL-160, scheduled in AGL-2431).\n *\n * IN THE MODEL, not beside the scan that uses them, so the console card can\n * apply the identical window without importing server code into a client\n * bundle. That sharing is the point rather than a convenience: a card that\n * drew its own boundary would report a queue depth the job does not act on,\n * and a merchant checking whether reminders are working would be reading a\n * number produced by different arithmetic than the sender's.\n */\nexport const REMINDER_WINDOW_START_HOURS = 23\nexport const REMINDER_WINDOW_END_HOURS = 25\n\n/**\n * Whether `booking` is one this pass would mail — the three tests the scan\n * applies, in one place both halves call.\n *\n * `nowMs` is a parameter and not `Date.now()` so the card and the job can be\n * asked the same question about the same instant, and so a test can pin a\n * booking exactly on either edge of the band.\n */\nexport function isBookingReminderDue(\n booking: {\n status?: string\n email?: string\n reminderSentAt?: unknown\n startsAtMs?: number\n },\n nowMs: number,\n): boolean {\n const startsAtMs = Number(booking.startsAtMs ?? 0)\n return (\n booking.status !== 'canceled' &&\n !booking.reminderSentAt &&\n Boolean(booking.email) &&\n startsAtMs >= nowMs + REMINDER_WINDOW_START_HOURS * 60 * 60 * 1000 &&\n startsAtMs <= nowMs + REMINDER_WINDOW_END_HOURS * 60 * 60 * 1000\n )\n}\n\nexport interface BookingSlot {\n startsAtMs: number\n endsAtMs: number\n}\n\n/**\n * Enumerates open slots for a service between two instants: windows are\n * walked in the service timezone, quantized to the service duration, and\n * intervals overlapping `booked` (or starting before `fromMs`) drop out.\n * Bounded: at most `limit` slots, never beyond BOOKING_MAX_DAYS_AHEAD.\n */\nexport function computeOpenSlots(\n service: HostBookingService,\n fromMs: number,\n toMs: number,\n booked: BookedInterval[] = [],\n limit = 200,\n): BookingSlot[] {\n const timezone = service.timezone || 'UTC'\n const duration = Math.min(\n Math.max(\n Math.round(service.durationMinutes || 0),\n BOOKING_MIN_DURATION_MINUTES,\n ),\n BOOKING_MAX_DURATION_MINUTES,\n )\n const durationMs = duration * 60_000\n const horizonMs = Math.min(\n toMs,\n fromMs + BOOKING_MAX_DAYS_AHEAD * 24 * 60 * 60_000,\n )\n const slots: BookingSlot[] = []\n // Walk in 15-minute steps and keep instants that start a window-aligned\n // slot — O(minutes/15) and immune to DST arithmetic because weekday and\n // minutes come from Intl per instant.\n const stepMs = 15 * 60_000\n const alignedFrom = Math.ceil(fromMs / stepMs) * stepMs\n for (\n let atMs = alignedFrom;\n atMs + durationMs <= horizonMs && slots.length < limit;\n atMs += stepMs\n ) {\n const { weekday, hour, minute } = zonedDateTime(atMs, timezone)\n const minutes = hour * 60 + minute\n const windows = service.windows?.[weekday] ?? []\n const fitsWindow = windows.some(\n (window) =>\n minutes >= window.start && minutes + duration <= window.end,\n )\n if (!fitsWindow) continue\n const endMs = atMs + durationMs\n const collides = booked.some(\n (interval) =>\n atMs < interval.endsAtMs && endMs > interval.startsAtMs,\n )\n if (collides) continue\n slots.push({ startsAtMs: atMs, endsAtMs: endMs })\n }\n return slots\n}\n\n/** True when the exact slot is open for the service (booking API check). */\nexport function isSlotOpen(\n service: HostBookingService,\n startsAtMs: number,\n booked: BookedInterval[] = [],\n): boolean {\n const slots = computeOpenSlots(\n service,\n startsAtMs,\n startsAtMs + BOOKING_MAX_DURATION_MINUTES * 60_000,\n booked,\n 1,\n )\n return slots.length > 0 && slots[0].startsAtMs === startsAtMs\n}\n\n/**\n * The page on the site that holds the Booking block, as the plugin's\n * `bookingPath` setting stores it (AGL-2660). The site root by default: a\n * site that put the block on its home page needs no setting, and a site\n * that put it elsewhere says where once, per site.\n */\nexport const BOOKING_PATH_DEFAULT = '/'\n\n/** `/book`, `book/`, ` /book?x ` → `/book`; anything empty → the root. */\nexport function normalizeBookingPath(raw: unknown): string {\n const text = String(raw ?? '')\n .trim()\n .split(/[?#]/)[0]\n if (!text) return BOOKING_PATH_DEFAULT\n const leading = text.startsWith('/') ? text : `/${text}`\n const trimmed = leading.replace(/\\/+$/, '')\n return trimmed || BOOKING_PATH_DEFAULT\n}\n\nexport interface BookingLinkInput {\n /** The host's public naming — its custom domain or its subdomain. */\n site: { cname?: string | null; subdomain?: string | null } | null | undefined\n /** The service the link opens the widget on. */\n service: { id: string }\n /** The site's `bookingPath` setting; the root when unset. */\n path?: string | null\n /** The CRM record the link is dropped from, when there is one. */\n crmRef?: CrmBookingRef | null\n}\n\n/**\n * The public URL a visitor books a service at (AGL-2660), or `null` for a\n * site with no public origin yet.\n *\n * PER SERVICE, not per member: the model carries no staff or per-member\n * notion — a service has one calendar — so there is one link per service\n * and nothing narrower. The Bookings plugin renders no route of its own\n * either; the widget is a block the site owner placed on a page, so the\n * link is that page (the `bookingPath` setting) with the service\n * preselected and, when dropped from a CRM record, the record carried\n * along so the booking can be attributed even when the booker uses a\n * different address.\n */\nexport function bookingLinkFor(input: BookingLinkInput): string | null {\n const origin = hostPublicOrigin(input.site)\n if (!origin) return null\n const query = new URLSearchParams({ [BOOKING_SERVICE_PARAM]: input.service.id })\n if (input.crmRef) query.set(CRM_BOOKING_REF_PARAM, formatCrmBookingRef(input.crmRef))\n return `${origin}${normalizeBookingPath(input.path)}?${query.toString()}`\n}\n"],"names":["BOOKING_SERVICE_PARAM","CRM_BOOKING_REF_PARAM","formatCrmBookingRef","hostPublicOrigin","zonedDateTime","BOOKING_MIN_DURATION_MINUTES","BOOKING_MAX_DURATION_MINUTES","BOOKING_MAX_DAYS_AHEAD","REMINDER_WINDOW_START_HOURS","REMINDER_WINDOW_END_HOURS","isBookingReminderDue","booking","nowMs","startsAtMs","Number","status","reminderSentAt","Boolean","email","computeOpenSlots","service","fromMs","toMs","booked","limit","timezone","duration","Math","min","max","round","durationMinutes","durationMs","horizonMs","slots","stepMs","alignedFrom","ceil","atMs","length","weekday","hour","minute","minutes","windows","fitsWindow","some","window","start","end","endMs","collides","interval","endsAtMs","push","isSlotOpen","BOOKING_PATH_DEFAULT","normalizeBookingPath","raw","text","String","trim","split","leading","startsWith","trimmed","replace","bookingLinkFor","input","origin","site","query","URLSearchParams","id","crmRef","set","path","toString"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Bookings domain model (AGL-412): slot computation relocated from core app-utils.
19
+ * Context-free — importable by client components, /server handlers, and
20
+ * other plugins/apps via `@aglyn/plugins-bookings/model`.
21
+ */
22
+ export * from './bookings';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * Bookings domain model (AGL-412): slot computation relocated from core app-utils.
18
+ * Context-free — importable by client components, /server handlers, and
19
+ * other plugins/apps via `@aglyn/plugins-bookings/model`.
20
+ */ export * from "./bookings.js";
21
+
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/index.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Bookings domain model (AGL-412): slot computation relocated from core app-utils.\n * Context-free — importable by client components, /server handlers, and\n * other plugins/apps via `@aglyn/plugins-bookings/model`.\n */\nexport * from './bookings'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GACD,cAAc,gBAAY"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginConfigSchema } from '@aglyn/aglyn';
18
+ /**
19
+ * Bookings plugin config schema (AGL-428) — the first adopter of the
20
+ * per-plugin settings framework. PURE DATA (type-only aglyn import): the
21
+ * client barrel registers it via '@aglyn/aglyn' and the /server entry via
22
+ * '@aglyn/aglyn/server', so neither bundle drags in the other's barrel.
23
+ */
24
+ export declare const BOOKINGS_CONFIG_SCHEMA: PluginConfigSchema;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { BOOKING_MAX_DAYS_AHEAD, BOOKING_PATH_DEFAULT } from "./model/index.js";
17
+ /**
18
+ * Bookings plugin config schema (AGL-428) — the first adopter of the
19
+ * per-plugin settings framework. PURE DATA (type-only aglyn import): the
20
+ * client barrel registers it via '@aglyn/aglyn' and the /server entry via
21
+ * '@aglyn/aglyn/server', so neither bundle drags in the other's barrel.
22
+ */ export const BOOKINGS_CONFIG_SCHEMA = {
23
+ pluginId: 'bookings',
24
+ fields: [
25
+ {
26
+ key: 'maxDaysAhead',
27
+ label: 'Booking horizon (days)',
28
+ type: 'number',
29
+ min: 1,
30
+ max: 365,
31
+ description: 'How far ahead visitors can book. Slots beyond this many days ' + 'from now are not offered.'
32
+ },
33
+ {
34
+ // Where a booking link sends people (AGL-2660). The Booking block is
35
+ // placed on whichever page the site owner chose, and nothing records
36
+ // which; the CRM's "Book a meeting" links and the "Insert booking
37
+ // link" chip build on this path. Per site, through the ordinary
38
+ // site-level override.
39
+ key: 'bookingPath',
40
+ label: 'Booking page path',
41
+ type: 'string',
42
+ description: 'The page on your site that holds the Booking block, e.g. /book. ' + 'Booking links from the CRM open this page with the service ' + 'preselected.'
43
+ }
44
+ ],
45
+ defaults: {
46
+ maxDaysAhead: BOOKING_MAX_DAYS_AHEAD,
47
+ bookingPath: BOOKING_PATH_DEFAULT
48
+ }
49
+ };
50
+
51
+ //# sourceMappingURL=plugin-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/plugins/bookings/src/lib/plugin-config.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginConfigSchema } from '@aglyn/aglyn'\nimport { BOOKING_MAX_DAYS_AHEAD, BOOKING_PATH_DEFAULT } from './model'\n\n/**\n * Bookings plugin config schema (AGL-428) — the first adopter of the\n * per-plugin settings framework. PURE DATA (type-only aglyn import): the\n * client barrel registers it via '@aglyn/aglyn' and the /server entry via\n * '@aglyn/aglyn/server', so neither bundle drags in the other's barrel.\n */\nexport const BOOKINGS_CONFIG_SCHEMA: PluginConfigSchema = {\n pluginId: 'bookings',\n fields: [\n {\n key: 'maxDaysAhead',\n label: 'Booking horizon (days)',\n type: 'number',\n min: 1,\n max: 365,\n description:\n 'How far ahead visitors can book. Slots beyond this many days ' +\n 'from now are not offered.',\n },\n {\n // Where a booking link sends people (AGL-2660). The Booking block is\n // placed on whichever page the site owner chose, and nothing records\n // which; the CRM's \"Book a meeting\" links and the \"Insert booking\n // link\" chip build on this path. Per site, through the ordinary\n // site-level override.\n key: 'bookingPath',\n label: 'Booking page path',\n type: 'string',\n description:\n 'The page on your site that holds the Booking block, e.g. /book. ' +\n 'Booking links from the CRM open this page with the service ' +\n 'preselected.',\n },\n ],\n defaults: { maxDaysAhead: BOOKING_MAX_DAYS_AHEAD, bookingPath: BOOKING_PATH_DEFAULT },\n}\n"],"names":["BOOKING_MAX_DAYS_AHEAD","BOOKING_PATH_DEFAULT","BOOKINGS_CONFIG_SCHEMA","pluginId","fields","key","label","type","min","max","description","defaults","maxDaysAhead","bookingPath"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,sBAAsB,EAAEC,oBAAoB,QAAQ,mBAAS;AAEtE;;;;;CAKC,GACD,OAAO,MAAMC,yBAA6C;IACxDC,UAAU;IACVC,QAAQ;QACN;YACEC,KAAK;YACLC,OAAO;YACPC,MAAM;YACNC,KAAK;YACLC,KAAK;YACLC,aACE,kEACA;QACJ;QACA;YACE,qEAAqE;YACrE,qEAAqE;YACrE,kEAAkE;YAClE,gEAAgE;YAChE,uBAAuB;YACvBL,KAAK;YACLC,OAAO;YACPC,MAAM;YACNG,aACE,qEACA,gEACA;QACJ;KACD;IACDC,UAAU;QAAEC,cAAcZ;QAAwBa,aAAaZ;IAAqB;AACtF,EAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * Console half only: registers the Bookings nav item + page. Safe to call
19
+ * at console app load — the page is lazy (no besigner/canvas code).
20
+ */
21
+ export declare function registerBookingsConsole(): void;
22
+ export * from './site';
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import * as Aglyn from "@aglyn/aglyn";
17
+ import { mdiCalendarClock } from "@aglyn/shared-data-mdi";
18
+ import { lazy } from "react";
19
+ import { BUNDLE_ID } from "./constants/bundle-common.js";
20
+ import { BOOKINGS_CONFIG_SCHEMA } from "./plugin-config.js";
21
+ /** Code-split: the Bookings console page only loads when opened. */ const BookingsConsolePage = lazy(()=>import("./components/bookings-console-page.js"));
22
+ /** "Book a meeting", loaded where a record page draws the zone for it. */ const BookMeetingButton = lazy(()=>import("./components/book-meeting-action.js"));
23
+ /**
24
+ * Console half only: registers the Bookings nav item + page. Safe to call
25
+ * at console app load — the page is lazy (no besigner/canvas code).
26
+ */ export function registerBookingsConsole() {
27
+ // Per-plugin settings (AGL-428): the schema powers the generic form on
28
+ // the Plugins & add-ons hub and defaults-merged reads everywhere.
29
+ Aglyn.registerPluginConfigSchema(BOOKINGS_CONFIG_SCHEMA);
30
+ Aglyn.registerConsoleExtension({
31
+ pluginId: BUNDLE_ID,
32
+ displayName: 'Bookings',
33
+ featureFlag: 'bookings',
34
+ // The control a CRM record carries for booking a meeting with the person.
35
+ // The CRM hosts the zone and hands it the record; the services, the page
36
+ // they are booked on and the link are this plugin's, so the control is
37
+ // too. It draws nothing where this site takes no bookings.
38
+ widgets: [
39
+ {
40
+ slot: 'crmRecordBooking',
41
+ widgetId: 'bookings-book-meeting',
42
+ title: 'Book a meeting',
43
+ Component: BookMeetingButton
44
+ }
45
+ ],
46
+ navItems: [
47
+ {
48
+ label: 'Bookings',
49
+ href: '/bookings',
50
+ // Reuse the existing release-flag nav-tab so staff-preview gating is
51
+ // unchanged now that the tab comes from the plugin (AGL-395).
52
+ navTabId: 'nav-tab-bookings',
53
+ icon: {
54
+ path: mdiCalendarClock.path
55
+ },
56
+ header: {
57
+ title: 'Bookings',
58
+ icon: {
59
+ path: mdiCalendarClock.path
60
+ },
61
+ docsTopic: 'bookings'
62
+ },
63
+ Component: BookingsConsolePage
64
+ }
65
+ ]
66
+ });
67
+ }
68
+ export * from "./site.js";
69
+
70
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/plugins/bookings/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiCalendarClock } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { BOOKINGS_CONFIG_SCHEMA } from './plugin-config'\n\n/** Code-split: the Bookings console page only loads when opened. */\nconst BookingsConsolePage = lazy(\n () => import('./components/bookings-console-page'),\n)\n\n/** \"Book a meeting\", loaded where a record page draws the zone for it. */\nconst BookMeetingButton = lazy(() => import('./components/book-meeting-action'))\n\n/**\n * Console half only: registers the Bookings nav item + page. Safe to call\n * at console app load — the page is lazy (no besigner/canvas code).\n */\nexport function registerBookingsConsole(): void {\n // Per-plugin settings (AGL-428): the schema powers the generic form on\n // the Plugins & add-ons hub and defaults-merged reads everywhere.\n Aglyn.registerPluginConfigSchema(BOOKINGS_CONFIG_SCHEMA)\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Bookings',\n featureFlag: 'bookings',\n // The control a CRM record carries for booking a meeting with the person.\n // The CRM hosts the zone and hands it the record; the services, the page\n // they are booked on and the link are this plugin's, so the control is\n // too. It draws nothing where this site takes no bookings.\n widgets: [\n {\n slot: 'crmRecordBooking',\n widgetId: 'bookings-book-meeting',\n title: 'Book a meeting',\n Component: BookMeetingButton,\n },\n ],\n navItems: [\n {\n label: 'Bookings',\n href: '/bookings',\n // Reuse the existing release-flag nav-tab so staff-preview gating is\n // unchanged now that the tab comes from the plugin (AGL-395).\n navTabId: 'nav-tab-bookings',\n icon: { path: mdiCalendarClock.path },\n header: {\n title: 'Bookings',\n icon: { path: mdiCalendarClock.path },\n docsTopic: 'bookings',\n },\n Component: BookingsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiCalendarClock","lazy","BUNDLE_ID","BOOKINGS_CONFIG_SCHEMA","BookingsConsolePage","BookMeetingButton","registerBookingsConsole","registerPluginConfigSchema","registerConsoleExtension","pluginId","displayName","featureFlag","widgets","slot","widgetId","title","Component","navItems","label","href","navTabId","icon","path","header","docsTopic"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,gBAAgB,QAAQ,yBAAwB;AACzD,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,sBAAsB,QAAQ,qBAAiB;AAExD,kEAAkE,GAClE,MAAMC,sBAAsBH,KAC1B,IAAM,MAAM,CAAC;AAGf,wEAAwE,GACxE,MAAMI,oBAAoBJ,KAAK,IAAM,MAAM,CAAC;AAE5C;;;CAGC,GACD,OAAO,SAASK;IACd,uEAAuE;IACvE,kEAAkE;IAClEP,MAAMQ,0BAA0B,CAACJ;IACjCJ,MAAMS,wBAAwB,CAAC;QAC7BC,UAAUP;QACVQ,aAAa;QACbC,aAAa;QACb,0EAA0E;QAC1E,yEAAyE;QACzE,uEAAuE;QACvE,2DAA2D;QAC3DC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVC,OAAO;gBACPC,WAAWX;YACb;SACD;QACDY,UAAU;YACR;gBACEC,OAAO;gBACPC,MAAM;gBACN,qEAAqE;gBACrE,8DAA8D;gBAC9DC,UAAU;gBACVC,MAAM;oBAAEC,MAAMtB,iBAAiBsB,IAAI;gBAAC;gBACpCC,QAAQ;oBACNR,OAAO;oBACPM,MAAM;wBAAEC,MAAMtB,iBAAiBsB,IAAI;oBAAC;oBACpCE,WAAW;gBACb;gBACAR,WAAWZ;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { BillingWebhookHandler } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Paid-booking section of the platform Stripe webhook (AGL-170/418):
20
+ * payment confirms the pendingPayment hold — relocated verbatim from the
21
+ * console route; registered via registerBookingsConsoleApi.
22
+ */
23
+ export declare const bookingsBillingWebhookHandler: BillingWebhookHandler;