@aglyn/tenant-data-admin 1.0.0-beta.218 → 1.0.0-beta.220
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/package.json +8 -8
- package/src/lib/server/api-http.js +5 -1
- package/src/lib/server/api-http.js.map +1 -1
- package/src/lib/server/api-v1-description.d.ts +112 -0
- package/src/lib/server/api-v1-description.js +126 -0
- package/src/lib/server/api-v1-description.js.map +1 -0
- package/src/lib/server/api-v1-kit.d.ts +163 -0
- package/src/lib/server/api-v1-kit.js +191 -0
- package/src/lib/server/api-v1-kit.js.map +1 -0
- package/src/lib/server/api-v1-resources.d.ts +143 -0
- package/src/lib/server/api-v1-resources.js +125 -0
- package/src/lib/server/api-v1-resources.js.map +1 -0
- package/src/lib/server/campaign-conversion-attribution.d.ts +1 -1
- package/src/lib/server/campaign-conversion-attribution.js +3 -3
- package/src/lib/server/campaign-conversion-attribution.js.map +1 -1
- package/src/lib/server/card-payment-velocity.d.ts +9 -0
- package/src/lib/server/card-payment-velocity.js +28 -2
- package/src/lib/server/card-payment-velocity.js.map +1 -1
- package/src/lib/server/consent-group-carry.js +1 -1
- package/src/lib/server/consent-group-carry.js.map +1 -1
- package/src/lib/server/consent-group-change.d.ts +2 -1
- package/src/lib/server/consent-group-change.js +43 -18
- package/src/lib/server/consent-group-change.js.map +1 -1
- package/src/lib/server/contact-email-index.d.ts +22 -0
- package/src/lib/server/contact-email-index.js +108 -0
- package/src/lib/server/contact-email-index.js.map +1 -1
- package/src/lib/server/crm-email-activity.d.ts +1 -1
- package/src/lib/server/crm-email-activity.js +1 -1
- package/src/lib/server/crm-email-activity.js.map +1 -1
- package/src/lib/server/crm-records.d.ts +9 -1
- package/src/lib/server/crm-records.js +19 -2
- package/src/lib/server/crm-records.js.map +1 -1
- package/src/lib/server/cron-auth.d.ts +44 -0
- package/src/lib/server/cron-auth.js +76 -0
- package/src/lib/server/cron-auth.js.map +1 -0
- package/src/lib/server/dataset-live-pages.js +6 -6
- package/src/lib/server/dataset-live-pages.js.map +1 -1
- package/src/lib/server/duplicate-resource.d.ts +26 -0
- package/src/lib/server/duplicate-resource.js +47 -25
- package/src/lib/server/duplicate-resource.js.map +1 -1
- package/src/lib/server/dynamic-list-materialize.js +6 -3
- package/src/lib/server/dynamic-list-materialize.js.map +1 -1
- package/src/lib/server/email-flow-gate.js +2 -2
- package/src/lib/server/email-flow-gate.js.map +1 -1
- package/src/lib/server/email-suppression.d.ts +11 -0
- package/src/lib/server/email-suppression.js +37 -2
- package/src/lib/server/email-suppression.js.map +1 -1
- package/src/lib/server/email-unsubscribe-link.d.ts +8 -5
- package/src/lib/server/email-unsubscribe-link.js +7 -7
- package/src/lib/server/email-unsubscribe-link.js.map +1 -1
- package/src/lib/server/erase.d.ts +19 -33
- package/src/lib/server/erase.js +37 -162
- package/src/lib/server/erase.js.map +1 -1
- package/src/lib/server/host-email-tokens.js +1 -1
- package/src/lib/server/host-email-tokens.js.map +1 -1
- package/src/lib/server/host-memberships.js +3 -3
- package/src/lib/server/host-memberships.js.map +1 -1
- package/src/lib/server/host-visitor-records.d.ts +32 -4
- package/src/lib/server/host-visitor-records.js +73 -14
- package/src/lib/server/host-visitor-records.js.map +1 -1
- package/src/lib/server/hosted-page-review.d.ts +27 -0
- package/src/lib/server/hosted-page-review.js +149 -12
- package/src/lib/server/hosted-page-review.js.map +1 -1
- package/src/lib/server/media-cdn-rate-limit.d.ts +6 -3
- package/src/lib/server/media-cdn-rate-limit.js +6 -3
- package/src/lib/server/media-cdn-rate-limit.js.map +1 -1
- package/src/lib/server/member-email-aliases.d.ts +10 -2
- package/src/lib/server/member-email-aliases.js +26 -2
- package/src/lib/server/member-email-aliases.js.map +1 -1
- package/src/lib/server/new-account-announcement.d.ts +62 -0
- package/src/lib/server/new-account-announcement.js +77 -0
- package/src/lib/server/new-account-announcement.js.map +1 -0
- package/src/lib/server/notifications.d.ts +22 -2
- package/src/lib/server/notifications.js +40 -3
- package/src/lib/server/notifications.js.map +1 -1
- package/src/lib/server/operator-alerts.d.ts +7 -1
- package/src/lib/server/operator-alerts.js +5 -3
- package/src/lib/server/operator-alerts.js.map +1 -1
- package/src/lib/server/org-containers.d.ts +66 -0
- package/src/lib/server/org-containers.js +95 -0
- package/src/lib/server/org-containers.js.map +1 -0
- package/src/lib/server/org-member-notice.js +3 -2
- package/src/lib/server/org-member-notice.js.map +1 -1
- package/src/lib/server/payment-fraud-signal.js +5 -1
- package/src/lib/server/payment-fraud-signal.js.map +1 -1
- package/src/lib/server/{connect-payout-failure.d.ts → payment-provider-stripe-connect-payouts.d.ts} +3 -0
- package/src/lib/server/{connect-payout-failure.js → payment-provider-stripe-connect-payouts.js} +36 -2
- package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -0
- package/src/lib/server/{connect-account-status.d.ts → payment-provider-stripe-connect-status.d.ts} +3 -0
- package/src/lib/server/{connect-account-status.js → payment-provider-stripe-connect-status.js} +1 -1
- package/src/lib/server/payment-provider-stripe-connect-status.js.map +1 -0
- package/src/lib/server/{stripe-account-mode.d.ts → payment-provider-stripe-connect.d.ts} +45 -70
- package/src/lib/server/{stripe-account-mode.js → payment-provider-stripe-connect.js} +78 -52
- package/src/lib/server/payment-provider-stripe-connect.js.map +1 -0
- package/src/lib/server/payment-provider.d.ts +173 -0
- package/src/lib/server/payment-provider.js +70 -0
- package/src/lib/server/payment-provider.js.map +1 -0
- package/src/lib/server/payment-risk-record.d.ts +1 -1
- package/src/lib/server/payment-risk-record.js +3 -1
- package/src/lib/server/payment-risk-record.js.map +1 -1
- package/src/lib/server/personal-data-export.js +1 -1
- package/src/lib/server/personal-data-export.js.map +1 -1
- package/src/lib/server/platform-marketing-consent.d.ts +1 -1
- package/src/lib/server/platform-marketing-consent.js +1 -1
- package/src/lib/server/platform-marketing-consent.js.map +1 -1
- package/src/lib/server/rate-limit-store.js +13 -3
- package/src/lib/server/rate-limit-store.js.map +1 -1
- package/src/lib/server/realm-plugins.js +8 -3
- package/src/lib/server/realm-plugins.js.map +1 -1
- package/src/lib/server/retained-refusals.d.ts +142 -0
- package/src/lib/server/retained-refusals.js +325 -0
- package/src/lib/server/retained-refusals.js.map +1 -0
- package/src/lib/server/risk-notice.d.ts +32 -1
- package/src/lib/server/risk-notice.js +90 -28
- package/src/lib/server/risk-notice.js.map +1 -1
- package/src/lib/server/sending-domains.d.ts +17 -0
- package/src/lib/server/sending-domains.js +28 -4
- package/src/lib/server/sending-domains.js.map +1 -1
- package/src/lib/server/sso-enforcement.js +7 -2
- package/src/lib/server/sso-enforcement.js.map +1 -1
- package/src/lib/server/staff-alert-email.js +4 -2
- package/src/lib/server/staff-alert-email.js.map +1 -1
- package/src/lib/server/test-firestore-queries.d.ts +7 -0
- package/src/lib/server/test-firestore-queries.js +70 -27
- package/src/lib/server/test-firestore-queries.js.map +1 -1
- package/src/lib/server/{email-topic-confirmation.js → topic-subscriptions.js} +15 -11
- package/src/lib/server/topic-subscriptions.js.map +1 -0
- package/src/lib/server/upsert-contact.d.ts +46 -1
- package/src/lib/server/upsert-contact.js +87 -20
- package/src/lib/server/upsert-contact.js.map +1 -1
- package/src/tenancy.d.ts +5 -8
- package/src/tenancy.js +12 -13
- package/src/tenancy.js.map +1 -1
- package/src/lib/server/connect-account-status.js.map +0 -1
- package/src/lib/server/connect-payout-failure.js.map +0 -1
- package/src/lib/server/contact-email-engagement.d.ts +0 -32
- package/src/lib/server/contact-email-engagement.js +0 -145
- package/src/lib/server/contact-email-engagement.js.map +0 -1
- package/src/lib/server/contact-lifecycle-floor.d.ts +0 -89
- package/src/lib/server/contact-lifecycle-floor.js +0 -91
- package/src/lib/server/contact-lifecycle-floor.js.map +0 -1
- package/src/lib/server/contact-merge.d.ts +0 -55
- package/src/lib/server/contact-merge.js +0 -288
- package/src/lib/server/contact-merge.js.map +0 -1
- package/src/lib/server/crm-booking-activity.d.ts +0 -86
- package/src/lib/server/crm-booking-activity.js +0 -257
- package/src/lib/server/crm-booking-activity.js.map +0 -1
- package/src/lib/server/crm-inbound-email.d.ts +0 -145
- package/src/lib/server/crm-inbound-email.js +0 -297
- package/src/lib/server/crm-inbound-email.js.map +0 -1
- package/src/lib/server/email-topic-confirmation.js.map +0 -1
- package/src/lib/server/form-dataset-binding-token.d.ts +0 -35
- package/src/lib/server/form-dataset-binding-token.js +0 -107
- package/src/lib/server/form-dataset-binding-token.js.map +0 -1
- package/src/lib/server/stripe-account-mode.js.map +0 -1
- /package/src/lib/server/{email-topic-confirmation.d.ts → topic-subscriptions.d.ts} +0 -0
|
@@ -1,257 +0,0 @@
|
|
|
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 { checkEntitlement, consentGroupForHost, CRM_COLLECTIONS, crmTaskListFields, crmActivityLogHasRoom, crmBookingFollowUpDueMs, crmBookingFollowUpTitle, crmBookingMeetingBody, crmScopeTokens, isHostPluginEnabled, parseCrmBookingRef, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
|
|
18
|
-
import { FieldValue } from "firebase-admin/firestore";
|
|
19
|
-
import { findContactByEmail } from "./contact-email-index.js";
|
|
20
|
-
import { readLeadForHost } from "./host-visitor-records.js";
|
|
21
|
-
import { countCrmActivitiesForRecord } from "./crm-records.js";
|
|
22
|
-
/**
|
|
23
|
-
* WHAT A BOOKING FILES ON THE CRM RECORD (AGL-2660).
|
|
24
|
-
*
|
|
25
|
-
* A confirmed booking is a meeting the record has on its calendar, and the
|
|
26
|
-
* timeline should say so without a rep logging it by hand. So once a
|
|
27
|
-
* booking's row is written — at creation for a free service, on payment
|
|
28
|
-
* for a paid one — the record is found and a `meeting` activity filed under
|
|
29
|
-
* it, with a follow-up task beside it when the service asks for one.
|
|
30
|
-
*
|
|
31
|
-
* ## Which record
|
|
32
|
-
*
|
|
33
|
-
* The reference the booking link carried wins: `crmRef` names the contact,
|
|
34
|
-
* lead or deal the rep dropped the link from, and a booking made through
|
|
35
|
-
* that link belongs to that record even when the booker typed a different
|
|
36
|
-
* address. Without one — a booking taken off the widget cold — the booker's
|
|
37
|
-
* address is looked up through the org's address index, narrowed to this
|
|
38
|
-
* site, exactly as the automation steps resolve an event's contact. A
|
|
39
|
-
* reference to a record the site cannot see, or that no longer exists,
|
|
40
|
-
* falls through to the address rather than filing under nobody.
|
|
41
|
-
*
|
|
42
|
-
* ## Where it lives
|
|
43
|
-
*
|
|
44
|
-
* In the admin library rather than the Bookings plugin, because the row is
|
|
45
|
-
* a CRM record: its scope, its ceiling and its collections are the CRM's,
|
|
46
|
-
* and a plugin that stamped its own would be a second creator to keep in
|
|
47
|
-
* step with `crmScopeTokens`. The Bookings plugin passes what it already
|
|
48
|
-
* holds and reads the outcome; the CRM plugin is never imported.
|
|
49
|
-
*
|
|
50
|
-
* Firestore is a parameter, as in `crm-records.ts`: the callers hold one,
|
|
51
|
-
* and a module that reached for the Admin app itself would drag it into
|
|
52
|
-
* every spec that only wants the arithmetic.
|
|
53
|
-
*
|
|
54
|
-
* ## Failure posture
|
|
55
|
-
*
|
|
56
|
-
* **Never throws.** A booking that could not be filed on the CRM is still
|
|
57
|
-
* a booking — the guest has the slot and the confirmation — so a failure
|
|
58
|
-
* here is logged and reported in the outcome, never raised into the
|
|
59
|
-
* request that took the booking or the webhook that confirmed it.
|
|
60
|
-
*/ /** The CRM plugin's id, as `enabledPlugins` and `disabledPlugins` name it. */ const CRM_PLUGIN_ID = 'crm';
|
|
61
|
-
/**
|
|
62
|
-
* The record a booking belongs to — the reference first, the address second.
|
|
63
|
-
*/ async function resolveBookingRecord(firestore, input, groupId) {
|
|
64
|
-
var _hit_data;
|
|
65
|
-
const { hostId, orgId } = input;
|
|
66
|
-
const orgRef = firestore.collection('orgs').doc(orgId);
|
|
67
|
-
const contactsRef = orgRef.collection('contacts');
|
|
68
|
-
const ref = parseCrmBookingRef(input.booking.crmRef);
|
|
69
|
-
if ((ref == null ? void 0 : ref.kind) === 'contact') {
|
|
70
|
-
const contact = await contactsRef.doc(ref.id).get();
|
|
71
|
-
if (contact.exists && visibleToHost(contact.get('visibleTo'), hostId)) {
|
|
72
|
-
var _contact_data;
|
|
73
|
-
const facet = readContactFacet((_contact_data = contact.data()) != null ? _contact_data : {}, groupId);
|
|
74
|
-
return _extends({
|
|
75
|
-
link: _extends({
|
|
76
|
-
contactId: contact.id
|
|
77
|
-
}, facet.companyId ? {
|
|
78
|
-
companyId: facet.companyId
|
|
79
|
-
} : {}),
|
|
80
|
-
matchedBy: 'crmRef'
|
|
81
|
-
}, facet.ownerUid ? {
|
|
82
|
-
ownerUid: facet.ownerUid
|
|
83
|
-
} : {});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if ((ref == null ? void 0 : ref.kind) === 'deal') {
|
|
87
|
-
const deal = await orgRef.collection(CRM_COLLECTIONS.deals).doc(ref.id).get();
|
|
88
|
-
if (deal.exists && visibleToHost(deal.get('visibleTo'), hostId)) {
|
|
89
|
-
var _deal_get, _deal_get1, _deal_get2;
|
|
90
|
-
const contactId = String((_deal_get = deal.get('contactId')) != null ? _deal_get : '');
|
|
91
|
-
const companyId = String((_deal_get1 = deal.get('companyId')) != null ? _deal_get1 : '');
|
|
92
|
-
const ownerUid = String((_deal_get2 = deal.get('ownerUid')) != null ? _deal_get2 : '');
|
|
93
|
-
return _extends({
|
|
94
|
-
link: _extends({
|
|
95
|
-
dealId: deal.id
|
|
96
|
-
}, contactId ? {
|
|
97
|
-
contactId
|
|
98
|
-
} : {}, companyId ? {
|
|
99
|
-
companyId
|
|
100
|
-
} : {}),
|
|
101
|
-
matchedBy: 'crmRef'
|
|
102
|
-
}, ownerUid ? {
|
|
103
|
-
ownerUid
|
|
104
|
-
} : {});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if ((ref == null ? void 0 : ref.kind) === 'lead') {
|
|
108
|
-
/*
|
|
109
|
-
* A lead carries its own `visibleTo` now (AGL-3275) — it used to be
|
|
110
|
-
* host-scoped by path, and the comment here said so. The seam reads the
|
|
111
|
-
* org row, and the site has to be allowed to see it before a booking is
|
|
112
|
-
* filed against it, or one brand's booking would name another's lead.
|
|
113
|
-
*/ const lead = await readLeadForHost(hostId, ref.id);
|
|
114
|
-
if (lead && visibleToHost(lead.get('visibleTo'), hostId)) {
|
|
115
|
-
var _lead_get;
|
|
116
|
-
const ownerUid = String((_lead_get = lead.get('ownerUid')) != null ? _lead_get : '');
|
|
117
|
-
return _extends({
|
|
118
|
-
link: {
|
|
119
|
-
leadId: lead.id
|
|
120
|
-
},
|
|
121
|
-
matchedBy: 'crmRef'
|
|
122
|
-
}, ownerUid ? {
|
|
123
|
-
ownerUid
|
|
124
|
-
} : {});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const hit = await findContactByEmail(contactsRef, input.booking.email, {
|
|
128
|
-
hostId
|
|
129
|
-
});
|
|
130
|
-
if (!hit) return null;
|
|
131
|
-
const facet = readContactFacet((_hit_data = hit.data()) != null ? _hit_data : {}, groupId);
|
|
132
|
-
return _extends({
|
|
133
|
-
link: _extends({
|
|
134
|
-
contactId: hit.id
|
|
135
|
-
}, facet.companyId ? {
|
|
136
|
-
companyId: facet.companyId
|
|
137
|
-
} : {}),
|
|
138
|
-
matchedBy: 'email'
|
|
139
|
-
}, facet.ownerUid ? {
|
|
140
|
-
ownerUid: facet.ownerUid
|
|
141
|
-
} : {});
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Files what a confirmed booking owes the CRM: the meeting, and the
|
|
145
|
-
* follow-up when the service asks for one. See the module comment.
|
|
146
|
-
*/ export async function recordCrmBooking(firestore, input) {
|
|
147
|
-
const { hostId, orgId, booking, service } = input;
|
|
148
|
-
try {
|
|
149
|
-
var _data, _input_org, _input_org1, _input_org2, _ref, _ref1;
|
|
150
|
-
if (!hostId || !orgId || !(booking == null ? void 0 : booking.id)) return {
|
|
151
|
-
filed: false,
|
|
152
|
-
reason: 'failed'
|
|
153
|
-
};
|
|
154
|
-
const wantsMeeting = (service == null ? void 0 : service.crmMeetingActivity) !== false;
|
|
155
|
-
const wantsTask = (service == null ? void 0 : service.crmFollowUpTask) === true;
|
|
156
|
-
if (!wantsMeeting && !wantsTask) return {
|
|
157
|
-
filed: false,
|
|
158
|
-
reason: 'nothing-to-file'
|
|
159
|
-
};
|
|
160
|
-
const host = input.host === undefined ? (_data = (await firestore.collection('hosts').doc(hostId).get()).data()) != null ? _data : null : input.host;
|
|
161
|
-
if (!isHostPluginEnabled((_input_org = input.org) != null ? _input_org : null, host, CRM_PLUGIN_ID)) {
|
|
162
|
-
return {
|
|
163
|
-
filed: false,
|
|
164
|
-
reason: 'crm-off'
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
if (!checkEntitlement(input.org, 'crm')) {
|
|
168
|
-
return {
|
|
169
|
-
filed: false,
|
|
170
|
-
reason: 'not-entitled'
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
const orgRef = firestore.collection('orgs').doc(orgId);
|
|
174
|
-
const activities = orgRef.collection(CRM_COLLECTIONS.activities);
|
|
175
|
-
// One meeting per booking, whatever path re-enters: the paid webhook is
|
|
176
|
-
// redelivered for days after any 500, and its own guard is the status,
|
|
177
|
-
// which this row is not part of.
|
|
178
|
-
const already = await activities.where('bookingId', '==', booking.id).limit(1).get();
|
|
179
|
-
if (!already.empty) return {
|
|
180
|
-
filed: false,
|
|
181
|
-
reason: 'already-filed'
|
|
182
|
-
};
|
|
183
|
-
const group = consentGroupForHost((_input_org1 = input.org) != null ? _input_org1 : null, hostId);
|
|
184
|
-
const record = await resolveBookingRecord(firestore, input, group.groupId);
|
|
185
|
-
if (!record) return {
|
|
186
|
-
filed: false,
|
|
187
|
-
reason: 'no-record'
|
|
188
|
-
};
|
|
189
|
-
const visibleTo = crmScopeTokens((_input_org2 = input.org) != null ? _input_org2 : null, group);
|
|
190
|
-
const serviceName = String((_ref = (_ref1 = service == null ? void 0 : service.name) != null ? _ref1 : booking.serviceName) != null ? _ref : '').trim();
|
|
191
|
-
const timezone = (service == null ? void 0 : service.timezone) || undefined;
|
|
192
|
-
let activityId = null;
|
|
193
|
-
if (wantsMeeting) {
|
|
194
|
-
// The per-record ceiling (AGL-2611): a full log refuses the meeting
|
|
195
|
-
// but not the follow-up — a task is owed either way.
|
|
196
|
-
const logged = await countCrmActivitiesForRecord(orgRef, record.link);
|
|
197
|
-
if (crmActivityLogHasRoom(logged)) {
|
|
198
|
-
const activity = _extends({
|
|
199
|
-
kind: 'meeting',
|
|
200
|
-
body: crmBookingMeetingBody({
|
|
201
|
-
serviceName,
|
|
202
|
-
startsAtMs: booking.startsAtMs,
|
|
203
|
-
timezone
|
|
204
|
-
}),
|
|
205
|
-
// When it HAPPENS, which is the slot — not when it was booked.
|
|
206
|
-
atMs: booking.startsAtMs,
|
|
207
|
-
byUid: ''
|
|
208
|
-
}, record.link, {
|
|
209
|
-
bookingId: booking.id,
|
|
210
|
-
hostId,
|
|
211
|
-
visibleTo,
|
|
212
|
-
createdAt: FieldValue.serverTimestamp(),
|
|
213
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
214
|
-
});
|
|
215
|
-
const written = await activities.add(activity);
|
|
216
|
-
activityId = written.id;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
let taskId = null;
|
|
220
|
-
if (wantsTask) {
|
|
221
|
-
const task = _extends({
|
|
222
|
-
title: crmBookingFollowUpTitle(serviceName),
|
|
223
|
-
kind: 'todo',
|
|
224
|
-
priority: 'normal',
|
|
225
|
-
status: 'open',
|
|
226
|
-
dueAtMs: crmBookingFollowUpDueMs(booking.endsAtMs, timezone)
|
|
227
|
-
}, record.ownerUid ? {
|
|
228
|
-
assigneeUid: record.ownerUid
|
|
229
|
-
} : {}, {
|
|
230
|
-
createdByUid: ''
|
|
231
|
-
}, record.link, {
|
|
232
|
-
hostId,
|
|
233
|
-
visibleTo,
|
|
234
|
-
createdAt: FieldValue.serverTimestamp(),
|
|
235
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
236
|
-
});
|
|
237
|
-
// What the Tasks list searches by (AGL-3321), beside the task.
|
|
238
|
-
const written = await orgRef.collection(CRM_COLLECTIONS.tasks).add(_extends({}, task, crmTaskListFields(task)));
|
|
239
|
-
taskId = written.id;
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
filed: true,
|
|
243
|
-
matchedBy: record.matchedBy,
|
|
244
|
-
link: record.link,
|
|
245
|
-
activityId,
|
|
246
|
-
taskId
|
|
247
|
-
};
|
|
248
|
-
} catch (error) {
|
|
249
|
-
console.error('[crm] booking could not be filed', hostId, booking == null ? void 0 : booking.id, error);
|
|
250
|
-
return {
|
|
251
|
-
filed: false,
|
|
252
|
-
reason: 'failed'
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
//# sourceMappingURL=crm-booking-activity.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/crm-booking-activity.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkEntitlement,\n consentGroupForHost,\n CRM_COLLECTIONS,\n crmTaskListFields,\n type CrmActivity,\n type CrmActivityLink,\n type CrmTask,\n crmActivityLogHasRoom,\n crmBookingFollowUpDueMs,\n crmBookingFollowUpTitle,\n crmBookingMeetingBody,\n crmScopeTokens,\n isHostPluginEnabled,\n parseCrmBookingRef,\n readContactFacet,\n visibleToHost,\n} from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { findContactByEmail } from './contact-email-index'\nimport { readLeadForHost } from './host-visitor-records'\nimport { countCrmActivitiesForRecord } from './crm-records'\n\n/**\n * WHAT A BOOKING FILES ON THE CRM RECORD (AGL-2660).\n *\n * A confirmed booking is a meeting the record has on its calendar, and the\n * timeline should say so without a rep logging it by hand. So once a\n * booking's row is written — at creation for a free service, on payment\n * for a paid one — the record is found and a `meeting` activity filed under\n * it, with a follow-up task beside it when the service asks for one.\n *\n * ## Which record\n *\n * The reference the booking link carried wins: `crmRef` names the contact,\n * lead or deal the rep dropped the link from, and a booking made through\n * that link belongs to that record even when the booker typed a different\n * address. Without one — a booking taken off the widget cold — the booker's\n * address is looked up through the org's address index, narrowed to this\n * site, exactly as the automation steps resolve an event's contact. A\n * reference to a record the site cannot see, or that no longer exists,\n * falls through to the address rather than filing under nobody.\n *\n * ## Where it lives\n *\n * In the admin library rather than the Bookings plugin, because the row is\n * a CRM record: its scope, its ceiling and its collections are the CRM's,\n * and a plugin that stamped its own would be a second creator to keep in\n * step with `crmScopeTokens`. The Bookings plugin passes what it already\n * holds and reads the outcome; the CRM plugin is never imported.\n *\n * Firestore is a parameter, as in `crm-records.ts`: the callers hold one,\n * and a module that reached for the Admin app itself would drag it into\n * every spec that only wants the arithmetic.\n *\n * ## Failure posture\n *\n * **Never throws.** A booking that could not be filed on the CRM is still\n * a booking — the guest has the slot and the confirmation — so a failure\n * here is logged and reported in the outcome, never raised into the\n * request that took the booking or the webhook that confirmed it.\n */\n\n/** The CRM plugin's id, as `enabledPlugins` and `disabledPlugins` name it. */\nconst CRM_PLUGIN_ID = 'crm'\n\nexport interface CrmBookingRecordInput {\n hostId: string\n /**\n * The owning org, as `getOrgForHost` returns it — the plugin set, the\n * consent groups and the scope default are all read off it.\n */\n org: Record<string, unknown> | null | undefined\n orgId: string\n /** The booking as its row was written. */\n booking: {\n id: string\n serviceId?: string\n serviceName?: string\n email?: string\n startsAtMs: number\n endsAtMs: number\n /** The `crm` query value the link carried, kept on the row. */\n crmRef?: string | null\n }\n /** The service as stored: its two CRM switches and its timezone. */\n service:\n | {\n name?: string\n timezone?: string\n crmFollowUpTask?: boolean\n crmMeetingActivity?: boolean\n }\n | null\n | undefined\n /**\n * The host document's plugin fields, when the caller already holds them;\n * read here otherwise. The deny-list is per site, and a site that has\n * switched the CRM off must file nothing.\n */\n host?: { disabledPlugins?: string[]; enabledPlugins?: string[] } | null\n}\n\nexport type CrmBookingRecordOutcome =\n | {\n filed: false\n reason:\n /** The CRM plugin does not run on this site. */\n | 'crm-off'\n /**\n * The org's plan does not carry the CRM suite, whose activities and\n * tasks these are (AGL-2787).\n */\n | 'not-entitled'\n /** Neither the reference nor the address named a record. */\n | 'no-record'\n /** A meeting for this booking is already on the timeline. */\n | 'already-filed'\n /** The service asks for neither a meeting nor a follow-up. */\n | 'nothing-to-file'\n /** A read or a write threw; logged, and nothing is promised. */\n | 'failed'\n }\n | {\n filed: true\n /** How the record was found. */\n matchedBy: 'crmRef' | 'email'\n link: CrmActivityLink\n /** The meeting's id, or `null` when the service files no meeting. */\n activityId: string | null\n /** The follow-up's id, or `null` when the service asks for none. */\n taskId: string | null\n }\n\ninterface ResolvedRecord {\n link: CrmActivityLink\n matchedBy: 'crmRef' | 'email'\n /** Whoever holds the relationship, for the follow-up's assignee. */\n ownerUid?: string\n}\n\n/**\n * The record a booking belongs to — the reference first, the address second.\n */\nasync function resolveBookingRecord(\n firestore: FirebaseFirestore.Firestore,\n input: CrmBookingRecordInput,\n groupId: string,\n): Promise<ResolvedRecord | null> {\n const { hostId, orgId } = input\n const orgRef = firestore.collection('orgs').doc(orgId)\n const contactsRef = orgRef.collection('contacts')\n const ref = parseCrmBookingRef(input.booking.crmRef)\n if (ref?.kind === 'contact') {\n const contact = await contactsRef.doc(ref.id).get()\n if (contact.exists && visibleToHost(contact.get('visibleTo'), hostId)) {\n const facet = readContactFacet(contact.data() ?? {}, groupId)\n return {\n link: {\n contactId: contact.id,\n ...(facet.companyId ? { companyId: facet.companyId } : {}),\n },\n matchedBy: 'crmRef',\n ...(facet.ownerUid ? { ownerUid: facet.ownerUid } : {}),\n }\n }\n }\n if (ref?.kind === 'deal') {\n const deal = await orgRef.collection(CRM_COLLECTIONS.deals).doc(ref.id).get()\n if (deal.exists && visibleToHost(deal.get('visibleTo'), hostId)) {\n const contactId = String(deal.get('contactId') ?? '')\n const companyId = String(deal.get('companyId') ?? '')\n const ownerUid = String(deal.get('ownerUid') ?? '')\n return {\n link: {\n dealId: deal.id,\n ...(contactId ? { contactId } : {}),\n ...(companyId ? { companyId } : {}),\n },\n matchedBy: 'crmRef',\n ...(ownerUid ? { ownerUid } : {}),\n }\n }\n }\n if (ref?.kind === 'lead') {\n /*\n * A lead carries its own `visibleTo` now (AGL-3275) — it used to be\n * host-scoped by path, and the comment here said so. The seam reads the\n * org row, and the site has to be allowed to see it before a booking is\n * filed against it, or one brand's booking would name another's lead.\n */\n const lead = await readLeadForHost(hostId, ref.id)\n if (lead && visibleToHost(lead.get('visibleTo'), hostId)) {\n const ownerUid = String(lead.get('ownerUid') ?? '')\n return {\n link: { leadId: lead.id },\n matchedBy: 'crmRef',\n ...(ownerUid ? { ownerUid } : {}),\n }\n }\n }\n const hit = await findContactByEmail(contactsRef, input.booking.email, { hostId })\n if (!hit) return null\n const facet = readContactFacet(hit.data() ?? {}, groupId)\n return {\n link: {\n contactId: hit.id,\n ...(facet.companyId ? { companyId: facet.companyId } : {}),\n },\n matchedBy: 'email',\n ...(facet.ownerUid ? { ownerUid: facet.ownerUid } : {}),\n }\n}\n\n/**\n * Files what a confirmed booking owes the CRM: the meeting, and the\n * follow-up when the service asks for one. See the module comment.\n */\nexport async function recordCrmBooking(\n firestore: FirebaseFirestore.Firestore,\n input: CrmBookingRecordInput,\n): Promise<CrmBookingRecordOutcome> {\n const { hostId, orgId, booking, service } = input\n try {\n if (!hostId || !orgId || !booking?.id) return { filed: false, reason: 'failed' }\n const wantsMeeting = service?.crmMeetingActivity !== false\n const wantsTask = service?.crmFollowUpTask === true\n if (!wantsMeeting && !wantsTask) return { filed: false, reason: 'nothing-to-file' }\n\n const host =\n input.host === undefined\n ? ((await firestore.collection('hosts').doc(hostId).get()).data() as\n | { disabledPlugins?: string[]; enabledPlugins?: string[] }\n | undefined) ?? null\n : input.host\n if (!isHostPluginEnabled(input.org ?? null, host, CRM_PLUGIN_ID)) {\n return { filed: false, reason: 'crm-off' }\n }\n if (!checkEntitlement(input.org as Parameters<typeof checkEntitlement>[0], 'crm')) {\n return { filed: false, reason: 'not-entitled' }\n }\n\n const orgRef = firestore.collection('orgs').doc(orgId)\n const activities = orgRef.collection(CRM_COLLECTIONS.activities)\n // One meeting per booking, whatever path re-enters: the paid webhook is\n // redelivered for days after any 500, and its own guard is the status,\n // which this row is not part of.\n const already = await activities.where('bookingId', '==', booking.id).limit(1).get()\n if (!already.empty) return { filed: false, reason: 'already-filed' }\n\n const group = consentGroupForHost(input.org ?? null, hostId)\n const record = await resolveBookingRecord(firestore, input, group.groupId)\n if (!record) return { filed: false, reason: 'no-record' }\n\n const visibleTo = crmScopeTokens(input.org ?? null, group)\n const serviceName = String(service?.name ?? booking.serviceName ?? '').trim()\n const timezone = service?.timezone || undefined\n\n let activityId: string | null = null\n if (wantsMeeting) {\n // The per-record ceiling (AGL-2611): a full log refuses the meeting\n // but not the follow-up — a task is owed either way.\n const logged = await countCrmActivitiesForRecord(orgRef, record.link)\n if (crmActivityLogHasRoom(logged)) {\n const activity: CrmActivity = {\n kind: 'meeting',\n body: crmBookingMeetingBody({\n serviceName,\n startsAtMs: booking.startsAtMs,\n timezone,\n }),\n // When it HAPPENS, which is the slot — not when it was booked.\n atMs: booking.startsAtMs,\n byUid: '',\n ...record.link,\n bookingId: booking.id,\n hostId,\n visibleTo,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n const written = await activities.add(activity)\n activityId = written.id\n }\n }\n\n let taskId: string | null = null\n if (wantsTask) {\n const task: CrmTask = {\n title: crmBookingFollowUpTitle(serviceName),\n kind: 'todo',\n priority: 'normal',\n status: 'open',\n dueAtMs: crmBookingFollowUpDueMs(booking.endsAtMs, timezone),\n ...(record.ownerUid ? { assigneeUid: record.ownerUid } : {}),\n createdByUid: '',\n ...record.link,\n hostId,\n visibleTo,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n // What the Tasks list searches by (AGL-3321), beside the task.\n const written = await orgRef\n .collection(CRM_COLLECTIONS.tasks)\n .add({ ...task, ...crmTaskListFields(task) })\n taskId = written.id\n }\n\n return { filed: true, matchedBy: record.matchedBy, link: record.link, activityId, taskId }\n } catch (error) {\n console.error('[crm] booking could not be filed', hostId, booking?.id, error)\n return { filed: false, reason: 'failed' }\n }\n}\n"],"names":["checkEntitlement","consentGroupForHost","CRM_COLLECTIONS","crmTaskListFields","crmActivityLogHasRoom","crmBookingFollowUpDueMs","crmBookingFollowUpTitle","crmBookingMeetingBody","crmScopeTokens","isHostPluginEnabled","parseCrmBookingRef","readContactFacet","visibleToHost","FieldValue","findContactByEmail","readLeadForHost","countCrmActivitiesForRecord","CRM_PLUGIN_ID","resolveBookingRecord","firestore","input","groupId","hit","hostId","orgId","orgRef","collection","doc","contactsRef","ref","booking","crmRef","kind","contact","id","get","exists","facet","data","link","contactId","companyId","matchedBy","ownerUid","deal","deals","String","dealId","lead","leadId","email","recordCrmBooking","service","filed","reason","wantsMeeting","crmMeetingActivity","wantsTask","crmFollowUpTask","host","undefined","org","activities","already","where","limit","empty","group","record","visibleTo","serviceName","name","trim","timezone","activityId","logged","activity","body","startsAtMs","atMs","byUid","bookingId","createdAt","serverTimestamp","updatedAt","written","add","taskId","task","title","priority","status","dueAtMs","endsAtMs","assigneeUid","createdByUid","tasks","error","console"],"mappings":"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"}
|
|
@@ -1,145 +0,0 @@
|
|
|
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 CrmActivityLink, type CrmEmailDirection, type CrmInboundCandidate, type CrmInboundMember } from '@aglyn/aglyn/server';
|
|
18
|
-
import type { ReceivedEmail } from '@aglyn/shared-util-email';
|
|
19
|
-
/**
|
|
20
|
-
* EMAIL CAPTURE (AGL-2657): the half with Firestore.
|
|
21
|
-
*
|
|
22
|
-
* The org's capture token — minted once, replaced on a rotation, found
|
|
23
|
-
* again from a message's recipient — and the filing of one received
|
|
24
|
-
* message: which record it was with, whether it has been filed before, and
|
|
25
|
-
* the row it becomes. The pure rules are `@aglyn/aglyn`'s
|
|
26
|
-
* `crm-inbound.ts`; this module is the reads and the one write.
|
|
27
|
-
*
|
|
28
|
-
* Firestore is a parameter throughout, as in `crm-records.ts` and
|
|
29
|
-
* `crm-email-activity.ts`: the routes hold one, and a module that reached
|
|
30
|
-
* for the Admin app itself would drag it into every spec that only wants
|
|
31
|
-
* the matching. The roster is read through that same store by
|
|
32
|
-
* `loadCrmInboundRoster`, and the caller writes the org's feed line, so a
|
|
33
|
-
* spec of this module needs a store and nothing else.
|
|
34
|
-
*/
|
|
35
|
-
/** The most candidate addresses one message is matched by. */
|
|
36
|
-
export declare const CRM_INBOUND_CANDIDATE_MAX = 6;
|
|
37
|
-
/** The most sites whose leads are consulted for one message. */
|
|
38
|
-
export declare const CRM_INBOUND_LEAD_HOST_MAX = 50;
|
|
39
|
-
export interface CrmInboundTokenResult {
|
|
40
|
-
token: string;
|
|
41
|
-
createdAtMs: number;
|
|
42
|
-
rotatedAtMs?: number;
|
|
43
|
-
/** True when this call replaced a token that existed. */
|
|
44
|
-
rotated: boolean;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* The org's capture token, minted the first time anybody asks and kept
|
|
48
|
-
* thereafter; with `rotate`, replaced. A transaction, so two members
|
|
49
|
-
* opening the settings page at once mint one token and not two, and a
|
|
50
|
-
* rotation lands on the token the reader saw.
|
|
51
|
-
*/
|
|
52
|
-
export declare function ensureCrmInboundToken(firestore: FirebaseFirestore.Firestore, orgId: string, options?: {
|
|
53
|
-
rotate?: boolean;
|
|
54
|
-
nowMs?: number;
|
|
55
|
-
}): Promise<CrmInboundTokenResult>;
|
|
56
|
-
/**
|
|
57
|
-
* The org a capture token names, or `null`. One equality on
|
|
58
|
-
* `crmInbound.token`, which Firestore indexes on its own; a token that is
|
|
59
|
-
* not one by shape is refused before the query, so a malformed local part
|
|
60
|
-
* costs no read.
|
|
61
|
-
*/
|
|
62
|
-
export declare function findOrgByCrmInboundToken(firestore: FirebaseFirestore.Firestore, token: unknown): Promise<{
|
|
63
|
-
orgId: string;
|
|
64
|
-
org: Record<string, unknown>;
|
|
65
|
-
} | null>;
|
|
66
|
-
/**
|
|
67
|
-
* Every site of the organization, by id, up to the ceiling — the set a
|
|
68
|
-
* message's lead lookup sweeps. One equality on `hosts.orgId`.
|
|
69
|
-
*/
|
|
70
|
-
export declare function crmInboundHostIds(firestore: FirebaseFirestore.Firestore, orgId: string): Promise<string[]>;
|
|
71
|
-
/** The record one of a message's addresses resolved to. */
|
|
72
|
-
export interface CrmInboundMatch {
|
|
73
|
-
email: string;
|
|
74
|
-
direction: CrmEmailDirection;
|
|
75
|
-
kind: 'contact' | 'lead';
|
|
76
|
-
/** Where the row is filed. */
|
|
77
|
-
link: CrmActivityLink;
|
|
78
|
-
/** The site the row is stamped with — the record's own. */
|
|
79
|
-
hostId: string;
|
|
80
|
-
/** Who may list the row — the record's own tokens. */
|
|
81
|
-
visibleTo: string[];
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* The first candidate that is a contact of the org — through the address
|
|
85
|
-
* index, so a merged record's alternate address answers — or a lead on any
|
|
86
|
-
* of its sites. Candidates are tried in the order the rule gave them and
|
|
87
|
-
* the first hit wins; a contact is looked for before a lead at each
|
|
88
|
-
* address, because a converted lead's address is its contact's.
|
|
89
|
-
*/
|
|
90
|
-
export declare function matchCrmInboundCorrespondent(firestore: FirebaseFirestore.Firestore, input: {
|
|
91
|
-
orgId: string;
|
|
92
|
-
org: Record<string, unknown>;
|
|
93
|
-
candidates: readonly CrmInboundCandidate[];
|
|
94
|
-
hostIds: readonly string[];
|
|
95
|
-
}): Promise<CrmInboundMatch | null>;
|
|
96
|
-
/**
|
|
97
|
-
* The workspace's roster as the filer matches against it: the member
|
|
98
|
-
* documents and every member's stored alias document, two reads under the
|
|
99
|
-
* org, reduced by `crmInboundRoster` to each member's sign-in address and
|
|
100
|
-
* the aliases they have confirmed (AGL-2975).
|
|
101
|
-
*/
|
|
102
|
-
export declare function loadCrmInboundRoster(firestore: FirebaseFirestore.Firestore, orgId: string): Promise<CrmInboundMember[]>;
|
|
103
|
-
export type CrmInboundFileOutcome =
|
|
104
|
-
/** A row was written. */
|
|
105
|
-
{
|
|
106
|
-
outcome: 'filed';
|
|
107
|
-
activityId: string;
|
|
108
|
-
match: CrmInboundMatch;
|
|
109
|
-
}
|
|
110
|
-
/** The message had been filed before; nothing written. */
|
|
111
|
-
| {
|
|
112
|
-
outcome: 'duplicate';
|
|
113
|
-
activityId: string;
|
|
114
|
-
}
|
|
115
|
-
/** No address on the message is a record of the org; nothing written. */
|
|
116
|
-
| {
|
|
117
|
-
outcome: 'unmatched';
|
|
118
|
-
senderDomain: string | null;
|
|
119
|
-
}
|
|
120
|
-
/** The record's activity log is at its ceiling; nothing written. */
|
|
121
|
-
| {
|
|
122
|
-
outcome: 'ceiling';
|
|
123
|
-
match: CrmInboundMatch;
|
|
124
|
-
};
|
|
125
|
-
/**
|
|
126
|
-
* Files one received message on the record it was with (AGL-2657).
|
|
127
|
-
*
|
|
128
|
-
* The order is the cost: the candidate rule is arithmetic; the match is
|
|
129
|
-
* the reads that decide everything after it; a message filed before is
|
|
130
|
-
* answered off one read of the row it would write, before the ceiling's
|
|
131
|
-
* aggregate; the ceiling comes last and refuses rather than dropping a row
|
|
132
|
-
* on a full log. The write is a `create`, so two deliveries racing past
|
|
133
|
-
* the read still yield one row. The BODY never leaves this function except
|
|
134
|
-
* as the bounded excerpt in the row.
|
|
135
|
-
*/
|
|
136
|
-
export declare function fileCrmInboundEmail(firestore: FirebaseFirestore.Firestore, input: {
|
|
137
|
-
orgId: string;
|
|
138
|
-
org: Record<string, unknown>;
|
|
139
|
-
message: ReceivedEmail;
|
|
140
|
-
/** The capture domain, so the capture address is never the correspondent. */
|
|
141
|
-
domain: string;
|
|
142
|
-
/** The roster, from `loadCrmInboundRoster`: sign-in addresses and confirmed aliases. */
|
|
143
|
-
members: readonly CrmInboundMember[];
|
|
144
|
-
hostIds: readonly string[];
|
|
145
|
-
}): Promise<CrmInboundFileOutcome>;
|