@aglyn/plugins-outreach 1.0.0-beta.220 → 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/package.json +14 -14
- package/src/lib/components/do-not-contact-domains.d.ts +6 -2
- package/src/lib/components/do-not-contact-domains.js +73 -11
- package/src/lib/components/do-not-contact-domains.js.map +1 -1
- package/src/lib/components/enrollments-table.d.ts +6 -0
- package/src/lib/components/enrollments-table.js +77 -4
- package/src/lib/components/enrollments-table.js.map +1 -1
- package/src/lib/components/sequence-detail.js +3 -1
- package/src/lib/components/sequence-detail.js.map +1 -1
- package/src/lib/components/sequence-report-card.js +7 -3
- package/src/lib/components/sequence-report-card.js.map +1 -1
- package/src/lib/components/use-outreach-crm.d.ts +18 -15
- package/src/lib/components/use-outreach-crm.js +118 -87
- package/src/lib/components/use-outreach-crm.js.map +1 -1
- package/src/lib/components/use-outreach-data.d.ts +2 -2
- package/src/lib/components/use-outreach-data.js.map +1 -1
- package/src/lib/constants/transfer-resources.d.ts +33 -0
- package/src/lib/constants/transfer-resources.js +38 -0
- package/src/lib/constants/transfer-resources.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +7 -1
- package/src/lib/declarations.console-server.js +66 -1
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/engine/click-tracking.js +1 -1
- package/src/lib/engine/click-tracking.js.map +1 -1
- package/src/lib/engine/ip-range.d.ts +26 -0
- package/src/lib/engine/ip-range.js +78 -0
- package/src/lib/engine/ip-range.js.map +1 -0
- package/src/lib/engine/open-source.d.ts +24 -0
- package/src/lib/engine/open-source.js +130 -0
- package/src/lib/engine/open-source.js.map +1 -0
- package/src/lib/engine/open-tracking.d.ts +15 -2
- package/src/lib/engine/open-tracking.js +59 -16
- package/src/lib/engine/open-tracking.js.map +1 -1
- package/src/lib/engine/outreach-access.d.ts +47 -0
- package/src/lib/engine/outreach-access.js +56 -0
- package/src/lib/engine/outreach-access.js.map +1 -0
- package/src/lib/enrollment/enroll-people.d.ts +10 -10
- package/src/lib/enrollment/enroll-people.js +30 -25
- package/src/lib/enrollment/enroll-people.js.map +1 -1
- package/src/lib/enrollment/gate-lookups.js +30 -15
- package/src/lib/enrollment/gate-lookups.js.map +1 -1
- package/src/lib/enrollment/message-templates.d.ts +24 -0
- package/src/lib/enrollment/message-templates.js +35 -0
- package/src/lib/enrollment/message-templates.js.map +1 -0
- package/src/lib/model/enrollment-engagement.d.ts +16 -0
- package/src/lib/model/enrollment-engagement.js +16 -0
- package/src/lib/model/enrollment-engagement.js.map +1 -1
- package/src/lib/model/enrollment-history.d.ts +8 -1
- package/src/lib/model/enrollment-history.js +17 -5
- package/src/lib/model/enrollment-history.js.map +1 -1
- package/src/lib/model/enrollment-timeline.d.ts +3 -0
- package/src/lib/model/enrollment-timeline.js +15 -4
- package/src/lib/model/enrollment-timeline.js.map +1 -1
- package/src/lib/model/outreach.types.d.ts +12 -3
- package/src/lib/model/outreach.types.js +12 -1
- package/src/lib/model/outreach.types.js.map +1 -1
- package/src/lib/model/sequence-report.d.ts +14 -7
- package/src/lib/model/sequence-report.js +12 -8
- package/src/lib/model/sequence-report.js.map +1 -1
- package/src/lib/plugin.js +18 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/routes/curate-routes.js +4 -6
- package/src/lib/routes/curate-routes.js.map +1 -1
- package/src/lib/routes/enroll-routes.d.ts +0 -12
- package/src/lib/routes/enroll-routes.js +52 -92
- package/src/lib/routes/enroll-routes.js.map +1 -1
- package/src/lib/routes/platform-deps.d.ts +4 -16
- package/src/lib/routes/platform-deps.js +5 -25
- package/src/lib/routes/platform-deps.js.map +1 -1
- package/src/lib/routes/preview-routes.js +25 -13
- package/src/lib/routes/preview-routes.js.map +1 -1
- package/src/lib/routes/register-routes.js +7 -5
- package/src/lib/routes/register-routes.js.map +1 -1
- package/src/lib/routes/route-gate.js +6 -17
- package/src/lib/routes/route-gate.js.map +1 -1
- package/src/lib/routes/sequence-routes.d.ts +46 -0
- package/src/lib/routes/sequence-routes.js +155 -63
- package/src/lib/routes/sequence-routes.js.map +1 -1
- package/src/lib/runtime/click-events.js +8 -8
- package/src/lib/runtime/click-events.js.map +1 -1
- package/src/lib/runtime/click-route.js +5 -2
- package/src/lib/runtime/click-route.js.map +1 -1
- package/src/lib/runtime/lead-records.d.ts +18 -10
- package/src/lib/runtime/lead-records.js +14 -60
- package/src/lib/runtime/lead-records.js.map +1 -1
- package/src/lib/runtime/open-events.d.ts +8 -0
- package/src/lib/runtime/open-events.js +10 -3
- package/src/lib/runtime/open-events.js.map +1 -1
- package/src/lib/runtime/platform-runtime-deps.d.ts +9 -5
- package/src/lib/runtime/platform-runtime-deps.js +31 -24
- package/src/lib/runtime/platform-runtime-deps.js.map +1 -1
- package/src/lib/runtime/send-job.js +20 -17
- package/src/lib/runtime/send-job.js.map +1 -1
- package/src/lib/runtime/sync-job.js +10 -5
- package/src/lib/runtime/sync-job.js.map +1 -1
- package/src/lib/storage/do-not-contact-store.d.ts +8 -0
- package/src/lib/storage/do-not-contact-store.js +13 -0
- package/src/lib/storage/do-not-contact-store.js.map +1 -1
- package/src/lib/testing/stand-in-record-system.d.ts +54 -0
- package/src/lib/testing/stand-in-record-system.js +243 -0
- package/src/lib/testing/stand-in-record-system.js.map +1 -0
- package/src/lib/transfer/do-not-contact-transfer.d.ts +129 -0
- package/src/lib/transfer/do-not-contact-transfer.js +801 -0
- package/src/lib/transfer/do-not-contact-transfer.js.map +1 -0
- package/src/lib/transfer/plan-gate.d.ts +25 -0
- package/src/lib/transfer/plan-gate.js +35 -0
- package/src/lib/transfer/plan-gate.js.map +1 -0
- package/src/lib/transfer/platform-transfer-deps.d.ts +24 -0
- package/src/lib/transfer/platform-transfer-deps.js +39 -0
- package/src/lib/transfer/platform-transfer-deps.js.map +1 -0
- package/src/lib/transfer/sequences-package.d.ts +48 -0
- package/src/lib/transfer/sequences-package.js +93 -0
- package/src/lib/transfer/sequences-package.js.map +1 -0
- package/src/lib/transfer/sequences-package.server.d.ts +31 -0
- package/src/lib/transfer/sequences-package.server.js +207 -0
- package/src/lib/transfer/sequences-package.server.js.map +1 -0
|
@@ -0,0 +1,243 @@
|
|
|
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 { personKey } from "@aglyn/aglyn/app-utils/person-key";
|
|
18
|
+
import { scopeTokensForHost, seenOnlyThroughGrant, visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
19
|
+
import { registerPluginPersonRecords } from "@aglyn/aglyn/plugin-manager/plugin-person-records";
|
|
20
|
+
import { registerPluginRecordEmailStateWriter } from "@aglyn/aglyn/plugin-manager/plugin-record-email-state";
|
|
21
|
+
import { registerPluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
|
|
22
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
23
|
+
const KIND_STORED_IN = {
|
|
24
|
+
contact: 'contacts',
|
|
25
|
+
lead: 'leads'
|
|
26
|
+
};
|
|
27
|
+
const email = (value)=>{
|
|
28
|
+
const text = String(value != null ? value : '').trim().toLowerCase();
|
|
29
|
+
return text.includes('@') ? text : null;
|
|
30
|
+
};
|
|
31
|
+
export function standInRecordSystem(options) {
|
|
32
|
+
const org = (orgId)=>options.firestore().collection('orgs').doc(orgId);
|
|
33
|
+
const person = (kind, snapshot)=>{
|
|
34
|
+
var _snapshot_data;
|
|
35
|
+
const data = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
|
|
36
|
+
return {
|
|
37
|
+
kind,
|
|
38
|
+
id: snapshot.id,
|
|
39
|
+
email: email(data['email']),
|
|
40
|
+
data
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const leadStatus = (lead)=>{
|
|
44
|
+
var _lead_status;
|
|
45
|
+
return String((_lead_status = lead['status']) != null ? _lead_status : 'new');
|
|
46
|
+
};
|
|
47
|
+
const moveLeads = async (request, from, to)=>{
|
|
48
|
+
let moved = 0;
|
|
49
|
+
for (const address of request.emails){
|
|
50
|
+
const key = personKey(address);
|
|
51
|
+
if (!key) continue;
|
|
52
|
+
const ref = org(request.orgId).collection('leads').doc(key);
|
|
53
|
+
const lead = (await ref.get()).data();
|
|
54
|
+
if (!lead || lead['convertedContactId']) continue;
|
|
55
|
+
if (!visibleToHost(lead['visibleTo'], request.hostId)) continue;
|
|
56
|
+
if (!from.includes(leadStatus(lead))) continue;
|
|
57
|
+
await ref.set({
|
|
58
|
+
status: to
|
|
59
|
+
}, {
|
|
60
|
+
merge: true
|
|
61
|
+
});
|
|
62
|
+
moved += 1;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
records: moved
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
registerPluginPersonRecords({
|
|
69
|
+
async find (request) {
|
|
70
|
+
var _request_orgId;
|
|
71
|
+
const address = email(request.email);
|
|
72
|
+
const orgId = String((_request_orgId = request.orgId) != null ? _request_orgId : '');
|
|
73
|
+
if (!address || !orgId) return null;
|
|
74
|
+
const found = (await org(orgId).collection('contacts').where('email', '==', address).limit(1).get()).docs[0];
|
|
75
|
+
if (found) {
|
|
76
|
+
const visible = !request.onlyVisibleToSite || visibleToHost(found.get('visibleTo'), String(request.hostId));
|
|
77
|
+
return visible ? person('contact', found) : null;
|
|
78
|
+
}
|
|
79
|
+
if (!request.anyKind) return null;
|
|
80
|
+
const lead = await org(orgId).collection('leads').doc(String(personKey(address))).get();
|
|
81
|
+
return lead.exists ? person('lead', lead) : null;
|
|
82
|
+
},
|
|
83
|
+
async read (request) {
|
|
84
|
+
return Promise.all(request.records.map(async (record)=>{
|
|
85
|
+
const collection = KIND_STORED_IN[record.kind];
|
|
86
|
+
if (!collection || !record.id) return null;
|
|
87
|
+
const snapshot = await org(request.orgId).collection(collection).doc(record.id).get();
|
|
88
|
+
return snapshot.exists ? person(record.kind, snapshot) : null;
|
|
89
|
+
}));
|
|
90
|
+
},
|
|
91
|
+
async fileUnder (request) {
|
|
92
|
+
var _ref;
|
|
93
|
+
const collection = KIND_STORED_IN[request.record.kind];
|
|
94
|
+
if (!collection) return {
|
|
95
|
+
filed: false
|
|
96
|
+
};
|
|
97
|
+
const ref = org(String(request.orgId)).collection(collection).doc(request.record.id);
|
|
98
|
+
if (!(await ref.get()).exists) return {
|
|
99
|
+
filed: false
|
|
100
|
+
};
|
|
101
|
+
const field = request.record.kind === 'lead' ? `${request.containerKind}Ids` : `facets.${(_ref = options.groupIdForHost == null ? void 0 : options.groupIdForHost.call(options, request.hostId)) != null ? _ref : request.hostId}.${request.containerKind}Ids`;
|
|
102
|
+
await ref.update({
|
|
103
|
+
[field]: FieldValue.arrayUnion(...request.ids)
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
filed: true
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
async peopleInView (request) {
|
|
110
|
+
var _ref, _ref1;
|
|
111
|
+
const view = (await org(request.orgId).collection('crmViews').doc(request.viewId).get()).data();
|
|
112
|
+
if (!view || view['shared'] !== true && view['ownerUid'] !== request.viewerUid) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
reason: 'not-found'
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (view['section'] === 'leads') {
|
|
119
|
+
var _ref2;
|
|
120
|
+
var _filters_find;
|
|
121
|
+
const filters = Array.isArray(view['filters']) ? view['filters'] : [];
|
|
122
|
+
const wanted = String((_ref2 = (_filters_find = filters.find((clause)=>clause['field'] === 'status')) == null ? void 0 : _filters_find['value']) != null ? _ref2 : 'open');
|
|
123
|
+
const window = await org(request.orgId).collection('leads').where('visibleTo', 'array-contains-any', scopeTokensForHost(request.hostId)).orderBy('lastSeenAtMs', 'desc').limit(201).get();
|
|
124
|
+
const matching = window.docs.slice(0, 200).filter((doc)=>{
|
|
125
|
+
var _doc_data;
|
|
126
|
+
const lead = (_doc_data = doc.data()) != null ? _doc_data : {};
|
|
127
|
+
if (seenOnlyThroughGrant(lead, request.hostId)) return false;
|
|
128
|
+
if (wanted === 'all') return true;
|
|
129
|
+
if (wanted === 'open') {
|
|
130
|
+
return [
|
|
131
|
+
'new',
|
|
132
|
+
'nurturing',
|
|
133
|
+
'working'
|
|
134
|
+
].includes(leadStatus(lead)) && !lead['convertedContactId'];
|
|
135
|
+
}
|
|
136
|
+
return leadStatus(lead) === wanted;
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
ok: true,
|
|
140
|
+
people: matching.slice(0, request.limit).map((doc)=>({
|
|
141
|
+
kind: 'lead',
|
|
142
|
+
id: doc.id
|
|
143
|
+
})),
|
|
144
|
+
total: matching.length,
|
|
145
|
+
truncated: matching.length > request.limit || window.docs.length > 200
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (view['section'] !== 'contacts') return {
|
|
149
|
+
ok: false,
|
|
150
|
+
reason: 'not-people'
|
|
151
|
+
};
|
|
152
|
+
const unsupported = (_ref = options.unsupported == null ? void 0 : options.unsupported.call(options, Array.isArray(view['filters']) ? view['filters'] : [])) != null ? _ref : [];
|
|
153
|
+
if (unsupported.length) return {
|
|
154
|
+
ok: false,
|
|
155
|
+
reason: 'unsupported',
|
|
156
|
+
unsupported
|
|
157
|
+
};
|
|
158
|
+
const { emails, complete } = (_ref1 = options.viewEmails == null ? void 0 : options.viewEmails.call(options, request.viewId)) != null ? _ref1 : {
|
|
159
|
+
emails: [],
|
|
160
|
+
complete: true
|
|
161
|
+
};
|
|
162
|
+
const ordered = [
|
|
163
|
+
...new Set(emails)
|
|
164
|
+
].sort();
|
|
165
|
+
const ids = new Set();
|
|
166
|
+
for (const address of ordered.slice(0, request.limit)){
|
|
167
|
+
const found = (await org(request.orgId).collection('contacts').where('email', '==', address).limit(1).get()).docs[0];
|
|
168
|
+
if (found && visibleToHost(found.get('visibleTo'), request.hostId)) ids.add(found.id);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
people: [
|
|
173
|
+
...ids
|
|
174
|
+
].map((id)=>({
|
|
175
|
+
kind: 'contact',
|
|
176
|
+
id
|
|
177
|
+
})),
|
|
178
|
+
total: ordered.length,
|
|
179
|
+
truncated: ordered.length > request.limit || !complete
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
async wroteIn (request) {
|
|
183
|
+
return Promise.all(request.records.map(async (record)=>{
|
|
184
|
+
const field = record.kind === 'contact' ? 'contactId' : record.kind === 'lead' ? 'leadId' : null;
|
|
185
|
+
if (!field || !record.id) return null;
|
|
186
|
+
const found = await org(request.orgId).collection('crmActivities').where(field, '==', record.id).where('direction', '==', 'inbound').limit(1).get();
|
|
187
|
+
return !found.empty;
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
}, {
|
|
191
|
+
pluginId: 'record-system'
|
|
192
|
+
});
|
|
193
|
+
const index = (collection, facts)=>({
|
|
194
|
+
async list () {
|
|
195
|
+
return {
|
|
196
|
+
records: [],
|
|
197
|
+
truncated: false
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
async get (request) {
|
|
201
|
+
var _data_name;
|
|
202
|
+
if (!request.orgId) return null;
|
|
203
|
+
const snapshot = await org(request.orgId).collection(collection).doc(request.id).get();
|
|
204
|
+
const data = snapshot.data();
|
|
205
|
+
return data ? {
|
|
206
|
+
id: snapshot.id,
|
|
207
|
+
name: String((_data_name = data['name']) != null ? _data_name : '') || snapshot.id,
|
|
208
|
+
facts: facts(data)
|
|
209
|
+
} : null;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
registerPluginRecordIndex('company', index('companies', (data)=>_extends({}, data)), {
|
|
213
|
+
pluginId: 'record-system'
|
|
214
|
+
});
|
|
215
|
+
registerPluginRecordIndex('messageTemplate', index('crmEmailTemplates', (data)=>{
|
|
216
|
+
var _data_subject, _data_body;
|
|
217
|
+
return {
|
|
218
|
+
kind: data['kind'] === 'snippet' ? 'snippet' : 'template',
|
|
219
|
+
subject: String((_data_subject = data['subject']) != null ? _data_subject : ''),
|
|
220
|
+
body: String((_data_body = data['body']) != null ? _data_body : '')
|
|
221
|
+
};
|
|
222
|
+
}), {
|
|
223
|
+
pluginId: 'record-system'
|
|
224
|
+
});
|
|
225
|
+
registerPluginRecordEmailStateWriter({
|
|
226
|
+
async stamp () {
|
|
227
|
+
return {
|
|
228
|
+
records: 0
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
reached: (request)=>moveLeads(request, [
|
|
232
|
+
'new'
|
|
233
|
+
], 'nurturing'),
|
|
234
|
+
replied: (request)=>moveLeads(request, [
|
|
235
|
+
'new',
|
|
236
|
+
'nurturing'
|
|
237
|
+
], 'working')
|
|
238
|
+
}, {
|
|
239
|
+
pluginId: 'record-system'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
//# sourceMappingURL=stand-in-record-system.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/outreach/src/lib/testing/stand-in-record-system.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 { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { scopeTokensForHost, seenOnlyThroughGrant, visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n registerPluginPersonRecords,\n type PluginPersonRecord,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport { registerPluginRecordEmailStateWriter } from '@aglyn/aglyn/plugin-manager/plugin-record-email-state'\nimport { registerPluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * A plugin that keeps people, standing in for the one that does (AGL-3080).\n *\n * Sequences read the people a rep enrolls, the company each works for, the\n * templates a step sends and the saved views people are taken from — and\n * tell the record system when a lead was mailed or wrote back — through the\n * core's seams (`plugin-person-records`, `plugin-record-index`,\n * `plugin-record-email-state`), importing no record system. Their specs\n * stand an owner up the way the loader would, over the spec's own Firestore\n * — the in-memory double or the emulator. One plugin may not import another,\n * which is why this is here rather than borrowed from the CRM.\n *\n * It imitates the owner's storage as the CRM keeps it (contacts, leads keyed\n * by the person, companies, saved views, templates, the activity log) so a\n * spec seeds documents the way it always has. That the real owner answers\n * the same is held by the CRM's own `person-records.spec.ts`,\n * `crm-record-indexes.spec.ts` and `lead-nurturing.spec.ts`.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\nconst KIND_STORED_IN: Readonly<Record<string, string>> = { contact: 'contacts', lead: 'leads' }\n\nconst email = (value: unknown): string | null => {\n const text = String(value ?? '').trim().toLowerCase()\n return text.includes('@') ? text : null\n}\n\nexport interface StandInRecordSystemOptions {\n firestore: () => Firestore\n /** What the dynamic-list sweep reads out of a saved Contacts view, by view id. */\n viewEmails?: (viewId: string) => { emails: string[]; complete: boolean }\n /**\n * The clauses of a saved Contacts view that only the list itself can\n * apply, named as the owner names them; none by default. A view with any\n * is refused whole, as the owner refuses it.\n */\n unsupported?: (filters: readonly Data[]) => string[]\n /** The consent group a site files a contact's facet under. The site alone by default. */\n groupIdForHost?: (hostId: string) => string\n}\n\nexport function standInRecordSystem(options: StandInRecordSystemOptions): void {\n const org = (orgId: string) => options.firestore().collection('orgs').doc(orgId)\n const person = (kind: string, snapshot: FirebaseFirestore.DocumentSnapshot): PluginPersonRecord => {\n const data = (snapshot.data() ?? {}) as Data\n return { kind, id: snapshot.id, email: email(data['email']), data }\n }\n const leadStatus = (lead: Data) => String(lead['status'] ?? 'new')\n const moveLeads = async (\n request: { orgId: string; hostId: string; emails: readonly string[] },\n from: readonly string[],\n to: string,\n ) => {\n let moved = 0\n for (const address of request.emails) {\n const key = personKey(address)\n if (!key) continue\n const ref = org(request.orgId).collection('leads').doc(key)\n const lead = (await ref.get()).data() as Data | undefined\n if (!lead || lead['convertedContactId']) continue\n if (!visibleToHost(lead['visibleTo'] as string[] | undefined, request.hostId)) continue\n if (!from.includes(leadStatus(lead))) continue\n await ref.set({ status: to }, { merge: true })\n moved += 1\n }\n return { records: moved }\n }\n\n registerPluginPersonRecords(\n {\n async find(request) {\n const address = email(request.email)\n const orgId = String(request.orgId ?? '')\n if (!address || !orgId) return null\n const found = (await org(orgId).collection('contacts').where('email', '==', address).limit(1).get()).docs[0]\n if (found) {\n const visible =\n !request.onlyVisibleToSite ||\n visibleToHost(found.get('visibleTo') as string[] | undefined, String(request.hostId))\n return visible ? person('contact', found) : null\n }\n if (!request.anyKind) return null\n const lead = await org(orgId).collection('leads').doc(String(personKey(address))).get()\n return lead.exists ? person('lead', lead) : null\n },\n async read(request) {\n return Promise.all(\n request.records.map(async (record) => {\n const collection = KIND_STORED_IN[record.kind]\n if (!collection || !record.id) return null\n const snapshot = await org(request.orgId).collection(collection).doc(record.id).get()\n return snapshot.exists ? person(record.kind, snapshot) : null\n }),\n )\n },\n async fileUnder(request) {\n const collection = KIND_STORED_IN[request.record.kind]\n if (!collection) return { filed: false }\n const ref = org(String(request.orgId)).collection(collection).doc(request.record.id)\n if (!(await ref.get()).exists) return { filed: false }\n const field =\n request.record.kind === 'lead'\n ? `${request.containerKind}Ids`\n : `facets.${options.groupIdForHost?.(request.hostId) ?? request.hostId}.${request.containerKind}Ids`\n await ref.update({ [field]: FieldValue.arrayUnion(...request.ids) })\n return { filed: true }\n },\n async peopleInView(request) {\n const view = (await org(request.orgId).collection('crmViews').doc(request.viewId).get()).data() as\n | Data\n | undefined\n if (!view || (view['shared'] !== true && view['ownerUid'] !== request.viewerUid)) {\n return { ok: false, reason: 'not-found' }\n }\n if (view['section'] === 'leads') {\n const filters = Array.isArray(view['filters']) ? (view['filters'] as Data[]) : []\n const wanted = String(filters.find((clause) => clause['field'] === 'status')?.['value'] ?? 'open')\n const window = await org(request.orgId)\n .collection('leads')\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(request.hostId))\n .orderBy('lastSeenAtMs', 'desc')\n .limit(201)\n .get()\n const matching = window.docs.slice(0, 200).filter((doc) => {\n const lead = (doc.data() ?? {}) as Data\n if (seenOnlyThroughGrant(lead, request.hostId)) return false\n if (wanted === 'all') return true\n if (wanted === 'open') {\n return ['new', 'nurturing', 'working'].includes(leadStatus(lead)) && !lead['convertedContactId']\n }\n return leadStatus(lead) === wanted\n })\n return {\n ok: true,\n people: matching.slice(0, request.limit).map((doc) => ({ kind: 'lead', id: doc.id })),\n total: matching.length,\n truncated: matching.length > request.limit || window.docs.length > 200,\n }\n }\n if (view['section'] !== 'contacts') return { ok: false, reason: 'not-people' }\n const unsupported = options.unsupported?.(Array.isArray(view['filters']) ? (view['filters'] as Data[]) : []) ?? []\n if (unsupported.length) return { ok: false, reason: 'unsupported', unsupported }\n const { emails, complete } = options.viewEmails?.(request.viewId) ?? { emails: [], complete: true }\n const ordered = [...new Set(emails)].sort()\n const ids = new Set<string>()\n for (const address of ordered.slice(0, request.limit)) {\n const found = (await org(request.orgId).collection('contacts').where('email', '==', address).limit(1).get())\n .docs[0]\n if (found && visibleToHost(found.get('visibleTo') as string[] | undefined, request.hostId)) ids.add(found.id)\n }\n return {\n ok: true,\n people: [...ids].map((id) => ({ kind: 'contact', id })),\n total: ordered.length,\n truncated: ordered.length > request.limit || !complete,\n }\n },\n async wroteIn(request) {\n return Promise.all(\n request.records.map(async (record) => {\n const field = record.kind === 'contact' ? 'contactId' : record.kind === 'lead' ? 'leadId' : null\n if (!field || !record.id) return null\n const found = await org(request.orgId)\n .collection('crmActivities')\n .where(field, '==', record.id)\n .where('direction', '==', 'inbound')\n .limit(1)\n .get()\n return !found.empty\n }),\n )\n },\n },\n { pluginId: 'record-system' },\n )\n\n const index = (collection: string, facts: (data: Data) => Data) => ({\n async list() {\n return { records: [], truncated: false }\n },\n async get(request: { orgId?: string | null; id: string }) {\n if (!request.orgId) return null\n const snapshot = await org(request.orgId).collection(collection).doc(request.id).get()\n const data = snapshot.data() as Data | undefined\n return data ? { id: snapshot.id, name: String(data['name'] ?? '') || snapshot.id, facts: facts(data) } : null\n },\n })\n registerPluginRecordIndex('company', index('companies', (data) => ({ ...data })), { pluginId: 'record-system' })\n registerPluginRecordIndex(\n 'messageTemplate',\n index('crmEmailTemplates', (data) => ({\n kind: data['kind'] === 'snippet' ? 'snippet' : 'template',\n subject: String(data['subject'] ?? ''),\n body: String(data['body'] ?? ''),\n })),\n { pluginId: 'record-system' },\n )\n\n registerPluginRecordEmailStateWriter(\n {\n async stamp() {\n return { records: 0 }\n },\n reached: (request) => moveLeads(request, ['new'], 'nurturing'),\n replied: (request) => moveLeads(request, ['new', 'nurturing'], 'working'),\n },\n { pluginId: 'record-system' },\n )\n}\n"],"names":["personKey","scopeTokensForHost","seenOnlyThroughGrant","visibleToHost","registerPluginPersonRecords","registerPluginRecordEmailStateWriter","registerPluginRecordIndex","FieldValue","KIND_STORED_IN","contact","lead","email","value","text","String","trim","toLowerCase","includes","standInRecordSystem","options","org","orgId","firestore","collection","doc","person","kind","snapshot","data","id","leadStatus","moveLeads","request","from","to","moved","address","emails","key","ref","get","hostId","set","status","merge","records","find","found","where","limit","docs","visible","onlyVisibleToSite","anyKind","exists","read","Promise","all","map","record","fileUnder","filed","field","containerKind","groupIdForHost","update","arrayUnion","ids","peopleInView","view","viewId","viewerUid","ok","reason","filters","Array","isArray","wanted","clause","window","orderBy","matching","slice","filter","people","total","length","truncated","unsupported","complete","viewEmails","ordered","Set","sort","add","wroteIn","empty","pluginId","index","facts","list","name","subject","body","stamp","reached","replied"],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
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 TransferCatalogInput, type TransferLockedRule } from '@aglyn/aglyn/data-transfer';
|
|
18
|
+
import type { PluginTransferResource, TransferRecordsHooks } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources';
|
|
19
|
+
import { type OutreachMembership } from '../engine/outreach-access';
|
|
20
|
+
/**
|
|
21
|
+
* THE DO-NOT-CONTACT LIST AS A FILE: `outreach.do-not-contact`, an
|
|
22
|
+
* organization's addresses and domains that no sequence emails.
|
|
23
|
+
*
|
|
24
|
+
* ## One file, two collections
|
|
25
|
+
*
|
|
26
|
+
* A row is one entry, and its Entry cell says which kind: an email address
|
|
27
|
+
* goes to `orgs/{orgId}/outreachDoNotContact/{key}`, anything else that reads
|
|
28
|
+
* as a domain to `orgs/{orgId}/outreachDoNotContactDomains/{domain}`. The
|
|
29
|
+
* Aglyn ID names the kind too — `domain:<domain>` or `address:<key>` — so a
|
|
30
|
+
* re-imported export finds its entries by id.
|
|
31
|
+
*
|
|
32
|
+
* ## Only ever added to
|
|
33
|
+
*
|
|
34
|
+
* Every entry is written once and never edited (the store's `create()`), and
|
|
35
|
+
* the first reason an entry was added is the record of why it is there. An
|
|
36
|
+
* import keeps that: a row that finds an entry already on the list is
|
|
37
|
+
* skipped whatever the wizard's policy says (`plan` forces it), and only the
|
|
38
|
+
* entries an import ADDED are taken off again by its undo.
|
|
39
|
+
*
|
|
40
|
+
* ## Addresses go in and never come out
|
|
41
|
+
*
|
|
42
|
+
* An address entry stores the address's fingerprint, never the address, so
|
|
43
|
+
* the list keeps a promise not to email someone without holding who they
|
|
44
|
+
* are. An export therefore reads domains only: an address cannot be read
|
|
45
|
+
* back out of its fingerprint.
|
|
46
|
+
*
|
|
47
|
+
* ## The same door as the Compliance page
|
|
48
|
+
*
|
|
49
|
+
* Every hook asks what the route gate asks — a member of this organization
|
|
50
|
+
* with reach over all of it, holding Use Sequences, in a workspace whose plan
|
|
51
|
+
* includes Sequences — through the same verdicts (`engine/outreach-access.ts`).
|
|
52
|
+
* A plan refuses every row for a member who would be refused, `apply` asks
|
|
53
|
+
* again before it writes, a read answers nothing, and undo stops. Writes go
|
|
54
|
+
* through the store's own adders, as a member's would: reason `manual`,
|
|
55
|
+
* source `member`, the member as `addedByUid`. Each written chunk files one
|
|
56
|
+
* line in the organization's activity log, as the page's add does per entry.
|
|
57
|
+
*/
|
|
58
|
+
/** The fields' ids. */
|
|
59
|
+
export declare const OUTREACH_DNC_FIELDS: {
|
|
60
|
+
readonly entry: "entry";
|
|
61
|
+
readonly kind: "kind";
|
|
62
|
+
readonly note: "note";
|
|
63
|
+
readonly reason: "reason";
|
|
64
|
+
readonly source: "source";
|
|
65
|
+
readonly addedByUid: "addedByUid";
|
|
66
|
+
readonly addedAt: "addedAt";
|
|
67
|
+
readonly sequenceId: "sequenceId";
|
|
68
|
+
};
|
|
69
|
+
/** Why a file never changes an entry already on the list. */
|
|
70
|
+
export declare const OUTREACH_DNC_ADD_ONLY_REASON = "The do-not-contact list only grows from a file: an entry already on it is left as it is.";
|
|
71
|
+
/** The activity lines an import and its undo file, one per chunk written or reverted. */
|
|
72
|
+
export declare const OUTREACH_DO_NOT_CONTACT_IMPORT_ACTIVITY: {
|
|
73
|
+
readonly add: (counts: EntryCounts) => string;
|
|
74
|
+
readonly undo: (counts: EntryCounts) => string;
|
|
75
|
+
};
|
|
76
|
+
/** How many of each kind a chunk wrote or removed. */
|
|
77
|
+
export interface EntryCounts {
|
|
78
|
+
domains: number;
|
|
79
|
+
addresses: number;
|
|
80
|
+
}
|
|
81
|
+
/** The entry a cell or an id names: an address, by its key, or a domain. */
|
|
82
|
+
export type OutreachDoNotContactTarget = {
|
|
83
|
+
kind: 'address';
|
|
84
|
+
id: string;
|
|
85
|
+
key: string;
|
|
86
|
+
email: string | null;
|
|
87
|
+
} | {
|
|
88
|
+
kind: 'domain';
|
|
89
|
+
id: string;
|
|
90
|
+
domain: string;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The entry a cell names, or `null` for a value that is neither an address
|
|
94
|
+
* nor a domain. An address-looking value that is not a usable address is
|
|
95
|
+
* refused rather than read as its domain: a typo in one address must not
|
|
96
|
+
* block a whole company.
|
|
97
|
+
*/
|
|
98
|
+
export declare function outreachDoNotContactTarget(value: unknown): OutreachDoNotContactTarget | null;
|
|
99
|
+
/** The entry an Aglyn ID names, or `null`. */
|
|
100
|
+
export declare function outreachDoNotContactTargetFromId(id: unknown): OutreachDoNotContactTarget | null;
|
|
101
|
+
/** The resource's catalog, the same for every organization. */
|
|
102
|
+
export declare function outreachDoNotContactCatalog(): TransferCatalogInput;
|
|
103
|
+
/** Note is written with a new entry and never onto one already on the list. */
|
|
104
|
+
export declare const OUTREACH_DNC_LOCKED_RULES: readonly TransferLockedRule[];
|
|
105
|
+
export interface OutreachDoNotContactTransferDeps {
|
|
106
|
+
firestore: FirebaseFirestore.Firestore;
|
|
107
|
+
now?: () => number;
|
|
108
|
+
/** The member's standing in the organization, as the route gate resolves it. */
|
|
109
|
+
resolveOrgPermissions(uid: string, context: {
|
|
110
|
+
orgId: string;
|
|
111
|
+
}): Promise<OutreachMembership>;
|
|
112
|
+
/** The organization document, or `null` when there is none. */
|
|
113
|
+
readOrg(orgId: string): Promise<Record<string, unknown> | null>;
|
|
114
|
+
logOrgActivity(orgId: string, actor: {
|
|
115
|
+
uid: string | null;
|
|
116
|
+
email?: string | null;
|
|
117
|
+
}, action: string, target: {
|
|
118
|
+
type: 'org';
|
|
119
|
+
id: string;
|
|
120
|
+
}): Promise<void>;
|
|
121
|
+
}
|
|
122
|
+
/** The hooks this resource answers, every one present. */
|
|
123
|
+
export type OutreachDoNotContactTransferResource = PluginTransferResource & Required<Pick<TransferRecordsHooks, 'fields' | 'count' | 'readPage' | 'lookup' | 'plan' | 'lockedRules' | 'apply' | 'revert'>>;
|
|
124
|
+
/**
|
|
125
|
+
* The do-not-contact list's server half. Specs pass an in-memory Firestore;
|
|
126
|
+
* the console's declarations pass the Admin SDK's and the platform's
|
|
127
|
+
* membership, organization and activity readers.
|
|
128
|
+
*/
|
|
129
|
+
export declare function createOutreachDoNotContactTransferResource(deps: OutreachDoNotContactTransferDeps): OutreachDoNotContactTransferResource;
|