@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.
Files changed (156) hide show
  1. package/package.json +8 -8
  2. package/src/lib/server/api-http.js +5 -1
  3. package/src/lib/server/api-http.js.map +1 -1
  4. package/src/lib/server/api-v1-description.d.ts +112 -0
  5. package/src/lib/server/api-v1-description.js +126 -0
  6. package/src/lib/server/api-v1-description.js.map +1 -0
  7. package/src/lib/server/api-v1-kit.d.ts +163 -0
  8. package/src/lib/server/api-v1-kit.js +191 -0
  9. package/src/lib/server/api-v1-kit.js.map +1 -0
  10. package/src/lib/server/api-v1-resources.d.ts +143 -0
  11. package/src/lib/server/api-v1-resources.js +125 -0
  12. package/src/lib/server/api-v1-resources.js.map +1 -0
  13. package/src/lib/server/campaign-conversion-attribution.d.ts +1 -1
  14. package/src/lib/server/campaign-conversion-attribution.js +3 -3
  15. package/src/lib/server/campaign-conversion-attribution.js.map +1 -1
  16. package/src/lib/server/card-payment-velocity.d.ts +9 -0
  17. package/src/lib/server/card-payment-velocity.js +28 -2
  18. package/src/lib/server/card-payment-velocity.js.map +1 -1
  19. package/src/lib/server/consent-group-carry.js +1 -1
  20. package/src/lib/server/consent-group-carry.js.map +1 -1
  21. package/src/lib/server/consent-group-change.d.ts +2 -1
  22. package/src/lib/server/consent-group-change.js +43 -18
  23. package/src/lib/server/consent-group-change.js.map +1 -1
  24. package/src/lib/server/contact-email-index.d.ts +22 -0
  25. package/src/lib/server/contact-email-index.js +108 -0
  26. package/src/lib/server/contact-email-index.js.map +1 -1
  27. package/src/lib/server/crm-email-activity.d.ts +1 -1
  28. package/src/lib/server/crm-email-activity.js +1 -1
  29. package/src/lib/server/crm-email-activity.js.map +1 -1
  30. package/src/lib/server/crm-records.d.ts +9 -1
  31. package/src/lib/server/crm-records.js +19 -2
  32. package/src/lib/server/crm-records.js.map +1 -1
  33. package/src/lib/server/cron-auth.d.ts +44 -0
  34. package/src/lib/server/cron-auth.js +76 -0
  35. package/src/lib/server/cron-auth.js.map +1 -0
  36. package/src/lib/server/dataset-live-pages.js +6 -6
  37. package/src/lib/server/dataset-live-pages.js.map +1 -1
  38. package/src/lib/server/duplicate-resource.d.ts +26 -0
  39. package/src/lib/server/duplicate-resource.js +47 -25
  40. package/src/lib/server/duplicate-resource.js.map +1 -1
  41. package/src/lib/server/dynamic-list-materialize.js +6 -3
  42. package/src/lib/server/dynamic-list-materialize.js.map +1 -1
  43. package/src/lib/server/email-flow-gate.js +2 -2
  44. package/src/lib/server/email-flow-gate.js.map +1 -1
  45. package/src/lib/server/email-suppression.d.ts +11 -0
  46. package/src/lib/server/email-suppression.js +37 -2
  47. package/src/lib/server/email-suppression.js.map +1 -1
  48. package/src/lib/server/email-unsubscribe-link.d.ts +8 -5
  49. package/src/lib/server/email-unsubscribe-link.js +7 -7
  50. package/src/lib/server/email-unsubscribe-link.js.map +1 -1
  51. package/src/lib/server/erase.d.ts +19 -33
  52. package/src/lib/server/erase.js +37 -162
  53. package/src/lib/server/erase.js.map +1 -1
  54. package/src/lib/server/host-email-tokens.js +1 -1
  55. package/src/lib/server/host-email-tokens.js.map +1 -1
  56. package/src/lib/server/host-memberships.js +3 -3
  57. package/src/lib/server/host-memberships.js.map +1 -1
  58. package/src/lib/server/host-visitor-records.d.ts +32 -4
  59. package/src/lib/server/host-visitor-records.js +73 -14
  60. package/src/lib/server/host-visitor-records.js.map +1 -1
  61. package/src/lib/server/hosted-page-review.d.ts +27 -0
  62. package/src/lib/server/hosted-page-review.js +149 -12
  63. package/src/lib/server/hosted-page-review.js.map +1 -1
  64. package/src/lib/server/media-cdn-rate-limit.d.ts +6 -3
  65. package/src/lib/server/media-cdn-rate-limit.js +6 -3
  66. package/src/lib/server/media-cdn-rate-limit.js.map +1 -1
  67. package/src/lib/server/member-email-aliases.d.ts +10 -2
  68. package/src/lib/server/member-email-aliases.js +26 -2
  69. package/src/lib/server/member-email-aliases.js.map +1 -1
  70. package/src/lib/server/new-account-announcement.d.ts +62 -0
  71. package/src/lib/server/new-account-announcement.js +77 -0
  72. package/src/lib/server/new-account-announcement.js.map +1 -0
  73. package/src/lib/server/notifications.d.ts +22 -2
  74. package/src/lib/server/notifications.js +40 -3
  75. package/src/lib/server/notifications.js.map +1 -1
  76. package/src/lib/server/operator-alerts.d.ts +7 -1
  77. package/src/lib/server/operator-alerts.js +5 -3
  78. package/src/lib/server/operator-alerts.js.map +1 -1
  79. package/src/lib/server/org-containers.d.ts +66 -0
  80. package/src/lib/server/org-containers.js +95 -0
  81. package/src/lib/server/org-containers.js.map +1 -0
  82. package/src/lib/server/org-member-notice.js +3 -2
  83. package/src/lib/server/org-member-notice.js.map +1 -1
  84. package/src/lib/server/payment-fraud-signal.js +5 -1
  85. package/src/lib/server/payment-fraud-signal.js.map +1 -1
  86. package/src/lib/server/{connect-payout-failure.d.ts → payment-provider-stripe-connect-payouts.d.ts} +3 -0
  87. package/src/lib/server/{connect-payout-failure.js → payment-provider-stripe-connect-payouts.js} +36 -2
  88. package/src/lib/server/payment-provider-stripe-connect-payouts.js.map +1 -0
  89. package/src/lib/server/{connect-account-status.d.ts → payment-provider-stripe-connect-status.d.ts} +3 -0
  90. package/src/lib/server/{connect-account-status.js → payment-provider-stripe-connect-status.js} +1 -1
  91. package/src/lib/server/payment-provider-stripe-connect-status.js.map +1 -0
  92. package/src/lib/server/{stripe-account-mode.d.ts → payment-provider-stripe-connect.d.ts} +45 -70
  93. package/src/lib/server/{stripe-account-mode.js → payment-provider-stripe-connect.js} +78 -52
  94. package/src/lib/server/payment-provider-stripe-connect.js.map +1 -0
  95. package/src/lib/server/payment-provider.d.ts +173 -0
  96. package/src/lib/server/payment-provider.js +70 -0
  97. package/src/lib/server/payment-provider.js.map +1 -0
  98. package/src/lib/server/payment-risk-record.d.ts +1 -1
  99. package/src/lib/server/payment-risk-record.js +3 -1
  100. package/src/lib/server/payment-risk-record.js.map +1 -1
  101. package/src/lib/server/personal-data-export.js +1 -1
  102. package/src/lib/server/personal-data-export.js.map +1 -1
  103. package/src/lib/server/platform-marketing-consent.d.ts +1 -1
  104. package/src/lib/server/platform-marketing-consent.js +1 -1
  105. package/src/lib/server/platform-marketing-consent.js.map +1 -1
  106. package/src/lib/server/rate-limit-store.js +13 -3
  107. package/src/lib/server/rate-limit-store.js.map +1 -1
  108. package/src/lib/server/realm-plugins.js +8 -3
  109. package/src/lib/server/realm-plugins.js.map +1 -1
  110. package/src/lib/server/retained-refusals.d.ts +142 -0
  111. package/src/lib/server/retained-refusals.js +325 -0
  112. package/src/lib/server/retained-refusals.js.map +1 -0
  113. package/src/lib/server/risk-notice.d.ts +32 -1
  114. package/src/lib/server/risk-notice.js +90 -28
  115. package/src/lib/server/risk-notice.js.map +1 -1
  116. package/src/lib/server/sending-domains.d.ts +17 -0
  117. package/src/lib/server/sending-domains.js +28 -4
  118. package/src/lib/server/sending-domains.js.map +1 -1
  119. package/src/lib/server/sso-enforcement.js +7 -2
  120. package/src/lib/server/sso-enforcement.js.map +1 -1
  121. package/src/lib/server/staff-alert-email.js +4 -2
  122. package/src/lib/server/staff-alert-email.js.map +1 -1
  123. package/src/lib/server/test-firestore-queries.d.ts +7 -0
  124. package/src/lib/server/test-firestore-queries.js +70 -27
  125. package/src/lib/server/test-firestore-queries.js.map +1 -1
  126. package/src/lib/server/{email-topic-confirmation.js → topic-subscriptions.js} +15 -11
  127. package/src/lib/server/topic-subscriptions.js.map +1 -0
  128. package/src/lib/server/upsert-contact.d.ts +46 -1
  129. package/src/lib/server/upsert-contact.js +87 -20
  130. package/src/lib/server/upsert-contact.js.map +1 -1
  131. package/src/tenancy.d.ts +5 -8
  132. package/src/tenancy.js +12 -13
  133. package/src/tenancy.js.map +1 -1
  134. package/src/lib/server/connect-account-status.js.map +0 -1
  135. package/src/lib/server/connect-payout-failure.js.map +0 -1
  136. package/src/lib/server/contact-email-engagement.d.ts +0 -32
  137. package/src/lib/server/contact-email-engagement.js +0 -145
  138. package/src/lib/server/contact-email-engagement.js.map +0 -1
  139. package/src/lib/server/contact-lifecycle-floor.d.ts +0 -89
  140. package/src/lib/server/contact-lifecycle-floor.js +0 -91
  141. package/src/lib/server/contact-lifecycle-floor.js.map +0 -1
  142. package/src/lib/server/contact-merge.d.ts +0 -55
  143. package/src/lib/server/contact-merge.js +0 -288
  144. package/src/lib/server/contact-merge.js.map +0 -1
  145. package/src/lib/server/crm-booking-activity.d.ts +0 -86
  146. package/src/lib/server/crm-booking-activity.js +0 -257
  147. package/src/lib/server/crm-booking-activity.js.map +0 -1
  148. package/src/lib/server/crm-inbound-email.d.ts +0 -145
  149. package/src/lib/server/crm-inbound-email.js +0 -297
  150. package/src/lib/server/crm-inbound-email.js.map +0 -1
  151. package/src/lib/server/email-topic-confirmation.js.map +0 -1
  152. package/src/lib/server/form-dataset-binding-token.d.ts +0 -35
  153. package/src/lib/server/form-dataset-binding-token.js +0 -107
  154. package/src/lib/server/form-dataset-binding-token.js.map +0 -1
  155. package/src/lib/server/stripe-account-mode.js.map +0 -1
  156. /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>;