@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 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/transfer/bookings-transfer-common.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 { MatchKeySpec } from '@aglyn/aglyn/data-transfer'\n\n/**\n * WHAT BOTH HALVES OF THE `bookings` TRANSFER RESOURCE SHARE, in a module\n * with no server dependency: the key, the match keys the declarations\n * register synchronously, and the list's filters.\n *\n * The Bookings page and the export's `readPage` must name a filter the same\n * way: the page builds the value it hands the export dialog, and the server\n * half parses it back into one Firestore query.\n *\n * One filter at a time, each a query an index serves:\n *\n * - `{ upcoming: true, asOfMs? }` — the page's default list: every booking\n * that has not ended by `asOfMs` (the moment the page drew the list; now\n * when absent), canceled ones included, as the list shows them.\n * - `{ email }` — one booker's bookings, past and upcoming, the list a\n * contact's record links to.\n * - `{ serviceId }` — one service's bookings.\n *\n * Anything else is refused rather than read as \"everything\": a file that\n * says it holds a filtered list and holds the whole site would be worse than\n * no file.\n */\n\n/** The resource key `plugins.config.json` declares under `transferResources`. */\nexport const BOOKINGS_TRANSFER_RESOURCE = 'bookings'\n\n/** The one key a row is named by: its Aglyn ID. Nothing is ever matched to import. */\nexport const BOOKINGS_TRANSFER_MATCH_KEYS: readonly MatchKeySpec[] = [\n { fieldId: 'id', normalizer: 'aglynId' },\n]\n\n/** One of the bookings list's filters, in the export's terms. */\nexport type BookingsTransferFilter =\n | { kind: 'upcoming'; asOfMs: number | null }\n | { kind: 'email'; email: string }\n | { kind: 'service'; serviceId: string }\n\n/** Why a filter could not be read, in a sentence the person reads. */\nexport class BookingsTransferFilterError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'BookingsTransferFilterError'\n }\n}\n\n/** The value the page hands the export dialog for its default list. */\nexport function upcomingBookingsFilterValue(\n asOfMs: number,\n): Record<string, unknown> {\n return { upcoming: true, asOfMs }\n}\n\n/** The value the page hands the export dialog when narrowed to one booker. */\nexport function bookerBookingsFilterValue(\n email: string,\n): Record<string, unknown> {\n return { email }\n}\n\n/**\n * The filter `value` names, or `null` for none (an empty object). Throws\n * {@link BookingsTransferFilterError} for anything else: an unknown key, two\n * filters at once, or a value of the wrong shape.\n */\nexport function parseBookingsTransferFilter(\n value: Readonly<Record<string, unknown>> | null | undefined,\n): BookingsTransferFilter | null {\n if (!value) return null\n const keys = Object.keys(value).filter((key) => value[key] !== undefined)\n if (!keys.length) return null\n const unknown = keys.filter(\n (key) => !['upcoming', 'asOfMs', 'email', 'serviceId'].includes(key),\n )\n if (unknown.length) {\n throw new BookingsTransferFilterError(\n `Bookings can be filtered by upcoming, email or service, not by ${unknown.join(', ')}.`,\n )\n }\n const named = ['upcoming', 'email', 'serviceId'].filter((key) =>\n keys.includes(key),\n )\n if (named.length > 1) {\n throw new BookingsTransferFilterError(\n 'Bookings are exported with one filter at a time.',\n )\n }\n if (keys.includes('asOfMs') && !keys.includes('upcoming')) {\n throw new BookingsTransferFilterError(\n 'A start time only narrows the upcoming bookings.',\n )\n }\n if (keys.includes('upcoming')) {\n if (value['upcoming'] !== true) {\n throw new BookingsTransferFilterError(\n 'The upcoming filter is on or absent.',\n )\n }\n const asOf = value['asOfMs']\n if (asOf === undefined || asOf === null)\n return { kind: 'upcoming', asOfMs: null }\n if (typeof asOf !== 'number' || !Number.isFinite(asOf)) {\n throw new BookingsTransferFilterError(\n 'The upcoming filter’s start time is not a time.',\n )\n }\n return { kind: 'upcoming', asOfMs: Math.floor(asOf) }\n }\n if (keys.includes('email')) {\n // Stored lowercased and trimmed by the booking route, so that is the\n // form an equality has to ask in.\n const email = String(value['email'] ?? '')\n .trim()\n .toLowerCase()\n if (!email || !email.includes('@')) {\n throw new BookingsTransferFilterError(\n 'The email filter needs an email address.',\n )\n }\n return { kind: 'email', email }\n }\n const serviceId = String(value['serviceId'] ?? '').trim()\n if (!serviceId || serviceId.includes('/')) {\n throw new BookingsTransferFilterError('The service filter needs a service.')\n }\n return { kind: 'service', serviceId }\n}\n"],"names":["BOOKINGS_TRANSFER_RESOURCE","BOOKINGS_TRANSFER_MATCH_KEYS","fieldId","normalizer","BookingsTransferFilterError","Error","message","name","upcomingBookingsFilterValue","asOfMs","upcoming","bookerBookingsFilterValue","email","parseBookingsTransferFilter","value","keys","Object","filter","key","undefined","length","unknown","includes","join","named","asOf","kind","Number","isFinite","Math","floor","String","trim","toLowerCase","serviceId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;;;;;;;;CAqBC,GAED,+EAA+E,GAC/E,OAAO,MAAMA,6BAA6B,WAAU;AAEpD,oFAAoF,GACpF,OAAO,MAAMC,+BAAwD;IACnE;QAAEC,SAAS;QAAMC,YAAY;IAAU;CACxC,CAAA;AAQD,oEAAoE,GACpE,OAAO,MAAMC,oCAAoCC;IAC/C,YAAYC,OAAe,CAAE;QAC3B,KAAK,CAACA;QACN,IAAI,CAACC,IAAI,GAAG;IACd;AACF;AAEA,qEAAqE,GACrE,OAAO,SAASC,4BACdC,MAAc;IAEd,OAAO;QAAEC,UAAU;QAAMD;IAAO;AAClC;AAEA,4EAA4E,GAC5E,OAAO,SAASE,0BACdC,KAAa;IAEb,OAAO;QAAEA;IAAM;AACjB;AAEA;;;;CAIC,GACD,OAAO,SAASC,4BACdC,KAA2D;QAuDlCA;IArDzB,IAAI,CAACA,OAAO,OAAO;IACnB,MAAMC,OAAOC,OAAOD,IAAI,CAACD,OAAOG,MAAM,CAAC,CAACC,MAAQJ,KAAK,CAACI,IAAI,KAAKC;IAC/D,IAAI,CAACJ,KAAKK,MAAM,EAAE,OAAO;IACzB,MAAMC,UAAUN,KAAKE,MAAM,CACzB,CAACC,MAAQ,CAAC;YAAC;YAAY;YAAU;YAAS;SAAY,CAACI,QAAQ,CAACJ;IAElE,IAAIG,QAAQD,MAAM,EAAE;QAClB,MAAM,IAAIhB,4BACR,CAAC,+DAA+D,EAAEiB,QAAQE,IAAI,CAAC,MAAM,CAAC,CAAC;IAE3F;IACA,MAAMC,QAAQ;QAAC;QAAY;QAAS;KAAY,CAACP,MAAM,CAAC,CAACC,MACvDH,KAAKO,QAAQ,CAACJ;IAEhB,IAAIM,MAAMJ,MAAM,GAAG,GAAG;QACpB,MAAM,IAAIhB,4BACR;IAEJ;IACA,IAAIW,KAAKO,QAAQ,CAAC,aAAa,CAACP,KAAKO,QAAQ,CAAC,aAAa;QACzD,MAAM,IAAIlB,4BACR;IAEJ;IACA,IAAIW,KAAKO,QAAQ,CAAC,aAAa;QAC7B,IAAIR,KAAK,CAAC,WAAW,KAAK,MAAM;YAC9B,MAAM,IAAIV,4BACR;QAEJ;QACA,MAAMqB,OAAOX,KAAK,CAAC,SAAS;QAC5B,IAAIW,SAASN,aAAaM,SAAS,MACjC,OAAO;YAAEC,MAAM;YAAYjB,QAAQ;QAAK;QAC1C,IAAI,OAAOgB,SAAS,YAAY,CAACE,OAAOC,QAAQ,CAACH,OAAO;YACtD,MAAM,IAAIrB,4BACR;QAEJ;QACA,OAAO;YAAEsB,MAAM;YAAYjB,QAAQoB,KAAKC,KAAK,CAACL;QAAM;IACtD;IACA,IAAIV,KAAKO,QAAQ,CAAC,UAAU;YAGLR;QAFrB,qEAAqE;QACrE,kCAAkC;QAClC,MAAMF,QAAQmB,QAAOjB,eAAAA,KAAK,CAAC,QAAQ,YAAdA,eAAkB,IACpCkB,IAAI,GACJC,WAAW;QACd,IAAI,CAACrB,SAAS,CAACA,MAAMU,QAAQ,CAAC,MAAM;YAClC,MAAM,IAAIlB,4BACR;QAEJ;QACA,OAAO;YAAEsB,MAAM;YAASd;QAAM;IAChC;IACA,MAAMsB,YAAYH,QAAOjB,mBAAAA,KAAK,CAAC,YAAY,YAAlBA,mBAAsB,IAAIkB,IAAI;IACvD,IAAI,CAACE,aAAaA,UAAUZ,QAAQ,CAAC,MAAM;QACzC,MAAM,IAAIlB,4BAA4B;IACxC;IACA,OAAO;QAAEsB,MAAM;QAAWQ;IAAU;AACtC"}
@@ -0,0 +1,86 @@
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 TransferCatalogInput } from '@aglyn/aglyn/data-transfer';
18
+ import type { PluginTransferResource } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources';
19
+ /**
20
+ * BOOKINGS, EXPORTED (the server half of the `bookings` transfer resource).
21
+ *
22
+ * Every booking of one site, `hosts/{hostId}/bookings/{id}`, as rows of the
23
+ * fields a person picks: the service and the slot, the customer, what was
24
+ * paid, and when the platform did what to it.
25
+ *
26
+ * ## Exported, never imported
27
+ *
28
+ * The resource declares `exportOnly`, so it registers no `apply` or
29
+ * `revert` (its `lookup` answers the Aglyn ID only) and the upload route
30
+ * refuses it. There is no safe
31
+ * write path to give it: the only thing that creates a booking is the
32
+ * booking route, which holds the slot, may start a Stripe charge, emails the
33
+ * customer and files the meeting in the CRM — and the hourly reminder job
34
+ * would email any confirmed booking it found a day out. A booking written
35
+ * from a file would be an appointment nobody booked.
36
+ *
37
+ * ## Which bookings a page reads
38
+ *
39
+ * - `ids`: the selection, read by id in the order given; ids that name no
40
+ * booking are skipped. The cursor is the offset into the list.
41
+ * - `filter`: one of the list's filters (`bookings-transfer-common.ts`), as
42
+ * one Firestore query:
43
+ * - none: every booking, by start, `startsAtMs` ascending — the
44
+ * single-field index;
45
+ * - upcoming: `endsAtMs >= asOf`, by end ascending — the single-field
46
+ * index on `endsAtMs` (the range field has to lead the order);
47
+ * - one service: `serviceId ==`, by start — the declared composite
48
+ * `serviceId ASC, startsAtMs ASC`;
49
+ * - one booker: `email ==`, by start — the composite
50
+ * `email ASC, startsAtMs ASC`, which `bookings-export-indexes.spec.ts`
51
+ * holds the index file to.
52
+ * Every order breaks ties by document id, and the cursor is the last row's
53
+ * order value and id, so a page never repeats or skips a row.
54
+ * - `scopeTokens` narrows nothing here. Bookings are a SITE's records and
55
+ * carry no `visibleTo`; the export route admits a reader only with
56
+ * `data.manage` on the site named, so a collaborator who reaches this read
57
+ * reaches every booking of that site, the way the Bookings page shows them.
58
+ *
59
+ * Times go out as ISO instants in UTC, beside the booking's own zone and
60
+ * the start and end as a wall clock in it. Amounts are dollars (every
61
+ * booking is charged in USD), from the stored cents.
62
+ */
63
+ type Firestore = FirebaseFirestore.Firestore;
64
+ export interface BookingsTransferDeps {
65
+ firestore(): Firestore;
66
+ /** The clock the upcoming filter reads when it names no moment. */
67
+ now?: () => number;
68
+ }
69
+ export declare const BOOKINGS_TRANSFER_GROUPS: readonly [{
70
+ readonly id: "booking";
71
+ readonly label: "Booking";
72
+ }, {
73
+ readonly id: "customer";
74
+ readonly label: "Customer";
75
+ }, {
76
+ readonly id: "payment";
77
+ readonly label: "Payment";
78
+ }, {
79
+ readonly id: "system";
80
+ readonly label: "System";
81
+ }];
82
+ /** The catalog, the same for every site. */
83
+ export declare function bookingsTransferCatalog(): TransferCatalogInput;
84
+ /** The `bookings` resource over `deps.firestore()`. Export only: a lookup by id, and no apply or revert. */
85
+ export declare function createBookingsTransferResource(deps: BookingsTransferDeps): PluginTransferResource;
86
+ export {};