@aglyn/plugins-bookings 1.0.0-beta.231 → 1.0.0-beta.233

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 (37) hide show
  1. package/README.md +3 -1
  2. package/package.json +11 -11
  3. package/src/lib/components/bookings-console-page.js +41 -96
  4. package/src/lib/components/bookings-console-page.js.map +1 -1
  5. package/src/lib/declarations.console-server.d.ts +3 -0
  6. package/src/lib/declarations.console-server.js +20 -0
  7. package/src/lib/declarations.console-server.js.map +1 -1
  8. package/src/lib/model/booking-events.d.ts +52 -0
  9. package/src/lib/model/booking-events.js +53 -0
  10. package/src/lib/model/booking-events.js.map +1 -0
  11. package/src/lib/model/booking-manage.d.ts +7 -0
  12. package/src/lib/model/booking-manage.js +14 -0
  13. package/src/lib/model/booking-manage.js.map +1 -1
  14. package/src/lib/model/booking-service-form.d.ts +104 -0
  15. package/src/lib/model/booking-service-form.js +131 -0
  16. package/src/lib/model/booking-service-form.js.map +1 -0
  17. package/src/lib/model/booking-view.d.ts +63 -0
  18. package/src/lib/model/booking-view.js +70 -0
  19. package/src/lib/model/booking-view.js.map +1 -0
  20. package/src/lib/server/api-v1/bookings.d.ts +33 -0
  21. package/src/lib/server/api-v1/bookings.js +103 -0
  22. package/src/lib/server/api-v1/bookings.js.map +1 -0
  23. package/src/lib/server/api-v1/openapi.d.ts +22 -0
  24. package/src/lib/server/api-v1/openapi.js +107 -0
  25. package/src/lib/server/api-v1/openapi.js.map +1 -0
  26. package/src/lib/server/billing-webhook.js +17 -0
  27. package/src/lib/server/billing-webhook.js.map +1 -1
  28. package/src/lib/server/booking-events.d.ts +40 -0
  29. package/src/lib/server/booking-events.js +50 -0
  30. package/src/lib/server/booking-events.js.map +1 -0
  31. package/src/lib/server/booking-manage.d.ts +16 -0
  32. package/src/lib/server/booking-manage.js +76 -2
  33. package/src/lib/server/booking-manage.js.map +1 -1
  34. package/src/lib/server/refund.js +17 -1
  35. package/src/lib/server/refund.js.map +1 -1
  36. package/src/lib/server.js +22 -3
  37. package/src/lib/server.js.map +1 -1
