@aglyn/plugins-bookings 1.0.0-beta.230 → 1.0.0-beta.232

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 (42) hide show
  1. package/README.md +3 -1
  2. package/package.json +11 -11
  3. package/src/lib/components/bookings-console-page.js +30 -4
  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-in-person.d.ts +72 -0
  12. package/src/lib/model/booking-in-person.js +71 -0
  13. package/src/lib/model/booking-in-person.js.map +1 -0
  14. package/src/lib/model/booking-manage.d.ts +73 -0
  15. package/src/lib/model/booking-manage.js +104 -0
  16. package/src/lib/model/booking-manage.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 +93 -0
  32. package/src/lib/server/booking-manage.js +380 -0
  33. package/src/lib/server/booking-manage.js.map +1 -0
  34. package/src/lib/server/in-person-payment.d.ts +20 -0
  35. package/src/lib/server/in-person-payment.js +469 -0
  36. package/src/lib/server/in-person-payment.js.map +1 -0
  37. package/src/lib/server/refund.js +17 -1
  38. package/src/lib/server/refund.js.map +1 -1
  39. package/src/lib/server.js +30 -2
  40. package/src/lib/server.js.map +1 -1
  41. package/src/lib/tenant-emails.js +59 -0
  42. package/src/lib/tenant-emails.js.map +1 -1
