@aglyn/plugins-crm 1.0.0-beta.224 → 1.0.0-beta.226
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 +13 -13
- package/src/lib/components/lead-properties-card.js +6 -1
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/leads-section.js +2 -5
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/picklist-values-card.d.ts +5 -4
- package/src/lib/components/picklist-values-card.js +42 -6
- package/src/lib/components/picklist-values-card.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +4 -3
- package/src/lib/model/crm-list-query.js +4 -3
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +2 -7
- package/src/lib/model/lead-filters.js +10 -28
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-source-direction.d.ts +3 -9
- package/src/lib/model/lead-source-direction.js +4 -18
- package/src/lib/model/lead-source-direction.js.map +1 -1
- package/src/lib/model/picklist-values.d.ts +25 -4
- package/src/lib/model/picklist-values.js +13 -4
- package/src/lib/model/picklist-values.js.map +1 -1
- package/src/lib/server/picklist-values.d.ts +14 -1
- package/src/lib/server/picklist-values.js +119 -20
- package/src/lib/server/picklist-values.js.map +1 -1
- package/src/lib/server/record-origin.js +6 -2
- package/src/lib/server/record-origin.js.map +1 -1
|
@@ -14,10 +14,10 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { consentGroupForHost, CONTACT_FACETS_FIELD, CRM_COLLECTIONS, CRM_CONTACT_FACET_KEYS_FIELD, CRM_LEAD_SOURCE_PICKLIST, crmContactFacetKeys, crmPicklistDefinition, crmPicklistKey, effectiveCrmPicklist, newResourceScopeFields, normalizeCrmPicklist, ORG_SCOPE_TOKEN } from "@aglyn/aglyn/server";
|
|
17
|
+
*/ import { consentGroupForHost, CONTACT_FACETS_FIELD, CRM_COLLECTIONS, CRM_CONTACT_FACET_KEYS_FIELD, CRM_LEAD_SOURCE_PICKLIST, crmContactFacetKeys, crmPicklistDefinition, crmPicklistKey, crmPicklistValueByLabel, effectiveCrmPicklist, newResourceScopeFields, normalizeCrmPicklist, ORG_SCOPE_TOKEN } from "@aglyn/aglyn/server";
|
|
18
18
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
19
19
|
import { FieldPath, FieldValue } from "firebase-admin/firestore";
|
|
20
|
-
import { deletePicklistValue, renamePicklistValue } from "../model/picklist-values.js";
|
|
20
|
+
import { addPicklistValue, deletePicklistValue, picklistKeepsGroups, renamePicklistValue, setPicklistValueGroup } from "../model/picklist-values.js";
|
|
21
21
|
import { orgHostIds, readCrmRouteScope } from "./org-caller.js";
|
|
22
22
|
import { authorizeCrmWriter } from "./task-routes.js";
|
|
23
23
|
/** What the suite gate names when a plan without the CRM asks. */ const suiteAct = (definition)=>`Editing the organization's ${definition.plural}`;
|
|
@@ -110,6 +110,45 @@ import { authorizeCrmWriter } from "./task-routes.js";
|
|
|
110
110
|
after = (_page_docs_ = page.docs[page.docs.length - 1]) != null ? _page_docs_ : null;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
/** The group a label is filed under in `picklist` — `null` for none, or a label it does not hold. */ export function picklistGroupOf(picklist, label) {
|
|
114
|
+
var _ref;
|
|
115
|
+
var _crmPicklistValueByLabel;
|
|
116
|
+
return (_ref = label === null ? null : (_crmPicklistValueByLabel = crmPicklistValueByLabel(picklist, label)) == null ? void 0 : _crmPicklistValueByLabel.group) != null ? _ref : null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Rewrite a target's GROUP field (AGL-3577) — a lead's
|
|
120
|
+
* `leadSourceDirection` — on every record holding `label` in any spelling
|
|
121
|
+
* (by its `keyField`) whose group is not `group` already. Answers how many
|
|
122
|
+
* were written. The field is derived, like the list keys a restamp writes,
|
|
123
|
+
* so `updatedAt` is left alone.
|
|
124
|
+
*
|
|
125
|
+
* Paged by document id rather than re-run: a record already holding the
|
|
126
|
+
* group stays in the query's answer.
|
|
127
|
+
*/ export async function regroupEverywhere(firestore, records, target, label, group) {
|
|
128
|
+
const key = crmPicklistKey(label);
|
|
129
|
+
if (!key || !target.keyField || !target.groupField) return 0;
|
|
130
|
+
const query = records.where(target.keyField, '==', key).orderBy(FieldPath.documentId()).limit(BATCH_SIZE);
|
|
131
|
+
let written = 0;
|
|
132
|
+
let after = null;
|
|
133
|
+
for(;;){
|
|
134
|
+
var _page_docs_;
|
|
135
|
+
const page = await (after ? query.startAfter(after) : query).get();
|
|
136
|
+
if (page.empty) return written;
|
|
137
|
+
const batch = firestore.batch();
|
|
138
|
+
let changed = 0;
|
|
139
|
+
for (const snapshot of page.docs){
|
|
140
|
+
if (snapshot.get(target.groupField) === group) continue;
|
|
141
|
+
batch.update(snapshot.ref, {
|
|
142
|
+
[target.groupField]: group
|
|
143
|
+
});
|
|
144
|
+
changed += 1;
|
|
145
|
+
}
|
|
146
|
+
if (changed) await batch.commit();
|
|
147
|
+
written += changed;
|
|
148
|
+
if (page.size < BATCH_SIZE) return written;
|
|
149
|
+
after = (_page_docs_ = page.docs[page.docs.length - 1]) != null ? _page_docs_ : null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
113
152
|
/** A contact as it reads once one holder's facet field holds `to` (or nothing, for `null`). */ function withFacetValue(data, groupId, field, to) {
|
|
114
153
|
var _data_CONTACT_FACETS_FIELD, _facets_groupId;
|
|
115
154
|
const facets = _extends({}, (_data_CONTACT_FACETS_FIELD = data[CONTACT_FACETS_FIELD]) != null ? _data_CONTACT_FACETS_FIELD : {});
|
|
@@ -128,9 +167,21 @@ import { authorizeCrmWriter } from "./task-routes.js";
|
|
|
128
167
|
* picklist fixed to the lead source.
|
|
129
168
|
*
|
|
130
169
|
* Body: `{ hostId | orgId, picklistId, action: 'rename', valueId, label }` or
|
|
131
|
-
* `{ hostId | orgId, picklistId, action: 'delete', valueId, replaceWith: label | null }
|
|
170
|
+
* `{ hostId | orgId, picklistId, action: 'delete', valueId, replaceWith: label | null }`,
|
|
171
|
+
* and, on a list whose records keep the value's group (`picklistKeepsGroups`,
|
|
172
|
+
* AGL-3577), `{ …, action: 'group', valueId, group: id | null }` or
|
|
173
|
+
* `{ …, action: 'add', label, group?, meaning? }`.
|
|
132
174
|
* `picklistId` is one of `CRM_PICKLIST_DEFINITIONS`; anything else is refused.
|
|
133
175
|
*
|
|
176
|
+
* ## A value's group on its records
|
|
177
|
+
*
|
|
178
|
+
* A lead keeps its lead source's group as `leadSourceDirection` — the field
|
|
179
|
+
* the Leads list's direction filter asks. Any move can change the group a
|
|
180
|
+
* label has: a regroup, an add of a value records already hold, a rename
|
|
181
|
+
* onto such a label, a delete. So after the move, every target naming a
|
|
182
|
+
* `groupField` has it rewritten on the records holding the old label and
|
|
183
|
+
* the new one, from the list as it now stands.
|
|
184
|
+
*
|
|
134
185
|
* Records store the value's LABEL (see the picklist block in `crm.ts`), so
|
|
135
186
|
* both moves change records as well as the list: every record the
|
|
136
187
|
* definition's `targets` name that holds the old label is rewritten to the
|
|
@@ -160,7 +211,7 @@ import { authorizeCrmWriter } from "./task-routes.js";
|
|
|
160
211
|
* reveals nothing about the records but their count.
|
|
161
212
|
*/ function picklistValuesHandler(fixed) {
|
|
162
213
|
return async (req, res)=>{
|
|
163
|
-
var _req_body, _body_valueId;
|
|
214
|
+
var _req_body, _body_valueId, _body_label;
|
|
164
215
|
if (req.method !== 'POST') {
|
|
165
216
|
res.setHeader('Allow', 'POST');
|
|
166
217
|
res.status(405).json({
|
|
@@ -186,12 +237,20 @@ import { authorizeCrmWriter } from "./task-routes.js";
|
|
|
186
237
|
const picklistId = definition.id;
|
|
187
238
|
const action = body['action'];
|
|
188
239
|
const valueId = String((_body_valueId = body['valueId']) != null ? _body_valueId : '').trim().slice(0, 64);
|
|
189
|
-
|
|
240
|
+
const regrouping = action === 'group' || action === 'add';
|
|
241
|
+
if (regrouping && !picklistKeepsGroups(definition)) {
|
|
242
|
+
res.status(400).json({
|
|
243
|
+
error: `The ${definition.plural} are added and grouped on the Fields page.`
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (!(action === 'rename' || action === 'delete' || regrouping) || (action === 'add' ? !String((_body_label = body['label']) != null ? _body_label : '').trim() : !valueId)) {
|
|
190
248
|
res.status(400).json({
|
|
191
249
|
error: 'Name a value, and whether to rename or delete it.'
|
|
192
250
|
});
|
|
193
251
|
return;
|
|
194
252
|
}
|
|
253
|
+
const asGroup = (raw)=>typeof raw === 'string' && raw ? raw : null;
|
|
195
254
|
const replaceWith = body['replaceWith'] === null || body['replaceWith'] === undefined ? null : String(body['replaceWith']);
|
|
196
255
|
try {
|
|
197
256
|
const writer = await authorizeCrmWriter(req, scope, {
|
|
@@ -208,16 +267,24 @@ import { authorizeCrmWriter } from "./task-routes.js";
|
|
|
208
267
|
* THE LIST. Read and written in one transaction, so two admins editing
|
|
209
268
|
* at once cannot each write a list missing the other's change.
|
|
210
269
|
*/ const moved = await firestore.runTransaction(async (transaction)=>{
|
|
211
|
-
var _body_label, _ref, _ref1;
|
|
212
|
-
var _move_picklist_values_find, _move_picklist_values_find1;
|
|
270
|
+
var _body_label, _body_label1, _ref, _ref1, _ref2, _ref3, _ref4;
|
|
271
|
+
var _move_picklist_values_, _move_picklist_values_find, _move_picklist_values_find1;
|
|
213
272
|
const snapshot = await transaction.get(listRef);
|
|
214
273
|
const before = effectiveCrmPicklist(picklistId, snapshot.data());
|
|
215
|
-
const value = before.values.find((entry)=>entry.id === valueId);
|
|
216
|
-
if (!value)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
274
|
+
const value = action === 'add' ? null : before.values.find((entry)=>entry.id === valueId);
|
|
275
|
+
if (action !== 'add' && !value) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
error: 'That value is no longer in the list.'
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const move = action === 'add' ? addPicklistValue(definition, before, String((_body_label = body['label']) != null ? _body_label : ''), {
|
|
282
|
+
group: asGroup(body['group']),
|
|
283
|
+
meaning: asGroup(body['meaning'])
|
|
284
|
+
}) : action === 'group' ? {
|
|
285
|
+
ok: true,
|
|
286
|
+
picklist: setPicklistValueGroup(definition, before, valueId, asGroup(body['group']))
|
|
287
|
+
} : action === 'rename' ? renamePicklistValue(before, valueId, String((_body_label1 = body['label']) != null ? _body_label1 : '')) : deletePicklistValue(definition, before, valueId, replaceWith);
|
|
221
288
|
if (move.ok === false) return move;
|
|
222
289
|
const created = normalizeCrmPicklist(snapshot.data()) === null;
|
|
223
290
|
transaction.set(listRef, _extends({
|
|
@@ -232,11 +299,14 @@ import { authorizeCrmWriter } from "./task-routes.js";
|
|
|
232
299
|
])) : {}), {
|
|
233
300
|
merge: true
|
|
234
301
|
});
|
|
235
|
-
|
|
302
|
+
// A regroup and an add move no label: `from` and `to` are the value's own.
|
|
303
|
+
const own = action === 'add' ? (_ref = (_move_picklist_values_ = move.picklist.values[move.picklist.values.length - 1]) == null ? void 0 : _move_picklist_values_.label) != null ? _ref : '' : (_ref1 = value == null ? void 0 : value.label) != null ? _ref1 : '';
|
|
304
|
+
const to = action === 'rename' ? (_ref2 = (_move_picklist_values_find = move.picklist.values.find((entry)=>entry.id === valueId)) == null ? void 0 : _move_picklist_values_find.label) != null ? _ref2 : null : regrouping ? own : replaceWith === null ? null : (_ref3 = (_move_picklist_values_find1 = move.picklist.values.find((entry)=>entry.label.toLowerCase() === replaceWith.trim().toLowerCase())) == null ? void 0 : _move_picklist_values_find1.label) != null ? _ref3 : null;
|
|
236
305
|
return {
|
|
237
306
|
ok: true,
|
|
238
|
-
from: value.label,
|
|
239
|
-
to
|
|
307
|
+
from: regrouping ? own : (_ref4 = value == null ? void 0 : value.label) != null ? _ref4 : '',
|
|
308
|
+
to,
|
|
309
|
+
after: move.picklist
|
|
240
310
|
};
|
|
241
311
|
});
|
|
242
312
|
if (moved.ok === false) {
|
|
@@ -257,11 +327,40 @@ import { authorizeCrmWriter } from "./task-routes.js";
|
|
|
257
327
|
for (const target of definition.targets){
|
|
258
328
|
var _updated, _target_object, _, _updated_target_object;
|
|
259
329
|
(_ = (_updated = updated)[_target_object = target.object]) != null ? _ : _updated[_target_object] = 0;
|
|
330
|
+
if (target.groupField && !target.facet && !target.arrayKey) {
|
|
331
|
+
var _updated_target_object1;
|
|
332
|
+
// The group each label now has, on every record holding either (AGL-3577).
|
|
333
|
+
const labels = new Map();
|
|
334
|
+
for (const label of [
|
|
335
|
+
moved.from,
|
|
336
|
+
moved.to
|
|
337
|
+
]){
|
|
338
|
+
const key = label === null ? null : crmPicklistKey(label);
|
|
339
|
+
if (key && label !== null) labels.set(key, label);
|
|
340
|
+
}
|
|
341
|
+
const regroup = async ()=>{
|
|
342
|
+
for (const label of labels.values()){
|
|
343
|
+
var _updated_target_object;
|
|
344
|
+
updated[target.object] = ((_updated_target_object = updated[target.object]) != null ? _updated_target_object : 0) + await regroupEverywhere(firestore, orgRef.collection(TARGET_COLLECTIONS[target.object]), target, label, picklistGroupOf(moved.after, label));
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
if (moved.from === moved.to) {
|
|
348
|
+
await regroup();
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
updated[target.object] = ((_updated_target_object1 = updated[target.object]) != null ? _updated_target_object1 : 0) + await replaceEverywhere(firestore, orgRef.collection(TARGET_COLLECTIONS[target.object]).where(target.field, '==', moved.from), target.field, moved.to, _extends({}, target.keyField ? {
|
|
352
|
+
[target.keyField]: crmPicklistKey(moved.to)
|
|
353
|
+
} : {}, {
|
|
354
|
+
[target.groupField]: picklistGroupOf(moved.after, moved.to)
|
|
355
|
+
}));
|
|
356
|
+
await regroup();
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
260
359
|
if (moved.from === moved.to) continue;
|
|
261
360
|
const records = orgRef.collection(TARGET_COLLECTIONS[target.object]);
|
|
262
361
|
if (target.arrayKey && target.keyField) {
|
|
263
|
-
var
|
|
264
|
-
updated[target.object] = ((
|
|
362
|
+
var _updated_target_object2;
|
|
363
|
+
updated[target.object] = ((_updated_target_object2 = updated[target.object]) != null ? _updated_target_object2 : 0) + await replaceInArrayEverywhere(firestore, records, _extends({}, target, {
|
|
265
364
|
arrayKey: target.arrayKey,
|
|
266
365
|
keyField: target.keyField
|
|
267
366
|
}), moved.from, moved.to);
|
|
@@ -275,9 +374,9 @@ import { authorizeCrmWriter } from "./task-routes.js";
|
|
|
275
374
|
].filter(Boolean);
|
|
276
375
|
}
|
|
277
376
|
for (const groupId of groupIds){
|
|
278
|
-
var
|
|
377
|
+
var _updated_target_object3;
|
|
279
378
|
const path = new FieldPath(CONTACT_FACETS_FIELD, groupId, target.field);
|
|
280
|
-
updated[target.object] = ((
|
|
379
|
+
updated[target.object] = ((_updated_target_object3 = updated[target.object]) != null ? _updated_target_object3 : 0) + await replaceEverywhere(firestore, records.where(path, '==', moved.from), path, moved.to, // A facet value a contact list filters by is keyed in
|
|
281
380
|
// `facetKeys` (AGL-3511), restamped from the record as moved.
|
|
282
381
|
(snapshot)=>({
|
|
283
382
|
[CRM_CONTACT_FACET_KEYS_FIELD]: crmContactFacetKeys(withFacetValue(snapshot.data(), groupId, target.field, moved.to))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/picklist-values.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 consentGroupForHost,\n CONTACT_FACETS_FIELD,\n CRM_COLLECTIONS,\n CRM_CONTACT_FACET_KEYS_FIELD,\n CRM_LEAD_SOURCE_PICKLIST,\n type CrmPicklist,\n type CrmPicklistDefinition,\n type CrmPicklistId,\n type CrmPicklistObject,\n type CrmPicklistTarget,\n crmContactFacetKeys,\n crmPicklistDefinition,\n crmPicklistKey,\n effectiveCrmPicklist,\n newResourceScopeFields,\n normalizeCrmPicklist,\n ORG_SCOPE_TOKEN,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport {\n deletePicklistValue,\n type PicklistMove,\n type PicklistValuesResponse,\n renamePicklistValue,\n} from '../model/picklist-values'\nimport { orgHostIds, readCrmRouteScope } from './org-caller'\nimport { authorizeCrmWriter } from './task-routes'\n\n/** What the suite gate names when a plan without the CRM asks. */\nconst suiteAct = (definition: CrmPicklistDefinition) =>\n `Editing the organization's ${definition.plural}`\n\n/** The org subcollection each target object's records live in. */\nconst TARGET_COLLECTIONS: Record<CrmPicklistObject, string> = {\n contact: 'contacts',\n lead: 'leads',\n company: CRM_COLLECTIONS.companies,\n deal: CRM_COLLECTIONS.deals,\n task: CRM_COLLECTIONS.tasks,\n}\n\n/** Writes per batch — under Firestore's 500, with room to spare. */\nconst BATCH_SIZE = 400\n\n/**\n * Set `field` to `to` (or remove it, for `null`) on every document `query`\n * finds holding `from`, a batch at a time, with `beside` written alongside.\n * Answers how many were written.\n *\n * The query is re-run after each batch rather than paged, because each\n * batch moves its documents OUT of the query's answer — the next run finds\n * only what is left, and a run that finds nothing is the end.\n */\nasync function replaceEverywhere(\n firestore: FirebaseFirestore.Firestore,\n query: FirebaseFirestore.Query,\n field: string | FirebaseFirestore.FieldPath,\n to: string | null,\n beside:\n | Record<string, unknown>\n | ((snapshot: FirebaseFirestore.QueryDocumentSnapshot) => Record<string, unknown>) = {},\n): Promise<number> {\n let written = 0\n for (;;) {\n const page = await query.limit(BATCH_SIZE).get()\n if (page.empty) return written\n const batch = firestore.batch()\n for (const snapshot of page.docs) {\n const alongside = typeof beside === 'function' ? beside(snapshot) : beside\n batch.update(\n snapshot.ref,\n field,\n to ?? FieldValue.delete(),\n 'updatedAt',\n FieldValue.serverTimestamp(),\n ...Object.entries(alongside).flat(),\n )\n }\n await batch.commit()\n written += page.size\n if (page.size < BATCH_SIZE) return written\n }\n}\n\n/**\n * Rewrite an ARRAY target (AGL-3521): every record whose `keyField` lists\n * the old label's key has each entry of `field` holding `from` at\n * `arrayKey` moved to `to` — or the entry's label removed, for `null` —\n * and its `keyField` restamped from the entries as moved. Answers how many\n * records were written.\n *\n * Paged by document id rather than re-run, because a rename that changes\n * only the label's case keeps its key, so a written record can stay in the\n * query's answer.\n */\nasync function replaceInArrayEverywhere(\n firestore: FirebaseFirestore.Firestore,\n records: FirebaseFirestore.CollectionReference,\n target: CrmPicklistTarget & { arrayKey: string; keyField: string },\n from: string,\n to: string | null,\n): Promise<number> {\n const fromKey = crmPicklistKey(from)\n if (!fromKey) return 0\n const query = records\n .where(target.keyField, 'array-contains', fromKey)\n .orderBy(FieldPath.documentId())\n .limit(BATCH_SIZE)\n let written = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n const page: FirebaseFirestore.QuerySnapshot = await (after ? query.startAfter(after) : query).get()\n if (page.empty) return written\n const batch = firestore.batch()\n let changed = 0\n for (const snapshot of page.docs) {\n const entries = snapshot.get(target.field)\n if (!Array.isArray(entries)) continue\n let moved = false\n const next = entries.map((entry: unknown) => {\n if (!entry || typeof entry !== 'object') return entry\n const row = entry as Record<string, unknown>\n if (row[target.arrayKey] !== from) return entry\n moved = true\n const rest = { ...row }\n delete rest[target.arrayKey]\n return to === null ? rest : { ...rest, [target.arrayKey]: to }\n })\n if (!moved) continue\n const keys = new Set<string>()\n for (const entry of next) {\n const key =\n entry && typeof entry === 'object'\n ? crmPicklistKey((entry as Record<string, unknown>)[target.arrayKey])\n : null\n if (key) keys.add(key)\n }\n batch.update(snapshot.ref, {\n [target.field]: next,\n [target.keyField]: [...keys],\n updatedAt: FieldValue.serverTimestamp(),\n })\n changed += 1\n }\n if (changed) await batch.commit()\n written += changed\n if (page.size < BATCH_SIZE) return written\n after = page.docs[page.docs.length - 1] ?? null\n }\n}\n\n/** A contact as it reads once one holder's facet field holds `to` (or nothing, for `null`). */\nfunction withFacetValue(\n data: Record<string, unknown>,\n groupId: string,\n field: string,\n to: string | null,\n): Record<string, unknown> {\n const facets = { ...((data[CONTACT_FACETS_FIELD] as Record<string, unknown> | undefined) ?? {}) }\n const facet = { ...((facets[groupId] as Record<string, unknown> | undefined) ?? {}) }\n if (to === null) delete facet[field]\n else facet[field] = to\n facets[groupId] = facet\n return { ...data, [CONTACT_FACETS_FIELD]: facets }\n}\n\n/**\n * `POST crm/picklist-values` — rename a value of one of the CRM's standard\n * picklists, or delete an added one and move its records to another\n * (AGL-3298, AGL-3510). `crm/lead-source-values` is the same route with the\n * picklist fixed to the lead source.\n *\n * Body: `{ hostId | orgId, picklistId, action: 'rename', valueId, label }` or\n * `{ hostId | orgId, picklistId, action: 'delete', valueId, replaceWith: label | null }`.\n * `picklistId` is one of `CRM_PICKLIST_DEFINITIONS`; anything else is refused.\n *\n * Records store the value's LABEL (see the picklist block in `crm.ts`), so\n * both moves change records as well as the list: every record the\n * definition's `targets` name that holds the old label is rewritten to the\n * new one — or, for a delete with no replacement, cleared. That is\n * Salesforce's Replace, done as part of the move, so a report grouped by\n * the field follows a rename instead of splitting into the old name and\n * the new. A standard value cannot be deleted, and on a definition with\n * meanings a delete's replacement must mean the same (see\n * `deletePicklistValue`).\n *\n * ## The list first, then the records\n *\n * The list is written in a transaction before any record: from that moment\n * no picker offers the old label and no import or API write accepts it, so\n * the sweep that follows cannot be outrun by a new record taking the name\n * it is removing. A sweep interrupted part-way leaves records holding a\n * label the list no longer has, which they show as \"(not in the list)\"; the\n * same request sent again finishes it, because the rename of a value to its\n * own current label is a no-op on the list and still sweeps.\n *\n * ## Who may\n *\n * Whoever may write the CRM — `authorizeCrmWriter`, the question every CRM\n * route asks — which is who the rules let edit the list itself. The sweep\n * reaches every record in the org whatever the caller's scope: it changes\n * one label to another that the same caller could already have picked, and\n * reveals nothing about the records but their count.\n */\nfunction picklistValuesHandler(fixed?: CrmPicklistId): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const definition = crmPicklistDefinition(fixed ?? body['picklistId'])\n if (!definition) {\n res.status(400).json({ error: 'Name one of the CRM’s picklists.' })\n return\n }\n const picklistId = definition.id as CrmPicklistId\n const action = body['action']\n const valueId = String(body['valueId'] ?? '').trim().slice(0, 64)\n if ((action !== 'rename' && action !== 'delete') || !valueId) {\n res.status(400).json({ error: 'Name a value, and whether to rename or delete it.' })\n return\n }\n const replaceWith =\n body['replaceWith'] === null || body['replaceWith'] === undefined\n ? null\n : String(body['replaceWith'])\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: suiteAct(definition) })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const listRef = orgRef.collection(CRM_COLLECTIONS.picklists).doc(picklistId)\n\n /*\n * THE LIST. Read and written in one transaction, so two admins editing\n * at once cannot each write a list missing the other's change.\n */\n const moved = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(listRef)\n const before: CrmPicklist = effectiveCrmPicklist(picklistId, snapshot.data())\n const value = before.values.find((entry) => entry.id === valueId)\n if (!value) return { ok: false as const, error: 'That value is no longer in the list.' }\n const move: PicklistMove =\n action === 'rename'\n ? renamePicklistValue(before, valueId, String(body['label'] ?? ''))\n : deletePicklistValue(definition, before, valueId, replaceWith)\n if (move.ok === false) return move\n const created = normalizeCrmPicklist(snapshot.data()) === null\n transaction.set(\n listRef,\n {\n values: move.picklist.values,\n defaultValueId: move.picklist.defaultValueId,\n updatedAt: FieldValue.serverTimestamp(),\n // The stamp every org-wide CRM document carries (see the Fields\n // page), written when this move is what creates the document.\n ...(created\n ? {\n hostId: scope.hostId || null,\n createdAt: FieldValue.serverTimestamp(),\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n }\n : {}),\n },\n { merge: true },\n )\n const to =\n action === 'rename'\n ? (move.picklist.values.find((entry) => entry.id === valueId)?.label ?? null)\n : replaceWith === null\n ? null\n : (move.picklist.values.find(\n (entry) => entry.label.toLowerCase() === replaceWith.trim().toLowerCase(),\n )?.label ?? null)\n return { ok: true as const, from: value.label, to }\n })\n if (moved.ok === false) {\n res.status(400).json({ error: moved.error })\n return\n }\n\n /*\n * THE RECORDS, target by target: a record's own field, with its query\n * key moved beside it; for an array target, each entry of a record's\n * list (a deal's contact roles); or, for a facet target, each holder's\n * facet on a contact — one query per consent group the org's sites\n * belong to.\n * A rename to the same label moves nothing, and says so.\n */\n const updated: PicklistValuesResponse['updated'] = {}\n let groupIds: string[] | null = null\n for (const target of definition.targets) {\n updated[target.object] ??= 0\n if (moved.from === moved.to) continue\n const records = orgRef.collection(TARGET_COLLECTIONS[target.object])\n if (target.arrayKey && target.keyField) {\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceInArrayEverywhere(\n firestore,\n records,\n { ...target, arrayKey: target.arrayKey, keyField: target.keyField },\n moved.from,\n moved.to,\n ))\n continue\n }\n if (target.facet) {\n if (!groupIds) {\n const hostIds = await orgHostIds(firestore, writer.orgId)\n groupIds = [\n ...new Set(hostIds.map((hostId) => consentGroupForHost(writer.org, hostId).groupId)),\n ].filter(Boolean)\n }\n for (const groupId of groupIds) {\n const path = new FieldPath(CONTACT_FACETS_FIELD, groupId, target.field)\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(path, '==', moved.from),\n path,\n moved.to,\n // A facet value a contact list filters by is keyed in\n // `facetKeys` (AGL-3511), restamped from the record as moved.\n (snapshot) => ({\n [CRM_CONTACT_FACET_KEYS_FIELD]: crmContactFacetKeys(\n withFacetValue(snapshot.data(), groupId, target.field, moved.to),\n ),\n }),\n ))\n }\n continue\n }\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(target.field, '==', moved.from),\n target.field,\n moved.to,\n target.keyField ? { [target.keyField]: crmPicklistKey(moved.to) } : {},\n ))\n }\n const answer: PicklistValuesResponse = { ok: true, updated }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] picklist-values failed', error)\n res.status(500).json({ error: `The ${definition.plural} could not be saved.` })\n }\n }\n}\n\n/** `POST crm/picklist-values` — any registered picklist, named by `picklistId`. */\nexport const crmPicklistValuesHandler: PluginApiHandler = picklistValuesHandler()\n\n/** `POST crm/lead-source-values` — the same route with the picklist fixed to the lead source. */\nexport const crmLeadSourceValuesHandler: PluginApiHandler =\n picklistValuesHandler(CRM_LEAD_SOURCE_PICKLIST)\n"],"names":["consentGroupForHost","CONTACT_FACETS_FIELD","CRM_COLLECTIONS","CRM_CONTACT_FACET_KEYS_FIELD","CRM_LEAD_SOURCE_PICKLIST","crmContactFacetKeys","crmPicklistDefinition","crmPicklistKey","effectiveCrmPicklist","newResourceScopeFields","normalizeCrmPicklist","ORG_SCOPE_TOKEN","firebaseAdmin","FieldPath","FieldValue","deletePicklistValue","renamePicklistValue","orgHostIds","readCrmRouteScope","authorizeCrmWriter","suiteAct","definition","plural","TARGET_COLLECTIONS","contact","lead","company","companies","deal","deals","task","tasks","BATCH_SIZE","replaceEverywhere","firestore","query","field","to","beside","written","page","limit","get","empty","batch","snapshot","docs","alongside","update","ref","delete","serverTimestamp","Object","entries","flat","commit","size","replaceInArrayEverywhere","records","target","from","fromKey","where","keyField","orderBy","documentId","after","startAfter","changed","Array","isArray","moved","next","map","entry","row","arrayKey","rest","keys","Set","key","add","updatedAt","length","withFacetValue","data","groupId","facets","facet","picklistValuesHandler","fixed","req","res","body","method","setHeader","status","json","error","scope","picklistId","id","action","valueId","String","trim","slice","replaceWith","undefined","writer","ok","app","orgRef","collection","doc","orgId","listRef","picklists","runTransaction","transaction","move","before","value","values","find","created","set","picklist","defaultValueId","hostId","createdAt","merge","label","toLowerCase","updated","groupIds","targets","object","hostIds","org","filter","Boolean","path","answer","console","crmPicklistValuesHandler","crmLeadSourceValuesHandler"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,mBAAmB,EACnBC,oBAAoB,EACpBC,eAAe,EACfC,4BAA4B,EAC5BC,wBAAwB,EAMxBC,mBAAmB,EACnBC,qBAAqB,EACrBC,cAAc,EACdC,oBAAoB,EACpBC,sBAAsB,EACtBC,oBAAoB,EACpBC,eAAe,QAEV,sBAAqB;AAC5B,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,SAAS,EAAEC,UAAU,QAAQ,2BAA0B;AAChE,SACEC,mBAAmB,EAGnBC,mBAAmB,QACd,8BAA0B;AACjC,SAASC,UAAU,EAAEC,iBAAiB,QAAQ,kBAAc;AAC5D,SAASC,kBAAkB,QAAQ,mBAAe;AAElD,gEAAgE,GAChE,MAAMC,WAAW,CAACC,aAChB,CAAC,2BAA2B,EAAEA,WAAWC,MAAM,EAAE;AAEnD,gEAAgE,GAChE,MAAMC,qBAAwD;IAC5DC,SAAS;IACTC,MAAM;IACNC,SAASxB,gBAAgByB,SAAS;IAClCC,MAAM1B,gBAAgB2B,KAAK;IAC3BC,MAAM5B,gBAAgB6B,KAAK;AAC7B;AAEA,kEAAkE,GAClE,MAAMC,aAAa;AAEnB;;;;;;;;CAQC,GACD,eAAeC,kBACbC,SAAsC,EACtCC,KAA8B,EAC9BC,KAA2C,EAC3CC,EAAiB,EACjBC,SAEuF,CAAC,CAAC;IAEzF,IAAIC,UAAU;IACd,OAAS;QACP,MAAMC,OAAO,MAAML,MAAMM,KAAK,CAACT,YAAYU,GAAG;QAC9C,IAAIF,KAAKG,KAAK,EAAE,OAAOJ;QACvB,MAAMK,QAAQV,UAAUU,KAAK;QAC7B,KAAK,MAAMC,YAAYL,KAAKM,IAAI,CAAE;YAChC,MAAMC,YAAY,OAAOT,WAAW,aAAaA,OAAOO,YAAYP;YACpEM,MAAMI,MAAM,CACVH,SAASI,GAAG,EACZb,OACAC,aAAAA,KAAMvB,WAAWoC,MAAM,IACvB,aACApC,WAAWqC,eAAe,OACvBC,OAAOC,OAAO,CAACN,WAAWO,IAAI;QAErC;QACA,MAAMV,MAAMW,MAAM;QAClBhB,WAAWC,KAAKgB,IAAI;QACpB,IAAIhB,KAAKgB,IAAI,GAAGxB,YAAY,OAAOO;IACrC;AACF;AAEA;;;;;;;;;;CAUC,GACD,eAAekB,yBACbvB,SAAsC,EACtCwB,OAA8C,EAC9CC,MAAkE,EAClEC,IAAY,EACZvB,EAAiB;IAEjB,MAAMwB,UAAUtD,eAAeqD;IAC/B,IAAI,CAACC,SAAS,OAAO;IACrB,MAAM1B,QAAQuB,QACXI,KAAK,CAACH,OAAOI,QAAQ,EAAE,kBAAkBF,SACzCG,OAAO,CAACnD,UAAUoD,UAAU,IAC5BxB,KAAK,CAACT;IACT,IAAIO,UAAU;IACd,IAAI2B,QAAwD;IAC5D,OAAS;YAqCC1B;QApCR,MAAMA,OAAwC,MAAM,AAAC0B,CAAAA,QAAQ/B,MAAMgC,UAAU,CAACD,SAAS/B,KAAI,EAAGO,GAAG;QACjG,IAAIF,KAAKG,KAAK,EAAE,OAAOJ;QACvB,MAAMK,QAAQV,UAAUU,KAAK;QAC7B,IAAIwB,UAAU;QACd,KAAK,MAAMvB,YAAYL,KAAKM,IAAI,CAAE;YAChC,MAAMO,UAAUR,SAASH,GAAG,CAACiB,OAAOvB,KAAK;YACzC,IAAI,CAACiC,MAAMC,OAAO,CAACjB,UAAU;YAC7B,IAAIkB,QAAQ;YACZ,MAAMC,OAAOnB,QAAQoB,GAAG,CAAC,CAACC;gBACxB,IAAI,CAACA,SAAS,OAAOA,UAAU,UAAU,OAAOA;gBAChD,MAAMC,MAAMD;gBACZ,IAAIC,GAAG,CAAChB,OAAOiB,QAAQ,CAAC,KAAKhB,MAAM,OAAOc;gBAC1CH,QAAQ;gBACR,MAAMM,OAAO,aAAKF;gBAClB,OAAOE,IAAI,CAAClB,OAAOiB,QAAQ,CAAC;gBAC5B,OAAOvC,OAAO,OAAOwC,OAAO,aAAKA;oBAAM,CAAClB,OAAOiB,QAAQ,CAAC,EAAEvC;;YAC5D;YACA,IAAI,CAACkC,OAAO;YACZ,MAAMO,OAAO,IAAIC;YACjB,KAAK,MAAML,SAASF,KAAM;gBACxB,MAAMQ,MACJN,SAAS,OAAOA,UAAU,WACtBnE,eAAe,AAACmE,KAAiC,CAACf,OAAOiB,QAAQ,CAAC,IAClE;gBACN,IAAII,KAAKF,KAAKG,GAAG,CAACD;YACpB;YACApC,MAAMI,MAAM,CAACH,SAASI,GAAG,EAAE;gBACzB,CAACU,OAAOvB,KAAK,CAAC,EAAEoC;gBAChB,CAACb,OAAOI,QAAQ,CAAC,EAAE;uBAAIe;iBAAK;gBAC5BI,WAAWpE,WAAWqC,eAAe;YACvC;YACAiB,WAAW;QACb;QACA,IAAIA,SAAS,MAAMxB,MAAMW,MAAM;QAC/BhB,WAAW6B;QACX,IAAI5B,KAAKgB,IAAI,GAAGxB,YAAY,OAAOO;QACnC2B,SAAQ1B,cAAAA,KAAKM,IAAI,CAACN,KAAKM,IAAI,CAACqC,MAAM,GAAG,EAAE,YAA/B3C,cAAmC;IAC7C;AACF;AAEA,6FAA6F,GAC7F,SAAS4C,eACPC,IAA6B,EAC7BC,OAAe,EACflD,KAAa,EACbC,EAAiB;QAEKgD,4BACDE;IADrB,MAAMA,SAAS,cAAOF,6BAAAA,IAAI,CAACpF,qBAAqB,YAA1BoF,6BAAsE,CAAC;IAC7F,MAAMG,QAAQ,cAAOD,kBAAAA,MAAM,CAACD,QAAQ,YAAfC,kBAA2D,CAAC;IACjF,IAAIlD,OAAO,MAAM,OAAOmD,KAAK,CAACpD,MAAM;SAC/BoD,KAAK,CAACpD,MAAM,GAAGC;IACpBkD,MAAM,CAACD,QAAQ,GAAGE;IAClB,OAAO,aAAKH;QAAM,CAACpF,qBAAqB,EAAEsF;;AAC5C;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GACD,SAASE,sBAAsBC,KAAqB;IAClD,OAAO,OAAOC,KAAKC;YAMHD,WAaSE;QAlBvB,IAAIF,IAAIG,MAAM,KAAK,QAAQ;YACzBF,IAAIG,SAAS,CAAC,SAAS;YACvBH,IAAII,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAqB;YACnD;QACF;QACA,MAAML,QAAQF,YAAAA,IAAIE,IAAI,YAARF,YAAY,CAAC;QAC3B,MAAMQ,QAAQjF,kBAAkB2E;QAChC,IAAI,CAACM,OAAO;YACVP,IAAII,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAiB;YAC/C;QACF;QACA,MAAM7E,aAAaf,sBAAsBoF,gBAAAA,QAASG,IAAI,CAAC,aAAa;QACpE,IAAI,CAACxE,YAAY;YACfuE,IAAII,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAmC;YACjE;QACF;QACA,MAAME,aAAa/E,WAAWgF,EAAE;QAChC,MAAMC,SAAST,IAAI,CAAC,SAAS;QAC7B,MAAMU,UAAUC,QAAOX,gBAAAA,IAAI,CAAC,UAAU,YAAfA,gBAAmB,IAAIY,IAAI,GAAGC,KAAK,CAAC,GAAG;QAC9D,IAAI,AAACJ,WAAW,YAAYA,WAAW,YAAa,CAACC,SAAS;YAC5DX,IAAII,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAoD;YAClF;QACF;QACA,MAAMS,cACJd,IAAI,CAAC,cAAc,KAAK,QAAQA,IAAI,CAAC,cAAc,KAAKe,YACpD,OACAJ,OAAOX,IAAI,CAAC,cAAc;QAEhC,IAAI;YACF,MAAMgB,SAAS,MAAM1F,mBAAmBwE,KAAKQ,OAAO;gBAAE/E,UAAUA,SAASC;YAAY;YACrF,IAAIwF,OAAOC,EAAE,KAAK,OAAO;gBACvBlB,IAAII,MAAM,CAACa,OAAOb,MAAM,EAAEC,IAAI,CAACY,OAAOhB,IAAI;gBAC1C;YACF;YACA,MAAM3D,YAAYtB,cAAcmG,GAAG,GAAG7E,SAAS;YAC/C,MAAM8E,SAAS9E,UAAU+E,UAAU,CAAC,QAAQC,GAAG,CAACL,OAAOM,KAAK;YAC5D,MAAMC,UAAUJ,OAAOC,UAAU,CAAC/G,gBAAgBmH,SAAS,EAAEH,GAAG,CAACd;YAEjE;;;OAGC,GACD,MAAM7B,QAAQ,MAAMrC,UAAUoF,cAAc,CAAC,OAAOC;oBAOA1B;oBAwB3C2B,4BAGEA;gBAjCT,MAAM3E,WAAW,MAAM0E,YAAY7E,GAAG,CAAC0E;gBACvC,MAAMK,SAAsBjH,qBAAqB4F,YAAYvD,SAASwC,IAAI;gBAC1E,MAAMqC,QAAQD,OAAOE,MAAM,CAACC,IAAI,CAAC,CAAClD,QAAUA,MAAM2B,EAAE,KAAKE;gBACzD,IAAI,CAACmB,OAAO,OAAO;oBAAEZ,IAAI;oBAAgBZ,OAAO;gBAAuC;gBACvF,MAAMsB,OACJlB,WAAW,WACPtF,oBAAoByG,QAAQlB,SAASC,QAAOX,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAiB,OAC7D9E,oBAAoBM,YAAYoG,QAAQlB,SAASI;gBACvD,IAAIa,KAAKV,EAAE,KAAK,OAAO,OAAOU;gBAC9B,MAAMK,UAAUnH,qBAAqBmC,SAASwC,IAAI,QAAQ;gBAC1DkC,YAAYO,GAAG,CACbV,SACA;oBACEO,QAAQH,KAAKO,QAAQ,CAACJ,MAAM;oBAC5BK,gBAAgBR,KAAKO,QAAQ,CAACC,cAAc;oBAC5C9C,WAAWpE,WAAWqC,eAAe;mBAGjC0E,UACA;oBACEI,QAAQ9B,MAAM8B,MAAM,IAAI;oBACxBC,WAAWpH,WAAWqC,eAAe;mBAClC1C,uBAAuB;oBAACE;iBAAgB,KAE7C,CAAC,IAEP;oBAAEwH,OAAO;gBAAK;gBAEhB,MAAM9F,KACJiE,WAAW,oBACNkB,6BAAAA,KAAKO,QAAQ,CAACJ,MAAM,CAACC,IAAI,CAAC,CAAClD,QAAUA,MAAM2B,EAAE,KAAKE,6BAAlDiB,2BAA4DY,KAAK,mBAAI,OACtEzB,gBAAgB,OACd,iBACCa,8BAAAA,KAAKO,QAAQ,CAACJ,MAAM,CAACC,IAAI,CACxB,CAAClD,QAAUA,MAAM0D,KAAK,CAACC,WAAW,OAAO1B,YAAYF,IAAI,GAAG4B,WAAW,wBADxEb,4BAEEY,KAAK,oBAAI;gBACpB,OAAO;oBAAEtB,IAAI;oBAAelD,MAAM8D,MAAMU,KAAK;oBAAE/F;gBAAG;YACpD;YACA,IAAIkC,MAAMuC,EAAE,KAAK,OAAO;gBACtBlB,IAAII,MAAM,CAAC,KAAKC,IAAI,CAAC;oBAAEC,OAAO3B,MAAM2B,KAAK;gBAAC;gBAC1C;YACF;YAEA;;;;;;;OAOC,GACD,MAAMoC,UAA6C,CAAC;YACpD,IAAIC,WAA4B;YAChC,KAAK,MAAM5E,UAAUtC,WAAWmH,OAAO,CAAE;oBACvCF,UAAQ3E,gBAAR2E,GA2CGA;iBA3CHA,IAAAA,CAAAA,WAAAA,QAAO,CAAC3E,iBAAAA,OAAO8E,MAAM,CAAC,YAAtBH,IAAAA,QAAO,CAAC3E,eAAc,GAAK;gBAC3B,IAAIY,MAAMX,IAAI,KAAKW,MAAMlC,EAAE,EAAE;gBAC7B,MAAMqB,UAAUsD,OAAOC,UAAU,CAAC1F,kBAAkB,CAACoC,OAAO8E,MAAM,CAAC;gBACnE,IAAI9E,OAAOiB,QAAQ,IAAIjB,OAAOI,QAAQ,EAAE;wBAEnCuE;oBADHA,OAAO,CAAC3E,OAAO8E,MAAM,CAAC,GACpB,EAACH,0BAAAA,OAAO,CAAC3E,OAAO8E,MAAM,CAAC,YAAtBH,0BAA0B,KAC1B,MAAM7E,yBACLvB,WACAwB,SACA,aAAKC;wBAAQiB,UAAUjB,OAAOiB,QAAQ;wBAAEb,UAAUJ,OAAOI,QAAQ;wBACjEQ,MAAMX,IAAI,EACVW,MAAMlC,EAAE;oBAEZ;gBACF;gBACA,IAAIsB,OAAO6B,KAAK,EAAE;oBAChB,IAAI,CAAC+C,UAAU;wBACb,MAAMG,UAAU,MAAMzH,WAAWiB,WAAW2E,OAAOM,KAAK;wBACxDoB,WAAW;+BACN,IAAIxD,IAAI2D,QAAQjE,GAAG,CAAC,CAACwD,SAAWjI,oBAAoB6G,OAAO8B,GAAG,EAAEV,QAAQ3C,OAAO;yBACnF,CAACsD,MAAM,CAACC;oBACX;oBACA,KAAK,MAAMvD,WAAWiD,SAAU;4BAG3BD;wBAFH,MAAMQ,OAAO,IAAIjI,UAAUZ,sBAAsBqF,SAAS3B,OAAOvB,KAAK;wBACtEkG,OAAO,CAAC3E,OAAO8E,MAAM,CAAC,GACpB,EAACH,0BAAAA,OAAO,CAAC3E,OAAO8E,MAAM,CAAC,YAAtBH,0BAA0B,KAC1B,MAAMrG,kBACLC,WACAwB,QAAQI,KAAK,CAACgF,MAAM,MAAMvE,MAAMX,IAAI,GACpCkF,MACAvE,MAAMlC,EAAE,EACR,sDAAsD;wBACtD,8DAA8D;wBAC9D,CAACQ,WAAc,CAAA;gCACb,CAAC1C,6BAA6B,EAAEE,oBAC9B+E,eAAevC,SAASwC,IAAI,IAAIC,SAAS3B,OAAOvB,KAAK,EAAEmC,MAAMlC,EAAE;4BAEnE,CAAA;oBAEN;oBACA;gBACF;gBACAiG,OAAO,CAAC3E,OAAO8E,MAAM,CAAC,GACpB,EAACH,yBAAAA,OAAO,CAAC3E,OAAO8E,MAAM,CAAC,YAAtBH,yBAA0B,KAC1B,MAAMrG,kBACLC,WACAwB,QAAQI,KAAK,CAACH,OAAOvB,KAAK,EAAE,MAAMmC,MAAMX,IAAI,GAC5CD,OAAOvB,KAAK,EACZmC,MAAMlC,EAAE,EACRsB,OAAOI,QAAQ,GAAG;oBAAE,CAACJ,OAAOI,QAAQ,CAAC,EAAExD,eAAegE,MAAMlC,EAAE;gBAAE,IAAI,CAAC;YAE3E;YACA,MAAM0G,SAAiC;gBAAEjC,IAAI;gBAAMwB;YAAQ;YAC3D1C,IAAII,MAAM,CAAC,KAAKC,IAAI,CAAC8C;QACvB,EAAE,OAAO7C,OAAO;YACd8C,QAAQ9C,KAAK,CAAC,gCAAgCA;YAC9CN,IAAII,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO,CAAC,IAAI,EAAE7E,WAAWC,MAAM,CAAC,oBAAoB,CAAC;YAAC;QAC/E;IACF;AACF;AAEA,iFAAiF,GACjF,OAAO,MAAM2H,2BAA6CxD,wBAAuB;AAEjF,+FAA+F,GAC/F,OAAO,MAAMyD,6BACXzD,sBAAsBrF,0BAAyB"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/picklist-values.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 consentGroupForHost,\n CONTACT_FACETS_FIELD,\n CRM_COLLECTIONS,\n CRM_CONTACT_FACET_KEYS_FIELD,\n CRM_LEAD_SOURCE_PICKLIST,\n type CrmPicklist,\n type CrmPicklistDefinition,\n type CrmPicklistId,\n type CrmPicklistObject,\n type CrmPicklistTarget,\n crmContactFacetKeys,\n crmPicklistDefinition,\n crmPicklistKey,\n crmPicklistValueByLabel,\n effectiveCrmPicklist,\n newResourceScopeFields,\n normalizeCrmPicklist,\n ORG_SCOPE_TOKEN,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport {\n addPicklistValue,\n deletePicklistValue,\n picklistKeepsGroups,\n type PicklistMove,\n type PicklistValuesResponse,\n renamePicklistValue,\n setPicklistValueGroup,\n} from '../model/picklist-values'\nimport { orgHostIds, readCrmRouteScope } from './org-caller'\nimport { authorizeCrmWriter } from './task-routes'\n\n/** What the suite gate names when a plan without the CRM asks. */\nconst suiteAct = (definition: CrmPicklistDefinition) =>\n `Editing the organization's ${definition.plural}`\n\n/** The org subcollection each target object's records live in. */\nconst TARGET_COLLECTIONS: Record<CrmPicklistObject, string> = {\n contact: 'contacts',\n lead: 'leads',\n company: CRM_COLLECTIONS.companies,\n deal: CRM_COLLECTIONS.deals,\n task: CRM_COLLECTIONS.tasks,\n}\n\n/** Writes per batch — under Firestore's 500, with room to spare. */\nconst BATCH_SIZE = 400\n\n/**\n * Set `field` to `to` (or remove it, for `null`) on every document `query`\n * finds holding `from`, a batch at a time, with `beside` written alongside.\n * Answers how many were written.\n *\n * The query is re-run after each batch rather than paged, because each\n * batch moves its documents OUT of the query's answer — the next run finds\n * only what is left, and a run that finds nothing is the end.\n */\nasync function replaceEverywhere(\n firestore: FirebaseFirestore.Firestore,\n query: FirebaseFirestore.Query,\n field: string | FirebaseFirestore.FieldPath,\n to: string | null,\n beside:\n | Record<string, unknown>\n | ((snapshot: FirebaseFirestore.QueryDocumentSnapshot) => Record<string, unknown>) = {},\n): Promise<number> {\n let written = 0\n for (;;) {\n const page = await query.limit(BATCH_SIZE).get()\n if (page.empty) return written\n const batch = firestore.batch()\n for (const snapshot of page.docs) {\n const alongside = typeof beside === 'function' ? beside(snapshot) : beside\n batch.update(\n snapshot.ref,\n field,\n to ?? FieldValue.delete(),\n 'updatedAt',\n FieldValue.serverTimestamp(),\n ...Object.entries(alongside).flat(),\n )\n }\n await batch.commit()\n written += page.size\n if (page.size < BATCH_SIZE) return written\n }\n}\n\n/**\n * Rewrite an ARRAY target (AGL-3521): every record whose `keyField` lists\n * the old label's key has each entry of `field` holding `from` at\n * `arrayKey` moved to `to` — or the entry's label removed, for `null` —\n * and its `keyField` restamped from the entries as moved. Answers how many\n * records were written.\n *\n * Paged by document id rather than re-run, because a rename that changes\n * only the label's case keeps its key, so a written record can stay in the\n * query's answer.\n */\nasync function replaceInArrayEverywhere(\n firestore: FirebaseFirestore.Firestore,\n records: FirebaseFirestore.CollectionReference,\n target: CrmPicklistTarget & { arrayKey: string; keyField: string },\n from: string,\n to: string | null,\n): Promise<number> {\n const fromKey = crmPicklistKey(from)\n if (!fromKey) return 0\n const query = records\n .where(target.keyField, 'array-contains', fromKey)\n .orderBy(FieldPath.documentId())\n .limit(BATCH_SIZE)\n let written = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n const page: FirebaseFirestore.QuerySnapshot = await (after ? query.startAfter(after) : query).get()\n if (page.empty) return written\n const batch = firestore.batch()\n let changed = 0\n for (const snapshot of page.docs) {\n const entries = snapshot.get(target.field)\n if (!Array.isArray(entries)) continue\n let moved = false\n const next = entries.map((entry: unknown) => {\n if (!entry || typeof entry !== 'object') return entry\n const row = entry as Record<string, unknown>\n if (row[target.arrayKey] !== from) return entry\n moved = true\n const rest = { ...row }\n delete rest[target.arrayKey]\n return to === null ? rest : { ...rest, [target.arrayKey]: to }\n })\n if (!moved) continue\n const keys = new Set<string>()\n for (const entry of next) {\n const key =\n entry && typeof entry === 'object'\n ? crmPicklistKey((entry as Record<string, unknown>)[target.arrayKey])\n : null\n if (key) keys.add(key)\n }\n batch.update(snapshot.ref, {\n [target.field]: next,\n [target.keyField]: [...keys],\n updatedAt: FieldValue.serverTimestamp(),\n })\n changed += 1\n }\n if (changed) await batch.commit()\n written += changed\n if (page.size < BATCH_SIZE) return written\n after = page.docs[page.docs.length - 1] ?? null\n }\n}\n\n/** The group a label is filed under in `picklist` — `null` for none, or a label it does not hold. */\nexport function picklistGroupOf(picklist: CrmPicklist, label: string | null): string | null {\n return (label === null ? null : crmPicklistValueByLabel(picklist, label)?.group) ?? null\n}\n\n/**\n * Rewrite a target's GROUP field (AGL-3577) — a lead's\n * `leadSourceDirection` — on every record holding `label` in any spelling\n * (by its `keyField`) whose group is not `group` already. Answers how many\n * were written. The field is derived, like the list keys a restamp writes,\n * so `updatedAt` is left alone.\n *\n * Paged by document id rather than re-run: a record already holding the\n * group stays in the query's answer.\n */\nexport async function regroupEverywhere(\n firestore: FirebaseFirestore.Firestore,\n records: FirebaseFirestore.CollectionReference,\n target: CrmPicklistTarget,\n label: string,\n group: string | null,\n): Promise<number> {\n const key = crmPicklistKey(label)\n if (!key || !target.keyField || !target.groupField) return 0\n const query = records\n .where(target.keyField, '==', key)\n .orderBy(FieldPath.documentId())\n .limit(BATCH_SIZE)\n let written = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n const page: FirebaseFirestore.QuerySnapshot = await (after ? query.startAfter(after) : query).get()\n if (page.empty) return written\n const batch = firestore.batch()\n let changed = 0\n for (const snapshot of page.docs) {\n if (snapshot.get(target.groupField) === group) continue\n batch.update(snapshot.ref, { [target.groupField]: group })\n changed += 1\n }\n if (changed) await batch.commit()\n written += changed\n if (page.size < BATCH_SIZE) return written\n after = page.docs[page.docs.length - 1] ?? null\n }\n}\n\n/** A contact as it reads once one holder's facet field holds `to` (or nothing, for `null`). */\nfunction withFacetValue(\n data: Record<string, unknown>,\n groupId: string,\n field: string,\n to: string | null,\n): Record<string, unknown> {\n const facets = { ...((data[CONTACT_FACETS_FIELD] as Record<string, unknown> | undefined) ?? {}) }\n const facet = { ...((facets[groupId] as Record<string, unknown> | undefined) ?? {}) }\n if (to === null) delete facet[field]\n else facet[field] = to\n facets[groupId] = facet\n return { ...data, [CONTACT_FACETS_FIELD]: facets }\n}\n\n/**\n * `POST crm/picklist-values` — rename a value of one of the CRM's standard\n * picklists, or delete an added one and move its records to another\n * (AGL-3298, AGL-3510). `crm/lead-source-values` is the same route with the\n * picklist fixed to the lead source.\n *\n * Body: `{ hostId | orgId, picklistId, action: 'rename', valueId, label }` or\n * `{ hostId | orgId, picklistId, action: 'delete', valueId, replaceWith: label | null }`,\n * and, on a list whose records keep the value's group (`picklistKeepsGroups`,\n * AGL-3577), `{ …, action: 'group', valueId, group: id | null }` or\n * `{ …, action: 'add', label, group?, meaning? }`.\n * `picklistId` is one of `CRM_PICKLIST_DEFINITIONS`; anything else is refused.\n *\n * ## A value's group on its records\n *\n * A lead keeps its lead source's group as `leadSourceDirection` — the field\n * the Leads list's direction filter asks. Any move can change the group a\n * label has: a regroup, an add of a value records already hold, a rename\n * onto such a label, a delete. So after the move, every target naming a\n * `groupField` has it rewritten on the records holding the old label and\n * the new one, from the list as it now stands.\n *\n * Records store the value's LABEL (see the picklist block in `crm.ts`), so\n * both moves change records as well as the list: every record the\n * definition's `targets` name that holds the old label is rewritten to the\n * new one — or, for a delete with no replacement, cleared. That is\n * Salesforce's Replace, done as part of the move, so a report grouped by\n * the field follows a rename instead of splitting into the old name and\n * the new. A standard value cannot be deleted, and on a definition with\n * meanings a delete's replacement must mean the same (see\n * `deletePicklistValue`).\n *\n * ## The list first, then the records\n *\n * The list is written in a transaction before any record: from that moment\n * no picker offers the old label and no import or API write accepts it, so\n * the sweep that follows cannot be outrun by a new record taking the name\n * it is removing. A sweep interrupted part-way leaves records holding a\n * label the list no longer has, which they show as \"(not in the list)\"; the\n * same request sent again finishes it, because the rename of a value to its\n * own current label is a no-op on the list and still sweeps.\n *\n * ## Who may\n *\n * Whoever may write the CRM — `authorizeCrmWriter`, the question every CRM\n * route asks — which is who the rules let edit the list itself. The sweep\n * reaches every record in the org whatever the caller's scope: it changes\n * one label to another that the same caller could already have picked, and\n * reveals nothing about the records but their count.\n */\nfunction picklistValuesHandler(fixed?: CrmPicklistId): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const definition = crmPicklistDefinition(fixed ?? body['picklistId'])\n if (!definition) {\n res.status(400).json({ error: 'Name one of the CRM’s picklists.' })\n return\n }\n const picklistId = definition.id as CrmPicklistId\n const action = body['action']\n const valueId = String(body['valueId'] ?? '').trim().slice(0, 64)\n const regrouping = action === 'group' || action === 'add'\n if (regrouping && !picklistKeepsGroups(definition)) {\n res.status(400).json({ error: `The ${definition.plural} are added and grouped on the Fields page.` })\n return\n }\n if (\n !(action === 'rename' || action === 'delete' || regrouping) ||\n (action === 'add' ? !String(body['label'] ?? '').trim() : !valueId)\n ) {\n res.status(400).json({ error: 'Name a value, and whether to rename or delete it.' })\n return\n }\n const asGroup = (raw: unknown) => (typeof raw === 'string' && raw ? raw : null)\n const replaceWith =\n body['replaceWith'] === null || body['replaceWith'] === undefined\n ? null\n : String(body['replaceWith'])\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: suiteAct(definition) })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const listRef = orgRef.collection(CRM_COLLECTIONS.picklists).doc(picklistId)\n\n /*\n * THE LIST. Read and written in one transaction, so two admins editing\n * at once cannot each write a list missing the other's change.\n */\n const moved = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(listRef)\n const before: CrmPicklist = effectiveCrmPicklist(picklistId, snapshot.data())\n const value =\n action === 'add' ? null : before.values.find((entry) => entry.id === valueId)\n if (action !== 'add' && !value) {\n return { ok: false as const, error: 'That value is no longer in the list.' }\n }\n const move: PicklistMove =\n action === 'add'\n ? addPicklistValue(definition, before, String(body['label'] ?? ''), {\n group: asGroup(body['group']),\n meaning: asGroup(body['meaning']),\n })\n : action === 'group'\n ? { ok: true, picklist: setPicklistValueGroup(definition, before, valueId, asGroup(body['group'])) }\n : action === 'rename'\n ? renamePicklistValue(before, valueId, String(body['label'] ?? ''))\n : deletePicklistValue(definition, before, valueId, replaceWith)\n if (move.ok === false) return move\n const created = normalizeCrmPicklist(snapshot.data()) === null\n transaction.set(\n listRef,\n {\n values: move.picklist.values,\n defaultValueId: move.picklist.defaultValueId,\n updatedAt: FieldValue.serverTimestamp(),\n // The stamp every org-wide CRM document carries (see the Fields\n // page), written when this move is what creates the document.\n ...(created\n ? {\n hostId: scope.hostId || null,\n createdAt: FieldValue.serverTimestamp(),\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n }\n : {}),\n },\n { merge: true },\n )\n // A regroup and an add move no label: `from` and `to` are the value's own.\n const own =\n action === 'add'\n ? (move.picklist.values[move.picklist.values.length - 1]?.label ?? '')\n : (value?.label ?? '')\n const to =\n action === 'rename'\n ? (move.picklist.values.find((entry) => entry.id === valueId)?.label ?? null)\n : regrouping\n ? own\n : replaceWith === null\n ? null\n : (move.picklist.values.find(\n (entry) => entry.label.toLowerCase() === replaceWith.trim().toLowerCase(),\n )?.label ?? null)\n return { ok: true as const, from: regrouping ? own : (value?.label ?? ''), to, after: move.picklist }\n })\n if (moved.ok === false) {\n res.status(400).json({ error: moved.error })\n return\n }\n\n /*\n * THE RECORDS, target by target: a record's own field, with its query\n * key moved beside it; for an array target, each entry of a record's\n * list (a deal's contact roles); or, for a facet target, each holder's\n * facet on a contact — one query per consent group the org's sites\n * belong to.\n * A rename to the same label moves nothing, and says so.\n */\n const updated: PicklistValuesResponse['updated'] = {}\n let groupIds: string[] | null = null\n for (const target of definition.targets) {\n updated[target.object] ??= 0\n if (target.groupField && !target.facet && !target.arrayKey) {\n // The group each label now has, on every record holding either (AGL-3577).\n const labels = new Map<string, string>()\n for (const label of [moved.from, moved.to]) {\n const key = label === null ? null : crmPicklistKey(label)\n if (key && label !== null) labels.set(key, label)\n }\n const regroup = async () => {\n for (const label of labels.values()) {\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await regroupEverywhere(\n firestore,\n orgRef.collection(TARGET_COLLECTIONS[target.object]),\n target,\n label,\n picklistGroupOf(moved.after, label),\n ))\n }\n }\n if (moved.from === moved.to) {\n await regroup()\n continue\n }\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n orgRef.collection(TARGET_COLLECTIONS[target.object]).where(target.field, '==', moved.from),\n target.field,\n moved.to,\n {\n ...(target.keyField ? { [target.keyField]: crmPicklistKey(moved.to) } : {}),\n [target.groupField]: picklistGroupOf(moved.after, moved.to),\n },\n ))\n await regroup()\n continue\n }\n if (moved.from === moved.to) continue\n const records = orgRef.collection(TARGET_COLLECTIONS[target.object])\n if (target.arrayKey && target.keyField) {\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceInArrayEverywhere(\n firestore,\n records,\n { ...target, arrayKey: target.arrayKey, keyField: target.keyField },\n moved.from,\n moved.to,\n ))\n continue\n }\n if (target.facet) {\n if (!groupIds) {\n const hostIds = await orgHostIds(firestore, writer.orgId)\n groupIds = [\n ...new Set(hostIds.map((hostId) => consentGroupForHost(writer.org, hostId).groupId)),\n ].filter(Boolean)\n }\n for (const groupId of groupIds) {\n const path = new FieldPath(CONTACT_FACETS_FIELD, groupId, target.field)\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(path, '==', moved.from),\n path,\n moved.to,\n // A facet value a contact list filters by is keyed in\n // `facetKeys` (AGL-3511), restamped from the record as moved.\n (snapshot) => ({\n [CRM_CONTACT_FACET_KEYS_FIELD]: crmContactFacetKeys(\n withFacetValue(snapshot.data(), groupId, target.field, moved.to),\n ),\n }),\n ))\n }\n continue\n }\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(target.field, '==', moved.from),\n target.field,\n moved.to,\n target.keyField ? { [target.keyField]: crmPicklistKey(moved.to) } : {},\n ))\n }\n const answer: PicklistValuesResponse = { ok: true, updated }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] picklist-values failed', error)\n res.status(500).json({ error: `The ${definition.plural} could not be saved.` })\n }\n }\n}\n\n/** `POST crm/picklist-values` — any registered picklist, named by `picklistId`. */\nexport const crmPicklistValuesHandler: PluginApiHandler = picklistValuesHandler()\n\n/** `POST crm/lead-source-values` — the same route with the picklist fixed to the lead source. */\nexport const crmLeadSourceValuesHandler: PluginApiHandler =\n picklistValuesHandler(CRM_LEAD_SOURCE_PICKLIST)\n"],"names":["consentGroupForHost","CONTACT_FACETS_FIELD","CRM_COLLECTIONS","CRM_CONTACT_FACET_KEYS_FIELD","CRM_LEAD_SOURCE_PICKLIST","crmContactFacetKeys","crmPicklistDefinition","crmPicklistKey","crmPicklistValueByLabel","effectiveCrmPicklist","newResourceScopeFields","normalizeCrmPicklist","ORG_SCOPE_TOKEN","firebaseAdmin","FieldPath","FieldValue","addPicklistValue","deletePicklistValue","picklistKeepsGroups","renamePicklistValue","setPicklistValueGroup","orgHostIds","readCrmRouteScope","authorizeCrmWriter","suiteAct","definition","plural","TARGET_COLLECTIONS","contact","lead","company","companies","deal","deals","task","tasks","BATCH_SIZE","replaceEverywhere","firestore","query","field","to","beside","written","page","limit","get","empty","batch","snapshot","docs","alongside","update","ref","delete","serverTimestamp","Object","entries","flat","commit","size","replaceInArrayEverywhere","records","target","from","fromKey","where","keyField","orderBy","documentId","after","startAfter","changed","Array","isArray","moved","next","map","entry","row","arrayKey","rest","keys","Set","key","add","updatedAt","length","picklistGroupOf","picklist","label","group","regroupEverywhere","groupField","withFacetValue","data","groupId","facets","facet","picklistValuesHandler","fixed","req","res","body","method","setHeader","status","json","error","scope","picklistId","id","action","valueId","String","trim","slice","regrouping","asGroup","raw","replaceWith","undefined","writer","ok","app","orgRef","collection","doc","orgId","listRef","picklists","runTransaction","transaction","move","before","value","values","find","meaning","created","set","defaultValueId","hostId","createdAt","merge","own","toLowerCase","updated","groupIds","targets","object","labels","Map","regroup","hostIds","org","filter","Boolean","path","answer","console","crmPicklistValuesHandler","crmLeadSourceValuesHandler"],"mappings":"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"}
|
|
@@ -35,7 +35,8 @@ import { readLeadSourcePicklist } from "./lead-source-picklist.js";
|
|
|
35
35
|
orgId = resolved.orgId;
|
|
36
36
|
org = resolved.org;
|
|
37
37
|
}
|
|
38
|
-
const
|
|
38
|
+
const leadSources = await readLeadSourcePicklist(firestore, orgId);
|
|
39
|
+
const label = crmLeadSourceForOrigin(leadSources, input.origin);
|
|
39
40
|
if (!label) return {
|
|
40
41
|
records: 0
|
|
41
42
|
};
|
|
@@ -59,7 +60,10 @@ import { readLeadSourcePicklist } from "./lead-source-picklist.js";
|
|
|
59
60
|
});
|
|
60
61
|
if (leadStamped) {
|
|
61
62
|
records += 1;
|
|
62
|
-
|
|
63
|
+
// Its direction is the label's group in the list just read (AGL-3577).
|
|
64
|
+
await restampCrmListFieldsAt(leadRef, 'leads', {
|
|
65
|
+
leadSources
|
|
66
|
+
});
|
|
63
67
|
}
|
|
64
68
|
/* THE CONTACT, on the site's own facet — present only once the site holds the person. */ const found = await findContactByEmail(orgRef.collection('contacts'), email);
|
|
65
69
|
if (found) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-origin.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 type {\n PluginRecordOriginReport,\n PluginRecordOriginRequest,\n PluginRecordOriginWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport {\n consentGroupForHost,\n CONTACT_FACETS_FIELD,\n contactFacetPath,\n crmLeadSourceForOrigin,\n crmPicklistKey,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n findContactByEmail,\n firebaseAdmin,\n getOrgForHost,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { readLeadSourcePicklist } from './lead-source-picklist'\n\n/*==========================================\n * A PERSON'S ORIGIN, AS A LEAD SOURCE (AGL-3519).\n *\n * The CRM's side of the platform's record-origin seam, and what its own\n * capture door calls after filing a person: a door's word for how it met\n * somebody — a form, a booking, a sequence — becomes the Lead source its\n * built-in value names (`CRM_LEAD_SOURCE_ORIGINS`), as the org spells it.\n *\n * - ORIGINAL SOURCE. A record that already names a lead source keeps it;\n * only an empty one is stamped, and a person the org cleared stays clear\n * to every door that meets them again (`firstTouchOnly`).\n * - A DEACTIVATED value stamps nothing: the org has said not to file\n * people under it.\n * - Both records the address is, as the site sees them: the lead, when the\n * site may see it, and the site's own facet on the contact — a holder's\n * knowledge of the person, like the rest of its profile.\n *\n * Each stamp is a transaction that reads the field and writes only into an\n * empty one, so a rep's pick made a moment earlier is never overwritten;\n * the list fields follow (`leadSourceKey`, `facetKeys`).\n *=========================================*/\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Where one stamp lands, for a caller that already holds the org. */\nexport interface CrmRecordOriginInput extends PluginRecordOriginRequest {\n org?: Record<string, unknown> | null\n}\n\n/** Stamps the origin on the lead and the site's contact facet the address is; how many took it. */\nexport async function stampCrmRecordOrigin(\n input: CrmRecordOriginInput,\n firestore: Firestore = firebaseAdmin.app().firestore(),\n): Promise<PluginRecordOriginReport> {\n const email = normalizeContactEmail(input.email)\n const key = email ? personKey(email) : null\n if (!email || !key || !input.hostId) return { records: 0 }\n let orgId = input.orgId\n let org = input.org ?? null\n if (!orgId || !org) {\n const resolved = await getOrgForHost(input.hostId)\n if (!resolved || (orgId && resolved.orgId !== orgId)) return { records: 0 }\n orgId = resolved.orgId\n org = resolved.org as Record<string, unknown>\n }\n const label = crmLeadSourceForOrigin(await readLeadSourcePicklist(firestore, orgId), input.origin)\n if (!label) return { records: 0 }\n const orgRef = firestore.collection('orgs').doc(orgId)\n let records = 0\n\n /* THE LEAD, keyed on the person and seen by the site. */\n const leadRef = orgRef.collection('leads').doc(key)\n const leadStamped = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(leadRef)\n if (!snapshot.exists) return false\n const lead = snapshot.data() ?? {}\n if (!visibleToHost(lead['visibleTo'] as string[] | undefined, input.hostId)) return false\n if (typeof lead['leadSource'] === 'string' && lead['leadSource'].trim()) return false\n if (input.firstTouchOnly && Number(lead['submissionCount'] ?? 1) > 1) return false\n tx.update(leadRef, {\n leadSource: label,\n leadSourceKey: crmPicklistKey(label),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n })\n if (leadStamped) {\n records += 1\n await restampCrmListFieldsAt(leadRef, 'leads')\n }\n\n /* THE CONTACT, on the site's own facet — present only once the site holds the person. */\n const found = await findContactByEmail(orgRef.collection('contacts'), email)\n if (found) {\n const groupId = consentGroupForHost(org, input.hostId).groupId\n const contactStamped = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(found.ref)\n const facets = (snapshot.data() ?? {})[CONTACT_FACETS_FIELD] as Record<string, unknown> | undefined\n const facet = facets?.[groupId] as Record<string, unknown> | undefined\n if (!facet || typeof facet !== 'object') return false\n if (typeof facet['leadSource'] === 'string' && facet['leadSource'].trim()) return false\n const interactions = Array.isArray(facet['interactions']) ? facet['interactions'].length : 0\n if (input.firstTouchOnly && interactions > 1) return false\n tx.update(found.ref, {\n [contactFacetPath(groupId, 'leadSource')]: label,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n })\n if (contactStamped) {\n records += 1\n await restampCrmListFieldsAt(found.ref, 'contacts')\n }\n }\n return { records }\n}\n\n/** The writer the CRM registers on the platform's record-origin seam. Never throws. */\nexport const crmRecordOriginWriter: PluginRecordOriginWriter = {\n async stamp(request) {\n try {\n return await stampCrmRecordOrigin(request)\n } catch (error) {\n console.error('[crm] the lead source could not be stamped', request.hostId, error)\n return { records: 0 }\n }\n },\n}\n"],"names":["consentGroupForHost","CONTACT_FACETS_FIELD","contactFacetPath","crmLeadSourceForOrigin","crmPicklistKey","normalizeContactEmail","personKey","visibleToHost","findContactByEmail","firebaseAdmin","getOrgForHost","restampCrmListFieldsAt","FieldValue","readLeadSourcePicklist","stampCrmRecordOrigin","input","firestore","app","email","key","hostId","records","orgId","org","resolved","label","origin","orgRef","collection","doc","leadRef","leadStamped","runTransaction","tx","snapshot","lead","get","exists","data","trim","firstTouchOnly","Number","update","leadSource","leadSourceKey","updatedAt","serverTimestamp","found","groupId","contactStamped","ref","facets","facet","interactions","Array","isArray","length","crmRecordOriginWriter","stamp","request","error","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-origin.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 type {\n PluginRecordOriginReport,\n PluginRecordOriginRequest,\n PluginRecordOriginWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport {\n consentGroupForHost,\n CONTACT_FACETS_FIELD,\n contactFacetPath,\n crmLeadSourceForOrigin,\n crmPicklistKey,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n findContactByEmail,\n firebaseAdmin,\n getOrgForHost,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { readLeadSourcePicklist } from './lead-source-picklist'\n\n/*==========================================\n * A PERSON'S ORIGIN, AS A LEAD SOURCE (AGL-3519).\n *\n * The CRM's side of the platform's record-origin seam, and what its own\n * capture door calls after filing a person: a door's word for how it met\n * somebody — a form, a booking, a sequence — becomes the Lead source its\n * built-in value names (`CRM_LEAD_SOURCE_ORIGINS`), as the org spells it.\n *\n * - ORIGINAL SOURCE. A record that already names a lead source keeps it;\n * only an empty one is stamped, and a person the org cleared stays clear\n * to every door that meets them again (`firstTouchOnly`).\n * - A DEACTIVATED value stamps nothing: the org has said not to file\n * people under it.\n * - Both records the address is, as the site sees them: the lead, when the\n * site may see it, and the site's own facet on the contact — a holder's\n * knowledge of the person, like the rest of its profile.\n *\n * Each stamp is a transaction that reads the field and writes only into an\n * empty one, so a rep's pick made a moment earlier is never overwritten;\n * the list fields follow (`leadSourceKey`, `leadSourceDirection`, `facetKeys`).\n *=========================================*/\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Where one stamp lands, for a caller that already holds the org. */\nexport interface CrmRecordOriginInput extends PluginRecordOriginRequest {\n org?: Record<string, unknown> | null\n}\n\n/** Stamps the origin on the lead and the site's contact facet the address is; how many took it. */\nexport async function stampCrmRecordOrigin(\n input: CrmRecordOriginInput,\n firestore: Firestore = firebaseAdmin.app().firestore(),\n): Promise<PluginRecordOriginReport> {\n const email = normalizeContactEmail(input.email)\n const key = email ? personKey(email) : null\n if (!email || !key || !input.hostId) return { records: 0 }\n let orgId = input.orgId\n let org = input.org ?? null\n if (!orgId || !org) {\n const resolved = await getOrgForHost(input.hostId)\n if (!resolved || (orgId && resolved.orgId !== orgId)) return { records: 0 }\n orgId = resolved.orgId\n org = resolved.org as Record<string, unknown>\n }\n const leadSources = await readLeadSourcePicklist(firestore, orgId)\n const label = crmLeadSourceForOrigin(leadSources, input.origin)\n if (!label) return { records: 0 }\n const orgRef = firestore.collection('orgs').doc(orgId)\n let records = 0\n\n /* THE LEAD, keyed on the person and seen by the site. */\n const leadRef = orgRef.collection('leads').doc(key)\n const leadStamped = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(leadRef)\n if (!snapshot.exists) return false\n const lead = snapshot.data() ?? {}\n if (!visibleToHost(lead['visibleTo'] as string[] | undefined, input.hostId)) return false\n if (typeof lead['leadSource'] === 'string' && lead['leadSource'].trim()) return false\n if (input.firstTouchOnly && Number(lead['submissionCount'] ?? 1) > 1) return false\n tx.update(leadRef, {\n leadSource: label,\n leadSourceKey: crmPicklistKey(label),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n })\n if (leadStamped) {\n records += 1\n // Its direction is the label's group in the list just read (AGL-3577).\n await restampCrmListFieldsAt(leadRef, 'leads', { leadSources })\n }\n\n /* THE CONTACT, on the site's own facet — present only once the site holds the person. */\n const found = await findContactByEmail(orgRef.collection('contacts'), email)\n if (found) {\n const groupId = consentGroupForHost(org, input.hostId).groupId\n const contactStamped = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(found.ref)\n const facets = (snapshot.data() ?? {})[CONTACT_FACETS_FIELD] as Record<string, unknown> | undefined\n const facet = facets?.[groupId] as Record<string, unknown> | undefined\n if (!facet || typeof facet !== 'object') return false\n if (typeof facet['leadSource'] === 'string' && facet['leadSource'].trim()) return false\n const interactions = Array.isArray(facet['interactions']) ? facet['interactions'].length : 0\n if (input.firstTouchOnly && interactions > 1) return false\n tx.update(found.ref, {\n [contactFacetPath(groupId, 'leadSource')]: label,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n })\n if (contactStamped) {\n records += 1\n await restampCrmListFieldsAt(found.ref, 'contacts')\n }\n }\n return { records }\n}\n\n/** The writer the CRM registers on the platform's record-origin seam. Never throws. */\nexport const crmRecordOriginWriter: PluginRecordOriginWriter = {\n async stamp(request) {\n try {\n return await stampCrmRecordOrigin(request)\n } catch (error) {\n console.error('[crm] the lead source could not be stamped', request.hostId, error)\n return { records: 0 }\n }\n },\n}\n"],"names":["consentGroupForHost","CONTACT_FACETS_FIELD","contactFacetPath","crmLeadSourceForOrigin","crmPicklistKey","normalizeContactEmail","personKey","visibleToHost","findContactByEmail","firebaseAdmin","getOrgForHost","restampCrmListFieldsAt","FieldValue","readLeadSourcePicklist","stampCrmRecordOrigin","input","firestore","app","email","key","hostId","records","orgId","org","resolved","leadSources","label","origin","orgRef","collection","doc","leadRef","leadStamped","runTransaction","tx","snapshot","lead","get","exists","data","trim","firstTouchOnly","Number","update","leadSource","leadSourceKey","updatedAt","serverTimestamp","found","groupId","contactStamped","ref","facets","facet","interactions","Array","isArray","length","crmRecordOriginWriter","stamp","request","error","console"],"mappings":"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"}
|