@aglyn/plugins-bookings 1.0.0-beta.221 → 1.0.0-beta.223

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 (52) hide show
  1. package/README.md +3 -2
  2. package/package.json +11 -11
  3. package/src/lib/components/book-meeting-action.js +5 -3
  4. package/src/lib/components/book-meeting-action.js.map +1 -1
  5. package/src/lib/components/booking.d.ts +33 -0
  6. package/src/lib/components/booking.js +260 -54
  7. package/src/lib/components/booking.js.map +1 -1
  8. package/src/lib/components/bookings-console-page.js +139 -15
  9. package/src/lib/components/bookings-console-page.js.map +1 -1
  10. package/src/lib/declarations.console-server.d.ts +34 -0
  11. package/src/lib/declarations.console-server.js +84 -0
  12. package/src/lib/declarations.console-server.js.map +1 -0
  13. package/src/lib/model/booking-contact-fields.d.ts +88 -0
  14. package/src/lib/model/booking-contact-fields.js +130 -0
  15. package/src/lib/model/booking-contact-fields.js.map +1 -0
  16. package/src/lib/model/booking-price.d.ts +51 -0
  17. package/src/lib/model/booking-price.js +61 -0
  18. package/src/lib/model/booking-price.js.map +1 -0
  19. package/src/lib/model/booking-record.d.ts +7 -0
  20. package/src/lib/model/booking-record.js +11 -2
  21. package/src/lib/model/booking-record.js.map +1 -1
  22. package/src/lib/model/bookings-record-counts.d.ts +25 -0
  23. package/src/lib/model/bookings-record-counts.js +34 -0
  24. package/src/lib/model/bookings-record-counts.js.map +1 -0
  25. package/src/lib/model/bookings.d.ts +61 -4
  26. package/src/lib/model/bookings.js +81 -15
  27. package/src/lib/model/bookings.js.map +1 -1
  28. package/src/lib/model/index.d.ts +2 -0
  29. package/src/lib/model/index.js +2 -0
  30. package/src/lib/model/index.js.map +1 -1
  31. package/src/lib/plugin.js +14 -0
  32. package/src/lib/plugin.js.map +1 -1
  33. package/src/lib/server/billing-webhook.js +33 -15
  34. package/src/lib/server/billing-webhook.js.map +1 -1
  35. package/src/lib/server/booking-crm.js +4 -1
  36. package/src/lib/server/booking-crm.js.map +1 -1
  37. package/src/lib/server/person-eraser.d.ts +36 -0
  38. package/src/lib/server/person-eraser.js +48 -0
  39. package/src/lib/server/person-eraser.js.map +1 -0
  40. package/src/lib/server/refund.js +2 -2
  41. package/src/lib/server/refund.js.map +1 -1
  42. package/src/lib/server.js +97 -36
  43. package/src/lib/server.js.map +1 -1
  44. package/src/lib/transfer/bookings-transfer-common.d.ts +68 -0
  45. package/src/lib/transfer/bookings-transfer-common.js +130 -0
  46. package/src/lib/transfer/bookings-transfer-common.js.map +1 -0
  47. package/src/lib/transfer/bookings-transfer.d.ts +86 -0
  48. package/src/lib/transfer/bookings-transfer.js +613 -0
  49. package/src/lib/transfer/bookings-transfer.js.map +1 -0
  50. package/src/lib/transfer/bookings-transfer.server.d.ts +26 -0
  51. package/src/lib/transfer/bookings-transfer.server.js +31 -0
  52. package/src/lib/transfer/bookings-transfer.server.js.map +1 -0