@@ -1 +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"}
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 { declarePluginDomainEvents } from '@aglyn/aglyn/plugin-manager/plugin-domain-events'\nimport { registerApiV1SiteResource } from '@aglyn/tenant-data-admin/server/api-v1-resources'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { BOOKING_EVENT_DECLARATIONS } from './model/booking-events'\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 * Its bookings on the customer REST API, `/v1/sites/{siteId}/bookings`\n * (AGL-3643), and the booking events it raises, declared.\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 // The booking events this plugin raises (AGL-3643), named for the pickers\n // and diagnostics that list what is declared; raising needs no declaration.\n declarePluginDomainEvents(BOOKING_EVENT_DECLARATIONS, { pluginId: BUNDLE_ID })\n // A site's bookings on the customer REST API (AGL-3643), read-only. The\n // handler asks its scope and then the plan, so no `entitlement` is named\n // here: the router would ask them the other way round.\n registerApiV1SiteResource(\n 'bookings',\n {\n handle: async (...args) => (await import('./server/api-v1/bookings')).handleBookings(...args),\n describe: async () => (await import('./server/api-v1/openapi')).BOOKINGS_API_V1_DESCRIPTION,\n },\n { pluginId: BUNDLE_ID },\n )\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","declarePluginDomainEvents","registerApiV1SiteResource","BUNDLE_ID","BOOKING_EVENT_DECLARATIONS","BOOKINGS_TRANSFER_MATCH_KEYS","BOOKINGS_TRANSFER_RESOURCE","bookingsTransfer","bookingsTransferResource","then","adminBookingsTransferResource","catch","error","registerBookingsConsoleServerDeclarations","pluginId","handle","args","handleBookings","describe","BOOKINGS_API_V1_DESCRIPTION","request","bookingsPersonEraser","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,yBAAyB,QAAQ,mDAAkD;AAC5F,SAASC,yBAAyB,QAAQ,mDAAkD;AAC5F,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,0BAA0B,QAAQ,4BAAwB;AACnE,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;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,SAASC;IACd,0EAA0E;IAC1E,4EAA4E;IAC5EZ,0BAA0BG,4BAA4B;QAAEU,UAAUX;IAAU;IAC5E,wEAAwE;IACxE,yEAAyE;IACzE,uDAAuD;IACvDD,0BACE,YACA;QACEa,QAAQ,OAAO,GAAGC,OAAS,AAAC,CAAA,MAAM,MAAM,CAAC,8BAA0B,EAAGC,cAAc,IAAID;QACxFE,UAAU,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,6BAAyB,EAAGC,2BAA2B;IAC7F,GACA;QAAEL,UAAUX;IAAU;IAExBJ,2BACE,OAAOqB,UACL,AAAC,CAAA,MAAM,MAAM,CAAC,4BAAwB,EAAGC,oBAAoB,CAACD,UAChE;QAAEN,UAAUX;IAAU;IAExBH,+BACEM,4BACA;QACEgB,QAAQ,OAAOC;;gBACb,SAAC;4BAAD,UAAA,CAAC,QAAA,MAAMf,4BAA4Bc,MAAM,qBAAzC,aAAC,OAA2CC,uBAAQ;gBAAEC,UAAU,EAAE;YAAC;;QACrE,wEAAwE;QACxEC,WAAWpB;QACXqB,OAAO,OAAOH,KAAKI;;gBACjB,QAAC;4BAAD,SAAA,CAAC,QAAA,MAAMnB,4BAA4BkB,KAAK,qBAAxC,YAAC,OAA0CH,KAAKI,2BAAY;;QAC9DC,UAAU,OAAOL,KAAKM,QAAQC,UAAUH;YACtC,MAAMI,WAAW,MAAMvB;YACvB,IAAI,CAACuB,SAASH,QAAQ,EAAE,MAAM,IAAII,MAAM;YACxC,OAAOD,SAASH,QAAQ,CAACL,KAAKM,QAAQC,UAAUH;QAClD;QACAM,QAAQ,OAAOV,KAAKW;YAClB,MAAMH,WAAW,MAAMvB;YACvB,IAAI,CAACuB,SAASE,MAAM,EAAE,MAAM,IAAID,MAAM;YACtC,OAAOD,SAASE,MAAM,CAACV,KAAKW;QAC9B;IACF,GACA;QAAEpB,UAAUX;IAAU;AAE1B"}
@@ -0,0 +1,52 @@
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 PluginDomainEvent, type PluginDomainEventDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-domain-events';
18
+ import type { BookingView } from './booking-view';
19
+ /**
20
+ * The booking events the bookings plugin raises (AGL-3643), for other
21
+ * plugins — the merchant's Zapier connections first — through core's
22
+ * domain-event outbox: written when the fact is, delivered at least once,
23
+ * retried with backoff. A subscriber names the event and never imports this
24
+ * plugin.
25
+ *
26
+ * Each payload is `{ booking }` in the public API's shape
27
+ * (`GET /v1/sites/{siteId}/bookings/{bookingId}`), as it stood once the fact
28
+ * was written.
29
+ *
30
+ * - `booking.created`: a booking is confirmed — a free one when it is made,
31
+ * a paid one when its payment lands. A checkout the guest never finished
32
+ * raises nothing.
33
+ * - `booking.rescheduled`: the site's team moved it to another time; one
34
+ * event per move.
35
+ * - `booking.canceled`: it was canceled — a free one by the site's team, a
36
+ * paid one by the refund that gave all of the money back.
37
+ */
38
+ export interface BookingEventPayload {
39
+ booking: BookingView;
40
+ }
41
+ export declare const BOOKING_CREATED_EVENT: PluginDomainEvent<BookingEventPayload>;
42
+ export declare const BOOKING_RESCHEDULED_EVENT: PluginDomainEvent<BookingEventPayload>;
43
+ export declare const BOOKING_CANCELED_EVENT: PluginDomainEvent<BookingEventPayload>;
44
+ export type BookingEventName = 'booking.created' | 'booking.rescheduled' | 'booking.canceled';
45
+ export declare const BOOKING_EVENT_DECLARATIONS: ReadonlyArray<PluginDomainEventDeclaration & {
46
+ event: PluginDomainEvent<unknown>;
47
+ }>;
48
+ /**
49
+ * The key naming one occurrence, so the same fact raised twice is one event:
50
+ * a booking is confirmed once, canceled once, and moved once to each time.
51
+ */
52
+ export declare function bookingEventKey(event: BookingEventName, bookingId: string, startsAtMs?: number): string;
@@ -0,0 +1,53 @@
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 { definePluginDomainEvent } from "@aglyn/aglyn/plugin-manager/plugin-domain-events";
17
+ export const BOOKING_CREATED_EVENT = definePluginDomainEvent('booking.created');
18
+ export const BOOKING_RESCHEDULED_EVENT = definePluginDomainEvent('booking.rescheduled');
19
+ export const BOOKING_CANCELED_EVENT = definePluginDomainEvent('booking.canceled');
20
+ export const BOOKING_EVENT_DECLARATIONS = [
21
+ {
22
+ event: BOOKING_CREATED_EVENT,
23
+ label: 'Booking confirmed',
24
+ description: 'A booking was confirmed: a free one when it was made, a paid one when its payment landed.',
25
+ payloadKeys: [
26
+ 'booking'
27
+ ]
28
+ },
29
+ {
30
+ event: BOOKING_RESCHEDULED_EVENT,
31
+ label: 'Booking rescheduled',
32
+ description: 'The site’s team moved a booking to another time, once per move.',
33
+ payloadKeys: [
34
+ 'booking'
35
+ ]
36
+ },
37
+ {
38
+ event: BOOKING_CANCELED_EVENT,
39
+ label: 'Booking canceled',
40
+ description: 'A booking was canceled: a free one by the site’s team, a paid one by a full refund.',
41
+ payloadKeys: [
42
+ 'booking'
43
+ ]
44
+ }
45
+ ];
46
+ /**
47
+ * The key naming one occurrence, so the same fact raised twice is one event:
48
+ * a booking is confirmed once, canceled once, and moved once to each time.
49
+ */ export function bookingEventKey(event, bookingId, startsAtMs) {
50
+ return event === 'booking.rescheduled' ? `${bookingId}@${Number(startsAtMs != null ? startsAtMs : 0)}` : bookingId;
51
+ }
52
+
53
+ //# sourceMappingURL=booking-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-events.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 definePluginDomainEvent,\n type PluginDomainEvent,\n type PluginDomainEventDeclaration,\n} from '@aglyn/aglyn/plugin-manager/plugin-domain-events'\nimport type { BookingView } from './booking-view'\n\n/**\n * The booking events the bookings plugin raises (AGL-3643), for other\n * plugins — the merchant's Zapier connections first — through core's\n * domain-event outbox: written when the fact is, delivered at least once,\n * retried with backoff. A subscriber names the event and never imports this\n * plugin.\n *\n * Each payload is `{ booking }` in the public API's shape\n * (`GET /v1/sites/{siteId}/bookings/{bookingId}`), as it stood once the fact\n * was written.\n *\n * - `booking.created`: a booking is confirmed — a free one when it is made,\n * a paid one when its payment lands. A checkout the guest never finished\n * raises nothing.\n * - `booking.rescheduled`: the site's team moved it to another time; one\n * event per move.\n * - `booking.canceled`: it was canceled — a free one by the site's team, a\n * paid one by the refund that gave all of the money back.\n */\nexport interface BookingEventPayload {\n booking: BookingView\n}\n\nexport const BOOKING_CREATED_EVENT = definePluginDomainEvent<BookingEventPayload>('booking.created')\nexport const BOOKING_RESCHEDULED_EVENT = definePluginDomainEvent<BookingEventPayload>('booking.rescheduled')\nexport const BOOKING_CANCELED_EVENT = definePluginDomainEvent<BookingEventPayload>('booking.canceled')\n\nexport type BookingEventName = 'booking.created' | 'booking.rescheduled' | 'booking.canceled'\n\nexport const BOOKING_EVENT_DECLARATIONS: ReadonlyArray<PluginDomainEventDeclaration & { event: PluginDomainEvent<unknown> }> = [\n {\n event: BOOKING_CREATED_EVENT,\n label: 'Booking confirmed',\n description: 'A booking was confirmed: a free one when it was made, a paid one when its payment landed.',\n payloadKeys: ['booking'],\n },\n {\n event: BOOKING_RESCHEDULED_EVENT,\n label: 'Booking rescheduled',\n description: 'The site’s team moved a booking to another time, once per move.',\n payloadKeys: ['booking'],\n },\n {\n event: BOOKING_CANCELED_EVENT,\n label: 'Booking canceled',\n description: 'A booking was canceled: a free one by the site’s team, a paid one by a full refund.',\n payloadKeys: ['booking'],\n },\n]\n\n/**\n * The key naming one occurrence, so the same fact raised twice is one event:\n * a booking is confirmed once, canceled once, and moved once to each time.\n */\nexport function bookingEventKey(event: BookingEventName, bookingId: string, startsAtMs?: number): string {\n return event === 'booking.rescheduled' ? `${bookingId}@${Number(startsAtMs ?? 0)}` : bookingId\n}\n"],"names":["definePluginDomainEvent","BOOKING_CREATED_EVENT","BOOKING_RESCHEDULED_EVENT","BOOKING_CANCELED_EVENT","BOOKING_EVENT_DECLARATIONS","event","label","description","payloadKeys","bookingEventKey","bookingId","startsAtMs","Number"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,uBAAuB,QAGlB,mDAAkD;AA0BzD,OAAO,MAAMC,wBAAwBD,wBAA6C,mBAAkB;AACpG,OAAO,MAAME,4BAA4BF,wBAA6C,uBAAsB;AAC5G,OAAO,MAAMG,yBAAyBH,wBAA6C,oBAAmB;AAItG,OAAO,MAAMI,6BAAkH;IAC7H;QACEC,OAAOJ;QACPK,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAU;IAC1B;IACA;QACEH,OAAOH;QACPI,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAU;IAC1B;IACA;QACEH,OAAOF;QACPG,OAAO;QACPC,aAAa;QACbC,aAAa;YAAC;SAAU;IAC1B;CACD,CAAA;AAED;;;CAGC,GACD,OAAO,SAASC,gBAAgBJ,KAAuB,EAAEK,SAAiB,EAAEC,UAAmB;IAC7F,OAAON,UAAU,wBAAwB,GAAGK,UAAU,CAAC,EAAEE,OAAOD,qBAAAA,aAAc,IAAI,GAAGD;AACvF"}
@@ -62,5 +62,12 @@ export declare function bookingActions(booking: ManagedBooking, nowMs: number):
62
62
  export declare function checkInRefusal(booking: ManagedBooking, checkedIn: boolean, nowMs: number): string | null;
