@aglyn/plugins-bookings 1.0.0-beta.221 → 1.0.0-beta.222
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 +3 -2
- package/package.json +11 -11
- package/src/lib/components/book-meeting-action.js +5 -3
- package/src/lib/components/book-meeting-action.js.map +1 -1
- package/src/lib/components/booking.d.ts +33 -0
- package/src/lib/components/booking.js +260 -54
- package/src/lib/components/booking.js.map +1 -1
- package/src/lib/components/bookings-console-page.js +139 -15
- package/src/lib/components/bookings-console-page.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +34 -0
- package/src/lib/declarations.console-server.js +84 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/model/booking-contact-fields.d.ts +88 -0
- package/src/lib/model/booking-contact-fields.js +130 -0
- package/src/lib/model/booking-contact-fields.js.map +1 -0
- package/src/lib/model/booking-price.d.ts +51 -0
- package/src/lib/model/booking-price.js +61 -0
- package/src/lib/model/booking-price.js.map +1 -0
- package/src/lib/model/booking-record.d.ts +7 -0
- package/src/lib/model/booking-record.js +11 -2
- package/src/lib/model/booking-record.js.map +1 -1
- package/src/lib/model/bookings-record-counts.d.ts +25 -0
- package/src/lib/model/bookings-record-counts.js +34 -0
- package/src/lib/model/bookings-record-counts.js.map +1 -0
- package/src/lib/model/bookings.d.ts +61 -4
- package/src/lib/model/bookings.js +81 -15
- package/src/lib/model/bookings.js.map +1 -1
- package/src/lib/model/index.d.ts +2 -0
- package/src/lib/model/index.js +2 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.js +14 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +33 -15
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/booking-crm.js +4 -1
- package/src/lib/server/booking-crm.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +36 -0
- package/src/lib/server/person-eraser.js +48 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server/refund.js +2 -2
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server.js +97 -36
- package/src/lib/server.js.map +1 -1
- package/src/lib/transfer/bookings-transfer-common.d.ts +68 -0
- package/src/lib/transfer/bookings-transfer-common.js +130 -0
- package/src/lib/transfer/bookings-transfer-common.js.map +1 -0
- package/src/lib/transfer/bookings-transfer.d.ts +86 -0
- package/src/lib/transfer/bookings-transfer.js +613 -0
- package/src/lib/transfer/bookings-transfer.js.map +1 -0
- package/src/lib/transfer/bookings-transfer.server.d.ts +26 -0
- package/src/lib/transfer/bookings-transfer.server.js +31 -0
- package/src/lib/transfer/bookings-transfer.server.js.map +1 -0
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, bookingTimeZone,
|
|
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";
|
|
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";
|
|
@@ -85,11 +85,12 @@ registerPluginJob({
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
});
|
|
88
|
-
import { firebaseAdmin, getOrgForHost, meterHostEmail, notifyHostManagers,
|
|
88
|
+
import { firebaseAdmin, getOrgForHost, meterHostEmail, notifyHostManagers, getPluginConfig, resolveOrgIdForHost, renderHostEmailWithTokens, hostSendingIdentity } from "@aglyn/tenant-data-admin";
|
|
89
89
|
import { merchantAccountIsReady } from "@aglyn/tenant-data-admin/server/payment-provider";
|
|
90
90
|
import { checkoutSessionCardAuthenticationParams } from "@aglyn/tenant-data-admin/server/stripe-card-authentication";
|
|
91
91
|
import { emitHostEvent } from "@aglyn/tenant-runtime";
|
|
92
92
|
import recordCapturedContact from "@aglyn/aglyn/plugin-manager/record-captured-contact";
|
|
93
|
+
import { CONVERSION_TOUCH_DETAIL, creditConversion, resolveConversionTouch } from "@aglyn/aglyn/plugin-manager/plugin-conversion-credit";
|
|
93
94
|
import { isEmailConfigured, sendEmail } from "@aglyn/shared-util-email";
|
|
94
95
|
import { FieldValue } from "firebase-admin/firestore";
|
|
95
96
|
import { NO_CLIENT_ADDRESS_BUCKET, readClientIp } from "@aglyn/aglyn/app-utils/request-ip";
|
|
@@ -129,7 +130,7 @@ export const slotsHandler = async (req, res)=>{
|
|
|
129
130
|
});
|
|
130
131
|
}
|
|
131
132
|
try {
|
|
132
|
-
var _config_maxDaysAhead,
|
|
133
|
+
var _config_maxDaysAhead, _req_query_from, _service_timezone;
|
|
133
134
|
const firestore = firebaseAdmin.app().firestore();
|
|
134
135
|
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
135
136
|
// No serviceId → public service directory for the booking widget
|
|
@@ -138,13 +139,21 @@ export const slotsHandler = async (req, res)=>{
|
|
|
138
139
|
const services = await hostRef.collection('services').limit(50).get();
|
|
139
140
|
return res.status(200).json({
|
|
140
141
|
services: services.docs.filter((doc)=>!doc.get('deletedAt')).map((doc)=>{
|
|
141
|
-
var _doc_get, _doc_get1, _doc_get2
|
|
142
|
+
var _doc_get, _doc_get1, _doc_get2;
|
|
142
143
|
return {
|
|
143
144
|
$id: doc.id,
|
|
144
145
|
name: (_doc_get = doc.get('name')) != null ? _doc_get : '',
|
|
145
146
|
durationMinutes: Number((_doc_get1 = doc.get('durationMinutes')) != null ? _doc_get1 : 30),
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
// What booking it charges, never a price a label stands in for
|
|
148
|
+
// (AGL-3475): a labeled service shows its label and charges 0.
|
|
149
|
+
priceUsd: bookingChargeUsd(doc.data()),
|
|
150
|
+
priceDisplay: bookingPriceDisplay(doc.get('priceDisplay')),
|
|
151
|
+
description: (_doc_get2 = doc.get('description')) != null ? _doc_get2 : '',
|
|
152
|
+
// What the widget asks for beyond a name and an email (AGL-3493),
|
|
153
|
+
// as the booking route will hold it: the widget renders and
|
|
154
|
+
// checks the fields from these, the route checks them again.
|
|
155
|
+
askPhone: bookingContactAsks(doc.data()).phone,
|
|
156
|
+
askAddress: bookingContactAsks(doc.data()).address
|
|
148
157
|
};
|
|
149
158
|
})
|
|
150
159
|
});
|
|
@@ -156,7 +165,7 @@ export const slotsHandler = async (req, res)=>{
|
|
|
156
165
|
error: 'Unknown service'
|
|
157
166
|
});
|
|
158
167
|
}
|
|
159
|
-
const
|
|
168
|
+
const nowMs = Date.now();
|
|
160
169
|
// Booking horizon (AGL-428): configurable via the plugin settings
|
|
161
170
|
// framework; defaults to BOOKING_MAX_DAYS_AHEAD through the schema.
|
|
162
171
|
//
|
|
@@ -169,9 +178,17 @@ export const slotsHandler = async (req, res)=>{
|
|
|
169
178
|
hostId
|
|
170
179
|
});
|
|
171
180
|
const maxDaysAhead = Number((_config_maxDaysAhead = config.maxDaysAhead) != null ? _config_maxDaysAhead : BOOKING_MAX_DAYS_AHEAD);
|
|
172
|
-
|
|
181
|
+
// Measured from NOW whichever page is asked for: the horizon is how far
|
|
182
|
+
// ahead a visitor may book, not how far past the page they are on.
|
|
183
|
+
const toMs = nowMs + maxDaysAhead * 24 * 60 * 60000;
|
|
184
|
+
// The page asked for (AGL-3492): `from` is the `nextFromMs` the previous
|
|
185
|
+
// page answered with. Clamped to now, so a stale or hand-written value
|
|
186
|
+
// can never list a time that has passed, and a value past the horizon
|
|
187
|
+
// lists nothing rather than reaching beyond it.
|
|
188
|
+
const askedFromMs = Number((_req_query_from = req.query['from']) != null ? _req_query_from : 0);
|
|
189
|
+
const fromMs = Number.isFinite(askedFromMs) ? Math.min(Math.max(askedFromMs, nowMs), toMs) : nowMs;
|
|
173
190
|
// Bounded at BOTH ends, to the window the slots are actually computed
|
|
174
|
-
// over. `
|
|
191
|
+
// over. `computeOpenSlotPage` below is handed `fromMs`/`toMs` and ignores
|
|
175
192
|
// anything outside them, so a booking past the horizon was read, billed
|
|
176
193
|
// and discarded — and worse, it competed for the 500 with the bookings
|
|
177
194
|
// that do matter, so a service booked far ahead could push the near-term
|
|
@@ -179,7 +196,10 @@ export const slotsHandler = async (req, res)=>{
|
|
|
179
196
|
//
|
|
180
197
|
// A second range on `startsAtMs` needs no new index: the field already
|
|
181
198
|
// carries one, and the range it narrows is the one already here.
|
|
182
|
-
const bookedSnapshot = await hostRef.collection('bookings').where('serviceId', '==', serviceId).where('startsAtMs', '>=', fromMs - 24 * 60 * 60000)
|
|
199
|
+
const bookedSnapshot = await hostRef.collection('bookings').where('serviceId', '==', serviceId).where('startsAtMs', '>=', fromMs - 24 * 60 * 60000)// One page never walks further than BOOKING_MAX_DAYS_AHEAD past its
|
|
200
|
+
// start, so a horizon set further out than that is read a page's
|
|
201
|
+
// worth at a time, not all at once.
|
|
202
|
+
.where('startsAtMs', '<=', Math.min(toMs, fromMs + BOOKING_MAX_DAYS_AHEAD * 24 * 60 * 60000)).limit(500).get();
|
|
183
203
|
const booked = bookedSnapshot.docs.filter((doc)=>{
|
|
184
204
|
var _doc_get;
|
|
185
205
|
return doc.get('status') !== 'canceled' && // Expired payment holds release the slot (AGL-170).
|
|
@@ -191,15 +211,25 @@ export const slotsHandler = async (req, res)=>{
|
|
|
191
211
|
endsAtMs: Number((_doc_get1 = doc.get('endsAtMs')) != null ? _doc_get1 : 0)
|
|
192
212
|
};
|
|
193
213
|
});
|
|
194
|
-
|
|
214
|
+
// Bounded in DAYS, not slots (AGL-3492): a page is the widget's day
|
|
215
|
+
// strip, whole, with every open time each of those days has. The flat
|
|
216
|
+
// cap of 120 it replaces ran out three and a half weekdays in at
|
|
217
|
+
// 15-minute steps, so the widget showed a calendar that ended on
|
|
218
|
+
// Thursday at lunch and a horizon that was never reachable.
|
|
219
|
+
const { slots, nextFromMs } = computeOpenSlotPage(service, fromMs, toMs, booked);
|
|
195
220
|
return res.status(200).json({
|
|
196
221
|
service: {
|
|
197
222
|
name: service.name,
|
|
198
223
|
durationMinutes: service.durationMinutes,
|
|
199
|
-
priceUsd: (
|
|
224
|
+
priceUsd: bookingChargeUsd(service),
|
|
225
|
+
priceDisplay: bookingPriceDisplay(service.priceDisplay),
|
|
200
226
|
timezone: (_service_timezone = service.timezone) != null ? _service_timezone : 'UTC'
|
|
201
227
|
},
|
|
202
|
-
slots
|
|
228
|
+
slots,
|
|
229
|
+
// Where the next page starts; `null` once this one reached the horizon.
|
|
230
|
+
nextFromMs,
|
|
231
|
+
// The configured horizon, so the widget's empty state names it.
|
|
232
|
+
horizonDays: maxDaysAhead
|
|
203
233
|
});
|
|
204
234
|
} catch (error) {
|
|
205
235
|
console.error(error);
|
|
@@ -269,8 +299,8 @@ export const slotsHandler = async (req, res)=>{
|
|
|
269
299
|
});
|
|
270
300
|
}
|
|
271
301
|
try {
|
|
272
|
-
var
|
|
273
|
-
var _this,
|
|
302
|
+
var _service_name, _service_name1, _service_name2, _service_name3;
|
|
303
|
+
var _req_body7, _req_body8, _this, _req_body9;
|
|
274
304
|
const firestore = firebaseAdmin.app().firestore();
|
|
275
305
|
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
276
306
|
const [hostSnapshot, serviceSnapshot] = await Promise.all([
|
|
@@ -288,6 +318,20 @@ export const slotsHandler = async (req, res)=>{
|
|
|
288
318
|
error: 'Unknown service'
|
|
289
319
|
});
|
|
290
320
|
}
|
|
321
|
+
// The phone and the address, held to what THIS service asks for
|
|
322
|
+
// (AGL-3493) and checked here whatever the widget already checked: this
|
|
323
|
+
// is a public door, and a request can be written by hand. A field the
|
|
324
|
+
// service does not ask for is dropped rather than stored.
|
|
325
|
+
const contact = readBookingContactFields(bookingContactAsks(service), {
|
|
326
|
+
phone: (_req_body7 = req.body) == null ? void 0 : _req_body7.phone,
|
|
327
|
+
address: (_req_body8 = req.body) == null ? void 0 : _req_body8.address
|
|
328
|
+
});
|
|
329
|
+
if ('error' in contact) return res.status(400).json({
|
|
330
|
+
error: contact.error
|
|
331
|
+
});
|
|
332
|
+
const { phone, address } = contact.fields;
|
|
333
|
+
// `Phone: …`, `Address: …`, for the managers' notification below.
|
|
334
|
+
const contactLines = bookingContactLines(contact.fields);
|
|
291
335
|
// Plan gate (dark-launch rule preserved). Plan/quota gates ride the
|
|
292
336
|
// owning org's doc (AGL-238). Hoisted out of its block for AGL-2315:
|
|
293
337
|
// the same org doc now also prices the platform's cut below, and reading
|
|
@@ -315,7 +359,9 @@ export const slotsHandler = async (req, res)=>{
|
|
|
315
359
|
// booking lands as `pendingPayment` with a 15-minute expiry (expired
|
|
316
360
|
// holds release the slot in the collision filters), and the visitor
|
|
317
361
|
// goes to Stripe Checkout; the webhook confirms + emails on payment.
|
|
318
|
-
|
|
362
|
+
// A service that states its price as a label books with no charge
|
|
363
|
+
// (AGL-3475), whatever price it stores.
|
|
364
|
+
const priceUsd = bookingChargeUsd(service);
|
|
319
365
|
const paid = priceUsd > 0;
|
|
320
366
|
if (paid && !process.env.STRIPE_SECRET_KEY) {
|
|
321
367
|
return res.status(501).json({
|
|
@@ -476,7 +522,12 @@ export const slotsHandler = async (req, res)=>{
|
|
|
476
522
|
serviceId,
|
|
477
523
|
serviceName: (_service_name = service.name) != null ? _service_name : '',
|
|
478
524
|
name,
|
|
479
|
-
email
|
|
525
|
+
email
|
|
526
|
+
}, phone ? {
|
|
527
|
+
phone
|
|
528
|
+
} : {}, address ? {
|
|
529
|
+
address
|
|
530
|
+
} : {}, {
|
|
480
531
|
startsAtMs,
|
|
481
532
|
endsAtMs,
|
|
482
533
|
timezone,
|
|
@@ -491,30 +542,29 @@ export const slotsHandler = async (req, res)=>{
|
|
|
491
542
|
return bookingRef.id;
|
|
492
543
|
});
|
|
493
544
|
/*
|
|
494
|
-
* THE
|
|
545
|
+
* WHERE THE VISITOR ARRIVED FROM, RESOLVED ONCE FOR THE WHOLE BOOKING.
|
|
495
546
|
*
|
|
496
547
|
* A booking is an identify moment: the visitor was anonymous while they
|
|
497
548
|
* browsed and picked a slot, and this request is the first thing that
|
|
498
|
-
* names them.
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
* revenue join credits the later click.
|
|
549
|
+
* names them. The plugin that credits outcomes
|
|
550
|
+
* (`plugin-conversion-credit`) is asked with what their device carried
|
|
551
|
+
* and the address they just typed, and decides what earns the credit.
|
|
502
552
|
*
|
|
503
553
|
* Resolved once and handed to the booking record, the contact and the
|
|
504
|
-
* lead below, so the
|
|
505
|
-
*
|
|
554
|
+
* lead below, so the lookup is paid once for the whole request and the
|
|
555
|
+
* three records cannot be credited to different arrivals.
|
|
506
556
|
*/ const bookedAtMs = Date.now();
|
|
507
|
-
const
|
|
557
|
+
const arrival = await resolveConversionTouch({
|
|
508
558
|
hostId,
|
|
509
|
-
wire: (
|
|
559
|
+
wire: (_req_body9 = req.body) == null ? void 0 : _req_body9.campaignTouch,
|
|
510
560
|
email,
|
|
511
561
|
atMs: bookedAtMs
|
|
512
562
|
});
|
|
513
|
-
void
|
|
563
|
+
void creditConversion({
|
|
514
564
|
hostId,
|
|
515
565
|
kind: 'booking',
|
|
516
566
|
refId: bookingId,
|
|
517
|
-
touch:
|
|
567
|
+
touch: arrival,
|
|
518
568
|
convertedAtMs: bookedAtMs
|
|
519
569
|
});
|
|
520
570
|
/*
|
|
@@ -550,11 +600,15 @@ export const slotsHandler = async (req, res)=>{
|
|
|
550
600
|
summary: `Booked "${String((_service_name = service.name) != null ? _service_name : 'a service').slice(0, 60)}"`
|
|
551
601
|
},
|
|
552
602
|
surface: 'lead'
|
|
553
|
-
},
|
|
603
|
+
}, phone ? {
|
|
604
|
+
profileFill: {
|
|
605
|
+
phone
|
|
606
|
+
}
|
|
607
|
+
} : {}, marketingConsent ? {
|
|
554
608
|
marketingConsent: true
|
|
555
|
-
} : {},
|
|
609
|
+
} : {}, arrival ? {
|
|
556
610
|
detail: {
|
|
557
|
-
|
|
611
|
+
[CONVERSION_TOUCH_DETAIL]: arrival
|
|
558
612
|
}
|
|
559
613
|
} : {}));
|
|
560
614
|
if (paid) {
|
|
@@ -672,8 +726,8 @@ export const slotsHandler = async (req, res)=>{
|
|
|
672
726
|
error: 'Payment setup failed'
|
|
673
727
|
});
|
|
674
728
|
}
|
|
675
|
-
// The lead landed above; the confirmation email
|
|
676
|
-
// from the payment webhook.
|
|
729
|
+
// The lead landed above; the confirmation email and the managers'
|
|
730
|
+
// notification go out from the payment webhook.
|
|
677
731
|
return res.status(200).json({
|
|
678
732
|
bookingId,
|
|
679
733
|
startsAtMs,
|
|
@@ -683,11 +737,13 @@ export const slotsHandler = async (req, res)=>{
|
|
|
683
737
|
}
|
|
684
738
|
// Event trigger (AGL-128/148/159).
|
|
685
739
|
// In-app notification to the site's managers (AGL-259): who booked what,
|
|
686
|
-
// and when, in the booking's own zone and naming it (AGL-3432)
|
|
740
|
+
// and when, in the booking's own zone and naming it (AGL-3432), and the
|
|
741
|
+
// phone and address the service asked for (AGL-3493) — the two things
|
|
742
|
+
// an on-site service needs before it can do anything with the booking.
|
|
687
743
|
void notifyHostManagers(hostId, {
|
|
688
744
|
type: 'content.booking',
|
|
689
745
|
title: 'New booking on {site}',
|
|
690
|
-
body: `${name} (${email}) booked ${String((_service_name1 = service.name) != null ? _service_name1 : 'a service')} ` + `on {site} for ${when} (${timezone})
|
|
746
|
+
body: `${name} (${email}) booked ${String((_service_name1 = service.name) != null ? _service_name1 : 'a service')} ` + `on {site} for ${when} (${timezone}).` + (contactLines.length ? ` ${contactLines.join('. ')}.` : ''),
|
|
691
747
|
link: `/${hostId}/bookings`
|
|
692
748
|
});
|
|
693
749
|
const { alerts } = await emitHostEvent(hostId, 'booking', {
|
|
@@ -753,7 +809,12 @@ export const slotsHandler = async (req, res)=>{
|
|
|
753
809
|
booking: _extends({
|
|
754
810
|
serviceId,
|
|
755
811
|
serviceName: (_service_name3 = service.name) != null ? _service_name3 : '',
|
|
756
|
-
email
|
|
812
|
+
email
|
|
813
|
+
}, phone ? {
|
|
814
|
+
phone
|
|
815
|
+
} : {}, address ? {
|
|
816
|
+
address
|
|
817
|
+
} : {}, {
|
|
757
818
|
startsAtMs,
|
|
758
819
|
endsAtMs
|
|
759
820
|
}, crmRef ? {
|