@@ -0,0 +1,84 @@
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
+ */ // The registry's own module, not the `@aglyn/aglyn/server` barrel: boot needs
17
+ // one registry, not the whole server surface.
18
+ import { registerPluginPersonEraser } from "@aglyn/aglyn/plugin-manager/plugin-person-erasure";
19
+ import { registerPluginTransferResource } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
20
+ import { BUNDLE_ID } from "./constants/bundle-common.js";
21
+ import { BOOKINGS_TRANSFER_MATCH_KEYS, BOOKINGS_TRANSFER_RESOURCE } from "./transfer/bookings-transfer-common.js";
22
+ /**
23
+ * The `bookings` transfer resource over the platform's Firestore, built on
24
+ * first use: the resource module and the Admin SDK load with the first
25
+ * export, not at boot.
26
+ */ let bookingsTransfer = null;
27
+ function bookingsTransferResource() {
28
+ return bookingsTransfer != null ? bookingsTransfer : bookingsTransfer = import("./transfer/bookings-transfer.server.js").then(({ adminBookingsTransferResource })=>adminBookingsTransferResource()).catch((error)=>{
29
+ // A failed load is retried by the next export rather than remembered.
30
+ bookingsTransfer = null;
31
+ throw error;
32
+ });
33
+ }
34
+ /**
35
+ * The bookings plugin's CONSOLE-ONLY server declarations (AGL-3080), named
36
+ * under `consoleServerDeclarations` in `plugins.config.json` and run at the
37
+ * console's boot. The tenant runtime registers none of it.
38
+ *
39
+ * Its share of a person erasure: every booking the person made on a site of
40
+ * the workspace keeps its service and its time, with the person taken off it
41
+ * (`server/person-eraser.ts`). Declared `requiredPersonEraser`, because the
42
+ * erasure promises it — a console whose boot skipped this refuses to erase.
43
+ *
44
+ * Its `bookings` transfer resource: every booking of a site, exported and
45
+ * never imported (`transfer/bookings-transfer.ts` says why).
46
+ *
47
+ * Light at boot: the eraser, the export and the Admin SDK they bring load
48
+ * with the first erasure or export. Registering again replaces this plugin's
49
+ * own.
50
+ */ export function registerBookingsConsoleServerDeclarations() {
51
+ registerPluginPersonEraser(async (request)=>(await import("./server/person-eraser.js")).bookingsPersonEraser(request), {
52
+ pluginId: BUNDLE_ID
53
+ });
54
+ registerPluginTransferResource(BOOKINGS_TRANSFER_RESOURCE, {
55
+ fields: async (ctx)=>{
56
+ var _ref;
57
+ var _fields, _this;
58
+ return (_ref = (_fields = (_this = await bookingsTransferResource()).fields) == null ? void 0 : _fields.call(_this, ctx)) != null ? _ref : {
59
+ standard: []
60
+ };
61
+ },
62
+ // Read synchronously by the routes, so a plain array, not a loaded one.
63
+ matchKeys: BOOKINGS_TRANSFER_MATCH_KEYS,
64
+ count: async (ctx, options)=>{
65
+ var _ref;
66
+ var _count, _this;
67
+ return (_ref = (_count = (_this = await bookingsTransferResource()).count) == null ? void 0 : _count.call(_this, ctx, options)) != null ? _ref : 0;
68
+ },
69
+ readPage: async (ctx, cursor, fieldIds, options)=>{
70
+ const resource = await bookingsTransferResource();
71
+ if (!resource.readPage) throw new Error('bookings export: no reader');
72
+ return resource.readPage(ctx, cursor, fieldIds, options);
73
+ },
74
+ lookup: async (ctx, requests)=>{
75
+ const resource = await bookingsTransferResource();
76
+ if (!resource.lookup) throw new Error('bookings export: no lookup');
77
+ return resource.lookup(ctx, requests);
78
+ }
79
+ }, {
80
+ pluginId: BUNDLE_ID
81
+ });
82
+ }
83
+
84
+ //# sourceMappingURL=declarations.console-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../libs/plugins/bookings/src/lib/declarations.console-server.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// The registry's own module, not the `@aglyn/aglyn/server` barrel: boot needs\n// one registry, not the whole server surface.\nimport { registerPluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport {\n registerPluginTransferResource,\n type PluginTransferResource,\n} from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport {\n BOOKINGS_TRANSFER_MATCH_KEYS,\n BOOKINGS_TRANSFER_RESOURCE,\n} from './transfer/bookings-transfer-common'\n\n/**\n * The `bookings` transfer resource over the platform's Firestore, built on\n * first use: the resource module and the Admin SDK load with the first\n * export, not at boot.\n */\nlet bookingsTransfer: Promise<PluginTransferResource> | null = null\nfunction bookingsTransferResource(): Promise<PluginTransferResource> {\n return (bookingsTransfer ??= import('./transfer/bookings-transfer.server')\n .then(({ adminBookingsTransferResource }) =>\n adminBookingsTransferResource(),\n )\n .catch((error: unknown) => {\n // A failed load is retried by the next export rather than remembered.\n bookingsTransfer = null\n throw error\n }))\n}\n\n/**\n * The bookings plugin's CONSOLE-ONLY server declarations (AGL-3080), named\n * under `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot. The tenant runtime registers none of it.\n *\n * Its share of a person erasure: every booking the person made on a site of\n * the workspace keeps its service and its time, with the person taken off it\n * (`server/person-eraser.ts`). Declared `requiredPersonEraser`, because the\n * erasure promises it — a console whose boot skipped this refuses to erase.\n *\n * Its `bookings` transfer resource: every booking of a site, exported and\n * never imported (`transfer/bookings-transfer.ts` says why).\n *\n * Light at boot: the eraser, the export and the Admin SDK they bring load\n * with the first erasure or export. Registering again replaces this plugin's\n * own.\n */\nexport function registerBookingsConsoleServerDeclarations(): void {\n registerPluginPersonEraser(\n async (request) =>\n (await import('./server/person-eraser')).bookingsPersonEraser(request),\n { pluginId: BUNDLE_ID },\n )\n registerPluginTransferResource(\n BOOKINGS_TRANSFER_RESOURCE,\n {\n fields: async (ctx) =>\n (await bookingsTransferResource()).fields?.(ctx) ?? { standard: [] },\n // Read synchronously by the routes, so a plain array, not a loaded one.\n matchKeys: BOOKINGS_TRANSFER_MATCH_KEYS,\n count: async (ctx, options) =>\n (await bookingsTransferResource()).count?.(ctx, options) ?? 0,\n readPage: async (ctx, cursor, fieldIds, options) => {\n const resource = await bookingsTransferResource()\n if (!resource.readPage) throw new Error('bookings export: no reader')\n return resource.readPage(ctx, cursor, fieldIds, options)\n },\n lookup: async (ctx, requests) => {\n const resource = await bookingsTransferResource()\n if (!resource.lookup) throw new Error('bookings export: no lookup')\n return resource.lookup(ctx, requests)\n },\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerPluginPersonEraser","registerPluginTransferResource","BUNDLE_ID","BOOKINGS_TRANSFER_MATCH_KEYS","BOOKINGS_TRANSFER_RESOURCE","bookingsTransfer","bookingsTransferResource","then","adminBookingsTransferResource","catch","error","registerBookingsConsoleServerDeclarations","request","bookingsPersonEraser","pluginId","fields","ctx","standard","matchKeys","count","options","readPage","cursor","fieldIds","resource","Error","lookup","requests"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,8EAA8E;AAC9E,8CAA8C;AAC9C,SAASA,0BAA0B,QAAQ,oDAAmD;AAC9F,SACEC,8BAA8B,QAEzB,wDAAuD;AAC9D,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SACEC,4BAA4B,EAC5BC,0BAA0B,QACrB,yCAAqC;AAE5C;;;;CAIC,GACD,IAAIC,mBAA2D;AAC/D,SAASC;IACP,OAAQD,2BAAAA,mBAAAA,mBAAqB,MAAM,CAAC,0CACjCE,IAAI,CAAC,CAAC,EAAEC,6BAA6B,EAAE,GACtCA,iCAEDC,KAAK,CAAC,CAACC;QACN,sEAAsE;QACtEL,mBAAmB;QACnB,MAAMK;IACR;AACJ;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASC;IACdX,2BACE,OAAOY,UACL,AAAC,CAAA,MAAM,MAAM,CAAC,4BAAwB,EAAGC,oBAAoB,CAACD,UAChE;QAAEE,UAAUZ;IAAU;IAExBD,+BACEG,4BACA;QACEW,QAAQ,OAAOC;;gBACb,SAAC;4BAAD,UAAA,CAAC,QAAA,MAAMV,4BAA4BS,MAAM,qBAAzC,aAAC,OAA2CC,uBAAQ;gBAAEC,UAAU,EAAE;YAAC;;QACrE,wEAAwE;QACxEC,WAAWf;QACXgB,OAAO,OAAOH,KAAKI;;gBACjB,QAAC;4BAAD,SAAA,CAAC,QAAA,MAAMd,4BAA4Ba,KAAK,qBAAxC,YAAC,OAA0CH,KAAKI,2BAAY;;QAC9DC,UAAU,OAAOL,KAAKM,QAAQC,UAAUH;YACtC,MAAMI,WAAW,MAAMlB;YACvB,IAAI,CAACkB,SAASH,QAAQ,EAAE,MAAM,IAAII,MAAM;YACxC,OAAOD,SAASH,QAAQ,CAACL,KAAKM,QAAQC,UAAUH;QAClD;QACAM,QAAQ,OAAOV,KAAKW;YAClB,MAAMH,WAAW,MAAMlB;YACvB,IAAI,CAACkB,SAASE,MAAM,EAAE,MAAM,IAAID,MAAM;YACtC,OAAOD,SAASE,MAAM,CAACV,KAAKW;QAC9B;IACF,GACA;QAAEb,UAAUZ;IAAU;AAE1B"}
@@ -0,0 +1,88 @@
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
+ /** How a service asks for one field. Absent, or anything else, is `off`. */
18
+ export type BookingFieldAsk = 'off' | 'optional' | 'required';
19
+ export declare const BOOKING_FIELD_ASKS: readonly BookingFieldAsk[];
20
+ /** The setting as stored, read so that only the two real answers ask. */
21
+ export declare function bookingFieldAsk(value: unknown): BookingFieldAsk;
22
+ /** The longest phone the booking keeps, as typed. */
23
+ export declare const BOOKING_PHONE_MAX = 40;
24
+ /** The longest address the booking keeps. */
25
+ export declare const BOOKING_ADDRESS_MAX = 300;
26
+ /** What a service asks for, as the widget and the route both read it. */
27
+ export interface BookingContactAsks {
28
+ phone: BookingFieldAsk;
29
+ address: BookingFieldAsk;
30
+ }
31
+ /** The two settings off a stored service (or its public listing). */
32
+ export declare function bookingContactAsks(service: {
33
+ askPhone?: unknown;
34
+ askAddress?: unknown;
35
+ } | null | undefined): BookingContactAsks;
36
+ /** What the booker gave, as the booking stores it. */
37
+ export interface BookingContactFields {
38
+ phone?: string;
39
+ address?: string;
40
+ }
41
+ /**
42
+ * A phone as typed, or `null` when it cannot be one.
43
+ *
44
+ * E.164 when core's normalizer is confident — a US or Canadian number, or
45
+ * one written with its `+` — and the number as typed otherwise, so a visitor
46
+ * abroad who writes theirs the local way is not refused a booking over a
47
+ * format. Seven to fifteen digits is the whole test: the shortest real
48
+ * subscriber number to the longest E.164 allows.
49
+ */
50
+ export declare function readBookingPhone(raw: unknown): string | null;
51
+ /**
52
+ * An address as typed: trimmed, each line's runs of space collapsed, empty
53
+ * lines dropped, and cut to {@link BOOKING_ADDRESS_MAX}. `null` when nothing
54
+ * is left.
55
+ *
56
+ * Free text, deliberately not the postal shape a contact's address is
57
+ * (`AglynPostalAddress`). A booking's address is where the job is, which is
58
+ * often not where the person lives, and a visitor booking an estimate types
59
+ * "12 Oak St, Austin" into one box; five boxes would cost bookings for a
60
+ * structure nothing here computes with.
61
+ */
62
+ export declare function readBookingAddress(raw: unknown): string | null;
63
+ /**
64
+ * The phone and address a booking request carried, held to what the
65
+ * service asks for: a field the service does not ask for is dropped
66
+ * whatever was sent, an optional one is kept when it reads, and a required
67
+ * one that is missing or unreadable refuses the request with the sentence
68
+ * the visitor sees.
69
+ */
70
+ export declare function readBookingContactFields(asks: BookingContactAsks, input: {
71
+ phone?: unknown;
72
+ address?: unknown;
73
+ }): {
74
+ ok: true;
75
+ fields: BookingContactFields;
76
+ } | {
77
+ ok: false;
78
+ error: string;
79
+ };
80
+ /**
81
+ * The given fields as lines a person reads — `Phone: …`, `Address: …` —
82
+ * for the managers' notification, the CRM meeting and the console row.
83
+ * Read off a stored booking, so only what is there and a string is shown.
84
+ */
85
+ export declare function bookingContactLines(booking: {
86
+ phone?: unknown;
87
+ address?: unknown;
88
+ }): string[];
@@ -0,0 +1,130 @@
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
+ * WHAT A SERVICE ASKS THE BOOKER FOR, BEYOND A NAME AND AN EMAIL (AGL-3493).
18
+ *
19
+ * An on-site service — an estimate at the job, a visit to the house — is
20
+ * no use to the business without a number to call and the place to go. So
21
+ * a service may ask for a phone and for an address, each off, optional or
22
+ * required, and every half reads the answer through this one module: the
23
+ * widget to render and check the fields, the booking route to check them
24
+ * again (a public door never trusts the page that called it), and the
25
+ * console, the notification and the CRM to show what was given.
26
+ *
27
+ * Client-safe: no I/O, and the only import is core's phone normalizer.
28
+ */ import { normalizePhone } from "@aglyn/aglyn/foundation/definitions/contact.types";
29
+ export const BOOKING_FIELD_ASKS = [
30
+ 'off',
31
+ 'optional',
32
+ 'required'
33
+ ];
34
+ /** The setting as stored, read so that only the two real answers ask. */ export function bookingFieldAsk(value) {
35
+ return value === 'optional' || value === 'required' ? value : 'off';
36
+ }
37
+ /** The longest phone the booking keeps, as typed. */ export const BOOKING_PHONE_MAX = 40;
38
+ /** The longest address the booking keeps. */ export const BOOKING_ADDRESS_MAX = 300;
39
+ /** The two settings off a stored service (or its public listing). */ export function bookingContactAsks(service) {
40
+ return {
41
+ phone: bookingFieldAsk(service == null ? void 0 : service.askPhone),
42
+ address: bookingFieldAsk(service == null ? void 0 : service.askAddress)
43
+ };
44
+ }
45
+ /**
46
+ * A phone as typed, or `null` when it cannot be one.
47
+ *
48
+ * E.164 when core's normalizer is confident — a US or Canadian number, or
49
+ * one written with its `+` — and the number as typed otherwise, so a visitor
50
+ * abroad who writes theirs the local way is not refused a booking over a
51
+ * format. Seven to fifteen digits is the whole test: the shortest real
52
+ * subscriber number to the longest E.164 allows.
53
+ */ export function readBookingPhone(raw) {
54
+ var _normalizePhone;
55
+ const text = String(raw != null ? raw : '').replace(/\s+/g, ' ').trim();
56
+ if (!text || text.length > BOOKING_PHONE_MAX) return null;
57
+ if (/[^\d\s()+\-./]/.test(text.replace(/\b(?:ext|x)\.?\s*\d+$/i, ''))) {
58
+ return null;
59
+ }
60
+ const digits = text.replace(/\D/g, '').length;
61
+ if (digits < 7 || digits > 20) return null;
62
+ return (_normalizePhone = normalizePhone(text)) != null ? _normalizePhone : text;
63
+ }
64
+ /**
65
+ * An address as typed: trimmed, each line's runs of space collapsed, empty
66
+ * lines dropped, and cut to {@link BOOKING_ADDRESS_MAX}. `null` when nothing
67
+ * is left.
68
+ *
69
+ * Free text, deliberately not the postal shape a contact's address is
70
+ * (`AglynPostalAddress`). A booking's address is where the job is, which is
71
+ * often not where the person lives, and a visitor booking an estimate types
72
+ * "12 Oak St, Austin" into one box; five boxes would cost bookings for a
73
+ * structure nothing here computes with.
74
+ */ export function readBookingAddress(raw) {
75
+ const text = String(raw != null ? raw : '').split(/\r?\n/).map((line)=>line.replace(/\s+/g, ' ').trim()).filter(Boolean).join('\n').slice(0, BOOKING_ADDRESS_MAX).trim();
76
+ return text || null;
77
+ }
78
+ /**
79
+ * The phone and address a booking request carried, held to what the
80
+ * service asks for: a field the service does not ask for is dropped
81
+ * whatever was sent, an optional one is kept when it reads, and a required
82
+ * one that is missing or unreadable refuses the request with the sentence
83
+ * the visitor sees.
84
+ */ export function readBookingContactFields(asks, input) {
85
+ const fields = {};
86
+ if (asks.phone !== 'off') {
87
+ var _input_phone;
88
+ const given = String((_input_phone = input.phone) != null ? _input_phone : '').trim();
89
+ const phone = readBookingPhone(given);
90
+ if (phone) fields.phone = phone;
91
+ else if (given) return {
92
+ ok: false,
93
+ error: 'Enter a valid phone number'
94
+ };
95
+ else if (asks.phone === 'required') {
96
+ return {
97
+ ok: false,
98
+ error: 'Enter your phone number'
99
+ };
100
+ }
101
+ }
102
+ if (asks.address !== 'off') {
103
+ const address = readBookingAddress(input.address);
104
+ if (address) fields.address = address;
105
+ else if (asks.address === 'required') {
106
+ return {
107
+ ok: false,
108
+ error: 'Enter the address'
109
+ };
110
+ }
111
+ }
112
+ return {
113
+ ok: true,
114
+ fields
115
+ };
116
+ }
117
+ /**
118
+ * The given fields as lines a person reads — `Phone: …`, `Address: …` —
119
+ * for the managers' notification, the CRM meeting and the console row.
120
+ * Read off a stored booking, so only what is there and a string is shown.
121
+ */ export function bookingContactLines(booking) {
122
+ const lines = [];
123
+ const phone = typeof booking.phone === 'string' ? booking.phone.trim() : '';
124
+ const address = typeof booking.address === 'string' ? booking.address.replace(/\s*\n\s*/g, ', ').trim() : '';
125
+ if (phone) lines.push(`Phone: ${phone}`);
126
+ if (address) lines.push(`Address: ${address}`);
127
+ return lines;
128
+ }
129
+
130
+ //# sourceMappingURL=booking-contact-fields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-contact-fields.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 * WHAT A SERVICE ASKS THE BOOKER FOR, BEYOND A NAME AND AN EMAIL (AGL-3493).\n *\n * An on-site service — an estimate at the job, a visit to the house — is\n * no use to the business without a number to call and the place to go. So\n * a service may ask for a phone and for an address, each off, optional or\n * required, and every half reads the answer through this one module: the\n * widget to render and check the fields, the booking route to check them\n * again (a public door never trusts the page that called it), and the\n * console, the notification and the CRM to show what was given.\n *\n * Client-safe: no I/O, and the only import is core's phone normalizer.\n */\n\nimport { normalizePhone } from '@aglyn/aglyn/foundation/definitions/contact.types'\n\n/** How a service asks for one field. Absent, or anything else, is `off`. */\nexport type BookingFieldAsk = 'off' | 'optional' | 'required'\n\nexport const BOOKING_FIELD_ASKS: readonly BookingFieldAsk[] = [\n 'off',\n 'optional',\n 'required',\n]\n\n/** The setting as stored, read so that only the two real answers ask. */\nexport function bookingFieldAsk(value: unknown): BookingFieldAsk {\n return value === 'optional' || value === 'required' ? value : 'off'\n}\n\n/** The longest phone the booking keeps, as typed. */\nexport const BOOKING_PHONE_MAX = 40\n/** The longest address the booking keeps. */\nexport const BOOKING_ADDRESS_MAX = 300\n\n/** What a service asks for, as the widget and the route both read it. */\nexport interface BookingContactAsks {\n phone: BookingFieldAsk\n address: BookingFieldAsk\n}\n\n/** The two settings off a stored service (or its public listing). */\nexport function bookingContactAsks(\n service: { askPhone?: unknown; askAddress?: unknown } | null | undefined,\n): BookingContactAsks {\n return {\n phone: bookingFieldAsk(service?.askPhone),\n address: bookingFieldAsk(service?.askAddress),\n }\n}\n\n/** What the booker gave, as the booking stores it. */\nexport interface BookingContactFields {\n phone?: string\n address?: string\n}\n\n/**\n * A phone as typed, or `null` when it cannot be one.\n *\n * E.164 when core's normalizer is confident — a US or Canadian number, or\n * one written with its `+` — and the number as typed otherwise, so a visitor\n * abroad who writes theirs the local way is not refused a booking over a\n * format. Seven to fifteen digits is the whole test: the shortest real\n * subscriber number to the longest E.164 allows.\n */\nexport function readBookingPhone(raw: unknown): string | null {\n const text = String(raw ?? '')\n .replace(/\\s+/g, ' ')\n .trim()\n if (!text || text.length > BOOKING_PHONE_MAX) return null\n if (/[^\\d\\s()+\\-./]/.test(text.replace(/\\b(?:ext|x)\\.?\\s*\\d+$/i, ''))) {\n return null\n }\n const digits = text.replace(/\\D/g, '').length\n if (digits < 7 || digits > 20) return null\n return normalizePhone(text) ?? text\n}\n\n/**\n * An address as typed: trimmed, each line's runs of space collapsed, empty\n * lines dropped, and cut to {@link BOOKING_ADDRESS_MAX}. `null` when nothing\n * is left.\n *\n * Free text, deliberately not the postal shape a contact's address is\n * (`AglynPostalAddress`). A booking's address is where the job is, which is\n * often not where the person lives, and a visitor booking an estimate types\n * \"12 Oak St, Austin\" into one box; five boxes would cost bookings for a\n * structure nothing here computes with.\n */\nexport function readBookingAddress(raw: unknown): string | null {\n const text = String(raw ?? '')\n .split(/\\r?\\n/)\n .map((line) => line.replace(/\\s+/g, ' ').trim())\n .filter(Boolean)\n .join('\\n')\n .slice(0, BOOKING_ADDRESS_MAX)\n .trim()\n return text || null\n}\n\n/**\n * The phone and address a booking request carried, held to what the\n * service asks for: a field the service does not ask for is dropped\n * whatever was sent, an optional one is kept when it reads, and a required\n * one that is missing or unreadable refuses the request with the sentence\n * the visitor sees.\n */\nexport function readBookingContactFields(\n asks: BookingContactAsks,\n input: { phone?: unknown; address?: unknown },\n): { ok: true; fields: BookingContactFields } | { ok: false; error: string } {\n const fields: BookingContactFields = {}\n if (asks.phone !== 'off') {\n const given = String(input.phone ?? '').trim()\n const phone = readBookingPhone(given)\n if (phone) fields.phone = phone\n else if (given) return { ok: false, error: 'Enter a valid phone number' }\n else if (asks.phone === 'required') {\n return { ok: false, error: 'Enter your phone number' }\n }\n }\n if (asks.address !== 'off') {\n const address = readBookingAddress(input.address)\n if (address) fields.address = address\n else if (asks.address === 'required') {\n return { ok: false, error: 'Enter the address' }\n }\n }\n return { ok: true, fields }\n}\n\n/**\n * The given fields as lines a person reads — `Phone: …`, `Address: …` —\n * for the managers' notification, the CRM meeting and the console row.\n * Read off a stored booking, so only what is there and a string is shown.\n */\nexport function bookingContactLines(booking: {\n phone?: unknown\n address?: unknown\n}): string[] {\n const lines: string[] = []\n const phone = typeof booking.phone === 'string' ? booking.phone.trim() : ''\n const address =\n typeof booking.address === 'string'\n ? booking.address.replace(/\\s*\\n\\s*/g, ', ').trim()\n : ''\n if (phone) lines.push(`Phone: ${phone}`)\n if (address) lines.push(`Address: ${address}`)\n return lines\n}\n"],"names":["normalizePhone","BOOKING_FIELD_ASKS","bookingFieldAsk","value","BOOKING_PHONE_MAX","BOOKING_ADDRESS_MAX","bookingContactAsks","service","phone","askPhone","address","askAddress","readBookingPhone","raw","text","String","replace","trim","length","test","digits","readBookingAddress","split","map","line","filter","Boolean","join","slice","readBookingContactFields","asks","input","fields","given","ok","error","bookingContactLines","booking","lines","push"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;CAYC,GAED,SAASA,cAAc,QAAQ,oDAAmD;AAKlF,OAAO,MAAMC,qBAAiD;IAC5D;IACA;IACA;CACD,CAAA;AAED,uEAAuE,GACvE,OAAO,SAASC,gBAAgBC,KAAc;IAC5C,OAAOA,UAAU,cAAcA,UAAU,aAAaA,QAAQ;AAChE;AAEA,mDAAmD,GACnD,OAAO,MAAMC,oBAAoB,GAAE;AACnC,2CAA2C,GAC3C,OAAO,MAAMC,sBAAsB,IAAG;AAQtC,mEAAmE,GACnE,OAAO,SAASC,mBACdC,OAAwE;IAExE,OAAO;QACLC,OAAON,gBAAgBK,2BAAAA,QAASE,QAAQ;QACxCC,SAASR,gBAAgBK,2BAAAA,QAASI,UAAU;IAC9C;AACF;AAQA;;;;;;;;CAQC,GACD,OAAO,SAASC,iBAAiBC,GAAY;QAUpCb;IATP,MAAMc,OAAOC,OAAOF,cAAAA,MAAO,IACxBG,OAAO,CAAC,QAAQ,KAChBC,IAAI;IACP,IAAI,CAACH,QAAQA,KAAKI,MAAM,GAAGd,mBAAmB,OAAO;IACrD,IAAI,iBAAiBe,IAAI,CAACL,KAAKE,OAAO,CAAC,0BAA0B,MAAM;QACrE,OAAO;IACT;IACA,MAAMI,SAASN,KAAKE,OAAO,CAAC,OAAO,IAAIE,MAAM;IAC7C,IAAIE,SAAS,KAAKA,SAAS,IAAI,OAAO;IACtC,QAAOpB,kBAAAA,eAAec,iBAAfd,kBAAwBc;AACjC;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASO,mBAAmBR,GAAY;IAC7C,MAAMC,OAAOC,OAAOF,cAAAA,MAAO,IACxBS,KAAK,CAAC,SACNC,GAAG,CAAC,CAACC,OAASA,KAAKR,OAAO,CAAC,QAAQ,KAAKC,IAAI,IAC5CQ,MAAM,CAACC,SACPC,IAAI,CAAC,MACLC,KAAK,CAAC,GAAGvB,qBACTY,IAAI;IACP,OAAOH,QAAQ;AACjB;AAEA;;;;;;CAMC,GACD,OAAO,SAASe,yBACdC,IAAwB,EACxBC,KAA6C;IAE7C,MAAMC,SAA+B,CAAC;IACtC,IAAIF,KAAKtB,KAAK,KAAK,OAAO;YACHuB;QAArB,MAAME,QAAQlB,QAAOgB,eAAAA,MAAMvB,KAAK,YAAXuB,eAAe,IAAId,IAAI;QAC5C,MAAMT,QAAQI,iBAAiBqB;QAC/B,IAAIzB,OAAOwB,OAAOxB,KAAK,GAAGA;aACrB,IAAIyB,OAAO,OAAO;YAAEC,IAAI;YAAOC,OAAO;QAA6B;aACnE,IAAIL,KAAKtB,KAAK,KAAK,YAAY;YAClC,OAAO;gBAAE0B,IAAI;gBAAOC,OAAO;YAA0B;QACvD;IACF;IACA,IAAIL,KAAKpB,OAAO,KAAK,OAAO;QAC1B,MAAMA,UAAUW,mBAAmBU,MAAMrB,OAAO;QAChD,IAAIA,SAASsB,OAAOtB,OAAO,GAAGA;aACzB,IAAIoB,KAAKpB,OAAO,KAAK,YAAY;YACpC,OAAO;gBAAEwB,IAAI;gBAAOC,OAAO;YAAoB;QACjD;IACF;IACA,OAAO;QAAED,IAAI;QAAMF;IAAO;AAC5B;AAEA;;;;CAIC,GACD,OAAO,SAASI,oBAAoBC,OAGnC;IACC,MAAMC,QAAkB,EAAE;IAC1B,MAAM9B,QAAQ,OAAO6B,QAAQ7B,KAAK,KAAK,WAAW6B,QAAQ7B,KAAK,CAACS,IAAI,KAAK;IACzE,MAAMP,UACJ,OAAO2B,QAAQ3B,OAAO,KAAK,WACvB2B,QAAQ3B,OAAO,CAACM,OAAO,CAAC,aAAa,MAAMC,IAAI,KAC/C;IACN,IAAIT,OAAO8B,MAAMC,IAAI,CAAC,CAAC,OAAO,EAAE/B,OAAO;IACvC,IAAIE,SAAS4B,MAAMC,IAAI,CAAC,CAAC,SAAS,EAAE7B,SAAS;IAC7C,OAAO4B;AACT"}
@@ -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
+ */
17
+ /**
18
+ * HOW A SERVICE STATES ITS PRICE (AGL-3475).
19
+ *
20
+ * A contractor who quotes at the job has no price to type, and "free" says
21
+ * the wrong thing about work that is not free. So a service states its price
22
+ * one of four ways: the price it charges (the default), or "Price varies",
23
+ * "Free estimate" or "Contact for price". Any of the three labels books with
24
+ * no charge, like an estimate appointment: the stored price is neither shown
25
+ * nor charged, and the business quotes afterwards.
26
+ *
27
+ * The widget, the booking route and the console all read the answer here, so
28
+ * the page never shows a price the route would not charge, and the route
29
+ * never charges one the page did not show.
30
+ *
31
+ * Client-safe: no I/O.
32
+ */
33
+ /** How a service states its price. Absent, or anything else, is `fixed`. */
34
+ export type BookingPriceDisplay = 'fixed' | 'varies' | 'estimate' | 'contact';
35
+ export declare const BOOKING_PRICE_DISPLAYS: readonly BookingPriceDisplay[];
36
+ /** What a visitor reads in place of a price, for each label. */
37
+ export declare const BOOKING_PRICE_LABELS: Readonly<Record<Exclude<BookingPriceDisplay, 'fixed'>, string>>;
38
+ /** A stored value as one of the four; anything unrecognized is `fixed`. */
39
+ export declare function bookingPriceDisplay(value: unknown): BookingPriceDisplay;
40
+ type PricedService = {
41
+ priceUsd?: unknown;
42
+ priceDisplay?: unknown;
43
+ } | null | undefined;
44
+ /** What booking the service charges, in dollars: its price when it states one, else 0. */
45
+ export declare function bookingChargeUsd(service: PricedService): number;
46
+ /**
47
+ * The price as a visitor reads it: `$120`, `Free`, or the service's label.
48
+ * A label wins over a stored price, which it is never shown beside.
49
+ */
50
+ export declare function bookingPriceText(service: PricedService): string;
51
+ export {};
@@ -0,0 +1,61 @@
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
+ * HOW A SERVICE STATES ITS PRICE (AGL-3475).
18
+ *
19
+ * A contractor who quotes at the job has no price to type, and "free" says
20
+ * the wrong thing about work that is not free. So a service states its price
21
+ * one of four ways: the price it charges (the default), or "Price varies",
22
+ * "Free estimate" or "Contact for price". Any of the three labels books with
23
+ * no charge, like an estimate appointment: the stored price is neither shown
24
+ * nor charged, and the business quotes afterwards.
25
+ *
26
+ * The widget, the booking route and the console all read the answer here, so
27
+ * the page never shows a price the route would not charge, and the route
28
+ * never charges one the page did not show.
29
+ *
30
+ * Client-safe: no I/O.
31
+ */ /** How a service states its price. Absent, or anything else, is `fixed`. */ export const BOOKING_PRICE_DISPLAYS = [
32
+ 'fixed',
33
+ 'varies',
34
+ 'estimate',
35
+ 'contact'
36
+ ];
37
+ /** What a visitor reads in place of a price, for each label. */ export const BOOKING_PRICE_LABELS = {
38
+ varies: 'Price varies',
39
+ estimate: 'Free estimate',
40
+ contact: 'Contact for price'
41
+ };
42
+ /** A stored value as one of the four; anything unrecognized is `fixed`. */ export function bookingPriceDisplay(value) {
43
+ return value === 'varies' || value === 'estimate' || value === 'contact' ? value : 'fixed';
44
+ }
45
+ /** What booking the service charges, in dollars: its price when it states one, else 0. */ export function bookingChargeUsd(service) {
46
+ var _ref;
47
+ if (bookingPriceDisplay(service == null ? void 0 : service.priceDisplay) !== 'fixed') return 0;
48
+ const price = Number((_ref = service == null ? void 0 : service.priceUsd) != null ? _ref : 0);
49
+ return Number.isFinite(price) && price > 0 ? price : 0;
50
+ }
51
+ /**
52
+ * The price as a visitor reads it: `$120`, `Free`, or the service's label.
53
+ * A label wins over a stored price, which it is never shown beside.
54
+ */ export function bookingPriceText(service) {
55
+ const display = bookingPriceDisplay(service == null ? void 0 : service.priceDisplay);
56
+ if (display !== 'fixed') return BOOKING_PRICE_LABELS[display];
57
+ const charge = bookingChargeUsd(service);
58
+ return charge > 0 ? `$${charge}` : 'Free';
59
+ }
60
+
61
+ //# sourceMappingURL=booking-price.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-price.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 * HOW A SERVICE STATES ITS PRICE (AGL-3475).\n *\n * A contractor who quotes at the job has no price to type, and \"free\" says\n * the wrong thing about work that is not free. So a service states its price\n * one of four ways: the price it charges (the default), or \"Price varies\",\n * \"Free estimate\" or \"Contact for price\". Any of the three labels books with\n * no charge, like an estimate appointment: the stored price is neither shown\n * nor charged, and the business quotes afterwards.\n *\n * The widget, the booking route and the console all read the answer here, so\n * the page never shows a price the route would not charge, and the route\n * never charges one the page did not show.\n *\n * Client-safe: no I/O.\n */\n\n/** How a service states its price. Absent, or anything else, is `fixed`. */\nexport type BookingPriceDisplay = 'fixed' | 'varies' | 'estimate' | 'contact'\n\nexport const BOOKING_PRICE_DISPLAYS: readonly BookingPriceDisplay[] = [\n 'fixed',\n 'varies',\n 'estimate',\n 'contact',\n]\n\n/** What a visitor reads in place of a price, for each label. */\nexport const BOOKING_PRICE_LABELS: Readonly<Record<Exclude<BookingPriceDisplay, 'fixed'>, string>> = {\n varies: 'Price varies',\n estimate: 'Free estimate',\n contact: 'Contact for price',\n}\n\n/** A stored value as one of the four; anything unrecognized is `fixed`. */\nexport function bookingPriceDisplay(value: unknown): BookingPriceDisplay {\n return value === 'varies' || value === 'estimate' || value === 'contact' ? value : 'fixed'\n}\n\ntype PricedService = { priceUsd?: unknown; priceDisplay?: unknown } | null | undefined\n\n/** What booking the service charges, in dollars: its price when it states one, else 0. */\nexport function bookingChargeUsd(service: PricedService): number {\n if (bookingPriceDisplay(service?.priceDisplay) !== 'fixed') return 0\n const price = Number(service?.priceUsd ?? 0)\n return Number.isFinite(price) && price > 0 ? price : 0\n}\n\n/**\n * The price as a visitor reads it: `$120`, `Free`, or the service's label.\n * A label wins over a stored price, which it is never shown beside.\n */\nexport function bookingPriceText(service: PricedService): string {\n const display = bookingPriceDisplay(service?.priceDisplay)\n if (display !== 'fixed') return BOOKING_PRICE_LABELS[display]\n const charge = bookingChargeUsd(service)\n return charge > 0 ? `$${charge}` : 'Free'\n}\n"],"names":["BOOKING_PRICE_DISPLAYS","BOOKING_PRICE_LABELS","varies","estimate","contact","bookingPriceDisplay","value","bookingChargeUsd","service","priceDisplay","price","Number","priceUsd","isFinite","bookingPriceText","display","charge"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;CAeC,GAED,0EAA0E,GAG1E,OAAO,MAAMA,yBAAyD;IACpE;IACA;IACA;IACA;CACD,CAAA;AAED,8DAA8D,GAC9D,OAAO,MAAMC,uBAAwF;IACnGC,QAAQ;IACRC,UAAU;IACVC,SAAS;AACX,EAAC;AAED,yEAAyE,GACzE,OAAO,SAASC,oBAAoBC,KAAc;IAChD,OAAOA,UAAU,YAAYA,UAAU,cAAcA,UAAU,YAAYA,QAAQ;AACrF;AAIA,wFAAwF,GACxF,OAAO,SAASC,iBAAiBC,OAAsB;;IACrD,IAAIH,oBAAoBG,2BAAAA,QAASC,YAAY,MAAM,SAAS,OAAO;IACnE,MAAMC,QAAQC,eAAOH,2BAAAA,QAASI,QAAQ,mBAAI;IAC1C,OAAOD,OAAOE,QAAQ,CAACH,UAAUA,QAAQ,IAAIA,QAAQ;AACvD;AAEA;;;CAGC,GACD,OAAO,SAASI,iBAAiBN,OAAsB;IACrD,MAAMO,UAAUV,oBAAoBG,2BAAAA,QAASC,YAAY;IACzD,IAAIM,YAAY,SAAS,OAAOd,oBAAoB,CAACc,QAAQ;IAC7D,MAAMC,SAAST,iBAAiBC;IAChC,OAAOQ,SAAS,IAAI,CAAC,CAAC,EAAEA,QAAQ,GAAG;AACrC"}
@@ -52,11 +52,18 @@ export declare function parseBookingRecordRef(raw: unknown): BookingRecordRef |
52
52
  * in the service's own timezone, with the zone named so a rep in another