63
63
  /** Why a booking cannot move to `startsAtMs`, or null. The slot itself is checked apart. */
64
64
  export declare function rescheduleRefusal(booking: ManagedBooking, startsAtMs: number, nowMs: number): string | null;
65
+ /**
66
+ * Why a booking cannot be canceled by the cancel route, or null (AGL-3643).
67
+ * A canceled booking is not refused: canceling it again changes nothing. A
68
+ * booking with money still paid on it cancels through the refund instead, so
69
+ * a guest is never left canceled and charged.
70
+ */
71
+ export declare function cancelRefusal(booking: ManagedBooking, nowMs: number): string | null;
65
72
  /** The booking's length, kept when it moves: what the guest booked, not what the service says today. */
66
73
  export declare function bookingDurationMs(booking: ManagedBooking, fallbackMinutes: number): number;
@@ -81,6 +81,20 @@ export const BOOKING_STATE_LABELS = {
81
81
  if (!Number.isFinite(startsAtMs) || startsAtMs <= nowMs) return 'Pick a time that has not passed';
82
82
  return null;
83
83
  }
84
+ /**
85
+ * Why a booking cannot be canceled by the cancel route, or null (AGL-3643).
86
+ * A canceled booking is not refused: canceling it again changes nothing. A
87
+ * booking with money still paid on it cancels through the refund instead, so
88
+ * a guest is never left canceled and charged.
89
+ */ export function cancelRefusal(booking, nowMs) {
90
+ var _booking_checkedInAtMs;
91
+ if (bookingState(booking, nowMs) === 'canceled') return null;
92
+ if (Number((_booking_checkedInAtMs = booking.checkedInAtMs) != null ? _booking_checkedInAtMs : 0) > 0) return 'This guest is already checked in';
93
+ if (bookingOutstandingCents(booking) > 0) {
94
+ return 'This booking was paid. Cancel it with a refund so the guest gets their money back';
95
+ }
96
+ return null;
97
+ }
84
98
  /** The booking's length, kept when it moves: what the guest booked, not what the service says today. */ export function bookingDurationMs(booking, fallbackMinutes) {
85
99
  var _booking_endsAtMs, _booking_startsAtMs;
86
100
  const stored = Number((_booking_endsAtMs = booking.endsAtMs) != null ? _booking_endsAtMs : 0) - Number((_booking_startsAtMs = booking.startsAtMs) != null ? _booking_startsAtMs : 0);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-manage.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 site's team may do to a booking once it exists (AGL-3621): check\n * the guest in, move it, cancel it. One pure rule set, read by the routes\n * that do it (`bookings/check-in`, `bookings/reschedule`) under their own\n * transaction, and the rules a client mirrors to decide what to OFFER (the\n * native Aglyn app's bookings screens port them, cased by this file's spec),\n * so a screen can only offer what the route would then do.\n *\n * No I/O and no React, so client and server code alike may import it.\n */\n\n/** `hosts/{hostId}/bookings/{id}`, the fields these rules read. */\nexport interface ManagedBooking {\n status?: string\n startsAtMs?: number\n endsAtMs?: number\n expiresAtMs?: number\n paidAmountCents?: number\n refundedCents?: number\n checkedInAtMs?: number\n}\n\nexport type BookingState = 'confirmed' | 'pendingPayment' | 'expired' | 'canceled'\n\n/**\n * The booking's state as a reader should treat it. A payment hold whose\n * fifteen minutes ran out released its slot (AGL-170) even while the\n * document still says `pendingPayment`.\n */\nexport function bookingState(booking: ManagedBooking, nowMs: number): BookingState {\n if (booking.status === 'canceled') return 'canceled'\n if (booking.status === 'pendingPayment') {\n return Number(booking.expiresAtMs ?? 0) < nowMs ? 'expired' : 'pendingPayment'\n }\n return 'confirmed'\n}\n\nexport const BOOKING_STATE_LABELS: Readonly<Record<BookingState, string>> = {\n confirmed: 'Confirmed',\n pendingPayment: 'Awaiting payment',\n expired: 'Payment not finished',\n canceled: 'Canceled',\n}\n\n/** The paid amount not yet refunded, in cents. */\nexport function bookingOutstandingCents(booking: ManagedBooking): number {\n const paid = Math.max(0, Math.round(Number(booking.paidAmountCents ?? 0)))\n const refunded = Math.max(0, Math.round(Number(booking.refundedCents ?? 0)))\n return Math.max(0, paid - refunded)\n}\n\nexport interface BookingActions {\n checkIn: boolean\n undoCheckIn: boolean\n reschedule: boolean\n /** Cancel; with `refundCents` above zero the cancel goes through the refund route. */\n cancel: boolean\n refundCents: number\n}\n\n/**\n * What may be done to a booking now. A guest is checked in only on a\n * confirmed booking; a booking moves only while it is confirmed and has not\n * ended; and nothing is done to a canceled one.\n */\nexport function bookingActions(booking: ManagedBooking, nowMs: number): BookingActions {\n const state = bookingState(booking, nowMs)\n const confirmed = state === 'confirmed'\n const checkedIn = Number(booking.checkedInAtMs ?? 0) > 0\n const ended = Number(booking.endsAtMs ?? 0) <= nowMs\n return {\n checkIn: confirmed && !checkedIn,\n undoCheckIn: confirmed && checkedIn,\n reschedule: confirmed && !checkedIn && !ended,\n cancel: state !== 'canceled' && !checkedIn,\n refundCents: bookingOutstandingCents(booking),\n }\n}\n\n/** Why a check-in (or its undo) cannot be written, or null. The route's own refusal. */\nexport function checkInRefusal(booking: ManagedBooking, checkedIn: boolean, nowMs: number): string | null {\n const state = bookingState(booking, nowMs)\n if (state === 'canceled') return 'This booking was canceled'\n if (state !== 'confirmed') return 'This booking is not paid yet'\n return checkedIn || Number(booking.checkedInAtMs ?? 0) > 0 ? null : 'This guest is not checked in'\n}\n\n/** Why a booking cannot move to `startsAtMs`, or null. The slot itself is checked apart. */\nexport function rescheduleRefusal(booking: ManagedBooking, startsAtMs: number, nowMs: number): string | null {\n const state = bookingState(booking, nowMs)\n if (state === 'canceled') return 'This booking was canceled'\n if (state !== 'confirmed') return 'This booking is not paid yet'\n if (Number(booking.checkedInAtMs ?? 0) > 0) return 'This guest is already checked in'\n if (Number(booking.endsAtMs ?? 0) <= nowMs) return 'This booking has already ended'\n if (!Number.isFinite(startsAtMs) || startsAtMs <= nowMs) return 'Pick a time that has not passed'\n return null\n}\n\n/** The booking's length, kept when it moves: what the guest booked, not what the service says today. */\nexport function bookingDurationMs(booking: ManagedBooking, fallbackMinutes: number): number {\n const stored = Number(booking.endsAtMs ?? 0) - Number(booking.startsAtMs ?? 0)\n return stored > 0 ? stored : Math.max(5, Math.round(fallbackMinutes || 30)) * 60_000\n}\n"],"names":["bookingState","booking","nowMs","status","Number","expiresAtMs","BOOKING_STATE_LABELS","confirmed","pendingPayment","expired","canceled","bookingOutstandingCents","paid","Math","max","round","paidAmountCents","refunded","refundedCents","bookingActions","state","checkedIn","checkedInAtMs","ended","endsAtMs","checkIn","undoCheckIn","reschedule","cancel","refundCents","checkInRefusal","rescheduleRefusal","startsAtMs","isFinite","bookingDurationMs","fallbackMinutes","stored"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GAED,iEAAiE,GAajE;;;;CAIC,GACD,OAAO,SAASA,aAAaC,OAAuB,EAAEC,KAAa;IACjE,IAAID,QAAQE,MAAM,KAAK,YAAY,OAAO;IAC1C,IAAIF,QAAQE,MAAM,KAAK,kBAAkB;YACzBF;QAAd,OAAOG,QAAOH,uBAAAA,QAAQI,WAAW,YAAnBJ,uBAAuB,KAAKC,QAAQ,YAAY;IAChE;IACA,OAAO;AACT;AAEA,OAAO,MAAMI,uBAA+D;IAC1EC,WAAW;IACXC,gBAAgB;IAChBC,SAAS;IACTC,UAAU;AACZ,EAAC;AAED,gDAAgD,GAChD,OAAO,SAASC,wBAAwBV,OAAuB;QAClBA,0BACIA;IAD/C,MAAMW,OAAOC,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACX,QAAOH,2BAAAA,QAAQe,eAAe,YAAvBf,2BAA2B;IACtE,MAAMgB,WAAWJ,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACX,QAAOH,yBAAAA,QAAQiB,aAAa,YAArBjB,yBAAyB;IACxE,OAAOY,KAAKC,GAAG,CAAC,GAAGF,OAAOK;AAC5B;AAWA;;;;CAIC,GACD,OAAO,SAASE,eAAelB,OAAuB,EAAEC,KAAa;QAG1CD,wBACJA;IAHrB,MAAMmB,QAAQpB,aAAaC,SAASC;IACpC,MAAMK,YAAYa,UAAU;IAC5B,MAAMC,YAAYjB,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK;IACvD,MAAMsB,QAAQnB,QAAOH,oBAAAA,QAAQuB,QAAQ,YAAhBvB,oBAAoB,MAAMC;IAC/C,OAAO;QACLuB,SAASlB,aAAa,CAACc;QACvBK,aAAanB,aAAac;QAC1BM,YAAYpB,aAAa,CAACc,aAAa,CAACE;QACxCK,QAAQR,UAAU,cAAc,CAACC;QACjCQ,aAAalB,wBAAwBV;IACvC;AACF;AAEA,sFAAsF,GACtF,OAAO,SAAS6B,eAAe7B,OAAuB,EAAEoB,SAAkB,EAAEnB,KAAa;QAI5DD;IAH3B,MAAMmB,QAAQpB,aAAaC,SAASC;IACpC,IAAIkB,UAAU,YAAY,OAAO;IACjC,IAAIA,UAAU,aAAa,OAAO;IAClC,OAAOC,aAAajB,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK,IAAI,OAAO;AACtE;AAEA,0FAA0F,GAC1F,OAAO,SAAS8B,kBAAkB9B,OAAuB,EAAE+B,UAAkB,EAAE9B,KAAa;QAI/ED,wBACAA;IAJX,MAAMmB,QAAQpB,aAAaC,SAASC;IACpC,IAAIkB,UAAU,YAAY,OAAO;IACjC,IAAIA,UAAU,aAAa,OAAO;IAClC,IAAIhB,QAAOH,yBAAAA,QAAQqB,aAAa,YAArBrB,yBAAyB,KAAK,GAAG,OAAO;IACnD,IAAIG,QAAOH,oBAAAA,QAAQuB,QAAQ,YAAhBvB,oBAAoB,MAAMC,OAAO,OAAO;IACnD,IAAI,CAACE,OAAO6B,QAAQ,CAACD,eAAeA,cAAc9B,OAAO,OAAO;IAChE,OAAO;AACT;AAEA,sGAAsG,GACtG,OAAO,SAASgC,kBAAkBjC,OAAuB,EAAEkC,eAAuB;QAC1DlC,mBAAgCA;IAAtD,MAAMmC,SAAShC,QAAOH,oBAAAA,QAAQuB,QAAQ,YAAhBvB,oBAAoB,KAAKG,QAAOH,sBAAAA,QAAQ+B,UAAU,YAAlB/B,sBAAsB;IAC5E,OAAOmC,SAAS,IAAIA,SAASvB,KAAKC,GAAG,CAAC,GAAGD,KAAKE,KAAK,CAACoB,mBAAmB,OAAO;AAChF"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-manage.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 site's team may do to a booking once it exists (AGL-3621): check\n * the guest in, move it, cancel it. One pure rule set, read by the routes\n * that do it (`bookings/check-in`, `bookings/reschedule`) under their own\n * transaction, and the rules a client mirrors to decide what to OFFER (the\n * native Aglyn app's bookings screens port them, cased by this file's spec),\n * so a screen can only offer what the route would then do.\n *\n * No I/O and no React, so client and server code alike may import it.\n */\n\n/** `hosts/{hostId}/bookings/{id}`, the fields these rules read. */\nexport interface ManagedBooking {\n status?: string\n startsAtMs?: number\n endsAtMs?: number\n expiresAtMs?: number\n paidAmountCents?: number\n refundedCents?: number\n checkedInAtMs?: number\n}\n\nexport type BookingState = 'confirmed' | 'pendingPayment' | 'expired' | 'canceled'\n\n/**\n * The booking's state as a reader should treat it. A payment hold whose\n * fifteen minutes ran out released its slot (AGL-170) even while the\n * document still says `pendingPayment`.\n */\nexport function bookingState(booking: ManagedBooking, nowMs: number): BookingState {\n if (booking.status === 'canceled') return 'canceled'\n if (booking.status === 'pendingPayment') {\n return Number(booking.expiresAtMs ?? 0) < nowMs ? 'expired' : 'pendingPayment'\n }\n return 'confirmed'\n}\n\nexport const BOOKING_STATE_LABELS: Readonly<Record<BookingState, string>> = {\n confirmed: 'Confirmed',\n pendingPayment: 'Awaiting payment',\n expired: 'Payment not finished',\n canceled: 'Canceled',\n}\n\n/** The paid amount not yet refunded, in cents. */\nexport function bookingOutstandingCents(booking: ManagedBooking): number {\n const paid = Math.max(0, Math.round(Number(booking.paidAmountCents ?? 0)))\n const refunded = Math.max(0, Math.round(Number(booking.refundedCents ?? 0)))\n return Math.max(0, paid - refunded)\n}\n\nexport interface BookingActions {\n checkIn: boolean\n undoCheckIn: boolean\n reschedule: boolean\n /** Cancel; with `refundCents` above zero the cancel goes through the refund route. */\n cancel: boolean\n refundCents: number\n}\n\n/**\n * What may be done to a booking now. A guest is checked in only on a\n * confirmed booking; a booking moves only while it is confirmed and has not\n * ended; and nothing is done to a canceled one.\n */\nexport function bookingActions(booking: ManagedBooking, nowMs: number): BookingActions {\n const state = bookingState(booking, nowMs)\n const confirmed = state === 'confirmed'\n const checkedIn = Number(booking.checkedInAtMs ?? 0) > 0\n const ended = Number(booking.endsAtMs ?? 0) <= nowMs\n return {\n checkIn: confirmed && !checkedIn,\n undoCheckIn: confirmed && checkedIn,\n reschedule: confirmed && !checkedIn && !ended,\n cancel: state !== 'canceled' && !checkedIn,\n refundCents: bookingOutstandingCents(booking),\n }\n}\n\n/** Why a check-in (or its undo) cannot be written, or null. The route's own refusal. */\nexport function checkInRefusal(booking: ManagedBooking, checkedIn: boolean, nowMs: number): string | null {\n const state = bookingState(booking, nowMs)\n if (state === 'canceled') return 'This booking was canceled'\n if (state !== 'confirmed') return 'This booking is not paid yet'\n return checkedIn || Number(booking.checkedInAtMs ?? 0) > 0 ? null : 'This guest is not checked in'\n}\n\n/** Why a booking cannot move to `startsAtMs`, or null. The slot itself is checked apart. */\nexport function rescheduleRefusal(booking: ManagedBooking, startsAtMs: number, nowMs: number): string | null {\n const state = bookingState(booking, nowMs)\n if (state === 'canceled') return 'This booking was canceled'\n if (state !== 'confirmed') return 'This booking is not paid yet'\n if (Number(booking.checkedInAtMs ?? 0) > 0) return 'This guest is already checked in'\n if (Number(booking.endsAtMs ?? 0) <= nowMs) return 'This booking has already ended'\n if (!Number.isFinite(startsAtMs) || startsAtMs <= nowMs) return 'Pick a time that has not passed'\n return null\n}\n\n/**\n * Why a booking cannot be canceled by the cancel route, or null (AGL-3643).\n * A canceled booking is not refused: canceling it again changes nothing. A\n * booking with money still paid on it cancels through the refund instead, so\n * a guest is never left canceled and charged.\n */\nexport function cancelRefusal(booking: ManagedBooking, nowMs: number): string | null {\n if (bookingState(booking, nowMs) === 'canceled') return null\n if (Number(booking.checkedInAtMs ?? 0) > 0) return 'This guest is already checked in'\n if (bookingOutstandingCents(booking) > 0) {\n return 'This booking was paid. Cancel it with a refund so the guest gets their money back'\n }\n return null\n}\n\n/** The booking's length, kept when it moves: what the guest booked, not what the service says today. */\nexport function bookingDurationMs(booking: ManagedBooking, fallbackMinutes: number): number {\n const stored = Number(booking.endsAtMs ?? 0) - Number(booking.startsAtMs ?? 0)\n return stored > 0 ? stored : Math.max(5, Math.round(fallbackMinutes || 30)) * 60_000\n}\n"],"names":["bookingState","booking","nowMs","status","Number","expiresAtMs","BOOKING_STATE_LABELS","confirmed","pendingPayment","expired","canceled","bookingOutstandingCents","paid","Math","max","round","paidAmountCents","refunded","refundedCents","bookingActions","state","checkedIn","checkedInAtMs","ended","endsAtMs","checkIn","undoCheckIn","reschedule","cancel","refundCents","checkInRefusal","rescheduleRefusal","startsAtMs","isFinite","cancelRefusal","bookingDurationMs","fallbackMinutes","stored"],"mappings":"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"}
@@ -0,0 +1,104 @@
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 BookingFieldAsk } from './booking-contact-fields';
18
+ import { type BookingPriceDisplay } from './booking-price';
19
+ /**
20
+ * THE SERVICE DIALOG'S RULES: what a service's editor starts from and what
21
+ * one save stores in `hosts/{hostId}/services/{id}`. The console's Bookings
22
+ * page and the native apps' service editors both hand their form to
23
+ * {@link bookingServiceFields} and store exactly what it answers (the apps
24
+ * replay its cases), so a duration is clamped, a price rounded and an
25
+ * hours line read the same way whichever of them saved it.
26
+ *
27
+ * Pure: no Firestore, no clock, no React.
28
+ */
29
+ /** The weekday names the dialog labels each hours line with, Sunday first (`windows` keys 0–6). */
30
+ export declare const BOOKING_WEEKDAYS: readonly string[];
31
+ /** One open interval on a weekday, in minutes since midnight in the service's zone. */
32
+ export interface BookingWindow {
33
+ start: number;
34
+ end: number;
35
+ }
36
+ /** Longest stored service name. */
37
+ export declare const BOOKING_SERVICE_NAME_MAX = 80;
38
+ /** Longest stored service description. */
39
+ export declare const BOOKING_SERVICE_DESCRIPTION_MAX = 500;
40
+ /** The service dialog's fields as typed: numbers and hours are text until saved. */
41
+ export interface BookingServiceDraft {
42
+ name: string;
43
+ durationMinutes: string;
44
+ priceUsd: string;
45
+ timezone: string;
46
+ description: string;
47
+ /** Per-weekday hours, Sunday first, e.g. "09:00-12:00, 13:00-17:00". */
48
+ windowText: string[];
49
+ crmMeetingActivity: boolean;
50
+ crmFollowUpTask: boolean;
51
+ askPhone: BookingFieldAsk;
52
+ askAddress: BookingFieldAsk;
53
+ priceDisplay: BookingPriceDisplay;
54
+ }
55
+ /** What one save stores: every editable key of the service, written explicitly. */
56
+ export type BookingServiceFields = {
57
+ name: string;
58
+ durationMinutes: number;
59
+ priceUsd: number;
60
+ timezone: string;
61
+ /** `''` when cleared, so a merge does not keep the old text. */
62
+ description: string;
63
+ /** Every weekday, 0–6, with `[]` for a closed day. */
64
+ windows: Record<string, BookingWindow[]>;
65
+ crmMeetingActivity: boolean;
66
+ crmFollowUpTask: boolean;
67
+ askPhone: BookingFieldAsk;
68
+ askAddress: BookingFieldAsk;
69
+ priceDisplay: BookingPriceDisplay;
70
+ };
71
+ /** "09:00-12:00, 13:00-17:00" → open intervals in minutes; anything unreadable is skipped. */
72
+ export declare function parseBookingWindows(input: string): BookingWindow[];
73
+ /** Open intervals → "09:00-12:00, 13:00-17:00". */
74
+ export declare function formatBookingWindows(windows: readonly BookingWindow[] | null | undefined): string;
75
+ /**
76
+ * A new service's dialog: thirty minutes, free, the editor's own zone, open
77
+ * nine to five on weekdays, a meeting on the contact's record and no
78
+ * follow-up, and only name and email asked.
79
+ */
80
+ export declare function newBookingServiceDraft(timeZone: string): BookingServiceDraft;
81
+ /** The dialog seeded from a stored service, so its switches show what the service does. */
82
+ export declare function bookingServiceDraftFrom(service: {
83
+ name?: unknown;
84
+ durationMinutes?: unknown;
85
+ priceUsd?: unknown;
86
+ timezone?: unknown;
87
+ description?: unknown;
88
+ windows?: Partial<Record<number | string, BookingWindow[]>> | null;
89
+ crmMeetingActivity?: unknown;
90
+ crmFollowUpTask?: unknown;
91
+ askPhone?: unknown;
92
+ askAddress?: unknown;
93
+ priceDisplay?: unknown;
94
+ }): BookingServiceDraft;
95
+ /** Why the dialog cannot save, or null. */
96
+ export declare function bookingServiceDraftProblem(draft: Pick<BookingServiceDraft, 'name'>): string | null;
97
+ /**
98
+ * What one save stores. Every editable key is written explicitly, because
99
+ * the edit is a `merge` and an absent key would keep its stored value: a
100
+ * switch turned off has to land `false`, and a weekday cleared has to land
101
+ * `[]`, or the old hours would go on taking bookings; a description
102
+ * cleared lands `''` for the same reason.
103
+ */
104
+ export declare function bookingServiceFields(draft: BookingServiceDraft): BookingServiceFields;
@@ -0,0 +1,131 @@
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 { bookingContactAsks } from "./booking-contact-fields.js";
17
+ import { bookingPriceDisplay } from "./booking-price.js";
18
+ /**
19
+ * THE SERVICE DIALOG'S RULES: what a service's editor starts from and what
20
+ * one save stores in `hosts/{hostId}/services/{id}`. The console's Bookings
21
+ * page and the native apps' service editors both hand their form to
22
+ * {@link bookingServiceFields} and store exactly what it answers (the apps
23
+ * replay its cases), so a duration is clamped, a price rounded and an
24
+ * hours line read the same way whichever of them saved it.
25
+ *
26
+ * Pure: no Firestore, no clock, no React.
27
+ */ /** The weekday names the dialog labels each hours line with, Sunday first (`windows` keys 0–6). */ export const BOOKING_WEEKDAYS = [
28
+ 'Sun',
29
+ 'Mon',
30
+ 'Tue',
31
+ 'Wed',
32
+ 'Thu',
33
+ 'Fri',
34
+ 'Sat'
35
+ ];
36
+ /** Longest stored service name. */ export const BOOKING_SERVICE_NAME_MAX = 80;
37
+ /** Longest stored service description. */ export const BOOKING_SERVICE_DESCRIPTION_MAX = 500;
38
+ /** "09:00-12:00, 13:00-17:00" → open intervals in minutes; anything unreadable is skipped. */ export function parseBookingWindows(input) {
39
+ const windows = [];
40
+ for (const chunk of String(input != null ? input : '').split(',')){
41
+ const match = chunk.trim().match(/^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$/);
42
+ if (!match) continue;
43
+ const start = Number(match[1]) * 60 + Number(match[2]);
44
+ const end = Number(match[3]) * 60 + Number(match[4]);
45
+ if (end > start && end <= 24 * 60) windows.push({
46
+ start,
47
+ end
48
+ });
49
+ }
50
+ return windows;
51
+ }
52
+ /** Open intervals → "09:00-12:00, 13:00-17:00". */ export function formatBookingWindows(windows) {
53
+ const pad = (minutes)=>`${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`;
54
+ return (windows != null ? windows : []).map((window)=>`${pad(window.start)}-${pad(window.end)}`).join(', ');
55
+ }
56
+ /**
57
+ * A new service's dialog: thirty minutes, free, the editor's own zone, open
58
+ * nine to five on weekdays, a meeting on the contact's record and no
59
+ * follow-up, and only name and email asked.
60
+ */ export function newBookingServiceDraft(timeZone) {
61
+ return {
62
+ name: '',
63
+ durationMinutes: '30',
64
+ priceUsd: '0',
65
+ timezone: timeZone || 'UTC',
66
+ description: '',
67
+ windowText: BOOKING_WEEKDAYS.map((_, index)=>index >= 1 && index <= 5 ? '09:00-17:00' : ''),
68
+ crmMeetingActivity: true,
69
+ crmFollowUpTask: false,
70
+ askPhone: 'off',
71
+ askAddress: 'off',
72
+ priceDisplay: 'fixed'
73
+ };
74
+ }
75
+ /** The dialog seeded from a stored service, so its switches show what the service does. */ export function bookingServiceDraftFrom(service) {
76
+ var _service_durationMinutes, _service_priceUsd;
77
+ const asks = bookingContactAsks(service);
78
+ return {
79
+ name: typeof service.name === 'string' ? service.name : '',
80
+ durationMinutes: String((_service_durationMinutes = service.durationMinutes) != null ? _service_durationMinutes : 30),
81
+ priceUsd: String((_service_priceUsd = service.priceUsd) != null ? _service_priceUsd : 0),
82
+ timezone: typeof service.timezone === 'string' ? service.timezone : 'UTC',
83
+ description: typeof service.description === 'string' ? service.description : '',
84
+ windowText: BOOKING_WEEKDAYS.map((_, index)=>{
85
+ var _service_windows;
86
+ return formatBookingWindows((_service_windows = service.windows) == null ? void 0 : _service_windows[index]);
87
+ }),
88
+ // Absent reads as ON for the meeting and OFF for the follow-up: the
89
+ // model's defaults.
90
+ crmMeetingActivity: service.crmMeetingActivity !== false,
91
+ crmFollowUpTask: service.crmFollowUpTask === true,
92
+ askPhone: asks.phone,
93
+ askAddress: asks.address,
94
+ priceDisplay: bookingPriceDisplay(service.priceDisplay)
95
+ };
96
+ }
97
+ /** Why the dialog cannot save, or null. */ export function bookingServiceDraftProblem(draft) {
98
+ var _draft_name;
99
+ return String((_draft_name = draft.name) != null ? _draft_name : '').trim() ? null : 'A service needs a name.';
100
+ }
101
+ /**
102
+ * What one save stores. Every editable key is written explicitly, because
103
+ * the edit is a `merge` and an absent key would keep its stored value: a
104
+ * switch turned off has to land `false`, and a weekday cleared has to land
105
+ * `[]`, or the old hours would go on taking bookings; a description
106
+ * cleared lands `''` for the same reason.
107
+ */ export function bookingServiceFields(draft) {
108
+ var _draft_description, _draft_name, _draft_timezone;
109
+ const windows = {};
110
+ BOOKING_WEEKDAYS.forEach((_, weekday)=>{
111
+ var _ref;
112
+ var _draft_windowText;
113
+ windows[String(weekday)] = parseBookingWindows((_ref = (_draft_windowText = draft.windowText) == null ? void 0 : _draft_windowText[weekday]) != null ? _ref : '');
114
+ });
115
+ const description = String((_draft_description = draft.description) != null ? _draft_description : '').trim();
116
+ return {
117
+ name: String((_draft_name = draft.name) != null ? _draft_name : '').trim().slice(0, BOOKING_SERVICE_NAME_MAX),
118
+ durationMinutes: Math.max(5, Math.min(480, Math.round(Number(draft.durationMinutes) || 30))),
119
+ priceUsd: Math.max(0, Math.round(Number(draft.priceUsd) || 0)),
120
+ timezone: String((_draft_timezone = draft.timezone) != null ? _draft_timezone : '').trim() || 'UTC',
121
+ description: description.slice(0, BOOKING_SERVICE_DESCRIPTION_MAX),
122
+ windows,
123
+ crmMeetingActivity: draft.crmMeetingActivity,
124
+ crmFollowUpTask: draft.crmFollowUpTask,
125
+ askPhone: draft.askPhone,
126
+ askAddress: draft.askAddress,
127
+ priceDisplay: draft.priceDisplay
128
+ };
129
+ }
130
+
131
+ //# sourceMappingURL=booking-service-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/model/booking-service-form.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 BookingFieldAsk, bookingContactAsks } from './booking-contact-fields'\nimport { type BookingPriceDisplay, bookingPriceDisplay } from './booking-price'\n\n/**\n * THE SERVICE DIALOG'S RULES: what a service's editor starts from and what\n * one save stores in `hosts/{hostId}/services/{id}`. The console's Bookings\n * page and the native apps' service editors both hand their form to\n * {@link bookingServiceFields} and store exactly what it answers (the apps\n * replay its cases), so a duration is clamped, a price rounded and an\n * hours line read the same way whichever of them saved it.\n *\n * Pure: no Firestore, no clock, no React.\n */\n\n/** The weekday names the dialog labels each hours line with, Sunday first (`windows` keys 0–6). */\nexport const BOOKING_WEEKDAYS: readonly string[] = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']\n\n/** One open interval on a weekday, in minutes since midnight in the service's zone. */\nexport interface BookingWindow {\n start: number\n end: number\n}\n\n/** Longest stored service name. */\nexport const BOOKING_SERVICE_NAME_MAX = 80\n/** Longest stored service description. */\nexport const BOOKING_SERVICE_DESCRIPTION_MAX = 500\n\n/** The service dialog's fields as typed: numbers and hours are text until saved. */\nexport interface BookingServiceDraft {\n name: string\n durationMinutes: string\n priceUsd: string\n timezone: string\n description: string\n /** Per-weekday hours, Sunday first, e.g. \"09:00-12:00, 13:00-17:00\". */\n windowText: string[]\n crmMeetingActivity: boolean\n crmFollowUpTask: boolean\n askPhone: BookingFieldAsk\n askAddress: BookingFieldAsk\n priceDisplay: BookingPriceDisplay\n}\n\n/** What one save stores: every editable key of the service, written explicitly. */\nexport type BookingServiceFields = {\n name: string\n durationMinutes: number\n priceUsd: number\n timezone: string\n /** `''` when cleared, so a merge does not keep the old text. */\n description: string\n /** Every weekday, 0–6, with `[]` for a closed day. */\n windows: Record<string, BookingWindow[]>\n crmMeetingActivity: boolean\n crmFollowUpTask: boolean\n askPhone: BookingFieldAsk\n askAddress: BookingFieldAsk\n priceDisplay: BookingPriceDisplay\n}\n\n/** \"09:00-12:00, 13:00-17:00\" → open intervals in minutes; anything unreadable is skipped. */\nexport function parseBookingWindows(input: string): BookingWindow[] {\n const windows: BookingWindow[] = []\n for (const chunk of String(input ?? '').split(',')) {\n const match = chunk.trim().match(/^(\\d{1,2}):(\\d{2})\\s*-\\s*(\\d{1,2}):(\\d{2})$/)\n if (!match) continue\n const start = Number(match[1]) * 60 + Number(match[2])\n const end = Number(match[3]) * 60 + Number(match[4])\n if (end > start && end <= 24 * 60) windows.push({ start, end })\n }\n return windows\n}\n\n/** Open intervals → \"09:00-12:00, 13:00-17:00\". */\nexport function formatBookingWindows(windows: readonly BookingWindow[] | null | undefined): string {\n const pad = (minutes: number) =>\n `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`\n return (windows ?? []).map((window) => `${pad(window.start)}-${pad(window.end)}`).join(', ')\n}\n\n/**\n * A new service's dialog: thirty minutes, free, the editor's own zone, open\n * nine to five on weekdays, a meeting on the contact's record and no\n * follow-up, and only name and email asked.\n */\nexport function newBookingServiceDraft(timeZone: string): BookingServiceDraft {\n return {\n name: '',\n durationMinutes: '30',\n priceUsd: '0',\n timezone: timeZone || 'UTC',\n description: '',\n windowText: BOOKING_WEEKDAYS.map((_, index) => (index >= 1 && index <= 5 ? '09:00-17:00' : '')),\n crmMeetingActivity: true,\n crmFollowUpTask: false,\n askPhone: 'off',\n askAddress: 'off',\n priceDisplay: 'fixed',\n }\n}\n\n/** The dialog seeded from a stored service, so its switches show what the service does. */\nexport function bookingServiceDraftFrom(service: {\n name?: unknown\n durationMinutes?: unknown\n priceUsd?: unknown\n timezone?: unknown\n description?: unknown\n windows?: Partial<Record<number | string, BookingWindow[]>> | null\n crmMeetingActivity?: unknown\n crmFollowUpTask?: unknown\n askPhone?: unknown\n askAddress?: unknown\n priceDisplay?: unknown\n}): BookingServiceDraft {\n const asks = bookingContactAsks(service)\n return {\n name: typeof service.name === 'string' ? service.name : '',\n durationMinutes: String(service.durationMinutes ?? 30),\n priceUsd: String(service.priceUsd ?? 0),\n timezone: typeof service.timezone === 'string' ? service.timezone : 'UTC',\n description: typeof service.description === 'string' ? service.description : '',\n windowText: BOOKING_WEEKDAYS.map((_, index) => formatBookingWindows(service.windows?.[index])),\n // Absent reads as ON for the meeting and OFF for the follow-up: the\n // model's defaults.\n crmMeetingActivity: service.crmMeetingActivity !== false,\n crmFollowUpTask: service.crmFollowUpTask === true,\n askPhone: asks.phone,\n askAddress: asks.address,\n priceDisplay: bookingPriceDisplay(service.priceDisplay),\n }\n}\n\n/** Why the dialog cannot save, or null. */\nexport function bookingServiceDraftProblem(draft: Pick<BookingServiceDraft, 'name'>): string | null {\n return String(draft.name ?? '').trim() ? null : 'A service needs a name.'\n}\n\n/**\n * What one save stores. Every editable key is written explicitly, because\n * the edit is a `merge` and an absent key would keep its stored value: a\n * switch turned off has to land `false`, and a weekday cleared has to land\n * `[]`, or the old hours would go on taking bookings; a description\n * cleared lands `''` for the same reason.\n */\nexport function bookingServiceFields(draft: BookingServiceDraft): BookingServiceFields {\n const windows: Record<string, BookingWindow[]> = {}\n BOOKING_WEEKDAYS.forEach((_, weekday) => {\n windows[String(weekday)] = parseBookingWindows(draft.windowText?.[weekday] ?? '')\n })\n const description = String(draft.description ?? '').trim()\n return {\n name: String(draft.name ?? '').trim().slice(0, BOOKING_SERVICE_NAME_MAX),\n durationMinutes: Math.max(5, Math.min(480, Math.round(Number(draft.durationMinutes) || 30))),\n priceUsd: Math.max(0, Math.round(Number(draft.priceUsd) || 0)),\n timezone: String(draft.timezone ?? '').trim() || 'UTC',\n description: description.slice(0, BOOKING_SERVICE_DESCRIPTION_MAX),\n windows,\n crmMeetingActivity: draft.crmMeetingActivity,\n crmFollowUpTask: draft.crmFollowUpTask,\n askPhone: draft.askPhone,\n askAddress: draft.askAddress,\n priceDisplay: draft.priceDisplay,\n }\n}\n"],"names":["bookingContactAsks","bookingPriceDisplay","BOOKING_WEEKDAYS","BOOKING_SERVICE_NAME_MAX","BOOKING_SERVICE_DESCRIPTION_MAX","parseBookingWindows","input","windows","chunk","String","split","match","trim","start","Number","end","push","formatBookingWindows","pad","minutes","Math","floor","padStart","map","window","join","newBookingServiceDraft","timeZone","name","durationMinutes","priceUsd","timezone","description","windowText","_","index","crmMeetingActivity","crmFollowUpTask","askPhone","askAddress","priceDisplay","bookingServiceDraftFrom","service","asks","phone","address","bookingServiceDraftProblem","draft","bookingServiceFields","forEach","weekday","slice","max","min","round"],"mappings":"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"}
@@ -0,0 +1,63 @@
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 BookingState } from './booking-manage';
18
+ /**
19
+ * A booking as the customer REST API publishes it (AGL-3643):
20
+ * `GET /v1/sites/{siteId}/bookings/{bookingId}`, and the `booking` every
21
+ * booking event carries, so a subscriber reads one documented object rather
22
+ * than the plugin's storage.
23
+ *
24
+ * Money is integer cents, in US dollars — a booking is charged in USD. Times
25
+ * are ISO 8601 in UTC, with the zone the booking was made in beside them.
26
+ * Never Stripe's handles, the CRM reference or the reminder stamps: those are
27
+ * how the platform runs a booking, not what one is.
28
+ *
29
+ * Pure: no I/O, so the API handler, the event raisers and a spec share it.
30
+ */
31
+ export interface BookingView {
32
+ id: string;
33
+ object: 'booking';
34
+ serviceId: string | null;
35
+ serviceName: string | null;
36
+ /**
37
+ * `confirmed`, `pendingPayment` (a paid booking whose checkout is still
38
+ * open), `expired` (that checkout ran out and the slot was released) or
39
+ * `canceled`.
40
+ */
41
+ status: BookingState;
42
+ name: string | null;
43
+ email: string | null;
44
+ phone: string | null;
45
+ address: Record<string, unknown> | string | null;
46
+ startsAt: string | null;
47
+ endsAt: string | null;
48
+ timeZone: string | null;
49
+ currency: 'usd';
50
+ /** What the guest paid, tax included; 0 for a free booking. */
51
+ paidCents: number;
52
+ taxCents: number;
53
+ refundedCents: number;
54
+ checkedIn: boolean;
55
+ checkedInAt: string | null;
56
+ /** Where a moved booking started before its latest move, else `null`. */
57
+ rescheduledFrom: string | null;
58
+ created: string | null;
59
+ }
60
+ /** Epoch milliseconds, a Firestore Timestamp or a Date, as ISO 8601. */
61
+ export declare function bookingInstant(value: unknown): string | null;
62
+ /** One stored booking as the API publishes it. `nowMs` decides an expired hold. */
63
+ export declare function bookingViewFromData(id: string, data: Record<string, unknown>, nowMs?: number): BookingView;
@@ -0,0 +1,70 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { bookingState } from "./booking-manage.js";
18
+ const text = (value)=>{
19
+ const out = typeof value === 'string' ? value.trim() : '';
20
+ return out ? out : null;
21
+ };
22
+ const cents = (value)=>{
23
+ const out = Math.round(Number(value != null ? value : 0));
24
+ return Number.isFinite(out) && out > 0 ? out : 0;
25
+ };
26
+ /** Epoch milliseconds, a Firestore Timestamp or a Date, as ISO 8601. */ export function bookingInstant(value) {
27
+ if (value == null) return null;
28
+ if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? new Date(value).toISOString() : null;
29
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.toISOString();
30
+ const stamp = value;
31
+ if (typeof stamp.toMillis === 'function') return bookingInstant(stamp.toMillis());
32
+ if (typeof stamp.toDate === 'function') return bookingInstant(stamp.toDate());
33
+ return null;
34
+ }
35
+ function addressOf(value) {
36
+ if (typeof value === 'string') return text(value);
37
+ if (value && typeof value === 'object' && !Array.isArray(value)) return _extends({}, value);
38
+ return null;
39
+ }
40
+ /** One stored booking as the API publishes it. `nowMs` decides an expired hold. */ export function bookingViewFromData(id, data, nowMs = Date.now()) {
41
+ var _data_checkedInAtMs, _data_expiresAtMs, _data_startsAtMs, _data_endsAtMs, _data_rescheduledFromMs;
42
+ const checkedInAtMs = Number((_data_checkedInAtMs = data['checkedInAtMs']) != null ? _data_checkedInAtMs : 0);
43
+ return {
44
+ id,
45
+ object: 'booking',
46
+ serviceId: text(data['serviceId']),
47
+ serviceName: text(data['serviceName']),
48
+ status: bookingState({
49
+ status: typeof data['status'] === 'string' ? data['status'] : undefined,
50
+ expiresAtMs: Number((_data_expiresAtMs = data['expiresAtMs']) != null ? _data_expiresAtMs : 0) || undefined
51
+ }, nowMs),
52
+ name: text(data['name']),
53
+ email: text(data['email']),
54
+ phone: text(data['phone']),
55
+ address: addressOf(data['address']),
56
+ startsAt: bookingInstant(Number((_data_startsAtMs = data['startsAtMs']) != null ? _data_startsAtMs : 0)),
57
+ endsAt: bookingInstant(Number((_data_endsAtMs = data['endsAtMs']) != null ? _data_endsAtMs : 0)),
58
+ timeZone: text(data['timezone']),
59
+ currency: 'usd',
60
+ paidCents: cents(data['paidAmountCents']),
61
+ taxCents: cents(data['taxCents']),
62
+ refundedCents: cents(data['refundedCents']),
63
+ checkedIn: checkedInAtMs > 0,
64
+ checkedInAt: checkedInAtMs > 0 ? bookingInstant(checkedInAtMs) : null,
65
+ rescheduledFrom: bookingInstant(Number((_data_rescheduledFromMs = data['rescheduledFromMs']) != null ? _data_rescheduledFromMs : 0)),
66
+ created: bookingInstant(data['createdAt'])
67
+ };
68
+ }
69
+
70
+ //# sourceMappingURL=booking-view.js.map