@aglyn/plugins-bookings 1.0.0-beta.229 → 1.0.0-beta.231
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.
- package/README.md +1 -1
- package/package.json +11 -11
- package/src/lib/components/book-meeting-action.js +3 -2
- package/src/lib/components/book-meeting-action.js.map +1 -1
- package/src/lib/components/booking.js +4 -0
- package/src/lib/components/booking.js.map +1 -1
- package/src/lib/components/bookings-console-page.js +62 -4
- package/src/lib/components/bookings-console-page.js.map +1 -1
- package/src/lib/model/booking-in-person.d.ts +72 -0
- package/src/lib/model/booking-in-person.js +71 -0
- package/src/lib/model/booking-in-person.js.map +1 -0
- package/src/lib/model/booking-manage.d.ts +66 -0
- package/src/lib/model/booking-manage.js +90 -0
- package/src/lib/model/booking-manage.js.map +1 -0
- package/src/lib/model/bookings.d.ts +22 -0
- package/src/lib/model/bookings.js +11 -0
- package/src/lib/model/bookings.js.map +1 -1
- package/src/lib/server/booking-manage.d.ts +77 -0
- package/src/lib/server/booking-manage.js +306 -0
- package/src/lib/server/booking-manage.js.map +1 -0
- package/src/lib/server/booking-service-ai-capability.d.ts +50 -0
- package/src/lib/server/booking-service-ai-capability.js +203 -0
- package/src/lib/server/booking-service-ai-capability.js.map +1 -0
- package/src/lib/server/booking-service-drafts.d.ts +107 -0
- package/src/lib/server/booking-service-drafts.js +320 -0
- package/src/lib/server/booking-service-drafts.js.map +1 -0
- package/src/lib/server/in-person-payment.d.ts +20 -0
- package/src/lib/server/in-person-payment.js +469 -0
- package/src/lib/server/in-person-payment.js.map +1 -0
- package/src/lib/server.js +24 -4
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.js +59 -0
- package/src/lib/tenant-emails.js.map +1 -1
- package/src/lib/utils/use-booking-purchase-event.js +5 -0
- package/src/lib/utils/use-booking-purchase-event.js.map +1 -1
|
@@ -0,0 +1,320 @@
|
|
|
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 { checkEntitlement, checkQuota } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
19
|
+
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
20
|
+
import { isValidTimeZone } from "@aglyn/shared-util-timestamp/zoned-time";
|
|
21
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
22
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
23
|
+
import { BOOKING_FIELD_ASKS } from "../model/booking-contact-fields.js";
|
|
24
|
+
import { BOOKING_PRICE_DISPLAYS, bookingPriceText } from "../model/booking-price.js";
|
|
25
|
+
import { BOOKING_MAX_DURATION_MINUTES, BOOKING_MIN_DURATION_MINUTES } from "../model/bookings.js";
|
|
26
|
+
/** The resource name this writer is registered under. */ export const BOOKING_SERVICE_DRAFT_RESOURCE = 'booking-service';
|
|
27
|
+
/** The longest name the console's dialog keeps. */ export const BOOKING_SERVICE_NAME_MAX = 80;
|
|
28
|
+
/** The longest description the console's dialog keeps. */ export const BOOKING_SERVICE_DESCRIPTION_MAX = 500;
|
|
29
|
+
/** The most open intervals one weekday holds. */ export const BOOKING_SERVICE_WINDOWS_PER_DAY_MAX = 12;
|
|
30
|
+
/** The highest fixed price a draft may state, in whole dollars. */ export const BOOKING_SERVICE_PRICE_MAX_USD = 100000;
|
|
31
|
+
/** The resources route's refusal for a member who may not write the site. */ export const BOOKING_SERVICE_ROLE_REFUSAL = 'Editing requires the editor role';
|
|
32
|
+
/** The resources route's refusal for a plan without the feature. */ export const BOOKING_SERVICE_PLAN_REFUSAL = 'This feature is not included in your plan — see Billing';
|
|
33
|
+
/** The resources route's refusal at the plan's allowance. */ export function bookingServiceLimitRefusal(limit) {
|
|
34
|
+
return `Your plan includes ${limit} ${limit === 1 ? 'service' : 'services'} — ` + 'upgrade in Billing for more';
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
/** Text on one line: controls and runs of space folded to one space. */ function line(value) {
|
|
40
|
+
return typeof value === 'string' ? value.replace(RegExp("[\\u0000-\\u001F\\u007F-\\u009F\\s]+", "gu"), ' ').trim() : '';
|
|
41
|
+
}
|
|
42
|
+
/** The weekly windows, or what is wrong with them. */ function readWindows(value, problems) {
|
|
43
|
+
const windows = {};
|
|
44
|
+
if (!isRecord(value)) {
|
|
45
|
+
problems.push('The service needs weekly hours: at least one open window on one weekday');
|
|
46
|
+
return windows;
|
|
47
|
+
}
|
|
48
|
+
for (const [key, raw] of Object.entries(value)){
|
|
49
|
+
const weekday = Number(key);
|
|
50
|
+
if (!/^[0-6]$/.test(key) || !Number.isInteger(weekday)) {
|
|
51
|
+
problems.push(`"${key}" is not a weekday: weekdays are 0 (Sunday) to 6 (Saturday)`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(raw)) {
|
|
55
|
+
problems.push(`The hours for weekday ${key} are not a list of windows`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!raw.length) continue;
|
|
59
|
+
if (raw.length > BOOKING_SERVICE_WINDOWS_PER_DAY_MAX) {
|
|
60
|
+
problems.push(`Weekday ${key} has more than ${BOOKING_SERVICE_WINDOWS_PER_DAY_MAX} windows`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const day = [];
|
|
64
|
+
for (const window of raw){
|
|
65
|
+
const start = isRecord(window) ? window['start'] : undefined;
|
|
66
|
+
const end = isRecord(window) ? window['end'] : undefined;
|
|
67
|
+
if (typeof start !== 'number' || typeof end !== 'number' || !Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end > 24 * 60 || end <= start) {
|
|
68
|
+
problems.push(`A window on weekday ${key} is not a start before its end, in whole minutes from 0 to 1440`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
day.push({
|
|
72
|
+
start,
|
|
73
|
+
end
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
day.sort((a, b)=>a.start - b.start);
|
|
77
|
+
if (day.some((window, index)=>index > 0 && window.start < day[index - 1].end)) {
|
|
78
|
+
problems.push(`Two windows on weekday ${key} overlap`);
|
|
79
|
+
}
|
|
80
|
+
if (day.length) windows[weekday] = day;
|
|
81
|
+
}
|
|
82
|
+
if (!Object.keys(windows).length && !problems.length) {
|
|
83
|
+
problems.push('The service needs weekly hours: at least one open window on one weekday');
|
|
84
|
+
}
|
|
85
|
+
return windows;
|
|
86
|
+
}
|
|
87
|
+
function readAsk(label, value, problems) {
|
|
88
|
+
if (value === undefined) return 'off';
|
|
89
|
+
if (BOOKING_FIELD_ASKS.includes(value)) return value;
|
|
90
|
+
problems.push(`Asking for ${label} is off, optional or required`);
|
|
91
|
+
return 'off';
|
|
92
|
+
}
|
|
93
|
+
function readSwitch(label, value, fallback, problems) {
|
|
94
|
+
if (value === undefined) return fallback;
|
|
95
|
+
if (typeof value === 'boolean') return value;
|
|
96
|
+
problems.push(`${label} is on or off`);
|
|
97
|
+
return fallback;
|
|
98
|
+
}
|
|
99
|
+
/** The content as the writer stores it, or every problem that stops it. */ export function readBookingServiceContent(content) {
|
|
100
|
+
const problems = [];
|
|
101
|
+
const name = line(content['name']);
|
|
102
|
+
if (!name) problems.push('The service needs a name');
|
|
103
|
+
else if (name.length > BOOKING_SERVICE_NAME_MAX) {
|
|
104
|
+
problems.push(`The name is longer than ${BOOKING_SERVICE_NAME_MAX} characters`);
|
|
105
|
+
}
|
|
106
|
+
const duration = content['durationMinutes'];
|
|
107
|
+
if (typeof duration !== 'number' || !Number.isInteger(duration) || duration < BOOKING_MIN_DURATION_MINUTES || duration > BOOKING_MAX_DURATION_MINUTES) {
|
|
108
|
+
problems.push(`The length is whole minutes from ${BOOKING_MIN_DURATION_MINUTES} to ${BOOKING_MAX_DURATION_MINUTES}`);
|
|
109
|
+
}
|
|
110
|
+
const rawDescription = content['description'];
|
|
111
|
+
const description = typeof rawDescription === 'string' ? rawDescription.trim() : '';
|
|
112
|
+
if (rawDescription !== undefined && typeof rawDescription !== 'string') {
|
|
113
|
+
problems.push('The description is text');
|
|
114
|
+
} else if (description.length > BOOKING_SERVICE_DESCRIPTION_MAX) {
|
|
115
|
+
problems.push(`The description is longer than ${BOOKING_SERVICE_DESCRIPTION_MAX} characters`);
|
|
116
|
+
}
|
|
117
|
+
const timezone = line(content['timezone']);
|
|
118
|
+
if (!isValidTimeZone(timezone)) {
|
|
119
|
+
problems.push('The time zone is an IANA zone, such as America/Chicago');
|
|
120
|
+
}
|
|
121
|
+
const windows = readWindows(content['windows'], problems);
|
|
122
|
+
// No price stated and no label asked for is "Contact for price": a draft
|
|
123
|
+
// never charges what the caller did not say.
|
|
124
|
+
const rawPrice = content['priceUsd'];
|
|
125
|
+
const askedDisplay = content['priceDisplay'];
|
|
126
|
+
let priceDisplay = 'contact';
|
|
127
|
+
let priceUsd = 0;
|
|
128
|
+
if (askedDisplay !== undefined) {
|
|
129
|
+
if (BOOKING_PRICE_DISPLAYS.includes(askedDisplay)) {
|
|
130
|
+
priceDisplay = askedDisplay;
|
|
131
|
+
} else {
|
|
132
|
+
problems.push('The price is stated as fixed, varies, estimate or contact');
|
|
133
|
+
}
|
|
134
|
+
} else if (rawPrice !== undefined) {
|
|
135
|
+
priceDisplay = 'fixed';
|
|
136
|
+
}
|
|
137
|
+
if (priceDisplay === 'fixed') {
|
|
138
|
+
if (typeof rawPrice !== 'number' || !Number.isInteger(rawPrice) || rawPrice < 0 || rawPrice > BOOKING_SERVICE_PRICE_MAX_USD) {
|
|
139
|
+
problems.push(`A fixed price is whole dollars from 0 to ${BOOKING_SERVICE_PRICE_MAX_USD}; ` + 'state it, or say the price varies, is a free estimate, or is on request');
|
|
140
|
+
} else {
|
|
141
|
+
priceUsd = rawPrice;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const askPhone = readAsk('a phone number', content['askPhone'], problems);
|
|
145
|
+
const askAddress = readAsk('an address', content['askAddress'], problems);
|
|
146
|
+
// The model's defaults: a booking files a meeting, and owes no follow-up.
|
|
147
|
+
const crmMeetingActivity = readSwitch('Filing a meeting', content['crmMeetingActivity'], true, problems);
|
|
148
|
+
const crmFollowUpTask = readSwitch('A follow-up task', content['crmFollowUpTask'], false, problems);
|
|
149
|
+
if (problems.length) return {
|
|
150
|
+
ok: false,
|
|
151
|
+
problems: [
|
|
152
|
+
...new Set(problems)
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
value: _extends({
|
|
158
|
+
name,
|
|
159
|
+
durationMinutes: duration
|
|
160
|
+
}, description ? {
|
|
161
|
+
description
|
|
162
|
+
} : {}, {
|
|
163
|
+
windows,
|
|
164
|
+
timezone,
|
|
165
|
+
priceDisplay,
|
|
166
|
+
priceUsd,
|
|
167
|
+
askPhone,
|
|
168
|
+
askAddress,
|
|
169
|
+
crmMeetingActivity,
|
|
170
|
+
crmFollowUpTask
|
|
171
|
+
})
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** What a caller is told about a service, written or about to be. */ function factsOf(value) {
|
|
175
|
+
var _value_priceDisplay, _value_timezone, _value_windows;
|
|
176
|
+
return {
|
|
177
|
+
status: 'draft',
|
|
178
|
+
durationMinutes: value.durationMinutes,
|
|
179
|
+
priceDisplay: (_value_priceDisplay = value.priceDisplay) != null ? _value_priceDisplay : 'fixed',
|
|
180
|
+
priceText: bookingPriceText(value),
|
|
181
|
+
timezone: (_value_timezone = value.timezone) != null ? _value_timezone : 'UTC',
|
|
182
|
+
openWeekdays: Object.keys((_value_windows = value.windows) != null ? _value_windows : {}).map(Number).sort((a, b)=>a - b)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** Whether content is a service this plugin would store, with what it says about it. Pure. */ export function checkBookingServiceContent(content) {
|
|
186
|
+
const read = readBookingServiceContent(content);
|
|
187
|
+
if (read.ok === false) return read;
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
facts: factsOf(read.value)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function recordOf(service) {
|
|
194
|
+
var _service_data, _data_name;
|
|
195
|
+
const data = (_service_data = service.data()) != null ? _service_data : {};
|
|
196
|
+
return {
|
|
197
|
+
id: service.id,
|
|
198
|
+
name: String((_data_name = data.name) != null ? _data_name : ''),
|
|
199
|
+
versionId: null,
|
|
200
|
+
facts: _extends({}, factsOf(data), {
|
|
201
|
+
status: data.status === 'draft' ? 'draft' : 'active'
|
|
202
|
+
})
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function roleRefusal(host, uid) {
|
|
206
|
+
var _host_get;
|
|
207
|
+
const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
|
|
208
|
+
return hostRoleCanWrite(role) ? null : {
|
|
209
|
+
status: 403,
|
|
210
|
+
error: BOOKING_SERVICE_ROLE_REFUSAL
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function planRefusal(org) {
|
|
214
|
+
return checkEntitlement(org, 'bookings') ? null : {
|
|
215
|
+
status: 403,
|
|
216
|
+
error: BOOKING_SERVICE_PLAN_REFUSAL
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The allowance, counted as `/api/hosts/resources` counts a service create:
|
|
221
|
+
* every document in the collection.
|
|
222
|
+
*/ function roomRefusal(org, used) {
|
|
223
|
+
const quota = checkQuota(org, 'servicesPerHost', used);
|
|
224
|
+
return quota.allowed ? null : {
|
|
225
|
+
status: 403,
|
|
226
|
+
error: bookingServiceLimitRefusal(quota.limit)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export function createBookingServiceDraftWriter(deps = {}) {
|
|
230
|
+
var _deps_firestore;
|
|
231
|
+
const firestore = (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore();
|
|
232
|
+
return {
|
|
233
|
+
refusal: async (context)=>{
|
|
234
|
+
var _roleRefusal;
|
|
235
|
+
const hostRef = firestore().collection('hosts').doc(context.hostId);
|
|
236
|
+
const host = await hostRef.get();
|
|
237
|
+
if (!host.exists) return {
|
|
238
|
+
status: 404,
|
|
239
|
+
error: 'Unknown site'
|
|
240
|
+
};
|
|
241
|
+
const early = (_roleRefusal = roleRefusal(host, context.uid)) != null ? _roleRefusal : planRefusal(context.org);
|
|
242
|
+
if (early) return early;
|
|
243
|
+
const used = (await hostRef.collection('services').count().get()).data().count;
|
|
244
|
+
return roomRefusal(context.org, Number(used) || 0);
|
|
245
|
+
},
|
|
246
|
+
check: (content)=>checkBookingServiceContent(content),
|
|
247
|
+
read: async ({ hostId, id })=>{
|
|
248
|
+
const service = await firestore().collection('hosts').doc(hostId).collection('services').doc(id).get();
|
|
249
|
+
return service.exists ? recordOf(service) : null;
|
|
250
|
+
},
|
|
251
|
+
write: async (request)=>{
|
|
252
|
+
// The request's name is the one asked for; the content's stands in.
|
|
253
|
+
const asked = line(request.name);
|
|
254
|
+
const read = readBookingServiceContent(asked ? _extends({}, request.content, {
|
|
255
|
+
name: asked
|
|
256
|
+
}) : request.content);
|
|
257
|
+
if (read.ok === false) return {
|
|
258
|
+
ok: false,
|
|
259
|
+
status: 400,
|
|
260
|
+
error: read.problems[0]
|
|
261
|
+
};
|
|
262
|
+
const db = firestore();
|
|
263
|
+
const hostRef = db.collection('hosts').doc(request.hostId);
|
|
264
|
+
const servicesRef = hostRef.collection('services');
|
|
265
|
+
const serviceRef = servicesRef.doc(request.id);
|
|
266
|
+
return db.runTransaction(async (tx)=>{
|
|
267
|
+
var _roleRefusal;
|
|
268
|
+
// Every read before any write, which Firestore requires.
|
|
269
|
+
const [host, existing] = await Promise.all([
|
|
270
|
+
tx.get(hostRef),
|
|
271
|
+
tx.get(serviceRef)
|
|
272
|
+
]);
|
|
273
|
+
if (!host.exists) return {
|
|
274
|
+
ok: false,
|
|
275
|
+
status: 404,
|
|
276
|
+
error: 'Unknown site'
|
|
277
|
+
};
|
|
278
|
+
// Asked again under the same id: the draft it already wrote.
|
|
279
|
+
if (existing.exists) return _extends({
|
|
280
|
+
ok: true,
|
|
281
|
+
replayed: true
|
|
282
|
+
}, recordOf(existing));
|
|
283
|
+
const early = (_roleRefusal = roleRefusal(host, request.uid)) != null ? _roleRefusal : planRefusal(request.org);
|
|
284
|
+
if (early) return _extends({
|
|
285
|
+
ok: false
|
|
286
|
+
}, early);
|
|
287
|
+
const used = (await tx.get(servicesRef.count())).data().count;
|
|
288
|
+
const room = roomRefusal(request.org, Number(used) || 0);
|
|
289
|
+
if (room) return _extends({
|
|
290
|
+
ok: false
|
|
291
|
+
}, room);
|
|
292
|
+
tx.create(serviceRef, _extends({}, read.value, {
|
|
293
|
+
status: 'draft',
|
|
294
|
+
createdAt: request.now,
|
|
295
|
+
updatedAt: request.now,
|
|
296
|
+
createdBy: request.uid
|
|
297
|
+
}));
|
|
298
|
+
return {
|
|
299
|
+
ok: true,
|
|
300
|
+
replayed: false,
|
|
301
|
+
id: request.id,
|
|
302
|
+
name: read.value.name,
|
|
303
|
+
versionId: null,
|
|
304
|
+
facts: factsOf(read.value)
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
export const bookingServiceDraftWriter = createBookingServiceDraftWriter();
|
|
311
|
+
/**
|
|
312
|
+
* Registers the writer; the console surface calls it, since only the console
|
|
313
|
+
* runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.
|
|
314
|
+
*/ export function registerBookingServiceDraftWriter() {
|
|
315
|
+
registerPluginResourceDraftWriter(BOOKING_SERVICE_DRAFT_RESOURCE, bookingServiceDraftWriter, {
|
|
316
|
+
pluginId: BUNDLE_ID
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
//# sourceMappingURL=booking-service-drafts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/server/booking-service-drafts.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 { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftContext,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { isValidTimeZone } from '@aglyn/shared-util-timestamp/zoned-time'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { BOOKING_FIELD_ASKS, type BookingFieldAsk } from '../model/booking-contact-fields'\nimport {\n BOOKING_PRICE_DISPLAYS,\n type BookingPriceDisplay,\n bookingPriceText,\n} from '../model/booking-price'\nimport {\n BOOKING_MAX_DURATION_MINUTES,\n BOOKING_MIN_DURATION_MINUTES,\n type HostBookingService,\n} from '../model/bookings'\n\n/**\n * A BOOKING SERVICE ANOTHER PLUGIN ASKS FOR (AGL-3616).\n *\n * The draft writer this plugin registers for the `booking-service` resource\n * on the core's resource-drafts seam. A plugin that sets a site up from a\n * brief asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is a `hosts/{hostId}/services/{id}` service in the shape\n * the console's service dialog saves, born `status: 'draft'`. A draft is\n * offered nowhere — not in the Booking block's list, not by its slots, not\n * by the booking route — until a person activates it on the Bookings page.\n * - THE ROOM is the `servicesPerHost` allowance, counted the way the\n * resources route counts it and inside the transaction that creates, so\n * two writes at once cannot both take the last slot. Free includes none.\n * - THE PLAN is the `bookings` feature, and THE ROLE is a member who may\n * write the site's content — the resources route's two gates, refused in\n * its words.\n * - THE PRICE is a label (`contact`, `varies`, `estimate`) unless the\n * content states a fixed price, so nothing the caller did not say is\n * charged. No price stated and no label asked for is `contact`.\n *\n * Nothing is published or charged. The writer touches the one new document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const BOOKING_SERVICE_DRAFT_RESOURCE = 'booking-service'\n\n/** The longest name the console's dialog keeps. */\nexport const BOOKING_SERVICE_NAME_MAX = 80\n/** The longest description the console's dialog keeps. */\nexport const BOOKING_SERVICE_DESCRIPTION_MAX = 500\n/** The most open intervals one weekday holds. */\nexport const BOOKING_SERVICE_WINDOWS_PER_DAY_MAX = 12\n/** The highest fixed price a draft may state, in whole dollars. */\nexport const BOOKING_SERVICE_PRICE_MAX_USD = 100_000\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const BOOKING_SERVICE_ROLE_REFUSAL = 'Editing requires the editor role'\n/** The resources route's refusal for a plan without the feature. */\nexport const BOOKING_SERVICE_PLAN_REFUSAL =\n 'This feature is not included in your plan — see Billing'\n\n/** The resources route's refusal at the plan's allowance. */\nexport function bookingServiceLimitRefusal(limit: number): string {\n return `Your plan includes ${limit} ${limit === 1 ? 'service' : 'services'} — ` +\n 'upgrade in Billing for more'\n}\n\n/**\n * What a caller sends as `content`. `windows` is keyed by weekday\n * (0 = Sunday … 6 = Saturday), each a list of `{ start, end }` in minutes\n * since midnight in `timezone`. `priceUsd` is read only with\n * `priceDisplay: 'fixed'`, which needs it.\n */\nexport interface BookingServiceDraftContent {\n name: string\n durationMinutes: number\n description?: string\n windows: Partial<Record<number, Array<{ start: number; end: number }>>>\n timezone: string\n priceDisplay?: BookingPriceDisplay\n priceUsd?: number\n askPhone?: BookingFieldAsk\n askAddress?: BookingFieldAsk\n crmFollowUpTask?: boolean\n crmMeetingActivity?: boolean\n}\n\n/** The service document's editable fields, as the writer stores them. */\nexport type BookingServiceDraftFields = Required<\n Pick<HostBookingService, 'name' | 'durationMinutes' | 'windows' | 'timezone' | 'priceDisplay' | 'priceUsd' | 'askPhone' | 'askAddress' | 'crmFollowUpTask' | 'crmMeetingActivity'>\n> &\n Pick<HostBookingService, 'description'>\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\n/** The weekly windows, or what is wrong with them. */\nfunction readWindows(\n value: unknown,\n problems: string[],\n): BookingServiceDraftFields['windows'] {\n const windows: BookingServiceDraftFields['windows'] = {}\n if (!isRecord(value)) {\n problems.push('The service needs weekly hours: at least one open window on one weekday')\n return windows\n }\n for (const [key, raw] of Object.entries(value)) {\n const weekday = Number(key)\n if (!/^[0-6]$/.test(key) || !Number.isInteger(weekday)) {\n problems.push(`\"${key}\" is not a weekday: weekdays are 0 (Sunday) to 6 (Saturday)`)\n continue\n }\n if (!Array.isArray(raw)) {\n problems.push(`The hours for weekday ${key} are not a list of windows`)\n continue\n }\n if (!raw.length) continue\n if (raw.length > BOOKING_SERVICE_WINDOWS_PER_DAY_MAX) {\n problems.push(`Weekday ${key} has more than ${BOOKING_SERVICE_WINDOWS_PER_DAY_MAX} windows`)\n continue\n }\n const day: Array<{ start: number; end: number }> = []\n for (const window of raw) {\n const start = isRecord(window) ? window['start'] : undefined\n const end = isRecord(window) ? window['end'] : undefined\n if (\n typeof start !== 'number' ||\n typeof end !== 'number' ||\n !Number.isInteger(start) ||\n !Number.isInteger(end) ||\n start < 0 ||\n end > 24 * 60 ||\n end <= start\n ) {\n problems.push(\n `A window on weekday ${key} is not a start before its end, in whole minutes from 0 to 1440`,\n )\n continue\n }\n day.push({ start, end })\n }\n day.sort((a, b) => a.start - b.start)\n if (day.some((window, index) => index > 0 && window.start < day[index - 1].end)) {\n problems.push(`Two windows on weekday ${key} overlap`)\n }\n if (day.length) windows[weekday] = day\n }\n if (!Object.keys(windows).length && !problems.length) {\n problems.push('The service needs weekly hours: at least one open window on one weekday')\n }\n return windows\n}\n\nfunction readAsk(label: string, value: unknown, problems: string[]): BookingFieldAsk {\n if (value === undefined) return 'off'\n if ((BOOKING_FIELD_ASKS as readonly unknown[]).includes(value)) return value as BookingFieldAsk\n problems.push(`Asking for ${label} is off, optional or required`)\n return 'off'\n}\n\nfunction readSwitch(label: string, value: unknown, fallback: boolean, problems: string[]): boolean {\n if (value === undefined) return fallback\n if (typeof value === 'boolean') return value\n problems.push(`${label} is on or off`)\n return fallback\n}\n\nexport type BookingServiceContentRead =\n | { ok: true; value: BookingServiceDraftFields }\n | { ok: false; problems: string[] }\n\n/** The content as the writer stores it, or every problem that stops it. */\nexport function readBookingServiceContent(\n content: Readonly<Record<string, unknown>>,\n): BookingServiceContentRead {\n const problems: string[] = []\n const name = line(content['name'])\n if (!name) problems.push('The service needs a name')\n else if (name.length > BOOKING_SERVICE_NAME_MAX) {\n problems.push(`The name is longer than ${BOOKING_SERVICE_NAME_MAX} characters`)\n }\n const duration = content['durationMinutes']\n if (\n typeof duration !== 'number' ||\n !Number.isInteger(duration) ||\n duration < BOOKING_MIN_DURATION_MINUTES ||\n duration > BOOKING_MAX_DURATION_MINUTES\n ) {\n problems.push(\n `The length is whole minutes from ${BOOKING_MIN_DURATION_MINUTES} to ${BOOKING_MAX_DURATION_MINUTES}`,\n )\n }\n const rawDescription = content['description']\n const description = typeof rawDescription === 'string' ? rawDescription.trim() : ''\n if (rawDescription !== undefined && typeof rawDescription !== 'string') {\n problems.push('The description is text')\n } else if (description.length > BOOKING_SERVICE_DESCRIPTION_MAX) {\n problems.push(`The description is longer than ${BOOKING_SERVICE_DESCRIPTION_MAX} characters`)\n }\n const timezone = line(content['timezone'])\n if (!isValidTimeZone(timezone)) {\n problems.push('The time zone is an IANA zone, such as America/Chicago')\n }\n const windows = readWindows(content['windows'], problems)\n\n // No price stated and no label asked for is \"Contact for price\": a draft\n // never charges what the caller did not say.\n const rawPrice = content['priceUsd']\n const askedDisplay = content['priceDisplay']\n let priceDisplay: BookingPriceDisplay = 'contact'\n let priceUsd = 0\n if (askedDisplay !== undefined) {\n if ((BOOKING_PRICE_DISPLAYS as readonly unknown[]).includes(askedDisplay)) {\n priceDisplay = askedDisplay as BookingPriceDisplay\n } else {\n problems.push('The price is stated as fixed, varies, estimate or contact')\n }\n } else if (rawPrice !== undefined) {\n priceDisplay = 'fixed'\n }\n if (priceDisplay === 'fixed') {\n if (\n typeof rawPrice !== 'number' ||\n !Number.isInteger(rawPrice) ||\n rawPrice < 0 ||\n rawPrice > BOOKING_SERVICE_PRICE_MAX_USD\n ) {\n problems.push(\n `A fixed price is whole dollars from 0 to ${BOOKING_SERVICE_PRICE_MAX_USD}; ` +\n 'state it, or say the price varies, is a free estimate, or is on request',\n )\n } else {\n priceUsd = rawPrice\n }\n }\n const askPhone = readAsk('a phone number', content['askPhone'], problems)\n const askAddress = readAsk('an address', content['askAddress'], problems)\n // The model's defaults: a booking files a meeting, and owes no follow-up.\n const crmMeetingActivity = readSwitch('Filing a meeting', content['crmMeetingActivity'], true, problems)\n const crmFollowUpTask = readSwitch('A follow-up task', content['crmFollowUpTask'], false, problems)\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n value: {\n name,\n durationMinutes: duration as number,\n ...(description ? { description } : {}),\n windows,\n timezone,\n priceDisplay,\n priceUsd,\n askPhone,\n askAddress,\n crmMeetingActivity,\n crmFollowUpTask,\n },\n }\n}\n\n/** What a caller is told about a service, written or about to be. */\nfunction factsOf(value: Pick<HostBookingService, 'durationMinutes' | 'priceUsd' | 'priceDisplay' | 'timezone' | 'windows'>) {\n return {\n status: 'draft',\n durationMinutes: value.durationMinutes,\n priceDisplay: value.priceDisplay ?? 'fixed',\n priceText: bookingPriceText(value),\n timezone: value.timezone ?? 'UTC',\n openWeekdays: Object.keys(value.windows ?? {})\n .map(Number)\n .sort((a, b) => a - b),\n }\n}\n\n/** Whether content is a service this plugin would store, with what it says about it. Pure. */\nexport function checkBookingServiceContent(\n content: Readonly<Record<string, unknown>>,\n): PluginDraftCheck {\n const read = readBookingServiceContent(content)\n if (read.ok === false) return read\n return { ok: true, facts: factsOf(read.value) }\n}\n\nfunction recordOf(service: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (service.data() ?? {}) as HostBookingService\n return {\n id: service.id,\n name: String(data.name ?? ''),\n versionId: null,\n facts: { ...factsOf(data), status: data.status === 'draft' ? 'draft' : 'active' },\n }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: BOOKING_SERVICE_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'bookings')\n ? null\n : { status: 403, error: BOOKING_SERVICE_PLAN_REFUSAL }\n}\n\n/**\n * The allowance, counted as `/api/hosts/resources` counts a service create:\n * every document in the collection.\n */\nfunction roomRefusal(org: PluginDraftContext['org'], used: number): PluginDraftRefusal | null {\n const quota = checkQuota(org as never, 'servicesPerHost', used)\n return quota.allowed ? null : { status: 403, error: bookingServiceLimitRefusal(quota.limit) }\n}\n\nexport interface BookingServiceDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createBookingServiceDraftWriter(\n deps: BookingServiceDraftWriterDeps = {},\n): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n const early = roleRefusal(host, context.uid) ?? planRefusal(context.org)\n if (early) return early\n const used = (await hostRef.collection('services').count().get()).data().count\n return roomRefusal(context.org, Number(used) || 0)\n },\n\n check: (content) => checkBookingServiceContent(content),\n\n read: async ({ hostId, id }) => {\n const service = await firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('services')\n .doc(id)\n .get()\n return service.exists ? recordOf(service) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = line(request.name)\n const read = readBookingServiceContent(asked ? { ...request.content, name: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const servicesRef = hostRef.collection('services')\n const serviceRef = servicesRef.doc(request.id)\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(serviceRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n // Asked again under the same id: the draft it already wrote.\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n const used = (await tx.get(servicesRef.count())).data().count\n const room = roomRefusal(request.org, Number(used) || 0)\n if (room) return { ok: false, ...room }\n tx.create(serviceRef, {\n ...read.value,\n status: 'draft',\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: read.value.name,\n versionId: null,\n facts: factsOf(read.value),\n }\n })\n },\n }\n}\n\nexport const bookingServiceDraftWriter = createBookingServiceDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerBookingServiceDraftWriter(): void {\n registerPluginResourceDraftWriter(BOOKING_SERVICE_DRAFT_RESOURCE, bookingServiceDraftWriter, {\n pluginId: BUNDLE_ID,\n })\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","checkQuota","registerPluginResourceDraftWriter","isValidTimeZone","firebaseAdmin","BUNDLE_ID","BOOKING_FIELD_ASKS","BOOKING_PRICE_DISPLAYS","bookingPriceText","BOOKING_MAX_DURATION_MINUTES","BOOKING_MIN_DURATION_MINUTES","BOOKING_SERVICE_DRAFT_RESOURCE","BOOKING_SERVICE_NAME_MAX","BOOKING_SERVICE_DESCRIPTION_MAX","BOOKING_SERVICE_WINDOWS_PER_DAY_MAX","BOOKING_SERVICE_PRICE_MAX_USD","BOOKING_SERVICE_ROLE_REFUSAL","BOOKING_SERVICE_PLAN_REFUSAL","bookingServiceLimitRefusal","limit","isRecord","value","Array","isArray","line","replace","trim","readWindows","problems","windows","push","key","raw","Object","entries","weekday","Number","test","isInteger","length","day","window","start","undefined","end","sort","a","b","some","index","keys","readAsk","label","includes","readSwitch","fallback","readBookingServiceContent","content","name","duration","rawDescription","description","timezone","rawPrice","askedDisplay","priceDisplay","priceUsd","askPhone","askAddress","crmMeetingActivity","crmFollowUpTask","ok","Set","durationMinutes","factsOf","status","priceText","openWeekdays","map","checkBookingServiceContent","read","facts","recordOf","service","data","id","String","versionId","roleRefusal","host","uid","role","get","error","planRefusal","org","roomRefusal","used","quota","allowed","createBookingServiceDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","doc","hostId","exists","early","count","check","write","request","asked","db","servicesRef","serviceRef","runTransaction","tx","existing","Promise","all","replayed","room","create","createdAt","now","updatedAt","createdBy","bookingServiceDraftWriter","registerBookingServiceDraftWriter","pluginId"],"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;
|