53
53
  * one is not misled. The body, not the subject: `subject` is the field a
54
54
  * sent email carries, and the timeline draws it as one.
55
+ *
56
+ * Then the phone and the address the booker gave, a line each, when the
57
+ * service asked for them (AGL-3493). The address lives HERE rather than on
58
+ * the person: it is where this job is, which is often not where the person
59
+ * lives, and the next booking may be somewhere else.
55
60
  */
56
61
  export declare function bookingMeetingBody(input: {
57
62
  serviceName: string;
58
63
  startsAtMs: number;
59
64
  timezone?: string | null;
65
+ phone?: unknown;
66
+ address?: unknown;
60
67
  }): string;
61
68
  /** The title of the task a service files when it asks for a follow-up. */
62
69
  export declare function bookingFollowUpTitle(serviceName: string): string;
@@ -25,7 +25,8 @@
25
25
  * finds the record. Neither plugin imports the other: what this module holds
26
26
  * — the link's query keys, the wire form of the record it carries, and the
27
27
  * words and the day of what is filed — is this plugin's own.
28
- */ import { isValidTimeZone, zonedDateTime } from "@aglyn/shared-util-timestamp/zoned-time";
28
+ */ import { bookingContactLines } from "./booking-contact-fields.js";
29
+ import { isValidTimeZone, zonedDateTime } from "@aglyn/shared-util-timestamp/zoned-time";
29
30
  /**
30
31
  * The query key a booking link carries the record on: `?crm=contact:abc`.
31
32
  *
@@ -93,11 +94,19 @@ const REF_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
93
94
  * in the service's own timezone, with the zone named so a rep in another
94
95
  * one is not misled. The body, not the subject: `subject` is the field a
95
96
  * sent email carries, and the timeline draws it as one.
97
+ *
98
+ * Then the phone and the address the booker gave, a line each, when the
99
+ * service asked for them (AGL-3493). The address lives HERE rather than on
100
+ * the person: it is where this job is, which is often not where the person
101
+ * lives, and the next booking may be somewhere else.
96
102
  */ export function bookingMeetingBody(input) {
97
103
  var _input_serviceName;
98
104
  const timezone = input.timezone || 'UTC';
99
105
  const service = String((_input_serviceName = input.serviceName) != null ? _input_serviceName : '').trim() || 'Booking';
100
- return `${service} — ${formatSlot(input.startsAtMs, timezone)} (${timezone})`;
106
+ return [
107
+ `${service} — ${formatSlot(input.startsAtMs, timezone)} (${timezone})`,
108
+ ...bookingContactLines(input)
109
+ ].join('\n');
101
110
  }
