@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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/package.json +11 -11
  3. package/src/lib/components/book-meeting-action.js +3 -2
  4. package/src/lib/components/book-meeting-action.js.map +1 -1
  5. package/src/lib/components/booking.js +4 -0
  6. package/src/lib/components/booking.js.map +1 -1
  7. package/src/lib/components/bookings-console-page.js +62 -4
  8. package/src/lib/components/bookings-console-page.js.map +1 -1
  9. package/src/lib/model/booking-in-person.d.ts +72 -0
  10. package/src/lib/model/booking-in-person.js +71 -0
  11. package/src/lib/model/booking-in-person.js.map +1 -0
  12. package/src/lib/model/booking-manage.d.ts +66 -0
  13. package/src/lib/model/booking-manage.js +90 -0
  14. package/src/lib/model/booking-manage.js.map +1 -0
  15. package/src/lib/model/bookings.d.ts +22 -0
  16. package/src/lib/model/bookings.js +11 -0
  17. package/src/lib/model/bookings.js.map +1 -1
  18. package/src/lib/server/booking-manage.d.ts +77 -0
  19. package/src/lib/server/booking-manage.js +306 -0
  20. package/src/lib/server/booking-manage.js.map +1 -0
  21. package/src/lib/server/booking-service-ai-capability.d.ts +50 -0
  22. package/src/lib/server/booking-service-ai-capability.js +203 -0
  23. package/src/lib/server/booking-service-ai-capability.js.map +1 -0
  24. package/src/lib/server/booking-service-drafts.d.ts +107 -0
  25. package/src/lib/server/booking-service-drafts.js +320 -0
  26. package/src/lib/server/booking-service-drafts.js.map +1 -0
  27. package/src/lib/server/in-person-payment.d.ts +20 -0
  28. package/src/lib/server/in-person-payment.js +469 -0
  29. package/src/lib/server/in-person-payment.js.map +1 -0
  30. package/src/lib/server.js +24 -4
  31. package/src/lib/server.js.map +1 -1
  32. package/src/lib/tenant-emails.js +59 -0
  33. package/src/lib/tenant-emails.js.map +1 -1
  34. package/src/lib/utils/use-booking-purchase-event.js +5 -0
  35. package/src/lib/utils/use-booking-purchase-event.js.map +1 -1