@@ -0,0 +1,50 @@
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 { raisePluginEvent, stagePluginEvent } from "@aglyn/tenant-data-admin/server/plugin-event-outbox";
17
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
18
+ import { bookingEventKey } from "../model/booking-events.js";
19
+ import { bookingViewFromData } from "../model/booking-view.js";
20
+ function requestFor(input) {
21
+ var _input_nowMs, _input_booking_startsAtMs;
22
+ const nowMs = (_input_nowMs = input.nowMs) != null ? _input_nowMs : Date.now();
23
+ const payload = {
24
+ booking: bookingViewFromData(input.bookingId, input.booking, nowMs)
25
+ };
26
+ return {
27
+ event: input.event,
28
+ pluginId: BUNDLE_ID,
29
+ hostId: input.hostId,
30
+ payload,
31
+ key: bookingEventKey(input.event, input.bookingId, Number((_input_booking_startsAtMs = input.booking['startsAtMs']) != null ? _input_booking_startsAtMs : 0)),
32
+ occurredAtMs: nowMs
33
+ };
34
+ }
35
+ /** Stages the event inside the caller's transaction or batch. */ export function stageBookingEvent(writer, firestore, input) {
36
+ return stagePluginEvent(writer, firestore, requestFor(input), input.nowMs);
37
+ }
38
+ /**
39
+ * Raises the event after the fact is written. Never throws: the booking has
40
+ * already changed, and a lost event is logged by the outbox rather than
41
+ * failing the guest's or the team's request.
42
+ */ export async function raiseBookingEvent(firestore, input) {
43
+ try {
44
+ await raisePluginEvent(firestore, requestFor(input), input.nowMs);
45
+ } catch (error) {
46
+ console.error('[bookings] event not raised', input.event, input.hostId, input.bookingId, error);
47
+ }
48
+ }
49
+
50
+ //# sourceMappingURL=booking-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/server/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 raisePluginEvent,\n stagePluginEvent,\n type PluginEventWriter,\n} from '@aglyn/tenant-data-admin/server/plugin-event-outbox'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { bookingEventKey, type BookingEventName, type BookingEventPayload } from '../model/booking-events'\nimport { bookingViewFromData } from '../model/booking-view'\n\n/**\n * Raising the booking events (AGL-3643). See `model/booking-events.ts` for\n * what each means. A raiser with a transaction stages its event in it, so\n * the event exists exactly when the fact does; one without raises it after\n * the write, keyed, so a retried request raises it once.\n */\n\nexport interface BookingEventInput {\n event: BookingEventName\n hostId: string\n bookingId: string\n /** The booking AS WRITTEN: the stored fields with this change applied. */\n booking: Record<string, unknown>\n nowMs?: number\n}\n\nfunction requestFor(input: BookingEventInput) {\n const nowMs = input.nowMs ?? Date.now()\n const payload: BookingEventPayload = { booking: bookingViewFromData(input.bookingId, input.booking, nowMs) }\n return {\n event: input.event,\n pluginId: BUNDLE_ID,\n hostId: input.hostId,\n payload,\n key: bookingEventKey(input.event, input.bookingId, Number(input.booking['startsAtMs'] ?? 0)),\n occurredAtMs: nowMs,\n }\n}\n\n/** Stages the event inside the caller's transaction or batch. */\nexport function stageBookingEvent(\n writer: PluginEventWriter,\n firestore: FirebaseFirestore.Firestore,\n input: BookingEventInput,\n): string {\n return stagePluginEvent(writer, firestore, requestFor(input), input.nowMs)\n}\n\n/**\n * Raises the event after the fact is written. Never throws: the booking has\n * already changed, and a lost event is logged by the outbox rather than\n * failing the guest's or the team's request.\n */\nexport async function raiseBookingEvent(\n firestore: FirebaseFirestore.Firestore,\n input: BookingEventInput,\n): Promise<void> {\n try {\n await raisePluginEvent(firestore, requestFor(input), input.nowMs)\n } catch (error) {\n console.error('[bookings] event not raised', input.event, input.hostId, input.bookingId, error)\n }\n}\n"],"names":["raisePluginEvent","stagePluginEvent","BUNDLE_ID","bookingEventKey","bookingViewFromData","requestFor","input","nowMs","Date","now","payload","booking","bookingId","event","pluginId","hostId","key","Number","occurredAtMs","stageBookingEvent","writer","firestore","raiseBookingEvent","error","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAChBC,gBAAgB,QAEX,sDAAqD;AAC5D,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,eAAe,QAAyD,6BAAyB;AAC1G,SAASC,mBAAmB,QAAQ,2BAAuB;AAkB3D,SAASC,WAAWC,KAAwB;QAC5BA,cAO8CA;IAP5D,MAAMC,SAAQD,eAAAA,MAAMC,KAAK,YAAXD,eAAeE,KAAKC,GAAG;IACrC,MAAMC,UAA+B;QAAEC,SAASP,oBAAoBE,MAAMM,SAAS,EAAEN,MAAMK,OAAO,EAAEJ;IAAO;IAC3G,OAAO;QACLM,OAAOP,MAAMO,KAAK;QAClBC,UAAUZ;QACVa,QAAQT,MAAMS,MAAM;QACpBL;QACAM,KAAKb,gBAAgBG,MAAMO,KAAK,EAAEP,MAAMM,SAAS,EAAEK,QAAOX,4BAAAA,MAAMK,OAAO,CAAC,aAAa,YAA3BL,4BAA+B;QACzFY,cAAcX;IAChB;AACF;AAEA,+DAA+D,GAC/D,OAAO,SAASY,kBACdC,MAAyB,EACzBC,SAAsC,EACtCf,KAAwB;IAExB,OAAOL,iBAAiBmB,QAAQC,WAAWhB,WAAWC,QAAQA,MAAMC,KAAK;AAC3E;AAEA;;;;CAIC,GACD,OAAO,eAAee,kBACpBD,SAAsC,EACtCf,KAAwB;IAExB,IAAI;QACF,MAAMN,iBAAiBqB,WAAWhB,WAAWC,QAAQA,MAAMC,KAAK;IAClE,EAAE,OAAOgB,OAAO;QACdC,QAAQD,KAAK,CAAC,+BAA+BjB,MAAMO,KAAK,EAAEP,MAAMS,MAAM,EAAET,MAAMM,SAAS,EAAEW;IAC3F;AACF"}
@@ -0,0 +1,93 @@
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 { PluginApiHandler } from '@aglyn/aglyn/app-utils/api-plugins';
18
+ import { type HostBookingService } from '../model/bookings';
19
+ import { type BookingEventInput } from './booking-events';
20
+ /** The Firestore surface these routes use: the Admin SDK's, narrowed so a spec can hand in a double. */
21
+ export interface ManageDocSnapshot {
22
+ readonly exists: boolean;
23
+ readonly id?: string;
24
+ get(field: string): unknown;
25
+ data(): Record<string, unknown> | undefined;
26
+ }
27
+ export interface ManageDocRef {
28
+ readonly id: string;
29
+ }
30
+ export interface ManageQuery {
31
+ where(field: string, op: '==' | '>=' | '<', value: unknown): ManageQuery;
32
+ limit(count: number): ManageQuery;
33
+ }
34
+ export interface ManageTransaction {
35
+ get(ref: ManageDocRef): Promise<ManageDocSnapshot>;
36
+ get(query: ManageQuery): Promise<{
37
+ docs: ManageDocSnapshot[];
38
+ }>;
39
+ update(ref: ManageDocRef, data: Record<string, unknown>): unknown;
40
+ }
41
+ export interface ManageFirestore {
42
+ doc(path: string): ManageDocRef & {
43
+ get(): Promise<ManageDocSnapshot>;
44
+ };
45
+ collection(path: string): ManageQuery;
46
+ runTransaction<T>(fn: (transaction: ManageTransaction) => Promise<T>): Promise<T>;
47
+ }
48
+ export interface RescheduleNotice {
49
+ hostId: string;
50
+ bookingId: string;
51
+ to: string;
52
+ name: string;
53
+ serviceName: string;
54
+ when: string;
55
+ previousWhen: string;
56
+ timezone: string;
57
+ }
58
+ export interface BookingManageDeps {
59
+ firestore: () => ManageFirestore;
60
+ verifyIdToken: (token: string) => Promise<{
61
+ uid: string;
62
+ }>;
63
+ /** True when the site is locked down and must not take writes. */
64
+ siteLocked: (hostId: string) => Promise<boolean>;
65
+ /** The zone a booking is told in when the document carries none. */
66
+ timeZoneFor: (hostId: string, service: HostBookingService | undefined) => Promise<string>;
67
+ notifyRescheduled: (notice: RescheduleNotice) => Promise<void>;
68
+ now: () => number;
69
+ /**
70
+ * Raises a booking event once the change is written (AGL-3643): the move,
71
+ * the cancel. Never throws. Absent, nothing is raised.
72
+ */
73
+ raiseEvent?: (input: BookingEventInput) => Promise<void>;
74
+ }
75
+ /** Field the booking stamps when the guest arrives. */
76
+ export declare const CHECKED_IN_AT = "checkedInAtMs";
77
+ export declare const CHECKED_IN_BY = "checkedInBy";
78
+ export declare function createBookingCheckInHandler(deps: BookingManageDeps): PluginApiHandler;
79
+ export declare function createBookingRescheduleHandler(deps: BookingManageDeps): PluginApiHandler;
80
+ /**
81
+ * Canceling a booking that has no money on it (AGL-3643): a free booking, or
82
+ * a paid one already refunded in full. The site's team used to write
83
+ * `status: 'canceled'` straight from the console; a route is where the
84
+ * cancel can be told to the plugins that listen for it (`booking.canceled`),
85
+ * and where the rule that a paid booking cancels through its refund is held
86
+ * rather than trusted to the screen. One transaction: a booking checked in
87
+ * or paid a moment ago is refused, and canceling it twice changes nothing.
88
+ */
89
+ export declare function createBookingCancelHandler(deps: BookingManageDeps): PluginApiHandler;
90
+ /** The email that tells a guest their booking moved, as the confirmation is sent. */
91
+ export declare function sendRescheduledNotice(notice: RescheduleNotice): Promise<void>;
92
+ /** The production wiring: the Admin SDK, the site lockdown verdict and the real send. */
93
+ export declare function bookingManageDeps(): BookingManageDeps;
@@ -0,0 +1,380 @@
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 { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
18
+ import { isEmailConfigured, sendEmail } from "@aglyn/shared-util-email";
19
+ import { firebaseAdmin, getOrgForHost, hostSendingIdentity, meterHostEmail, renderHostEmailWithTokens } from "@aglyn/tenant-data-admin";
20
+ import { getSiteLockdown } from "@aglyn/tenant-data-admin/server/tenant-write-lockdown";
21
+ import { resolveBrandingProfile } from "@aglyn/aglyn/server";
22
+ import { bookingDurationMs, bookingState, cancelRefusal, checkInRefusal, rescheduleRefusal } from "../model/booking-manage.js";
23
+ import { isSlotOpen } from "../model/bookings.js";
24
+ import { bookingTimeZone, formatBookingWhen, storedBookingTimeZone } from "../model/booking-time.js";
25
+ import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
26
+ import { raiseBookingEvent } from "./booking-events.js";
27
+ /** Field the booking stamps when the guest arrives. */ export const CHECKED_IN_AT = 'checkedInAtMs';
28
+ export const CHECKED_IN_BY = 'checkedInBy';
29
+ let Refusal = class Refusal extends Error {
30
+ constructor(status, message){
31
+ super(message), this.status = status;
32
+ }
33
+ };
34
+ const bearerOf = (req)=>{
35
+ var _ref, _req_headers_authorization;
36
+ const header = String((_ref = (_req_headers_authorization = req.headers['authorization']) != null ? _req_headers_authorization : req.headers['Authorization']) != null ? _ref : '');
37
+ return header.startsWith('Bearer ') ? header.slice('Bearer '.length) : '';
38
+ };
39
+ const bodyOf = (req)=>{
40
+ var _req_body;
41
+ if (typeof req.body === 'string') {
42
+ try {
43
+ return JSON.parse(req.body);
44
+ } catch (unused) {
45
+ return {};
46
+ }
47
+ }
48
+ return (_req_body = req.body) != null ? _req_body : {};
49
+ };
50
+ /** The member, their right to write this site's bookings, and the site being open. */ async function authorize(deps, req, hostId) {
51
+ var _host_get;
52
+ const token = bearerOf(req);
53
+ if (!token) throw new Refusal(401, 'Unauthenticated');
54
+ let uid;
55
+ try {
56
+ uid = (await deps.verifyIdToken(token)).uid;
57
+ } catch (error) {
58
+ if (!isRefusedIdToken(error)) throw error;
59
+ throw new Refusal(401, 'Unauthenticated');
60
+ }
61
+ const host = await deps.firestore().doc(`hosts/${hostId}`).get();
62
+ if (!host.exists) throw new Refusal(404, 'Unknown site');
63
+ const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
64
+ if (!hostRoleCanWrite(role)) throw new Refusal(403, 'Your role on this site cannot change bookings');
65
+ if (await deps.siteLocked(hostId)) throw new Refusal(423, 'This site is locked and cannot take changes right now');
66
+ return {
67
+ uid
68
+ };
69
+ }
70
+ function answer(res, error) {
71
+ if (error instanceof Refusal) {
72
+ res.status(error.status).json({
73
+ error: error.message
74
+ });
75
+ return;
76
+ }
77
+ console.error('[bookings/manage]', error);
78
+ res.status(500).json({
79
+ error: 'That did not go through. Try again.'
80
+ });
81
+ }
82
+ export function createBookingCheckInHandler(deps) {
83
+ return async (req, res)=>{
84
+ var _body_hostId, _body_bookingId;
85
+ if (req.method !== 'POST') return res.status(405).json({
86
+ error: 'Method not allowed'
87
+ });
88
+ const body = bodyOf(req);
89
+ const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
90
+ const bookingId = String((_body_bookingId = body['bookingId']) != null ? _body_bookingId : '');
91
+ const checkedIn = body['checkedIn'] !== false;
92
+ if (!hostId || !bookingId) return res.status(400).json({
93
+ error: 'Missing hostId or bookingId'
94
+ });
95
+ try {
96
+ const { uid } = await authorize(deps, req, hostId);
97
+ const firestore = deps.firestore();
98
+ const ref = firestore.doc(`hosts/${hostId}/bookings/${bookingId}`);
99
+ const result = await firestore.runTransaction(async (transaction)=>{
100
+ var _snapshot_data, _booking_checkedInAtMs;
101
+ const snapshot = await transaction.get(ref);
102
+ if (!snapshot.exists) throw new Refusal(404, 'Unknown booking');
103
+ const booking = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
104
+ const nowMs = deps.now();
105
+ const refusal = checkInRefusal(booking, checkedIn, nowMs);
106
+ if (refusal) throw new Refusal(409, refusal);
107
+ const already = Number((_booking_checkedInAtMs = booking.checkedInAtMs) != null ? _booking_checkedInAtMs : 0) > 0;
108
+ // Already where it was asked to be: a retried tap changes nothing.
109
+ if (already === checkedIn) return {
110
+ checkedInAtMs: already ? Number(booking.checkedInAtMs) : null
111
+ };
112
+ if (checkedIn) {
113
+ transaction.update(ref, {
114
+ [CHECKED_IN_AT]: nowMs,
115
+ [CHECKED_IN_BY]: uid
116
+ });
117
+ return {
118
+ checkedInAtMs: nowMs
119
+ };
120
+ }
121
+ transaction.update(ref, {
122
+ [CHECKED_IN_AT]: null,
123
+ [CHECKED_IN_BY]: null
124
+ });
125
+ return {
126
+ checkedInAtMs: null
127
+ };
128
+ });
129
+ return res.status(200).json(_extends({
130
+ ok: true
131
+ }, result));
132
+ } catch (error) {
133
+ return answer(res, error);
134
+ }
135
+ };
136
+ }
137
+ export function createBookingRescheduleHandler(deps) {
138
+ return async (req, res)=>{
139
+ var _body_hostId, _body_bookingId;
140
+ if (req.method !== 'POST') return res.status(405).json({
141
+ error: 'Method not allowed'
142
+ });
143
+ const body = bodyOf(req);
144
+ const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
145
+ const bookingId = String((_body_bookingId = body['bookingId']) != null ? _body_bookingId : '');
146
+ const startsAtMs = Number(body['startsAtMs']);
147
+ if (!hostId || !bookingId) return res.status(400).json({
148
+ error: 'Missing hostId or bookingId'
149
+ });
150
+ if (!Number.isFinite(startsAtMs) || startsAtMs <= 0) return res.status(400).json({
151
+ error: 'Pick a new time'
152
+ });
153
+ try {
154
+ var _first_get, _moved_booking_email;
155
+ const { uid } = await authorize(deps, req, hostId);
156
+ const firestore = deps.firestore();
157
+ const ref = firestore.doc(`hosts/${hostId}/bookings/${bookingId}`);
158
+ const first = await ref.get();
159
+ if (!first.exists) throw new Refusal(404, 'Unknown booking');
160
+ const serviceId = String((_first_get = first.get('serviceId')) != null ? _first_get : '');
161
+ const serviceSnapshot = serviceId ? await firestore.doc(`hosts/${hostId}/services/${serviceId}`).get() : null;
162
+ const service = (serviceSnapshot == null ? void 0 : serviceSnapshot.exists) ? serviceSnapshot.data() : undefined;
163
+ if (!service || (serviceSnapshot == null ? void 0 : serviceSnapshot.get('deletedAt'))) {
164
+ throw new Refusal(409, 'This booking’s service no longer exists, so it cannot move');
165
+ }
166
+ const moved = await firestore.runTransaction(async (transaction)=>{
167
+ var _snapshot_data, _booking_startsAtMs;
168
+ const snapshot = await transaction.get(ref);
169
+ if (!snapshot.exists) throw new Refusal(404, 'Unknown booking');
170
+ const booking = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
171
+ const nowMs = deps.now();
172
+ const refusal = rescheduleRefusal(booking, startsAtMs, nowMs);
173
+ if (refusal) throw new Refusal(409, refusal);
174
+ const previousStartsAtMs = Number((_booking_startsAtMs = booking.startsAtMs) != null ? _booking_startsAtMs : 0);
175
+ // Already at that time: a retried move writes nothing and tells no one twice.
176
+ if (previousStartsAtMs === startsAtMs) return null;
177
+ const durationMs = bookingDurationMs(booking, service.durationMinutes);
178
+ // The public booking route's own read: every booking of this service
179
+ // from a day before the new time, so one running into it counts.
180
+ const nearby = await transaction.get(firestore.collection(`hosts/${hostId}/bookings`).where('serviceId', '==', serviceId).where('startsAtMs', '>=', startsAtMs - 24 * 60 * 60000).limit(500));
181
+ const booked = nearby.docs.filter((doc)=>doc.id !== bookingId).filter((doc)=>{
182
+ var _doc_get;
183
+ const status = doc.get('status');
184
+ if (status === 'canceled') return false;
185
+ return !(status === 'pendingPayment' && Number((_doc_get = doc.get('expiresAtMs')) != null ? _doc_get : 0) < nowMs);
186
+ }).map((doc)=>{
187
+ var _doc_get, _doc_get1;
188
+ return {
189
+ startsAtMs: Number((_doc_get = doc.get('startsAtMs')) != null ? _doc_get : 0),
190
+ endsAtMs: Number((_doc_get1 = doc.get('endsAtMs')) != null ? _doc_get1 : 0)
191
+ };
192
+ });
193
+ if (!isSlotOpen(service, startsAtMs, booked)) throw new Refusal(409, 'That time is not open. Pick another.');
194
+ const endsAtMs = startsAtMs + durationMs;
195
+ transaction.update(ref, {
196
+ startsAtMs,
197
+ endsAtMs,
198
+ rescheduledAtMs: nowMs,
199
+ rescheduledBy: uid,
200
+ rescheduledFromMs: previousStartsAtMs,
201
+ // The reminder is owed again, for the new time.
202
+ reminderSentAt: null
203
+ });
204
+ return {
205
+ booking,
206
+ previousStartsAtMs,
207
+ endsAtMs
208
+ };
209
+ });
210
+ if (!moved) {
211
+ var _first_get1;
212
+ return res.status(200).json({
213
+ ok: true,
214
+ startsAtMs,
215
+ endsAtMs: Number((_first_get1 = first.get('endsAtMs')) != null ? _first_get1 : 0),
216
+ notified: false
217
+ });
218
+ }
219
+ // Keyed by the booking and its new time: a retried move raises it once.
220
+ await (deps.raiseEvent == null ? void 0 : deps.raiseEvent.call(deps, {
221
+ event: 'booking.rescheduled',
222
+ hostId,
223
+ bookingId,
224
+ booking: _extends({}, moved.booking, {
225
+ startsAtMs,
226
+ endsAtMs: moved.endsAtMs,
227
+ rescheduledFromMs: moved.previousStartsAtMs
228
+ })
229
+ }));
230
+ const to = String((_moved_booking_email = moved.booking['email']) != null ? _moved_booking_email : '').trim();
231
+ let notified = false;
232
+ if (to) {
233
+ var _storedBookingTimeZone;
234
+ const timezone = (_storedBookingTimeZone = storedBookingTimeZone({
235
+ timezone: moved.booking['timezone']
236
+ })) != null ? _storedBookingTimeZone : await deps.timeZoneFor(hostId, service);
237
+ try {
238
+ var _moved_booking_name, _ref, _moved_booking_serviceName;
239
+ await deps.notifyRescheduled({
240
+ hostId,
241
+ bookingId,
242
+ to,
243
+ name: String((_moved_booking_name = moved.booking['name']) != null ? _moved_booking_name : ''),
244
+ serviceName: String((_ref = (_moved_booking_serviceName = moved.booking['serviceName']) != null ? _moved_booking_serviceName : service.name) != null ? _ref : ''),
245
+ when: formatBookingWhen(startsAtMs, timezone),
246
+ previousWhen: formatBookingWhen(moved.previousStartsAtMs, timezone),
247
+ timezone
248
+ });
249
+ notified = true;
250
+ } catch (error) {
251
+ // The booking has moved whatever the mail did; the screen says the guest was not told.
252
+ console.error('[bookings/reschedule] notice failed', hostId, bookingId, error);
253
+ }
254
+ }
255
+ return res.status(200).json({
256
+ ok: true,
257
+ startsAtMs,
258
+ endsAtMs: moved.endsAtMs,
259
+ notified
260
+ });
261
+ } catch (error) {
262
+ return answer(res, error);
263
+ }
264
+ };
265
+ }
266
+ /**
267
+ * Canceling a booking that has no money on it (AGL-3643): a free booking, or
268
+ * a paid one already refunded in full. The site's team used to write
269
+ * `status: 'canceled'` straight from the console; a route is where the
270
+ * cancel can be told to the plugins that listen for it (`booking.canceled`),
271
+ * and where the rule that a paid booking cancels through its refund is held
272
+ * rather than trusted to the screen. One transaction: a booking checked in
273
+ * or paid a moment ago is refused, and canceling it twice changes nothing.
274
+ */ export function createBookingCancelHandler(deps) {
275
+ return async (req, res)=>{
276
+ var _body_hostId, _body_bookingId;
277
+ if (req.method !== 'POST') return res.status(405).json({
278
+ error: 'Method not allowed'
279
+ });
280
+ const body = bodyOf(req);
281
+ const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
282
+ const bookingId = String((_body_bookingId = body['bookingId']) != null ? _body_bookingId : '');
283
+ if (!hostId || !bookingId) return res.status(400).json({
284
+ error: 'Missing hostId or bookingId'
285
+ });
286
+ try {
287
+ const { uid } = await authorize(deps, req, hostId);
288
+ const firestore = deps.firestore();
289
+ const ref = firestore.doc(`hosts/${hostId}/bookings/${bookingId}`);
290
+ const canceled = await firestore.runTransaction(async (transaction)=>{
291
+ var _snapshot_data;
292
+ const snapshot = await transaction.get(ref);
293
+ if (!snapshot.exists) throw new Refusal(404, 'Unknown booking');
294
+ const booking = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
295
+ const nowMs = deps.now();
296
+ const refusal = cancelRefusal(booking, nowMs);
297
+ if (refusal) throw new Refusal(409, refusal);
298
+ // Already canceled: a retried tap writes nothing and tells no one twice.
299
+ if (bookingState(booking, nowMs) === 'canceled') return null;
300
+ transaction.update(ref, {
301
+ status: 'canceled',
302
+ canceledAtMs: nowMs,
303
+ canceledBy: uid
304
+ });
305
+ return _extends({}, booking, {
306
+ status: 'canceled',
307
+ canceledAtMs: nowMs
308
+ });
309
+ });
310
+ if (canceled) {
311
+ await (deps.raiseEvent == null ? void 0 : deps.raiseEvent.call(deps, {
312
+ event: 'booking.canceled',
313
+ hostId,
314
+ bookingId,
315
+ booking: canceled
316
+ }));
317
+ }
318
+ return res.status(200).json({
319
+ ok: true,
320
+ canceled: true
321
+ });
322
+ } catch (error) {
323
+ return answer(res, error);
324
+ }
325
+ };
326
+ }
327
+ /** The email that tells a guest their booking moved, as the confirmation is sent. */ export async function sendRescheduledNotice(notice) {
328
+ var _ref;
329
+ var _this;
330
+ if (!isEmailConfigured()) throw new Error('Email is not configured');
331
+ const firestore = firebaseAdmin.app().firestore();
332
+ const fallbackText = `Hi ${notice.name},\n\nYour booking for "${notice.serviceName}" is now on ` + `${notice.when} (${notice.timezone}). It was on ${notice.previousWhen}.\n\n` + `Reference: ${notice.bookingId}`;
333
+ const designed = await renderHostEmailWithTokens(firestore, notice.hostId, 'booking-rescheduled', {
334
+ name: notice.name,
335
+ 'service.name': notice.serviceName,
336
+ when: notice.when,
337
+ previousWhen: notice.previousWhen,
338
+ timezone: notice.timezone,
339
+ 'booking.ref': notice.bookingId
340
+ });
341
+ const branding = resolveBrandingProfile((_this = await getOrgForHost(notice.hostId).catch(()=>null)) == null ? void 0 : _this.org);
342
+ await sendEmail(_extends({
343
+ to: notice.to,
344
+ subject: (_ref = designed == null ? void 0 : designed.subject) != null ? _ref : `New time for ${notice.serviceName}`,
345
+ text: (designed == null ? void 0 : designed.text) || fallbackText
346
+ }, (designed == null ? void 0 : designed.html) ? {
347
+ html: designed.html
348
+ } : {}, {
349
+ fromName: branding.fromName,
350
+ sendingIdentity: await hostSendingIdentity(notice.hostId),
351
+ audience: 'tenant',
352
+ context: 'booking rescheduled',
353
+ owedFor: 'booking'
354
+ }));
355
+ await meterHostEmail(notice.hostId);
356
+ }
357
+ /** The production wiring: the Admin SDK, the site lockdown verdict and the real send. */ export function bookingManageDeps() {
358
+ return {
359
+ firestore: ()=>firebaseAdmin.app().firestore(),
360
+ verifyIdToken: (token)=>firebaseAdmin.app().auth().verifyIdToken(token),
361
+ siteLocked: async (hostId)=>Boolean(await getSiteLockdown(hostId)),
362
+ timeZoneFor: async (hostId, service)=>{
363
+ const firestore = firebaseAdmin.app().firestore();
364
+ const [host, owner] = await Promise.all([
365
+ firestore.collection('hosts').doc(hostId).get(),
366
+ getOrgForHost(hostId).catch(()=>null)
367
+ ]);
368
+ return bookingTimeZone({
369
+ service: service != null ? service : null,
370
+ host: host.data(),
371
+ org: owner == null ? void 0 : owner.org
372
+ });
373
+ },
374
+ notifyRescheduled: sendRescheduledNotice,
375
+ now: ()=>Date.now(),
376
+ raiseEvent: (input)=>raiseBookingEvent(firebaseAdmin.app().firestore(), input)
377
+ };
378
+ }
379
+
380
+ //# sourceMappingURL=booking-manage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/server/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\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport type { PluginApiHandler, PluginApiRequest } from '@aglyn/aglyn/app-utils/api-plugins'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n renderHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin'\nimport { getSiteLockdown } from '@aglyn/tenant-data-admin/server/tenant-write-lockdown'\nimport { resolveBrandingProfile } from '@aglyn/aglyn/server'\nimport {\n bookingDurationMs,\n bookingState,\n cancelRefusal,\n checkInRefusal,\n type ManagedBooking,\n rescheduleRefusal,\n} from '../model/booking-manage'\nimport { type BookedInterval, type HostBookingService, isSlotOpen } from '../model/bookings'\nimport { bookingTimeZone, formatBookingWhen, storedBookingTimeZone } from '../model/booking-time'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { type BookingEventInput, raiseBookingEvent } from './booking-events'\n\n/*\n * Checking a guest in and moving a booking (AGL-3621), for the site's team.\n *\n * The native Aglyn app's bookings calendar calls these; any console surface\n * may too. Each is a member route: the caller's ID token names them, the\n * host's `memberRoles` must give them a content-writing role (the same roles the rules let write\n * a booking directly), and a locked-down site refuses. Each decides inside\n * ONE transaction, re-reading the booking, so two phones acting at once\n * cannot both move it or check in a guest whose booking was just canceled.\n *\n * Moving a booking re-checks the new slot against the service's hours and\n * every other live booking of that service in the same transaction the\n * public booking route uses, so a move can never double-book. It keeps the\n * booking's own length, clears the reminder stamp so the 24-hour reminder\n * goes out for the new time, and tells the guest by email.\n */\n\n/** The Firestore surface these routes use: the Admin SDK's, narrowed so a spec can hand in a double. */\nexport interface ManageDocSnapshot {\n readonly exists: boolean\n readonly id?: string\n get(field: string): unknown\n data(): Record<string, unknown> | undefined\n}\nexport interface ManageDocRef {\n readonly id: string\n}\nexport interface ManageQuery {\n where(field: string, op: '==' | '>=' | '<', value: unknown): ManageQuery\n limit(count: number): ManageQuery\n}\nexport interface ManageTransaction {\n get(ref: ManageDocRef): Promise<ManageDocSnapshot>\n get(query: ManageQuery): Promise<{ docs: ManageDocSnapshot[] }>\n update(ref: ManageDocRef, data: Record<string, unknown>): unknown\n}\nexport interface ManageFirestore {\n doc(path: string): ManageDocRef & { get(): Promise<ManageDocSnapshot> }\n collection(path: string): ManageQuery\n runTransaction<T>(fn: (transaction: ManageTransaction) => Promise<T>): Promise<T>\n}\n\nexport interface RescheduleNotice {\n hostId: string\n bookingId: string\n to: string\n name: string\n serviceName: string\n when: string\n previousWhen: string\n timezone: string\n}\n\nexport interface BookingManageDeps {\n firestore: () => ManageFirestore\n verifyIdToken: (token: string) => Promise<{ uid: string }>\n /** True when the site is locked down and must not take writes. */\n siteLocked: (hostId: string) => Promise<boolean>\n /** The zone a booking is told in when the document carries none. */\n timeZoneFor: (hostId: string, service: HostBookingService | undefined) => Promise<string>\n notifyRescheduled: (notice: RescheduleNotice) => Promise<void>\n now: () => number\n /**\n * Raises a booking event once the change is written (AGL-3643): the move,\n * the cancel. Never throws. Absent, nothing is raised.\n */\n raiseEvent?: (input: BookingEventInput) => Promise<void>\n}\n\n/** Field the booking stamps when the guest arrives. */\nexport const CHECKED_IN_AT = 'checkedInAtMs'\nexport const CHECKED_IN_BY = 'checkedInBy'\n\nclass Refusal extends Error {\n constructor(\n readonly status: number,\n message: string,\n ) {\n super(message)\n }\n}\n\nconst bearerOf = (req: PluginApiRequest): string => {\n const header = String(req.headers['authorization'] ?? req.headers['Authorization'] ?? '')\n return header.startsWith('Bearer ') ? header.slice('Bearer '.length) : ''\n}\n\nconst bodyOf = (req: PluginApiRequest): Record<string, unknown> => {\n if (typeof req.body === 'string') {\n try {\n return JSON.parse(req.body) as Record<string, unknown>\n } catch {\n return {}\n }\n }\n return (req.body ?? {}) as Record<string, unknown>\n}\n\n/** The member, their right to write this site's bookings, and the site being open. */\nasync function authorize(\n deps: BookingManageDeps,\n req: PluginApiRequest,\n hostId: string,\n): Promise<{ uid: string }> {\n const token = bearerOf(req)\n if (!token) throw new Refusal(401, 'Unauthenticated')\n let uid: string\n try {\n uid = (await deps.verifyIdToken(token)).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n throw new Refusal(401, 'Unauthenticated')\n }\n const host = await deps.firestore().doc(`hosts/${hostId}`).get()\n if (!host.exists) throw new Refusal(404, 'Unknown site')\n const role = ((host.get('memberRoles') ?? {}) as Record<string, unknown>)[uid]\n if (!hostRoleCanWrite(role)) throw new Refusal(403, 'Your role on this site cannot change bookings')\n if (await deps.siteLocked(hostId)) throw new Refusal(423, 'This site is locked and cannot take changes right now')\n return { uid }\n}\n\nfunction answer(res: Parameters<PluginApiHandler>[1], error: unknown): void {\n if (error instanceof Refusal) {\n res.status(error.status).json({ error: error.message })\n return\n }\n console.error('[bookings/manage]', error)\n res.status(500).json({ error: 'That did not go through. Try again.' })\n}\n\nexport function createBookingCheckInHandler(deps: BookingManageDeps): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = bodyOf(req)\n const hostId = String(body['hostId'] ?? '')\n const bookingId = String(body['bookingId'] ?? '')\n const checkedIn = body['checkedIn'] !== false\n if (!hostId || !bookingId) return res.status(400).json({ error: 'Missing hostId or bookingId' })\n try {\n const { uid } = await authorize(deps, req, hostId)\n const firestore = deps.firestore()\n const ref = firestore.doc(`hosts/${hostId}/bookings/${bookingId}`)\n const result = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) throw new Refusal(404, 'Unknown booking')\n const booking = (snapshot.data() ?? {}) as ManagedBooking\n const nowMs = deps.now()\n const refusal = checkInRefusal(booking, checkedIn, nowMs)\n if (refusal) throw new Refusal(409, refusal)\n const already = Number(booking.checkedInAtMs ?? 0) > 0\n // Already where it was asked to be: a retried tap changes nothing.\n if (already === checkedIn) return { checkedInAtMs: already ? Number(booking.checkedInAtMs) : null }\n if (checkedIn) {\n transaction.update(ref, { [CHECKED_IN_AT]: nowMs, [CHECKED_IN_BY]: uid })\n return { checkedInAtMs: nowMs }\n }\n transaction.update(ref, { [CHECKED_IN_AT]: null, [CHECKED_IN_BY]: null })\n return { checkedInAtMs: null }\n })\n return res.status(200).json({ ok: true, ...result })\n } catch (error) {\n return answer(res, error)\n }\n }\n}\n\nexport function createBookingRescheduleHandler(deps: BookingManageDeps): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = bodyOf(req)\n const hostId = String(body['hostId'] ?? '')\n const bookingId = String(body['bookingId'] ?? '')\n const startsAtMs = Number(body['startsAtMs'])\n if (!hostId || !bookingId) return res.status(400).json({ error: 'Missing hostId or bookingId' })\n if (!Number.isFinite(startsAtMs) || startsAtMs <= 0) return res.status(400).json({ error: 'Pick a new time' })\n try {\n const { uid } = await authorize(deps, req, hostId)\n const firestore = deps.firestore()\n const ref = firestore.doc(`hosts/${hostId}/bookings/${bookingId}`)\n const first = await ref.get()\n if (!first.exists) throw new Refusal(404, 'Unknown booking')\n const serviceId = String(first.get('serviceId') ?? '')\n const serviceSnapshot = serviceId ? await firestore.doc(`hosts/${hostId}/services/${serviceId}`).get() : null\n const service = serviceSnapshot?.exists ? (serviceSnapshot.data() as unknown as HostBookingService) : undefined\n if (!service || serviceSnapshot?.get('deletedAt')) {\n throw new Refusal(409, 'This booking’s service no longer exists, so it cannot move')\n }\n\n const moved = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) throw new Refusal(404, 'Unknown booking')\n const booking = (snapshot.data() ?? {}) as ManagedBooking & Record<string, unknown>\n const nowMs = deps.now()\n const refusal = rescheduleRefusal(booking, startsAtMs, nowMs)\n if (refusal) throw new Refusal(409, refusal)\n const previousStartsAtMs = Number(booking.startsAtMs ?? 0)\n // Already at that time: a retried move writes nothing and tells no one twice.\n if (previousStartsAtMs === startsAtMs) return null\n const durationMs = bookingDurationMs(booking, service.durationMinutes)\n // The public booking route's own read: every booking of this service\n // from a day before the new time, so one running into it counts.\n const nearby = await transaction.get(\n firestore\n .collection(`hosts/${hostId}/bookings`)\n .where('serviceId', '==', serviceId)\n .where('startsAtMs', '>=', startsAtMs - 24 * 60 * 60_000)\n .limit(500),\n )\n const booked: BookedInterval[] = nearby.docs\n .filter((doc) => doc.id !== bookingId)\n .filter((doc) => {\n const status = doc.get('status')\n if (status === 'canceled') return false\n return !(status === 'pendingPayment' && Number(doc.get('expiresAtMs') ?? 0) < nowMs)\n })\n .map((doc) => ({ startsAtMs: Number(doc.get('startsAtMs') ?? 0), endsAtMs: Number(doc.get('endsAtMs') ?? 0) }))\n if (!isSlotOpen(service, startsAtMs, booked)) throw new Refusal(409, 'That time is not open. Pick another.')\n const endsAtMs = startsAtMs + durationMs\n transaction.update(ref, {\n startsAtMs,\n endsAtMs,\n rescheduledAtMs: nowMs,\n rescheduledBy: uid,\n rescheduledFromMs: previousStartsAtMs,\n // The reminder is owed again, for the new time.\n reminderSentAt: null,\n })\n return { booking, previousStartsAtMs, endsAtMs }\n })\n\n if (!moved) {\n return res.status(200).json({ ok: true, startsAtMs, endsAtMs: Number(first.get('endsAtMs') ?? 0), notified: false })\n }\n // Keyed by the booking and its new time: a retried move raises it once.\n await deps.raiseEvent?.({\n event: 'booking.rescheduled',\n hostId,\n bookingId,\n booking: {\n ...moved.booking,\n startsAtMs,\n endsAtMs: moved.endsAtMs,\n rescheduledFromMs: moved.previousStartsAtMs,\n },\n })\n const to = String(moved.booking['email'] ?? '').trim()\n let notified = false\n if (to) {\n const timezone = storedBookingTimeZone({ timezone: moved.booking['timezone'] }) ?? (await deps.timeZoneFor(hostId, service))\n try {\n await deps.notifyRescheduled({\n hostId,\n bookingId,\n to,\n name: String(moved.booking['name'] ?? ''),\n serviceName: String(moved.booking['serviceName'] ?? service.name ?? ''),\n when: formatBookingWhen(startsAtMs, timezone),\n previousWhen: formatBookingWhen(moved.previousStartsAtMs, timezone),\n timezone,\n })\n notified = true\n } catch (error) {\n // The booking has moved whatever the mail did; the screen says the guest was not told.\n console.error('[bookings/reschedule] notice failed', hostId, bookingId, error)\n }\n }\n return res.status(200).json({ ok: true, startsAtMs, endsAtMs: moved.endsAtMs, notified })\n } catch (error) {\n return answer(res, error)\n }\n }\n}\n\n/**\n * Canceling a booking that has no money on it (AGL-3643): a free booking, or\n * a paid one already refunded in full. The site's team used to write\n * `status: 'canceled'` straight from the console; a route is where the\n * cancel can be told to the plugins that listen for it (`booking.canceled`),\n * and where the rule that a paid booking cancels through its refund is held\n * rather than trusted to the screen. One transaction: a booking checked in\n * or paid a moment ago is refused, and canceling it twice changes nothing.\n */\nexport function createBookingCancelHandler(deps: BookingManageDeps): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = bodyOf(req)\n const hostId = String(body['hostId'] ?? '')\n const bookingId = String(body['bookingId'] ?? '')\n if (!hostId || !bookingId) return res.status(400).json({ error: 'Missing hostId or bookingId' })\n try {\n const { uid } = await authorize(deps, req, hostId)\n const firestore = deps.firestore()\n const ref = firestore.doc(`hosts/${hostId}/bookings/${bookingId}`)\n const canceled = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) throw new Refusal(404, 'Unknown booking')\n const booking = (snapshot.data() ?? {}) as ManagedBooking & Record<string, unknown>\n const nowMs = deps.now()\n const refusal = cancelRefusal(booking, nowMs)\n if (refusal) throw new Refusal(409, refusal)\n // Already canceled: a retried tap writes nothing and tells no one twice.\n if (bookingState(booking, nowMs) === 'canceled') return null\n transaction.update(ref, { status: 'canceled', canceledAtMs: nowMs, canceledBy: uid })\n return { ...booking, status: 'canceled', canceledAtMs: nowMs }\n })\n if (canceled) {\n await deps.raiseEvent?.({ event: 'booking.canceled', hostId, bookingId, booking: canceled })\n }\n return res.status(200).json({ ok: true, canceled: true })\n } catch (error) {\n return answer(res, error)\n }\n }\n}\n\n/** The email that tells a guest their booking moved, as the confirmation is sent. */\nexport async function sendRescheduledNotice(notice: RescheduleNotice): Promise<void> {\n if (!isEmailConfigured()) throw new Error('Email is not configured')\n const firestore = firebaseAdmin.app().firestore()\n const fallbackText =\n `Hi ${notice.name},\\n\\nYour booking for \"${notice.serviceName}\" is now on ` +\n `${notice.when} (${notice.timezone}). It was on ${notice.previousWhen}.\\n\\n` +\n `Reference: ${notice.bookingId}`\n const designed = await renderHostEmailWithTokens(firestore, notice.hostId, 'booking-rescheduled', {\n name: notice.name,\n 'service.name': notice.serviceName,\n when: notice.when,\n previousWhen: notice.previousWhen,\n timezone: notice.timezone,\n 'booking.ref': notice.bookingId,\n })\n const branding = resolveBrandingProfile((await getOrgForHost(notice.hostId).catch(() => null))?.org as never)\n await sendEmail({\n to: notice.to,\n subject: designed?.subject ?? `New time for ${notice.serviceName}`,\n text: designed?.text || fallbackText,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(notice.hostId),\n audience: 'tenant',\n context: 'booking rescheduled',\n owedFor: 'booking',\n })\n await meterHostEmail(notice.hostId)\n}\n\n/** The production wiring: the Admin SDK, the site lockdown verdict and the real send. */\nexport function bookingManageDeps(): BookingManageDeps {\n return {\n firestore: () => firebaseAdmin.app().firestore() as unknown as ManageFirestore,\n verifyIdToken: (token) => firebaseAdmin.app().auth().verifyIdToken(token),\n siteLocked: async (hostId) => Boolean(await getSiteLockdown(hostId)),\n timeZoneFor: async (hostId, service) => {\n const firestore = firebaseAdmin.app().firestore()\n const [host, owner] = await Promise.all([\n firestore.collection('hosts').doc(hostId).get(),\n getOrgForHost(hostId).catch(() => null),\n ])\n return bookingTimeZone({\n service: service ?? null,\n host: host.data() as { timeZone?: string } | undefined,\n org: owner?.org as { timeZone?: string } | null | undefined,\n })\n },\n notifyRescheduled: sendRescheduledNotice,\n now: () => Date.now(),\n raiseEvent: (input) => raiseBookingEvent(firebaseAdmin.app().firestore(), input),\n }\n}\n"],"names":["hostRoleCanWrite","isEmailConfigured","sendEmail","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","renderHostEmailWithTokens","getSiteLockdown","resolveBrandingProfile","bookingDurationMs","bookingState","cancelRefusal","checkInRefusal","rescheduleRefusal","isSlotOpen","bookingTimeZone","formatBookingWhen","storedBookingTimeZone","isRefusedIdToken","raiseBookingEvent","CHECKED_IN_AT","CHECKED_IN_BY","Refusal","Error","status","message","bearerOf","req","header","String","headers","startsWith","slice","length","bodyOf","body","JSON","parse","authorize","deps","hostId","host","token","uid","verifyIdToken","error","firestore","doc","get","exists","role","siteLocked","answer","res","json","console","createBookingCheckInHandler","method","bookingId","checkedIn","ref","result","runTransaction","transaction","snapshot","booking","data","nowMs","now","refusal","already","Number","checkedInAtMs","update","ok","createBookingRescheduleHandler","startsAtMs","isFinite","first","moved","serviceId","serviceSnapshot","service","undefined","previousStartsAtMs","durationMs","durationMinutes","nearby","collection","where","limit","booked","docs","filter","id","map","endsAtMs","rescheduledAtMs","rescheduledBy","rescheduledFromMs","reminderSentAt","notified","raiseEvent","event","to","trim","timezone","timeZoneFor","notifyRescheduled","name","serviceName","when","previousWhen","createBookingCancelHandler","canceled","canceledAtMs","canceledBy","sendRescheduledNotice","notice","app","fallbackText","designed","branding","catch","org","subject","text","html","fromName","sendingIdentity","audience","context","owedFor","bookingManageDeps","auth","Boolean","owner","Promise","all","Date","input"],"mappings":"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"}
@@ -0,0 +1,20 @@
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 PluginApiHandler } from '@aglyn/aglyn/server';
18
+ /** Card-present payments: always in test mode, in live mode once switched on. */
19
+ export declare function inPersonCardsAvailable(): boolean;
20
+ export declare const bookingInPersonPaymentHandler: PluginApiHandler;