102
111
  /** The title of the task a service files when it asks for a follow-up. */ export function bookingFollowUpTitle(serviceName) {
103
112
  const service = String(serviceName != null ? serviceName : '').trim() || 'the booking';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-record.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 * WHAT A BOOKING CARRIES BACK TO THE RECORD IT CAME FROM (AGL-2660).\n *\n * A rep drops a booking link into an email from a record; the visitor books\n * through it; the booking comes back to the record as a meeting on its\n * timeline and, when the service asks for one, a follow-up task. The record\n * system draws the \"Book a meeting\" zone and hands it the record by its kind\n * and id; this plugin builds the link, carries the record on it, and files\n * the meeting back through the core's record-timeline seam, where the owner\n * finds the record. Neither plugin imports the other: what this module holds\n * — the link's query keys, the wire form of the record it carries, and the\n * words and the day of what is filed — is this plugin's own.\n */\n\nimport {\n isValidTimeZone,\n zonedDateTime,\n} from '@aglyn/shared-util-timestamp/zoned-time'\n\n/**\n * The query key a booking link carries the record on: `?crm=contact:abc`.\n *\n * The reference rides the LINK rather than relying on the booker's address\n * because the two routinely differ — a rep emails somebody's work address\n * and they book with a personal one — and a booking that could only be\n * matched by address would then land on nobody. The widget copies the value\n * onto the booking request, the booking row keeps it as `crmRef`, and the\n * server hands it to the record system to file the meeting under. The key\n * is the one links already sent carry, so it is never renamed.\n */\nexport const BOOKING_RECORD_PARAM = 'crm'\n\n/**\n * The query key that preselects a service in the booking widget:\n * `?service={serviceId}`. The widget lists every active service and this\n * opens it on the one the link was about.\n */\nexport const BOOKING_SERVICE_PARAM = 'service'\n\n/** A record of the record system's, by its kind and id in its own words. */\nexport interface BookingRecordRef {\n kind: string\n id: string\n}\n\n/**\n * What a kind and an id may look like inside the reference: a lowercase\n * word, and an id with nothing a path or a query could be built from. The\n * value arrives on a public, unauthenticated request; which kinds a record\n * system keeps is its own to say when the booking is filed.\n */\nconst REF_KIND_PATTERN = /^[a-z][a-z-]{0,31}$/\nconst REF_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\n/** `contact:abc` — the wire form of a reference. */\nexport function formatBookingRecordRef(ref: BookingRecordRef): string {\n return `${ref.kind}:${ref.id}`\n}\n\n/**\n * The reference a query value names, or `null` for anything that is not one.\n *\n * Strict on purpose: the value arrives on a public, unauthenticated booking\n * request, so a malformed one is dropped rather than stored — a booking\n * still lands, and is matched by the booker's address instead.\n */\nexport function parseBookingRecordRef(raw: unknown): BookingRecordRef | null {\n if (typeof raw !== 'string') return null\n const separator = raw.indexOf(':')\n if (separator <= 0) return null\n const kind = raw.slice(0, separator)\n const id = raw.slice(separator + 1)\n if (!REF_KIND_PATTERN.test(kind) || !REF_ID_PATTERN.test(id)) return null\n return { kind, id }\n}\n\n/** The slot, as the meeting entry and the task print it. */\nfunction formatSlot(startsAtMs: number, timezone: string): string {\n try {\n return new Intl.DateTimeFormat('en-US', {\n timeZone: timezone,\n dateStyle: 'full',\n timeStyle: 'short',\n }).format(new Date(startsAtMs))\n } catch {\n // An unknown zone name on the service falls back to UTC rather than\n // failing the write: a meeting filed with a slightly wrong clock is\n // better than no meeting at all.\n return new Intl.DateTimeFormat('en-US', {\n timeZone: 'UTC',\n dateStyle: 'full',\n timeStyle: 'short',\n }).format(new Date(startsAtMs))\n }\n}\n\n/**\n * What the meeting entry on the timeline says — the service and the slot,\n * in the service's own timezone, with the zone named so a rep in another\n * one is not misled. The body, not the subject: `subject` is the field a\n * sent email carries, and the timeline draws it as one.\n */\nexport function bookingMeetingBody(input: {\n serviceName: string\n startsAtMs: number\n timezone?: string | null\n}): string {\n const timezone = input.timezone || 'UTC'\n const service = String(input.serviceName ?? '').trim() || 'Booking'\n return `${service} — ${formatSlot(input.startsAtMs, timezone)} (${timezone})`\n}\n\n/** The title of the task a service files when it asks for a follow-up. */\nexport function bookingFollowUpTitle(serviceName: string): string {\n const service = String(serviceName ?? '').trim() || 'the booking'\n return `Follow up after ${service}`\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/**\n * Weekday (0 = Sunday … 6 = Saturday) of an instant in a timezone, read in\n * UTC when the zone name is one `Intl` does not know — for the reason\n * `formatSlot` gives.\n */\nfunction weekdayIn(atMs: number, timezone: string): number {\n return zonedDateTime(atMs, isValidTimeZone(timezone) ? timezone : 'UTC').weekday\n}\n\n/**\n * When the follow-up falls due: ONE BUSINESS DAY after the slot, at the\n * slot's own clock time.\n *\n * A meeting on a Friday is followed up on Monday, one on a Saturday or a\n * Sunday on Monday too; any other day is followed up the next. Whole days\n * are added to the instant rather than a calendar walked, so the arithmetic\n * cannot land on a clock time that does not exist across a DST change —\n * the task is due \"about this time tomorrow\", and an hour either way is not\n * a fact the task keeps.\n */\nexport function bookingFollowUpDueMs(\n endsAtMs: number,\n timezone?: string | null,\n): number {\n const weekday = weekdayIn(endsAtMs, timezone || 'UTC')\n // Friday (5) skips the weekend; Saturday (6) reaches Monday in two days.\n const daysAhead = weekday === 5 ? 3 : weekday === 6 ? 2 : 1\n return endsAtMs + daysAhead * DAY_MS\n}\n"],"names":["isValidTimeZone","zonedDateTime","BOOKING_RECORD_PARAM","BOOKING_SERVICE_PARAM","REF_KIND_PATTERN","REF_ID_PATTERN","formatBookingRecordRef","ref","kind","id","parseBookingRecordRef","raw","separator","indexOf","slice","test","formatSlot","startsAtMs","timezone","Intl","DateTimeFormat","timeZone","dateStyle","timeStyle","format","Date","bookingMeetingBody","input","service","String","serviceName","trim","bookingFollowUpTitle","DAY_MS","weekdayIn","atMs","weekday","bookingFollowUpDueMs","endsAtMs","daysAhead"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;CAYC,GAED,SACEA,eAAe,EACfC,aAAa,QACR,0CAAyC;AAEhD;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,uBAAuB,MAAK;AAEzC;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB,UAAS;AAQ9C;;;;;CAKC,GACD,MAAMC,mBAAmB;AACzB,MAAMC,iBAAiB;AAEvB,kDAAkD,GAClD,OAAO,SAASC,uBAAuBC,GAAqB;IAC1D,OAAO,GAAGA,IAAIC,IAAI,CAAC,CAAC,EAAED,IAAIE,EAAE,EAAE;AAChC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,sBAAsBC,GAAY;IAChD,IAAI,OAAOA,QAAQ,UAAU,OAAO;IACpC,MAAMC,YAAYD,IAAIE,OAAO,CAAC;IAC9B,IAAID,aAAa,GAAG,OAAO;IAC3B,MAAMJ,OAAOG,IAAIG,KAAK,CAAC,GAAGF;IAC1B,MAAMH,KAAKE,IAAIG,KAAK,CAACF,YAAY;IACjC,IAAI,CAACR,iBAAiBW,IAAI,CAACP,SAAS,CAACH,eAAeU,IAAI,CAACN,KAAK,OAAO;IACrE,OAAO;QAAED;QAAMC;IAAG;AACpB;AAEA,0DAA0D,GAC1D,SAASO,WAAWC,UAAkB,EAAEC,QAAgB;IACtD,IAAI;QACF,OAAO,IAAIC,KAAKC,cAAc,CAAC,SAAS;YACtCC,UAAUH;YACVI,WAAW;YACXC,WAAW;QACb,GAAGC,MAAM,CAAC,IAAIC,KAAKR;IACrB,EAAE,eAAM;QACN,oEAAoE;QACpE,oEAAoE;QACpE,iCAAiC;QACjC,OAAO,IAAIE,KAAKC,cAAc,CAAC,SAAS;YACtCC,UAAU;YACVC,WAAW;YACXC,WAAW;QACb,GAAGC,MAAM,CAAC,IAAIC,KAAKR;IACrB;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASS,mBAAmBC,KAIlC;QAEwBA;IADvB,MAAMT,WAAWS,MAAMT,QAAQ,IAAI;IACnC,MAAMU,UAAUC,QAAOF,qBAAAA,MAAMG,WAAW,YAAjBH,qBAAqB,IAAII,IAAI,MAAM;IAC1D,OAAO,GAAGH,QAAQ,GAAG,EAAEZ,WAAWW,MAAMV,UAAU,EAAEC,UAAU,EAAE,EAAEA,SAAS,CAAC,CAAC;AAC/E;AAEA,wEAAwE,GACxE,OAAO,SAASc,qBAAqBF,WAAmB;IACtD,MAAMF,UAAUC,OAAOC,sBAAAA,cAAe,IAAIC,IAAI,MAAM;IACpD,OAAO,CAAC,gBAAgB,EAAEH,SAAS;AACrC;AAEA,MAAMK,SAAS,KAAK,KAAK,KAAK;AAE9B;;;;CAIC,GACD,SAASC,UAAUC,IAAY,EAAEjB,QAAgB;IAC/C,OAAOjB,cAAckC,MAAMnC,gBAAgBkB,YAAYA,WAAW,OAAOkB,OAAO;AAClF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,qBACdC,QAAgB,EAChBpB,QAAwB;IAExB,MAAMkB,UAAUF,UAAUI,UAAUpB,YAAY;IAChD,yEAAyE;IACzE,MAAMqB,YAAYH,YAAY,IAAI,IAAIA,YAAY,IAAI,IAAI;IAC1D,OAAOE,WAAWC,YAAYN;AAChC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-record.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 * WHAT A BOOKING CARRIES BACK TO THE RECORD IT CAME FROM (AGL-2660).\n *\n * A rep drops a booking link into an email from a record; the visitor books\n * through it; the booking comes back to the record as a meeting on its\n * timeline and, when the service asks for one, a follow-up task. The record\n * system draws the \"Book a meeting\" zone and hands it the record by its kind\n * and id; this plugin builds the link, carries the record on it, and files\n * the meeting back through the core's record-timeline seam, where the owner\n * finds the record. Neither plugin imports the other: what this module holds\n * — the link's query keys, the wire form of the record it carries, and the\n * words and the day of what is filed — is this plugin's own.\n */\n\nimport { bookingContactLines } from './booking-contact-fields'\nimport {\n isValidTimeZone,\n zonedDateTime,\n} from '@aglyn/shared-util-timestamp/zoned-time'\n\n/**\n * The query key a booking link carries the record on: `?crm=contact:abc`.\n *\n * The reference rides the LINK rather than relying on the booker's address\n * because the two routinely differ — a rep emails somebody's work address\n * and they book with a personal one — and a booking that could only be\n * matched by address would then land on nobody. The widget copies the value\n * onto the booking request, the booking row keeps it as `crmRef`, and the\n * server hands it to the record system to file the meeting under. The key\n * is the one links already sent carry, so it is never renamed.\n */\nexport const BOOKING_RECORD_PARAM = 'crm'\n\n/**\n * The query key that preselects a service in the booking widget:\n * `?service={serviceId}`. The widget lists every active service and this\n * opens it on the one the link was about.\n */\nexport const BOOKING_SERVICE_PARAM = 'service'\n\n/** A record of the record system's, by its kind and id in its own words. */\nexport interface BookingRecordRef {\n kind: string\n id: string\n}\n\n/**\n * What a kind and an id may look like inside the reference: a lowercase\n * word, and an id with nothing a path or a query could be built from. The\n * value arrives on a public, unauthenticated request; which kinds a record\n * system keeps is its own to say when the booking is filed.\n */\nconst REF_KIND_PATTERN = /^[a-z][a-z-]{0,31}$/\nconst REF_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\n/** `contact:abc` — the wire form of a reference. */\nexport function formatBookingRecordRef(ref: BookingRecordRef): string {\n return `${ref.kind}:${ref.id}`\n}\n\n/**\n * The reference a query value names, or `null` for anything that is not one.\n *\n * Strict on purpose: the value arrives on a public, unauthenticated booking\n * request, so a malformed one is dropped rather than stored — a booking\n * still lands, and is matched by the booker's address instead.\n */\nexport function parseBookingRecordRef(raw: unknown): BookingRecordRef | null {\n if (typeof raw !== 'string') return null\n const separator = raw.indexOf(':')\n if (separator <= 0) return null\n const kind = raw.slice(0, separator)\n const id = raw.slice(separator + 1)\n if (!REF_KIND_PATTERN.test(kind) || !REF_ID_PATTERN.test(id)) return null\n return { kind, id }\n}\n\n/** The slot, as the meeting entry and the task print it. */\nfunction formatSlot(startsAtMs: number, timezone: string): string {\n try {\n return new Intl.DateTimeFormat('en-US', {\n timeZone: timezone,\n dateStyle: 'full',\n timeStyle: 'short',\n }).format(new Date(startsAtMs))\n } catch {\n // An unknown zone name on the service falls back to UTC rather than\n // failing the write: a meeting filed with a slightly wrong clock is\n // better than no meeting at all.\n return new Intl.DateTimeFormat('en-US', {\n timeZone: 'UTC',\n dateStyle: 'full',\n timeStyle: 'short',\n }).format(new Date(startsAtMs))\n }\n}\n\n/**\n * What the meeting entry on the timeline says — the service and the slot,\n * in the service's own timezone, with the zone named so a rep in another\n * one is not misled. The body, not the subject: `subject` is the field a\n * sent email carries, and the timeline draws it as one.\n *\n * Then the phone and the address the booker gave, a line each, when the\n * service asked for them (AGL-3493). The address lives HERE rather than on\n * the person: it is where this job is, which is often not where the person\n * lives, and the next booking may be somewhere else.\n */\nexport function bookingMeetingBody(input: {\n serviceName: string\n startsAtMs: number\n timezone?: string | null\n phone?: unknown\n address?: unknown\n}): string {\n const timezone = input.timezone || 'UTC'\n const service = String(input.serviceName ?? '').trim() || 'Booking'\n return [\n `${service} — ${formatSlot(input.startsAtMs, timezone)} (${timezone})`,\n ...bookingContactLines(input),\n ].join('\\n')\n}\n\n/** The title of the task a service files when it asks for a follow-up. */\nexport function bookingFollowUpTitle(serviceName: string): string {\n const service = String(serviceName ?? '').trim() || 'the booking'\n return `Follow up after ${service}`\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000\n\n/**\n * Weekday (0 = Sunday … 6 = Saturday) of an instant in a timezone, read in\n * UTC when the zone name is one `Intl` does not know — for the reason\n * `formatSlot` gives.\n */\nfunction weekdayIn(atMs: number, timezone: string): number {\n return zonedDateTime(atMs, isValidTimeZone(timezone) ? timezone : 'UTC').weekday\n}\n\n/**\n * When the follow-up falls due: ONE BUSINESS DAY after the slot, at the\n * slot's own clock time.\n *\n * A meeting on a Friday is followed up on Monday, one on a Saturday or a\n * Sunday on Monday too; any other day is followed up the next. Whole days\n * are added to the instant rather than a calendar walked, so the arithmetic\n * cannot land on a clock time that does not exist across a DST change —\n * the task is due \"about this time tomorrow\", and an hour either way is not\n * a fact the task keeps.\n */\nexport function bookingFollowUpDueMs(\n endsAtMs: number,\n timezone?: string | null,\n): number {\n const weekday = weekdayIn(endsAtMs, timezone || 'UTC')\n // Friday (5) skips the weekend; Saturday (6) reaches Monday in two days.\n const daysAhead = weekday === 5 ? 3 : weekday === 6 ? 2 : 1\n return endsAtMs + daysAhead * DAY_MS\n}\n"],"names":["bookingContactLines","isValidTimeZone","zonedDateTime","BOOKING_RECORD_PARAM","BOOKING_SERVICE_PARAM","REF_KIND_PATTERN","REF_ID_PATTERN","formatBookingRecordRef","ref","kind","id","parseBookingRecordRef","raw","separator","indexOf","slice","test","formatSlot","startsAtMs","timezone","Intl","DateTimeFormat","timeZone","dateStyle","timeStyle","format","Date","bookingMeetingBody","input","service","String","serviceName","trim","join","bookingFollowUpTitle","DAY_MS","weekdayIn","atMs","weekday","bookingFollowUpDueMs","endsAtMs","daysAhead"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;CAYC,GAED,SAASA,mBAAmB,QAAQ,8BAA0B;AAC9D,SACEC,eAAe,EACfC,aAAa,QACR,0CAAyC;AAEhD;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,uBAAuB,MAAK;AAEzC;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB,UAAS;AAQ9C;;;;;CAKC,GACD,MAAMC,mBAAmB;AACzB,MAAMC,iBAAiB;AAEvB,kDAAkD,GAClD,OAAO,SAASC,uBAAuBC,GAAqB;IAC1D,OAAO,GAAGA,IAAIC,IAAI,CAAC,CAAC,EAAED,IAAIE,EAAE,EAAE;AAChC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,sBAAsBC,GAAY;IAChD,IAAI,OAAOA,QAAQ,UAAU,OAAO;IACpC,MAAMC,YAAYD,IAAIE,OAAO,CAAC;IAC9B,IAAID,aAAa,GAAG,OAAO;IAC3B,MAAMJ,OAAOG,IAAIG,KAAK,CAAC,GAAGF;IAC1B,MAAMH,KAAKE,IAAIG,KAAK,CAACF,YAAY;IACjC,IAAI,CAACR,iBAAiBW,IAAI,CAACP,SAAS,CAACH,eAAeU,IAAI,CAACN,KAAK,OAAO;IACrE,OAAO;QAAED;QAAMC;IAAG;AACpB;AAEA,0DAA0D,GAC1D,SAASO,WAAWC,UAAkB,EAAEC,QAAgB;IACtD,IAAI;QACF,OAAO,IAAIC,KAAKC,cAAc,CAAC,SAAS;YACtCC,UAAUH;YACVI,WAAW;YACXC,WAAW;QACb,GAAGC,MAAM,CAAC,IAAIC,KAAKR;IACrB,EAAE,eAAM;QACN,oEAAoE;QACpE,oEAAoE;QACpE,iCAAiC;QACjC,OAAO,IAAIE,KAAKC,cAAc,CAAC,SAAS;YACtCC,UAAU;YACVC,WAAW;YACXC,WAAW;QACb,GAAGC,MAAM,CAAC,IAAIC,KAAKR;IACrB;AACF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASS,mBAAmBC,KAMlC;QAEwBA;IADvB,MAAMT,WAAWS,MAAMT,QAAQ,IAAI;IACnC,MAAMU,UAAUC,QAAOF,qBAAAA,MAAMG,WAAW,YAAjBH,qBAAqB,IAAII,IAAI,MAAM;IAC1D,OAAO;QACL,GAAGH,QAAQ,GAAG,EAAEZ,WAAWW,MAAMV,UAAU,EAAEC,UAAU,EAAE,EAAEA,SAAS,CAAC,CAAC;WACnEnB,oBAAoB4B;KACxB,CAACK,IAAI,CAAC;AACT;AAEA,wEAAwE,GACxE,OAAO,SAASC,qBAAqBH,WAAmB;IACtD,MAAMF,UAAUC,OAAOC,sBAAAA,cAAe,IAAIC,IAAI,MAAM;IACpD,OAAO,CAAC,gBAAgB,EAAEH,SAAS;AACrC;AAEA,MAAMM,SAAS,KAAK,KAAK,KAAK;AAE9B;;;;CAIC,GACD,SAASC,UAAUC,IAAY,EAAElB,QAAgB;IAC/C,OAAOjB,cAAcmC,MAAMpC,gBAAgBkB,YAAYA,WAAW,OAAOmB,OAAO;AAClF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,qBACdC,QAAgB,EAChBrB,QAAwB;IAExB,MAAMmB,UAAUF,UAAUI,UAAUrB,YAAY;IAChD,yEAAyE;IACzE,MAAMsB,YAAYH,YAAY,IAAI,IAAIA,YAAY,IAAI,IAAI;IAC1D,OAAOE,WAAWC,YAAYN;AAChC"}
@@ -0,0 +1,25 @@
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 PluginRecordCountSource } from '@aglyn/aglyn/plugin-manager/plugin-record-counts';
18
+ /**
19
+ * How many bookings a site took, counted in the console for another plugin's
20
+ * figure (AGL-3080) — a campaign's booking conversions out of all of them.
21
+ * Every booking is the site's own.
22
+ */
23
+ export declare const bookingRecordCountSource: PluginRecordCountSource;
24
+ /** Called from the console registrar, owner named for a spec that calls it directly. */
25
+ export declare function registerBookingsRecordCounts(): void;
@@ -0,0 +1,34 @@
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 { registerPluginRecordCountSource } from "@aglyn/aglyn/plugin-manager/plugin-record-counts";
17
+ import { collection } from "firebase/firestore";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ /**
20
+ * How many bookings a site took, counted in the console for another plugin's
21
+ * figure (AGL-3080) — a campaign's booking conversions out of all of them.
22
+ * Every booking is the site's own.
23
+ */ export const bookingRecordCountSource = {
24
+ query (firestore, request) {
25
+ return request.hostId ? collection(firestore, 'hosts', request.hostId, 'bookings') : null;
26
+ }
27
+ };
28
+ /** Called from the console registrar, owner named for a spec that calls it directly. */ export function registerBookingsRecordCounts() {
29
+ registerPluginRecordCountSource('booking', bookingRecordCountSource, {
30
+ pluginId: BUNDLE_ID
31
+ });
32
+ }
33
+
34
+ //# sourceMappingURL=bookings-record-counts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/bookings-record-counts.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 {\n registerPluginRecordCountSource,\n type PluginRecordCountSource,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-counts'\nimport { collection } from 'firebase/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * How many bookings a site took, counted in the console for another plugin's\n * figure (AGL-3080) — a campaign's booking conversions out of all of them.\n * Every booking is the site's own.\n */\nexport const bookingRecordCountSource: PluginRecordCountSource = {\n query(firestore, request) {\n return request.hostId ? collection(firestore, 'hosts', request.hostId, 'bookings') : null\n },\n}\n\n/** Called from the console registrar, owner named for a spec that calls it directly. */\nexport function registerBookingsRecordCounts(): void {\n registerPluginRecordCountSource('booking', bookingRecordCountSource, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginRecordCountSource","collection","BUNDLE_ID","bookingRecordCountSource","query","firestore","request","hostId","registerBookingsRecordCounts","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,+BAA+B,QAE1B,mDAAkD;AACzD,SAASC,UAAU,QAAQ,qBAAoB;AAC/C,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;CAIC,GACD,OAAO,MAAMC,2BAAoD;IAC/DC,OAAMC,SAAS,EAAEC,OAAO;QACtB,OAAOA,QAAQC,MAAM,GAAGN,WAAWI,WAAW,SAASC,QAAQC,MAAM,EAAE,cAAc;IACvF;AACF,EAAC;AAED,sFAAsF,GACtF,OAAO,SAASC;IACdR,gCAAgC,WAAWG,0BAA0B;QAAEM,UAAUP;IAAU;AAC7F"}