@@ -0,0 +1,469 @@
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 { createHash } from "crypto";
18
+ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
19
+ import { merchantAccountIsReady } from "@aglyn/tenant-data-admin/server/payment-provider";
20
+ import { checkEntitlement, resolveTransactionFeeCents } from "@aglyn/aglyn/server";
21
+ import { pluginTaxProfile } from "@aglyn/aglyn/plugin-manager/plugin-tax-profile";
22
+ import { bookingInPersonAmountProblem, bookingInPersonState } from "../model/booking-in-person.js";
23
+ import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
24
+ /*==========================================
25
+ * `POST /api/bookings/in-person-payment` (AGL-3618).
26
+ *
27
+ * Takes payment for a booking at the counter, on the card reader the native
28
+ * Aglyn POS app has connected (Tap to Pay or a Bluetooth reader). The server
29
+ * owns the money; the app only presents the card:
30
+ *
31
+ * - `start` prices the charge (the amount staff typed, plus the merchant's
32
+ * own service tax), and creates a card-present PaymentIntent: a
33
+ * DESTINATION charge to the merchant, settled on Aglyn's platform account
34
+ * with no `on_behalf_of` (ToS §10.7, AGL-3607), with the take and
35
+ * Stripe's card cost as `application_fee_amount` — exactly the fee a paid
36
+ * booking already carries online (`resolveTransactionFeeCents`, `service`).
37
+ * Returns the intent's client secret for the app's Terminal SDK.
38
+ * - `settle` re-reads the intent from Stripe (never trusting the app's word),
39
+ * captures an authorized one, and records the booking paid with the same
40
+ * fields an online payment writes, so the console's refund and the dispute
41
+ * handling work on it unchanged.
42
+ * - `cancel` releases an intent that was not collected.
43
+ *
44
+ * MANUAL capture, so a card that authorizes but whose booking was changed in
45
+ * between is released rather than charged. On-reader tipping is not offered
46
+ * here; the amount is the amount.
47
+ *
48
+ * Gate: a site `admin` or `editor`, and the owning org's `bookings` and
49
+ * `pos` entitlements (taking card-present payments is a POS capability).
50
+ *=========================================*/ const BOOKING_ID = /^[A-Za-z0-9_-]{1,128}$/;
51
+ const PAYMENT_INTENT_ID = /^pi_[A-Za-z0-9_]{6,128}$/;
52
+ function requestBody(req) {
53
+ if (typeof req.body === 'string') {
54
+ try {
55
+ const parsed = JSON.parse(req.body);
56
+ return parsed && typeof parsed === 'object' ? parsed : {};
57
+ } catch (unused) {
58
+ return {};
59
+ }
60
+ }
61
+ return req.body && typeof req.body === 'object' ? req.body : {};
62
+ }
63
+ /** Card-present payments: always in test mode, in live mode once switched on. */ export function inPersonCardsAvailable() {
64
+ var _process_env_STRIPE_SECRET_KEY, _process_env_STRIPE_TERMINAL_LIVE_ENABLED;
65
+ const key = String((_process_env_STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY) != null ? _process_env_STRIPE_SECRET_KEY : '');
66
+ if (!key) return false;
67
+ return key.startsWith('sk_test_') || String((_process_env_STRIPE_TERMINAL_LIVE_ENABLED = process.env.STRIPE_TERMINAL_LIVE_ENABLED) != null ? _process_env_STRIPE_TERMINAL_LIVE_ENABLED : '').toLowerCase() === 'true';
68
+ }
69
+ async function stripe(method, path, options = {}) {
70
+ var _options_params;
71
+ const form = new URLSearchParams();
72
+ for (const [key, value] of Object.entries((_options_params = options.params) != null ? _options_params : {})){
73
+ if (value !== undefined) form.append(key, String(value));
74
+ }
75
+ const query = method === 'GET' && form.toString() ? `?${form}` : '';
76
+ const response = await fetch(`https://api.stripe.com/v1/${path}${query}`, _extends({
77
+ method,
78
+ headers: _extends({
79
+ Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
80
+ 'Content-Type': 'application/x-www-form-urlencoded'
81
+ }, options.idempotencyKey ? {
82
+ 'Idempotency-Key': options.idempotencyKey
83
+ } : {})
84
+ }, method === 'POST' ? {
85
+ body: form.toString()
86
+ } : {}));
87
+ const body = await response.json().catch(()=>null);
88
+ return {
89
+ ok: response.ok,
90
+ status: response.status,
91
+ body
92
+ };
93
+ }
94
+ function stripeMessage(body, fallback) {
95
+ var _body_error;
96
+ const message = body == null ? void 0 : (_body_error = body.error) == null ? void 0 : _body_error.message;
97
+ return typeof message === 'string' && message ? message : fallback;
98
+ }
99
+ async function authorize(req, hostId) {
100
+ var _req_headers_authorization, _host_get, _ref;
101
+ var _this;
102
+ const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
103
+ const idToken = authorization.startsWith('Bearer ') ? authorization.slice(7) : '';
104
+ if (!idToken) return {
105
+ ok: false,
106
+ status: 401,
107
+ error: 'Unauthenticated'
108
+ };
109
+ if (!hostId) return {
110
+ ok: false,
111
+ status: 400,
112
+ error: 'Missing hostId'
113
+ };
114
+ let uid;
115
+ try {
116
+ uid = (await firebaseAdmin.app().auth().verifyIdToken(idToken)).uid;
117
+ } catch (error) {
118
+ if (!isRefusedIdToken(error)) throw error;
119
+ return {
120
+ ok: false,
121
+ status: 401,
122
+ error: 'Unauthenticated'
123
+ };
124
+ }
125
+ const host = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get();
126
+ if (!host.exists) return {
127
+ ok: false,
128
+ status: 404,
129
+ error: 'Unknown site'
130
+ };
131
+ const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
132
+ if (role !== 'admin' && role !== 'editor') return {
133
+ ok: false,
134
+ status: 403,
135
+ error: 'Not permitted'
136
+ };
137
+ const org = (_ref = (_this = await getOrgForHost(hostId)) == null ? void 0 : _this.org) != null ? _ref : null;
138
+ if (!checkEntitlement(org, 'bookings') || !checkEntitlement(org, 'pos')) {
139
+ return {
140
+ ok: false,
141
+ status: 403,
142
+ error: 'Taking booking payments in person needs the POS plan.'
143
+ };
144
+ }
145
+ return {
146
+ ok: true,
147
+ staff: {
148
+ uid,
149
+ hostId,
150
+ org
151
+ }
152
+ };
153
+ }
154
+ export const bookingInPersonPaymentHandler = async (req, res)=>{
155
+ var _body_hostId, _body_bookingId, _body_action;
156
+ if (req.method !== 'POST') return res.status(405).json({
157
+ error: 'Method not allowed'
158
+ });
159
+ const body = requestBody(req);
160
+ const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
161
+ const bookingId = String((_body_bookingId = body['bookingId']) != null ? _body_bookingId : '');
162
+ const gate = await authorize(req, hostId);
163
+ if ('error' in gate) return res.status(gate.status).json({
164
+ error: gate.error
165
+ });
166
+ if (!BOOKING_ID.test(bookingId)) return res.status(400).json({
167
+ error: 'Missing bookingId'
168
+ });
169
+ if (!inPersonCardsAvailable()) {
170
+ return res.status(409).json({
171
+ error: 'Card readers are not available yet.'
172
+ });
173
+ }
174
+ const action = String((_body_action = body['action']) != null ? _body_action : '');
175
+ try {
176
+ switch(action){
177
+ case 'start':
178
+ return await start(gate.staff, bookingId, body, req, res);
179
+ case 'settle':
180
+ return await settle(gate.staff, bookingId, res);
181
+ case 'cancel':
182
+ return await cancel(gate.staff, bookingId, res);
183
+ default:
184
+ return res.status(400).json({
185
+ error: 'Unknown action'
186
+ });
187
+ }
188
+ } catch (error) {
189
+ console.error('[bookings/in-person-payment]', action, error);
190
+ return res.status(500).json({
191
+ error: 'The payment could not be updated. Check the booking before trying again.'
192
+ });
193
+ }
194
+ };
195
+ function bookingRef(hostId, bookingId) {
196
+ return firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection('bookings').doc(bookingId);
197
+ }
198
+ async function merchantAccount(org, hostId) {
199
+ var _ref, _profile_get;
200
+ const ownerUid = String((_ref = org == null ? void 0 : org['ownerUid']) != null ? _ref : '');
201
+ if (!ownerUid) return null;
202
+ const profile = await firebaseAdmin.app().firestore().collection('profiles').doc(ownerUid).get();
203
+ const accountId = String((_profile_get = profile.get('stripeAccountId')) != null ? _profile_get : '');
204
+ return merchantAccountIsReady({
205
+ accountId,
206
+ chargesEnabled: profile.get('stripeChargesEnabled'),
207
+ accountLivemode: profile.get('stripeAccountLivemode')
208
+ }, {
209
+ subject: `booking in-person host ${hostId}`
210
+ }) ? accountId : null;
211
+ }
212
+ async function start(staff, bookingId, body, req, res) {
213
+ var _ref, _req_headers_idempotencykey, _booking_serviceName;
214
+ var _intent_body, _intent_body1;
215
+ const attemptKey = String((_ref = (_req_headers_idempotencykey = req.headers['idempotency-key']) != null ? _req_headers_idempotencykey : req.headers['Idempotency-Key']) != null ? _ref : '').trim().slice(0, 200);
216
+ if (!attemptKey) return res.status(400).json({
217
+ error: 'Missing Idempotency-Key'
218
+ });
219
+ const serviceCents = typeof body['amountCents'] === 'number' ? body['amountCents'] : NaN;
220
+ const problem = bookingInPersonAmountProblem(serviceCents);
221
+ if (problem) return res.status(400).json({
222
+ error: problem
223
+ });
224
+ const ref = bookingRef(staff.hostId, bookingId);
225
+ const snapshot = await ref.get();
226
+ if (!snapshot.exists) return res.status(404).json({
227
+ error: 'Unknown booking'
228
+ });
229
+ const booking = snapshot.data();
230
+ const state = bookingInPersonState(booking);
231
+ const existing = booking['inPersonPayment'];
232
+ const attempt = createHash('sha256').update(`${bookingId}:${attemptKey}`).digest('hex').slice(0, 40);
233
+ if (state === 'paid') return res.status(409).json({
234
+ error: 'This booking is already paid.'
235
+ });
236
+ if (state === 'canceled') return res.status(409).json({
237
+ error: 'This booking was canceled.'
238
+ });
239
+ if (state === 'awaiting-online') {
240
+ return res.status(409).json({
241
+ error: 'The customer is paying for this booking online right now.'
242
+ });
243
+ }
244
+ const destination = await merchantAccount(staff.org, staff.hostId);
245
+ if (!destination) return res.status(409).json({
246
+ error: 'Finish setting up payments before taking cards.'
247
+ });
248
+ // The merchant's own service tax, off unless they set one (AGL-2028): the
249
+ // same rate a paid booking is charged online.
250
+ const tax = pluginTaxProfile().flatTax(await pluginTaxProfile().flatRate(staff.hostId, 'service'), serviceCents, 'Service tax');
251
+ const amountCents = serviceCents + tax.taxCents;
252
+ const feeCents = resolveTransactionFeeCents(staff.org, 'service', serviceCents, amountCents);
253
+ // A pending intent for this booking that is not this attempt's: release it
254
+ // first, so one booking never has two cards in flight.
255
+ if ((existing == null ? void 0 : existing.status) === 'pending' && existing.paymentIntentId) {
256
+ const sameAttempt = existing.attempt === attempt;
257
+ if (sameAttempt) {
258
+ var _replay_body;
259
+ const replay = await stripe('GET', `payment_intents/${existing.paymentIntentId}`);
260
+ if (replay.ok && ((_replay_body = replay.body) == null ? void 0 : _replay_body.client_secret)) {
261
+ return res.status(200).json(startAnswer(existing, String(replay.body.client_secret)));
262
+ }
263
+ } else {
264
+ const released = await releaseIntent(existing.paymentIntentId);
265
+ if (!released) {
266
+ return res.status(409).json({
267
+ error: 'A card was already authorized for this booking. Settle it before starting another payment.'
268
+ });
269
+ }
270
+ }
271
+ }
272
+ const intent = await stripe('POST', 'payment_intents', {
273
+ idempotencyKey: `booking-in-person:${attempt}`,
274
+ params: _extends({
275
+ amount: amountCents,
276
+ currency: 'usd',
277
+ 'payment_method_types[]': 'card_present',
278
+ capture_method: 'manual',
279
+ 'transfer_data[destination]': destination
280
+ }, feeCents > 0 ? {
281
+ application_fee_amount: feeCents
282
+ } : {}, {
283
+ description: `Booking: ${String((_booking_serviceName = booking['serviceName']) != null ? _booking_serviceName : 'appointment').slice(0, 80)}`,
284
+ 'metadata[type]': 'booking-in-person',
285
+ 'metadata[hostId]': staff.hostId,
286
+ 'metadata[bookingId]': bookingId,
287
+ 'metadata[feeCents]': feeCents,
288
+ 'metadata[cashierId]': staff.uid
289
+ }, tax.taxCents > 0 ? {
290
+ 'metadata[taxCents]': tax.taxCents,
291
+ 'metadata[taxPct]': tax.pct
292
+ } : {})
293
+ });
294
+ if (!intent.ok || !((_intent_body = intent.body) == null ? void 0 : _intent_body.id) || !((_intent_body1 = intent.body) == null ? void 0 : _intent_body1.client_secret)) {
295
+ return res.status(502).json({
296
+ error: stripeMessage(intent.body, 'The card payment could not be started.')
297
+ });
298
+ }
299
+ const payment = {
300
+ paymentIntentId: String(intent.body.id),
301
+ amountCents,
302
+ serviceCents,
303
+ taxCents: tax.taxCents,
304
+ feeCents,
305
+ status: 'pending',
306
+ startedAtMs: Date.now(),
307
+ startedBy: staff.uid,
308
+ attempt
309
+ };
310
+ // Recorded in a transaction that re-checks the booking: a payment that
311
+ // landed meanwhile (online, or on another device) wins, and this intent is
312
+ // released instead of left to authorize a second charge.
313
+ const recorded = await firebaseAdmin.app().firestore().runTransaction(async (transaction)=>{
314
+ var _fresh_data;
315
+ const fresh = await transaction.get(ref);
316
+ const freshState = bookingInPersonState((_fresh_data = fresh.data()) != null ? _fresh_data : {});
317
+ const freshPending = fresh.get('inPersonPayment');
318
+ if (freshState === 'paid' || freshState === 'canceled' || freshState === 'awaiting-online') return false;
319
+ if ((freshPending == null ? void 0 : freshPending.status) === 'pending' && freshPending.paymentIntentId !== payment.paymentIntentId && freshPending.paymentIntentId !== (existing == null ? void 0 : existing.paymentIntentId)) {
320
+ return false;
321
+ }
322
+ transaction.set(ref, {
323
+ inPersonPayment: payment
324
+ }, {
325
+ merge: true
326
+ });
327
+ return true;
328
+ });
329
+ if (!recorded) {
330
+ await releaseIntent(payment.paymentIntentId);
331
+ return res.status(409).json({
332
+ error: 'This booking changed while the payment started. Open it again.'
333
+ });
334
+ }
335
+ return res.status(200).json(startAnswer(payment, String(intent.body.client_secret)));
336
+ }
337
+ function startAnswer(payment, clientSecret) {
338
+ return {
339
+ paymentIntentId: payment.paymentIntentId,
340
+ clientSecret,
341
+ amountCents: payment.amountCents,
342
+ serviceCents: payment.serviceCents,
343
+ taxCents: payment.taxCents
344
+ };
345
+ }
346
+ /** Cancels an intent that holds no money yet; false when it does. */ async function releaseIntent(paymentIntentId) {
347
+ var _ref, _ref1;
348
+ var _current_body, _canceled_body;
349
+ const current = await stripe('GET', `payment_intents/${paymentIntentId}`);
350
+ const status = String((_ref = (_current_body = current.body) == null ? void 0 : _current_body.status) != null ? _ref : '');
351
+ if (status === 'canceled') return true;
352
+ if (status === 'succeeded' || status === 'requires_capture' || status === 'processing') return false;
353
+ const canceled = await stripe('POST', `payment_intents/${paymentIntentId}/cancel`, {
354
+ idempotencyKey: `booking-in-person-cancel:${paymentIntentId}`
355
+ });
356
+ return canceled.ok || String((_ref1 = (_canceled_body = canceled.body) == null ? void 0 : _canceled_body.status) != null ? _ref1 : '') === 'canceled';
357
+ }
358
+ async function settle(staff, bookingId, res) {
359
+ var _ref, _intent_amount_received;
360
+ var _intent_metadata, _intent_metadata1, _intent_metadata2;
361
+ const ref = bookingRef(staff.hostId, bookingId);
362
+ const snapshot = await ref.get();
363
+ if (!snapshot.exists) return res.status(404).json({
364
+ error: 'Unknown booking'
365
+ });
366
+ const payment = snapshot.get('inPersonPayment');
367
+ if ((payment == null ? void 0 : payment.status) === 'paid') return res.status(200).json({
368
+ status: 'paid',
369
+ amountCents: payment.amountCents
370
+ });
371
+ if (!payment || payment.status !== 'pending' || !PAYMENT_INTENT_ID.test(String(payment.paymentIntentId))) {
372
+ return res.status(409).json({
373
+ error: 'No card payment is in progress for this booking.'
374
+ });
375
+ }
376
+ let intent = (await stripe('GET', `payment_intents/${payment.paymentIntentId}`)).body;
377
+ // The intent must be the one this route made for THIS booking on THIS site.
378
+ if ((intent == null ? void 0 : (_intent_metadata = intent.metadata) == null ? void 0 : _intent_metadata.type) !== 'booking-in-person' || (intent == null ? void 0 : (_intent_metadata1 = intent.metadata) == null ? void 0 : _intent_metadata1.hostId) !== staff.hostId || (intent == null ? void 0 : (_intent_metadata2 = intent.metadata) == null ? void 0 : _intent_metadata2.bookingId) !== bookingId) {
379
+ return res.status(409).json({
380
+ error: 'That payment is not for this booking.'
381
+ });
382
+ }
383
+ if (intent.status === 'requires_capture') {
384
+ const captured = await stripe('POST', `payment_intents/${payment.paymentIntentId}/capture`, {
385
+ idempotencyKey: `booking-in-person-capture:${payment.paymentIntentId}`,
386
+ params: payment.feeCents > 0 ? {
387
+ application_fee_amount: payment.feeCents
388
+ } : {}
389
+ });
390
+ if (!captured.ok) {
391
+ return res.status(502).json({
392
+ error: stripeMessage(captured.body, 'The card could not be charged. Try again.')
393
+ });
394
+ }
395
+ intent = captured.body;
396
+ }
397
+ if ((intent == null ? void 0 : intent.status) !== 'succeeded') {
398
+ // Not collected yet, declined, or canceled: the booking stays payable.
399
+ const status = (intent == null ? void 0 : intent.status) === 'canceled' ? 'canceled' : 'pending';
400
+ if (status === 'canceled') {
401
+ await ref.set({
402
+ inPersonPayment: _extends({}, payment, {
403
+ status: 'canceled'
404
+ })
405
+ }, {
406
+ merge: true
407
+ });
408
+ }
409
+ return res.status(200).json({
410
+ status
411
+ });
412
+ }
413
+ const paidAmountCents = Math.max(0, Math.round(Number((_ref = (_intent_amount_received = intent.amount_received) != null ? _intent_amount_received : intent.amount) != null ? _ref : 0)));
414
+ const paidAtMs = Date.now();
415
+ await firebaseAdmin.app().firestore().runTransaction(async (transaction)=>{
416
+ const fresh = await transaction.get(ref);
417
+ if (fresh.get('paymentIntentId')) return;
418
+ transaction.set(ref, _extends({
419
+ // The same fields an online payment writes (billing-webhook.ts), so
420
+ // the console's refund and dispute handling read it unchanged.
421
+ paidAmountCents,
422
+ paymentIntentId: payment.paymentIntentId,
423
+ feeCents: payment.feeCents,
424
+ taxMode: pluginTaxProfile().taxModeOf(intent, payment.taxCents)
425
+ }, payment.taxCents > 0 ? {
426
+ taxCents: payment.taxCents
427
+ } : {}, {
428
+ paidInPerson: true,
429
+ inPersonPayment: _extends({}, payment, {
430
+ status: 'paid',
431
+ paidAtMs
432
+ })
433
+ }), {
434
+ merge: true
435
+ });
436
+ });
437
+ return res.status(200).json({
438
+ status: 'paid',
439
+ amountCents: paidAmountCents
440
+ });
441
+ }
442
+ async function cancel(staff, bookingId, res) {
443
+ const ref = bookingRef(staff.hostId, bookingId);
444
+ const snapshot = await ref.get();
445
+ if (!snapshot.exists) return res.status(404).json({
446
+ error: 'Unknown booking'
447
+ });
448
+ const payment = snapshot.get('inPersonPayment');
449
+ if (!payment || payment.status !== 'pending') return res.status(200).json({
450
+ status: 'none'
451
+ });
452
+ if (!await releaseIntent(payment.paymentIntentId)) {
453
+ return res.status(409).json({
454
+ error: 'The card was already authorized. Settle the payment instead.'
455
+ });
456
+ }
457
+ await ref.set({
458
+ inPersonPayment: _extends({}, payment, {
459
+ status: 'canceled'
460
+ })
461
+ }, {
462
+ merge: true
463
+ });
464
+ return res.status(200).json({
465
+ status: 'canceled'
466
+ });
467
+ }
468
+
469
+ //# sourceMappingURL=in-person-payment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/bookings/src/lib/server/in-person-payment.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 { createHash } from 'crypto'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { merchantAccountIsReady } from '@aglyn/tenant-data-admin/server/payment-provider'\nimport {\n checkEntitlement,\n resolveTransactionFeeCents,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { pluginTaxProfile } from '@aglyn/aglyn/plugin-manager/plugin-tax-profile'\nimport {\n bookingInPersonAmountProblem,\n bookingInPersonState,\n type BookingInPersonPayment,\n} from '../model/booking-in-person'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/*==========================================\n * `POST /api/bookings/in-person-payment` (AGL-3618).\n *\n * Takes payment for a booking at the counter, on the card reader the native\n * Aglyn POS app has connected (Tap to Pay or a Bluetooth reader). The server\n * owns the money; the app only presents the card:\n *\n * - `start` prices the charge (the amount staff typed, plus the merchant's\n * own service tax), and creates a card-present PaymentIntent: a\n * DESTINATION charge to the merchant, settled on Aglyn's platform account\n * with no `on_behalf_of` (ToS §10.7, AGL-3607), with the take and\n * Stripe's card cost as `application_fee_amount` — exactly the fee a paid\n * booking already carries online (`resolveTransactionFeeCents`, `service`).\n * Returns the intent's client secret for the app's Terminal SDK.\n * - `settle` re-reads the intent from Stripe (never trusting the app's word),\n * captures an authorized one, and records the booking paid with the same\n * fields an online payment writes, so the console's refund and the dispute\n * handling work on it unchanged.\n * - `cancel` releases an intent that was not collected.\n *\n * MANUAL capture, so a card that authorizes but whose booking was changed in\n * between is released rather than charged. On-reader tipping is not offered\n * here; the amount is the amount.\n *\n * Gate: a site `admin` or `editor`, and the owning org's `bookings` and\n * `pos` entitlements (taking card-present payments is a POS capability).\n *=========================================*/\n\nconst BOOKING_ID = /^[A-Za-z0-9_-]{1,128}$/\nconst PAYMENT_INTENT_ID = /^pi_[A-Za-z0-9_]{6,128}$/\n\ntype Res = Parameters<PluginApiHandler>[1]\n\ninterface Staff {\n uid: string\n hostId: string\n org: Record<string, any> | null\n}\n\nfunction requestBody(req: Parameters<PluginApiHandler>[0]): Record<string, any> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body)\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? req.body : {}\n}\n\n/** Card-present payments: always in test mode, in live mode once switched on. */\nexport function inPersonCardsAvailable(): boolean {\n const key = String(process.env.STRIPE_SECRET_KEY ?? '')\n if (!key) return false\n return (\n key.startsWith('sk_test_') ||\n String(process.env.STRIPE_TERMINAL_LIVE_ENABLED ?? '').toLowerCase() === 'true'\n )\n}\n\nasync function stripe(\n method: 'GET' | 'POST',\n path: string,\n options: { params?: Record<string, string | number | boolean | undefined>; idempotencyKey?: string } = {},\n): Promise<{ ok: boolean; status: number; body: any }> {\n const form = new URLSearchParams()\n for (const [key, value] of Object.entries(options.params ?? {})) {\n if (value !== undefined) form.append(key, String(value))\n }\n const query = method === 'GET' && form.toString() ? `?${form}` : ''\n const response = await fetch(`https://api.stripe.com/v1/${path}${query}`, {\n method,\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n ...(options.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : {}),\n },\n ...(method === 'POST' ? { body: form.toString() } : {}),\n })\n const body = await response.json().catch(() => null)\n return { ok: response.ok, status: response.status, body }\n}\n\nfunction stripeMessage(body: any, fallback: string): string {\n const message = body?.error?.message\n return typeof message === 'string' && message ? message : fallback\n}\n\nasync function authorize(\n req: Parameters<PluginApiHandler>[0],\n hostId: string,\n): Promise<{ ok: true; staff: Staff } | { ok: false; status: number; error: string }> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice(7) : ''\n if (!idToken) return { ok: false, status: 401, error: 'Unauthenticated' }\n if (!hostId) return { ok: false, status: 400, error: 'Missing hostId' }\n let uid: string\n try {\n uid = (await firebaseAdmin.app().auth().verifyIdToken(idToken)).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return { ok: false, status: 401, error: 'Unauthenticated' }\n }\n const host = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get()\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n const role = (host.get('memberRoles') ?? {})[uid]\n if (role !== 'admin' && role !== 'editor') return { ok: false, status: 403, error: 'Not permitted' }\n const org = ((await getOrgForHost(hostId))?.org as Record<string, any> | undefined) ?? null\n if (!checkEntitlement(org as never, 'bookings') || !checkEntitlement(org as never, 'pos')) {\n return { ok: false, status: 403, error: 'Taking booking payments in person needs the POS plan.' }\n }\n return { ok: true, staff: { uid, hostId, org } }\n}\n\nexport const bookingInPersonPaymentHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = requestBody(req)\n const hostId = String(body['hostId'] ?? '')\n const bookingId = String(body['bookingId'] ?? '')\n const gate = await authorize(req, hostId)\n if ('error' in gate) return res.status(gate.status).json({ error: gate.error })\n if (!BOOKING_ID.test(bookingId)) return res.status(400).json({ error: 'Missing bookingId' })\n if (!inPersonCardsAvailable()) {\n return res.status(409).json({ error: 'Card readers are not available yet.' })\n }\n const action = String(body['action'] ?? '')\n try {\n switch (action) {\n case 'start':\n return await start(gate.staff, bookingId, body, req, res)\n case 'settle':\n return await settle(gate.staff, bookingId, res)\n case 'cancel':\n return await cancel(gate.staff, bookingId, res)\n default:\n return res.status(400).json({ error: 'Unknown action' })\n }\n } catch (error) {\n console.error('[bookings/in-person-payment]', action, error)\n return res.status(500).json({ error: 'The payment could not be updated. Check the booking before trying again.' })\n }\n}\n\nfunction bookingRef(hostId: string, bookingId: string) {\n return firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('bookings')\n .doc(bookingId)\n}\n\nasync function merchantAccount(org: Record<string, any> | null, hostId: string): Promise<string | null> {\n const ownerUid = String(org?.['ownerUid'] ?? '')\n if (!ownerUid) return null\n const profile = await firebaseAdmin.app().firestore().collection('profiles').doc(ownerUid).get()\n const accountId = String(profile.get('stripeAccountId') ?? '')\n return merchantAccountIsReady(\n {\n accountId,\n chargesEnabled: profile.get('stripeChargesEnabled'),\n accountLivemode: profile.get('stripeAccountLivemode'),\n },\n { subject: `booking in-person host ${hostId}` },\n )\n ? accountId\n : null\n}\n\nasync function start(\n staff: Staff,\n bookingId: string,\n body: Record<string, any>,\n req: Parameters<PluginApiHandler>[0],\n res: Res,\n) {\n const attemptKey = String(req.headers['idempotency-key'] ?? req.headers['Idempotency-Key'] ?? '')\n .trim()\n .slice(0, 200)\n if (!attemptKey) return res.status(400).json({ error: 'Missing Idempotency-Key' })\n const serviceCents = typeof body['amountCents'] === 'number' ? body['amountCents'] : NaN\n const problem = bookingInPersonAmountProblem(serviceCents)\n if (problem) return res.status(400).json({ error: problem })\n\n const ref = bookingRef(staff.hostId, bookingId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return res.status(404).json({ error: 'Unknown booking' })\n const booking = snapshot.data() as Record<string, any>\n const state = bookingInPersonState(booking)\n const existing = booking['inPersonPayment'] as BookingInPersonPayment | undefined\n const attempt = createHash('sha256').update(`${bookingId}:${attemptKey}`).digest('hex').slice(0, 40)\n\n if (state === 'paid') return res.status(409).json({ error: 'This booking is already paid.' })\n if (state === 'canceled') return res.status(409).json({ error: 'This booking was canceled.' })\n if (state === 'awaiting-online') {\n return res.status(409).json({ error: 'The customer is paying for this booking online right now.' })\n }\n\n const destination = await merchantAccount(staff.org, staff.hostId)\n if (!destination) return res.status(409).json({ error: 'Finish setting up payments before taking cards.' })\n\n // The merchant's own service tax, off unless they set one (AGL-2028): the\n // same rate a paid booking is charged online.\n const tax = pluginTaxProfile().flatTax(\n await pluginTaxProfile().flatRate(staff.hostId, 'service'),\n serviceCents,\n 'Service tax',\n )\n const amountCents = serviceCents + tax.taxCents\n const feeCents = resolveTransactionFeeCents(staff.org as never, 'service', serviceCents, amountCents)\n\n // A pending intent for this booking that is not this attempt's: release it\n // first, so one booking never has two cards in flight.\n if (existing?.status === 'pending' && existing.paymentIntentId) {\n const sameAttempt = existing.attempt === attempt\n if (sameAttempt) {\n const replay = await stripe('GET', `payment_intents/${existing.paymentIntentId}`)\n if (replay.ok && replay.body?.client_secret) {\n return res.status(200).json(startAnswer(existing, String(replay.body.client_secret)))\n }\n } else {\n const released = await releaseIntent(existing.paymentIntentId)\n if (!released) {\n return res.status(409).json({\n error: 'A card was already authorized for this booking. Settle it before starting another payment.',\n })\n }\n }\n }\n\n const intent = await stripe('POST', 'payment_intents', {\n idempotencyKey: `booking-in-person:${attempt}`,\n params: {\n amount: amountCents,\n currency: 'usd',\n 'payment_method_types[]': 'card_present',\n capture_method: 'manual',\n 'transfer_data[destination]': destination,\n ...(feeCents > 0 ? { application_fee_amount: feeCents } : {}),\n description: `Booking: ${String(booking['serviceName'] ?? 'appointment').slice(0, 80)}`,\n 'metadata[type]': 'booking-in-person',\n 'metadata[hostId]': staff.hostId,\n 'metadata[bookingId]': bookingId,\n 'metadata[feeCents]': feeCents,\n 'metadata[cashierId]': staff.uid,\n ...(tax.taxCents > 0 ? { 'metadata[taxCents]': tax.taxCents, 'metadata[taxPct]': tax.pct } : {}),\n },\n })\n if (!intent.ok || !intent.body?.id || !intent.body?.client_secret) {\n return res.status(502).json({ error: stripeMessage(intent.body, 'The card payment could not be started.') })\n }\n const payment: BookingInPersonPayment = {\n paymentIntentId: String(intent.body.id),\n amountCents,\n serviceCents,\n taxCents: tax.taxCents,\n feeCents,\n status: 'pending',\n startedAtMs: Date.now(),\n startedBy: staff.uid,\n attempt,\n }\n // Recorded in a transaction that re-checks the booking: a payment that\n // landed meanwhile (online, or on another device) wins, and this intent is\n // released instead of left to authorize a second charge.\n const recorded = await firebaseAdmin.app().firestore().runTransaction(async (transaction: any) => {\n const fresh = await transaction.get(ref)\n const freshState = bookingInPersonState((fresh.data() ?? {}) as Record<string, any>)\n const freshPending = fresh.get('inPersonPayment') as BookingInPersonPayment | undefined\n if (freshState === 'paid' || freshState === 'canceled' || freshState === 'awaiting-online') return false\n if (\n freshPending?.status === 'pending' &&\n freshPending.paymentIntentId !== payment.paymentIntentId &&\n freshPending.paymentIntentId !== existing?.paymentIntentId\n ) {\n return false\n }\n transaction.set(ref, { inPersonPayment: payment }, { merge: true })\n return true\n })\n if (!recorded) {\n await releaseIntent(payment.paymentIntentId)\n return res.status(409).json({ error: 'This booking changed while the payment started. Open it again.' })\n }\n return res.status(200).json(startAnswer(payment, String(intent.body.client_secret)))\n}\n\nfunction startAnswer(payment: BookingInPersonPayment, clientSecret: string) {\n return {\n paymentIntentId: payment.paymentIntentId,\n clientSecret,\n amountCents: payment.amountCents,\n serviceCents: payment.serviceCents,\n taxCents: payment.taxCents,\n }\n}\n\n/** Cancels an intent that holds no money yet; false when it does. */\nasync function releaseIntent(paymentIntentId: string): Promise<boolean> {\n const current = await stripe('GET', `payment_intents/${paymentIntentId}`)\n const status = String(current.body?.status ?? '')\n if (status === 'canceled') return true\n if (status === 'succeeded' || status === 'requires_capture' || status === 'processing') return false\n const canceled = await stripe('POST', `payment_intents/${paymentIntentId}/cancel`, {\n idempotencyKey: `booking-in-person-cancel:${paymentIntentId}`,\n })\n return canceled.ok || String(canceled.body?.status ?? '') === 'canceled'\n}\n\nasync function settle(staff: Staff, bookingId: string, res: Res) {\n const ref = bookingRef(staff.hostId, bookingId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return res.status(404).json({ error: 'Unknown booking' })\n const payment = snapshot.get('inPersonPayment') as BookingInPersonPayment | undefined\n if (payment?.status === 'paid') return res.status(200).json({ status: 'paid', amountCents: payment.amountCents })\n if (!payment || payment.status !== 'pending' || !PAYMENT_INTENT_ID.test(String(payment.paymentIntentId))) {\n return res.status(409).json({ error: 'No card payment is in progress for this booking.' })\n }\n let intent = (await stripe('GET', `payment_intents/${payment.paymentIntentId}`)).body\n // The intent must be the one this route made for THIS booking on THIS site.\n if (\n intent?.metadata?.type !== 'booking-in-person' ||\n intent?.metadata?.hostId !== staff.hostId ||\n intent?.metadata?.bookingId !== bookingId\n ) {\n return res.status(409).json({ error: 'That payment is not for this booking.' })\n }\n if (intent.status === 'requires_capture') {\n const captured = await stripe('POST', `payment_intents/${payment.paymentIntentId}/capture`, {\n idempotencyKey: `booking-in-person-capture:${payment.paymentIntentId}`,\n params: payment.feeCents > 0 ? { application_fee_amount: payment.feeCents } : {},\n })\n if (!captured.ok) {\n return res.status(502).json({ error: stripeMessage(captured.body, 'The card could not be charged. Try again.') })\n }\n intent = captured.body\n }\n if (intent?.status !== 'succeeded') {\n // Not collected yet, declined, or canceled: the booking stays payable.\n const status = intent?.status === 'canceled' ? 'canceled' : 'pending'\n if (status === 'canceled') {\n await ref.set({ inPersonPayment: { ...payment, status: 'canceled' } }, { merge: true })\n }\n return res.status(200).json({ status })\n }\n const paidAmountCents = Math.max(0, Math.round(Number(intent.amount_received ?? intent.amount ?? 0)))\n const paidAtMs = Date.now()\n await firebaseAdmin.app().firestore().runTransaction(async (transaction: any) => {\n const fresh = await transaction.get(ref)\n if (fresh.get('paymentIntentId')) return\n transaction.set(\n ref,\n {\n // The same fields an online payment writes (billing-webhook.ts), so\n // the console's refund and dispute handling read it unchanged.\n paidAmountCents,\n paymentIntentId: payment.paymentIntentId,\n feeCents: payment.feeCents,\n taxMode: pluginTaxProfile().taxModeOf(intent, payment.taxCents),\n ...(payment.taxCents > 0 ? { taxCents: payment.taxCents } : {}),\n paidInPerson: true,\n inPersonPayment: { ...payment, status: 'paid', paidAtMs },\n },\n { merge: true },\n )\n })\n return res.status(200).json({ status: 'paid', amountCents: paidAmountCents })\n}\n\nasync function cancel(staff: Staff, bookingId: string, res: Res) {\n const ref = bookingRef(staff.hostId, bookingId)\n const snapshot = await ref.get()\n if (!snapshot.exists) return res.status(404).json({ error: 'Unknown booking' })\n const payment = snapshot.get('inPersonPayment') as BookingInPersonPayment | undefined\n if (!payment || payment.status !== 'pending') return res.status(200).json({ status: 'none' })\n if (!(await releaseIntent(payment.paymentIntentId))) {\n return res.status(409).json({ error: 'The card was already authorized. Settle the payment instead.' })\n }\n await ref.set({ inPersonPayment: { ...payment, status: 'canceled' } }, { merge: true })\n return res.status(200).json({ status: 'canceled' })\n}\n"],"names":["createHash","firebaseAdmin","getOrgForHost","merchantAccountIsReady","checkEntitlement","resolveTransactionFeeCents","pluginTaxProfile","bookingInPersonAmountProblem","bookingInPersonState","isRefusedIdToken","BOOKING_ID","PAYMENT_INTENT_ID","requestBody","req","body","parsed","JSON","parse","inPersonCardsAvailable","process","key","String","env","STRIPE_SECRET_KEY","startsWith","STRIPE_TERMINAL_LIVE_ENABLED","toLowerCase","stripe","method","path","options","form","URLSearchParams","value","Object","entries","params","undefined","append","query","toString","response","fetch","headers","Authorization","idempotencyKey","json","catch","ok","status","stripeMessage","fallback","message","error","authorize","hostId","host","authorization","idToken","slice","uid","app","auth","verifyIdToken","firestore","collection","doc","get","exists","role","org","staff","bookingInPersonPaymentHandler","res","bookingId","gate","test","action","start","settle","cancel","console","bookingRef","merchantAccount","profile","ownerUid","accountId","chargesEnabled","accountLivemode","subject","booking","intent","attemptKey","trim","serviceCents","NaN","problem","ref","snapshot","data","state","existing","attempt","update","digest","destination","tax","flatTax","flatRate","amountCents","taxCents","feeCents","paymentIntentId","sameAttempt","replay","client_secret","startAnswer","released","releaseIntent","amount","currency","capture_method","application_fee_amount","description","pct","id","payment","startedAtMs","Date","now","startedBy","recorded","runTransaction","transaction","fresh","freshState","freshPending","set","inPersonPayment","merge","clientSecret","current","canceled","metadata","type","captured","paidAmountCents","Math","max","round","Number","amount_received","paidAtMs","taxMode","taxModeOf","paidInPerson"],"mappings":"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"}
package/src/lib/server.js CHANGED
@@ -22,7 +22,7 @@ import { checkEntitlement, registerPluginConfigSchema, registerPluginJob, resolv
22
22
  // reach the real verdict.
23
23
  import { siteLockdownJobGate } from "@aglyn/tenant-data-admin/server/tenant-write-lockdown";
24
24
  import { loadHostEmailWithTokens, renderLoadedHostEmailWithTokens } from "@aglyn/tenant-data-admin/server/host-email-tokens";
25
- import { BOOKING_MAX_DAYS_AHEAD, bookingChargeUsd, bookingContactAsks, bookingPriceDisplay, bookingContactLines, readBookingContactFields, bookingTimeZone, computeOpenSlotPage, formatBookingWhen, isBookingReminderDue, isSlotOpen, REMINDER_WINDOW_END_HOURS, REMINDER_WINDOW_START_HOURS } from "./model/index.js";
25
+ import { BOOKING_MAX_DAYS_AHEAD, bookingChargeUsd, bookingContactAsks, bookingPriceDisplay, bookingContactLines, readBookingContactFields, bookingServiceIsOffered, bookingTimeZone, computeOpenSlotPage, formatBookingWhen, isBookingReminderDue, isSlotOpen, REMINDER_WINDOW_END_HOURS, REMINDER_WINDOW_START_HOURS } from "./model/index.js";
26
26
  import { bookingTimeZoneFor } from "./server/booking-time-zone.js";
27
27
  import { registerBillingWebhookHandler, registerPluginApiRoute } from "@aglyn/aglyn/server";
28
28
  import { pluginTaxProfile } from "@aglyn/aglyn/plugin-manager/plugin-tax-profile";
@@ -31,7 +31,11 @@ import { BOOKINGS_CONFIG_SCHEMA } from "./plugin-config.js";
31
31
  import { bookingsBillingWebhookHandler } from "./server/billing-webhook.js";
32
32
  import { bookingAnalyticsHandler } from "./server/booking-analytics.js";
33
33
  import { registerBookingFigureReader } from "./server/booking-figures.js";
34
+ import { registerBookingServiceAiCapability } from "./server/booking-service-ai-capability.js";
35
+ import { registerBookingServiceDraftWriter } from "./server/booking-service-drafts.js";
34
36
  import { bookingRefundHandler } from "./server/refund.js";
37
+ import { bookingInPersonPaymentHandler } from "./server/in-person-payment.js";
38
+ import { bookingManageDeps, createBookingCheckInHandler, createBookingRescheduleHandler } from "./server/booking-manage.js";
35
39
  // The booking's way back to the record it was booked from (AGL-2660): the
36
40
  // reference a booking link carried, and the meeting a free booking files on
37
41
  // landing.
@@ -138,7 +142,8 @@ export const slotsHandler = async (req, res)=>{
138
142
  if (!serviceId) {
139
143
  const services = await hostRef.collection('services').limit(50).get();
140
144
  return res.status(200).json({
141
- services: services.docs.filter((doc)=>!doc.get('deletedAt')).map((doc)=>{
145
+ // A draft service is offered nowhere until it is activated (AGL-3616).
146
+ services: services.docs.filter((doc)=>bookingServiceIsOffered(doc.data())).map((doc)=>{
142
147
  var _doc_get, _doc_get1, _doc_get2;
143
148
  return {
144
149
  $id: doc.id,
@@ -160,7 +165,8 @@ export const slotsHandler = async (req, res)=>{
160
165
  }
161
166
  const serviceSnapshot = await hostRef.collection('services').doc(serviceId).get();
162
167
  const service = serviceSnapshot.data();
163
- if (!service || serviceSnapshot.get('deletedAt')) {
168
+ // A draft has no slots to show (AGL-3616): it reads as no service at all.
169
+ if (!service || !bookingServiceIsOffered(service)) {
164
170
  return res.status(404).json({
165
171
  error: 'Unknown service'
166
172
  });
@@ -313,7 +319,9 @@ export const slotsHandler = async (req, res)=>{
313
319
  });
314
320
  }
315
321
  const service = serviceSnapshot.data();
316
- if (!service || serviceSnapshot.get('deletedAt')) {
322
+ // Refused for a draft as for a deleted service (AGL-3616): a request can
323
+ // name a service id the directory never listed.
324
+ if (!service || !bookingServiceIsOffered(service)) {
317
325
  return res.status(404).json({
318
326
  error: 'Unknown service'
319
327
  });
@@ -1049,6 +1057,13 @@ export const slotsHandler = async (req, res)=>{
1049
1057
  // Refunding a paid booking (AGL-2315). Console-side, because it is
1050
1058
  // site-admin-gated and moves money — never a site-facing route.
1051
1059
  registerPluginApiRoute('bookings/refund', bookingRefundHandler);
1060
+ // Paying for a booking at the counter, on the native Aglyn POS app's reader
1061
+ // (AGL-3618). Staff-gated like a refund, and it moves money the same way.
1062
+ registerPluginApiRoute('bookings/in-person-payment', bookingInPersonPaymentHandler);
1063
+ // Checking a guest in and moving a booking (AGL-3621), for the site's
1064
+ // team in the native Aglyn app: member routes, one transaction each.
1065
+ registerPluginApiRoute('bookings/check-in', createBookingCheckInHandler(bookingManageDeps()));
1066
+ registerPluginApiRoute('bookings/reschedule', createBookingRescheduleHandler(bookingManageDeps()));
1052
1067
  // The merchant-side `purchase` lookup (AGL-2481). A public, unauthenticated
1053
1068
  // read like `bookings/slots`, authorised by the unguessable Stripe session
1054
1069
  // id and answering with a projection that carries no guest identity.
@@ -1058,6 +1073,11 @@ export const slotsHandler = async (req, res)=>{
1058
1073
  // Bookings by service as a figure table (AGL-2915), for the AI plugin's
1059
1074
  // insights to read by id; the console runs insight jobs.
1060
1075
  registerBookingFigureReader(()=>firebaseAdmin.app().firestore());
1076
+ // A draft service another plugin asks for by name (AGL-3616) — an AI build
1077
+ // setting a site up from a brief. The console runs AI jobs (AGL-3026).
1078
+ registerBookingServiceDraftWriter();
1079
+ // …and the operation an AI build plans for it, which that writer executes.
1080
+ registerBookingServiceAiCapability();
1061
1081
  }
1062
1082
 
1063
1083
  //# sourceMappingURL=server.js.map