@aglyn/plugins-crm 1.0.0-beta.244 → 1.0.0-beta.247
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +14 -14
- package/src/lib/components/contact-custom-columns.d.ts +15 -6
- package/src/lib/components/contact-custom-columns.js +27 -7
- package/src/lib/components/contact-custom-columns.js.map +1 -1
- package/src/lib/components/contact-field-drawer.d.ts +2 -0
- package/src/lib/components/contact-field-drawer.js +29 -15
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-custom-field-control.d.ts +4 -2
- package/src/lib/components/crm-custom-field-control.js +53 -1
- package/src/lib/components/crm-custom-field-control.js.map +1 -1
- package/src/lib/components/fields-section.js +4 -2
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/form-contact-fields-card.js +2 -0
- package/src/lib/components/form-contact-fields-card.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +7 -0
- package/src/lib/constants/contact-filters.js +8 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +7 -0
- package/src/lib/declarations.console-server.js +39 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/model/crm-picklist-setup-content.d.ts +121 -0
- package/src/lib/model/crm-picklist-setup-content.js +262 -0
- package/src/lib/model/crm-picklist-setup-content.js.map +1 -0
- package/src/lib/model/crm-record-routes.js +18 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-setup-content.d.ts +199 -0
- package/src/lib/model/crm-setup-content.js +427 -0
- package/src/lib/model/crm-setup-content.js.map +1 -0
- package/src/lib/server/crm-setup-ai-capabilities.d.ts +68 -0
- package/src/lib/server/crm-setup-ai-capabilities.js +290 -0
- package/src/lib/server/crm-setup-ai-capabilities.js.map +1 -0
- package/src/lib/server/crm-setup-writers.server.d.ts +120 -0
- package/src/lib/server/crm-setup-writers.server.js +492 -0
- package/src/lib/server/crm-setup-writers.server.js.map +1 -0
- package/src/lib/server/picklist-regroup.d.ts +30 -0
- package/src/lib/server/picklist-regroup.js +61 -0
- package/src/lib/server/picklist-regroup.js.map +1 -0
- package/src/lib/server/picklist-values.d.ts +2 -14
- package/src/lib/server/picklist-values.js +3 -40
- package/src/lib/server/picklist-values.js.map +1 -1
- package/src/lib/transfer/common.d.ts +4 -2
- package/src/lib/transfer/common.js +14 -3
- package/src/lib/transfer/common.js.map +1 -1
- package/src/lib/transfer/fields.js +10 -1
- package/src/lib/transfer/fields.js.map +1 -1
- package/src/lib/transfer/pipelines.js +4 -2
- package/src/lib/transfer/pipelines.js.map +1 -1
|
@@ -0,0 +1,492 @@
|
|
|
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 { consentGroupForHost } from "@aglyn/aglyn/app-utils/consent-groups";
|
|
18
|
+
import { CONTACT_FIELDS_MAX_PER_ORG } from "@aglyn/aglyn/app-utils/contact-custom-fields";
|
|
19
|
+
import { CRM_COLLECTIONS, crmPicklistDefinition, crmReadTokens, crmScopeTokens, effectiveCrmPicklist, normalizeCrmPicklist } from "@aglyn/aglyn/app-utils/crm";
|
|
20
|
+
import { memberCanSee } from "@aglyn/aglyn/app-utils/organizations";
|
|
21
|
+
import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
22
|
+
import { newResourceScopeFields, ORG_SCOPE_TOKEN } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
23
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
24
|
+
import { memberHasOrgPermission, resolveOrgIdForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin/server/organizations";
|
|
25
|
+
import { FieldPath, FieldValue } from "firebase-admin/firestore";
|
|
26
|
+
import { checkCrmPicklistValuesSetup, CRM_CUSTOM_CHOICE_LIST, crmPicklistValuesName, planCrmCustomChoiceValues, planCrmPicklistValues, readCrmPicklistValuesSetup } from "../model/crm-picklist-setup-content.js";
|
|
27
|
+
import { checkCrmFieldSetup, checkCrmPipelineSetup, crmPipelineSetupFacts, planCrmFieldSetup, planCrmPipelineSetup, readCrmFieldSetup, readCrmPipelineSetup } from "../model/crm-setup-content.js";
|
|
28
|
+
import { picklistKeepsGroups } from "../model/picklist-values.js";
|
|
29
|
+
import { picklistGroupOf, regroupEverywhere } from "./picklist-regroup.js";
|
|
30
|
+
import { crmSuiteRefusalMessage } from "./suite-gate.js";
|
|
31
|
+
/** The refusals, in the console's and the routes' words. */ export const CRM_SETUP_ROLE_REFUSAL = 'Editing the CRM requires the editor role';
|
|
32
|
+
export const CRM_SETUP_PERMISSION_REFUSAL = 'Your organization role does not allow editing organization data';
|
|
33
|
+
export const CRM_SETUP_SCOPE_REFUSAL = 'Your access to this organization does not reach that site';
|
|
34
|
+
export const CRM_SETUP_FIELD_SCOPE_REFUSAL = 'Custom fields are organization-wide, and your access reaches only some sites';
|
|
35
|
+
export const CRM_SETUP_PICKLIST_SCOPE_REFUSAL = 'Picklists are organization-wide, and your access reaches only some sites';
|
|
36
|
+
/** Roles allowed to write the CRM's setup — the rules' `canWriteOrgData()`. */ const WRITER_ROLES = new Set([
|
|
37
|
+
'owner',
|
|
38
|
+
'admin',
|
|
39
|
+
'editor'
|
|
40
|
+
]);
|
|
41
|
+
/** The most pipelines one site's read returns, as the board reads them. */ const PIPELINE_READ_LIMIT = 50;
|
|
42
|
+
/** The org subcollection each picklist target's records live in. */ const TARGET_COLLECTIONS = {
|
|
43
|
+
contact: 'contacts',
|
|
44
|
+
lead: 'leads',
|
|
45
|
+
company: CRM_COLLECTIONS.companies,
|
|
46
|
+
deal: CRM_COLLECTIONS.deals,
|
|
47
|
+
task: CRM_COLLECTIONS.tasks
|
|
48
|
+
};
|
|
49
|
+
async function platformMember(uid, orgId) {
|
|
50
|
+
var _member_role;
|
|
51
|
+
const membership = await resolveOrgMembership(uid, orgId);
|
|
52
|
+
const member = membership == null ? void 0 : membership.member;
|
|
53
|
+
if (!member) return null;
|
|
54
|
+
return {
|
|
55
|
+
role: String((_member_role = member.role) != null ? _member_role : ''),
|
|
56
|
+
canManageData: await memberHasOrgPermission(orgId, member, 'data.manage'),
|
|
57
|
+
member: member
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** The CRM's collections over the Admin SDK. */ export function adminCrmSetupStore(firestore = ()=>firebaseAdmin.app().firestore()) {
|
|
61
|
+
const org = (orgId)=>firestore().collection('orgs').doc(orgId);
|
|
62
|
+
const pipelinesOf = (orgId)=>org(orgId).collection(CRM_COLLECTIONS.pipelines);
|
|
63
|
+
const fieldsOf = (orgId)=>org(orgId).collection(CRM_COLLECTIONS.contactFields);
|
|
64
|
+
const withId = (snap)=>_extends({}, snap.data(), {
|
|
65
|
+
$id: snap.id
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
pipelines: async (orgId, readTokens)=>{
|
|
69
|
+
const snap = await pipelinesOf(orgId).where('visibleTo', 'array-contains-any', [
|
|
70
|
+
...readTokens
|
|
71
|
+
]).orderBy(FieldPath.documentId()).limit(PIPELINE_READ_LIMIT).get();
|
|
72
|
+
return snap.docs.map((one)=>withId(one));
|
|
73
|
+
},
|
|
74
|
+
pipeline: async (orgId, id)=>{
|
|
75
|
+
const snap = await pipelinesOf(orgId).doc(id).get();
|
|
76
|
+
return snap.exists ? withId(snap) : null;
|
|
77
|
+
},
|
|
78
|
+
savePipeline: async (orgId, write, now)=>{
|
|
79
|
+
const batch = firestore().batch();
|
|
80
|
+
const ref = pipelinesOf(orgId).doc(write.id);
|
|
81
|
+
if (write.create) batch.create(ref, write.create);
|
|
82
|
+
else if (write.update) batch.update(ref, _extends({}, write.update, {
|
|
83
|
+
updatedAt: now
|
|
84
|
+
}));
|
|
85
|
+
for (const id of write.defaultsOff){
|
|
86
|
+
batch.update(pipelinesOf(orgId).doc(id), {
|
|
87
|
+
isDefault: false,
|
|
88
|
+
updatedAt: now
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
await batch.commit();
|
|
92
|
+
},
|
|
93
|
+
fields: async (orgId)=>{
|
|
94
|
+
const snap = await fieldsOf(orgId).limit(CONTACT_FIELDS_MAX_PER_ORG + 1).get();
|
|
95
|
+
return snap.docs.map((one)=>withId(one));
|
|
96
|
+
},
|
|
97
|
+
field: async (orgId, id)=>{
|
|
98
|
+
const snap = await fieldsOf(orgId).doc(id).get();
|
|
99
|
+
return snap.exists ? withId(snap) : null;
|
|
100
|
+
},
|
|
101
|
+
createField: async (orgId, id, document)=>{
|
|
102
|
+
await fieldsOf(orgId).doc(id).create(document);
|
|
103
|
+
},
|
|
104
|
+
setFieldOptions: async (orgId, id, options, now)=>{
|
|
105
|
+
await fieldsOf(orgId).doc(id).update({
|
|
106
|
+
options,
|
|
107
|
+
updatedAt: now
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
picklist: async (orgId, id)=>{
|
|
111
|
+
var _snap_data;
|
|
112
|
+
const snap = await org(orgId).collection(CRM_COLLECTIONS.picklists).doc(id).get();
|
|
113
|
+
return snap.exists ? (_snap_data = snap.data()) != null ? _snap_data : {} : null;
|
|
114
|
+
},
|
|
115
|
+
changePicklist: async (orgId, id, change, stamp)=>{
|
|
116
|
+
const ref = org(orgId).collection(CRM_COLLECTIONS.picklists).doc(id);
|
|
117
|
+
return firestore().runTransaction(async (transaction)=>{
|
|
118
|
+
var _snap_data;
|
|
119
|
+
const snap = await transaction.get(ref);
|
|
120
|
+
const stored = snap.exists ? (_snap_data = snap.data()) != null ? _snap_data : {} : null;
|
|
121
|
+
const { write, result } = change(stored);
|
|
122
|
+
if (write) {
|
|
123
|
+
// The stamp the console's first write gives an org-wide CRM document.
|
|
124
|
+
const created = normalizeCrmPicklist(stored) === null;
|
|
125
|
+
transaction.set(ref, _extends({
|
|
126
|
+
values: write.values,
|
|
127
|
+
defaultValueId: write.defaultValueId,
|
|
128
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
129
|
+
}, created ? _extends({
|
|
130
|
+
hostId: stamp.hostId,
|
|
131
|
+
createdAt: FieldValue.serverTimestamp()
|
|
132
|
+
}, newResourceScopeFields([
|
|
133
|
+
ORG_SCOPE_TOKEN
|
|
134
|
+
])) : {}), {
|
|
135
|
+
merge: true
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
regroup: (orgId, object, target, label, group)=>regroupEverywhere(firestore(), org(orgId).collection(TARGET_COLLECTIONS[object]), target, label, group)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/** The scope a pipeline made from this site is stamped with: the board seed's. */ function pipelineScope(context) {
|
|
145
|
+
var _context_org;
|
|
146
|
+
const org = (_context_org = context.org) != null ? _context_org : null;
|
|
147
|
+
return crmScopeTokens(org, consentGroupForHost(org, context.hostId));
|
|
148
|
+
}
|
|
149
|
+
/** What a pipeline's board at this site reads: the org's rows and the site group's. */ function pipelineReadTokens(context) {
|
|
150
|
+
var _context_org;
|
|
151
|
+
const org = (_context_org = context.org) != null ? _context_org : null;
|
|
152
|
+
return crmReadTokens(consentGroupForHost(org, context.hostId));
|
|
153
|
+
}
|
|
154
|
+
function pipelineRecord(pipeline) {
|
|
155
|
+
var _pipeline_name, _pipeline_stages;
|
|
156
|
+
return {
|
|
157
|
+
id: pipeline.$id,
|
|
158
|
+
name: String((_pipeline_name = pipeline.name) != null ? _pipeline_name : ''),
|
|
159
|
+
versionId: null,
|
|
160
|
+
facts: crmPipelineSetupFacts((_pipeline_stages = pipeline.stages) != null ? _pipeline_stages : [])
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function fieldRecord(field) {
|
|
164
|
+
var _field_label, _field_object;
|
|
165
|
+
return {
|
|
166
|
+
id: field.$id,
|
|
167
|
+
name: String((_field_label = field.label) != null ? _field_label : ''),
|
|
168
|
+
versionId: null,
|
|
169
|
+
facts: _extends({
|
|
170
|
+
object: (_field_object = field.object) != null ? _field_object : 'contact',
|
|
171
|
+
key: field.key,
|
|
172
|
+
type: field.type
|
|
173
|
+
}, field.options ? {
|
|
174
|
+
options: field.options
|
|
175
|
+
} : {}, field.currency ? {
|
|
176
|
+
currency: field.currency
|
|
177
|
+
} : {})
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function createCrmSetupGate(deps) {
|
|
181
|
+
var _deps_orgIdForHost, _deps_member;
|
|
182
|
+
const orgIdForHost = (_deps_orgIdForHost = deps.orgIdForHost) != null ? _deps_orgIdForHost : (hostId)=>resolveOrgIdForHost(hostId);
|
|
183
|
+
const memberOf = (_deps_member = deps.member) != null ? _deps_member : platformMember;
|
|
184
|
+
/** Everything but the scope: the site is the org's, the plan, the member's role and permission. */ return async (context, scope, scopeRefusal)=>{
|
|
185
|
+
if (await orgIdForHost(context.hostId) !== context.orgId) return {
|
|
186
|
+
status: 404,
|
|
187
|
+
error: 'Unknown site'
|
|
188
|
+
};
|
|
189
|
+
if (!checkEntitlement(context.org, 'crm')) {
|
|
190
|
+
return {
|
|
191
|
+
status: 403,
|
|
192
|
+
error: crmSuiteRefusalMessage('Setting up the CRM')
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const member = await memberOf(context.uid, context.orgId);
|
|
196
|
+
if (!member || !WRITER_ROLES.has(member.role) || member.member['orgSuspended'] === true) {
|
|
197
|
+
return {
|
|
198
|
+
status: 403,
|
|
199
|
+
error: CRM_SETUP_ROLE_REFUSAL
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (!member.canManageData) return {
|
|
203
|
+
status: 403,
|
|
204
|
+
error: CRM_SETUP_PERMISSION_REFUSAL
|
|
205
|
+
};
|
|
206
|
+
if (!memberCanSee(member.member, scope)) return {
|
|
207
|
+
status: 403,
|
|
208
|
+
error: scopeRefusal
|
|
209
|
+
};
|
|
210
|
+
return null;
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/** The `crm-pipeline` writer: creates or brings a pipeline to the ask. */ export function createCrmPipelineSetupWriter(deps = {}) {
|
|
214
|
+
var _deps_store, _deps_orgIdForHost;
|
|
215
|
+
const store = (_deps_store = deps.store) != null ? _deps_store : adminCrmSetupStore();
|
|
216
|
+
const orgIdForHost = (_deps_orgIdForHost = deps.orgIdForHost) != null ? _deps_orgIdForHost : (hostId)=>resolveOrgIdForHost(hostId);
|
|
217
|
+
const gate = createCrmSetupGate(deps);
|
|
218
|
+
return {
|
|
219
|
+
refusal: (context)=>gate(context, pipelineScope(context), CRM_SETUP_SCOPE_REFUSAL),
|
|
220
|
+
check: (content)=>checkCrmPipelineSetup(content),
|
|
221
|
+
read: async ({ hostId, id })=>{
|
|
222
|
+
const orgId = await orgIdForHost(hostId);
|
|
223
|
+
const pipeline = orgId ? await store.pipeline(orgId, id) : null;
|
|
224
|
+
return pipeline ? pipelineRecord(pipeline) : null;
|
|
225
|
+
},
|
|
226
|
+
write: async (request)=>{
|
|
227
|
+
const read = readCrmPipelineSetup(request.content);
|
|
228
|
+
if (read.ok === false) return {
|
|
229
|
+
ok: false,
|
|
230
|
+
status: 400,
|
|
231
|
+
error: read.problems[0]
|
|
232
|
+
};
|
|
233
|
+
const visibleTo = pipelineScope(request);
|
|
234
|
+
const refused = await gate(request, visibleTo, CRM_SETUP_SCOPE_REFUSAL);
|
|
235
|
+
if (refused) return _extends({
|
|
236
|
+
ok: false
|
|
237
|
+
}, refused);
|
|
238
|
+
// Asked again under the same id: the pipeline it already made.
|
|
239
|
+
const made = await store.pipeline(request.orgId, request.id);
|
|
240
|
+
if (made) return _extends({
|
|
241
|
+
ok: true,
|
|
242
|
+
replayed: true
|
|
243
|
+
}, pipelineRecord(made));
|
|
244
|
+
const pipelines = await store.pipelines(request.orgId, pipelineReadTokens(request));
|
|
245
|
+
const plan = planCrmPipelineSetup(read.value, pipelines, {
|
|
246
|
+
visibleTo,
|
|
247
|
+
hostId: request.hostId,
|
|
248
|
+
uid: request.uid,
|
|
249
|
+
nowMs: request.now.getTime()
|
|
250
|
+
});
|
|
251
|
+
if (plan.kind === 'refuse') return {
|
|
252
|
+
ok: false,
|
|
253
|
+
status: 409,
|
|
254
|
+
error: plan.error
|
|
255
|
+
};
|
|
256
|
+
if (plan.kind === 'create') {
|
|
257
|
+
await store.savePipeline(request.orgId, {
|
|
258
|
+
id: request.id,
|
|
259
|
+
create: plan.document,
|
|
260
|
+
defaultsOff: plan.defaultsOff
|
|
261
|
+
}, request.now);
|
|
262
|
+
return _extends({
|
|
263
|
+
ok: true,
|
|
264
|
+
replayed: false
|
|
265
|
+
}, pipelineRecord(_extends({}, plan.document, {
|
|
266
|
+
$id: request.id
|
|
267
|
+
})));
|
|
268
|
+
}
|
|
269
|
+
const existing = pipelines.find((one)=>one.$id === plan.id);
|
|
270
|
+
const changed = Object.keys(plan.patch).length > 0 || plan.defaultsOff.length > 0;
|
|
271
|
+
if (changed) {
|
|
272
|
+
await store.savePipeline(request.orgId, {
|
|
273
|
+
id: plan.id,
|
|
274
|
+
update: plan.patch,
|
|
275
|
+
defaultsOff: plan.defaultsOff
|
|
276
|
+
}, request.now);
|
|
277
|
+
}
|
|
278
|
+
return _extends({
|
|
279
|
+
ok: true,
|
|
280
|
+
replayed: !changed
|
|
281
|
+
}, pipelineRecord(_extends({}, existing, plan.patch)));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/** The `crm-field` writer: creates a custom field, or finds the one asked for. */ export function createCrmFieldSetupWriter(deps = {}) {
|
|
286
|
+
var _deps_store, _deps_orgIdForHost;
|
|
287
|
+
const store = (_deps_store = deps.store) != null ? _deps_store : adminCrmSetupStore();
|
|
288
|
+
const orgIdForHost = (_deps_orgIdForHost = deps.orgIdForHost) != null ? _deps_orgIdForHost : (hostId)=>resolveOrgIdForHost(hostId);
|
|
289
|
+
const gate = createCrmSetupGate(deps);
|
|
290
|
+
const scope = [
|
|
291
|
+
ORG_SCOPE_TOKEN
|
|
292
|
+
];
|
|
293
|
+
return {
|
|
294
|
+
refusal: (context)=>gate(context, scope, CRM_SETUP_FIELD_SCOPE_REFUSAL),
|
|
295
|
+
check: (content)=>checkCrmFieldSetup(content),
|
|
296
|
+
read: async ({ hostId, id })=>{
|
|
297
|
+
const orgId = await orgIdForHost(hostId);
|
|
298
|
+
const field = orgId ? await store.field(orgId, id) : null;
|
|
299
|
+
return field ? fieldRecord(field) : null;
|
|
300
|
+
},
|
|
301
|
+
write: async (request)=>{
|
|
302
|
+
var _existing_options;
|
|
303
|
+
const read = readCrmFieldSetup(request.content);
|
|
304
|
+
if (read.ok === false) return {
|
|
305
|
+
ok: false,
|
|
306
|
+
status: 400,
|
|
307
|
+
error: read.problems[0]
|
|
308
|
+
};
|
|
309
|
+
const refused = await gate(request, scope, CRM_SETUP_FIELD_SCOPE_REFUSAL);
|
|
310
|
+
if (refused) return _extends({
|
|
311
|
+
ok: false
|
|
312
|
+
}, refused);
|
|
313
|
+
const made = await store.field(request.orgId, request.id);
|
|
314
|
+
if (made) return _extends({
|
|
315
|
+
ok: true,
|
|
316
|
+
replayed: true
|
|
317
|
+
}, fieldRecord(made));
|
|
318
|
+
const definitions = await store.fields(request.orgId);
|
|
319
|
+
const plan = planCrmFieldSetup(read.value, definitions, {
|
|
320
|
+
hostId: request.hostId,
|
|
321
|
+
now: request.now,
|
|
322
|
+
visibleTo: scope
|
|
323
|
+
});
|
|
324
|
+
if (plan.kind === 'refuse') return {
|
|
325
|
+
ok: false,
|
|
326
|
+
status: plan.status,
|
|
327
|
+
error: plan.error
|
|
328
|
+
};
|
|
329
|
+
if (plan.kind === 'create') {
|
|
330
|
+
await store.createField(request.orgId, request.id, plan.document);
|
|
331
|
+
return _extends({
|
|
332
|
+
ok: true,
|
|
333
|
+
replayed: false
|
|
334
|
+
}, fieldRecord(_extends({}, plan.document, {
|
|
335
|
+
$id: request.id
|
|
336
|
+
})));
|
|
337
|
+
}
|
|
338
|
+
const existing = definitions.find((one)=>one.$id === plan.id);
|
|
339
|
+
if (!plan.options.length) return _extends({
|
|
340
|
+
ok: true,
|
|
341
|
+
replayed: true
|
|
342
|
+
}, fieldRecord(existing));
|
|
343
|
+
const options = [
|
|
344
|
+
...(_existing_options = existing.options) != null ? _existing_options : [],
|
|
345
|
+
...plan.options
|
|
346
|
+
];
|
|
347
|
+
await store.setFieldOptions(request.orgId, plan.id, options, request.now);
|
|
348
|
+
return _extends({
|
|
349
|
+
ok: true,
|
|
350
|
+
replayed: false
|
|
351
|
+
}, fieldRecord(_extends({}, existing, {
|
|
352
|
+
options
|
|
353
|
+
})));
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
export const crmPipelineSetupWriter = createCrmPipelineSetupWriter();
|
|
358
|
+
export const crmFieldSetupWriter = createCrmFieldSetupWriter();
|
|
359
|
+
/*==========================================
|
|
360
|
+
* PICKLIST VALUES (AGL-3756)
|
|
361
|
+
*=========================================*/ /** The record id a custom Choice field's values are reported under, beside a picklist's own id. */ const CUSTOM_RECORD_PREFIX = `${CRM_CUSTOM_CHOICE_LIST}:`;
|
|
362
|
+
function picklistRecord(definition, picklist, outcome) {
|
|
363
|
+
return {
|
|
364
|
+
id: definition.id,
|
|
365
|
+
name: crmPicklistValuesName(definition),
|
|
366
|
+
versionId: null,
|
|
367
|
+
facts: _extends({
|
|
368
|
+
list: definition.id,
|
|
369
|
+
values: picklist.values.filter((value)=>value.active).map((value)=>value.label)
|
|
370
|
+
}, outcome != null ? outcome : {})
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function choiceRecord(field, outcome) {
|
|
374
|
+
var _field_label, _field_object, _field_options;
|
|
375
|
+
return {
|
|
376
|
+
id: `${CUSTOM_RECORD_PREFIX}${field.$id}`,
|
|
377
|
+
name: String((_field_label = field.label) != null ? _field_label : ''),
|
|
378
|
+
versionId: null,
|
|
379
|
+
facts: _extends({
|
|
380
|
+
list: CRM_CUSTOM_CHOICE_LIST,
|
|
381
|
+
object: (_field_object = field.object) != null ? _field_object : 'contact',
|
|
382
|
+
values: (_field_options = field.options) != null ? _field_options : []
|
|
383
|
+
}, outcome != null ? outcome : {})
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* The `crm-picklist-values` writer: adds values to a standard CRM picklist
|
|
388
|
+
* or a custom Choice field's choices, never removing, renaming, reordering
|
|
389
|
+
* or reactivating one. Gated as the Fields page is — the `crm` feature, an
|
|
390
|
+
* owner, admin or editor with `data.manage` — and org-wide, as every
|
|
391
|
+
* picklist and field document is. A lead source added where records
|
|
392
|
+
* already hold its label files them under its group (`leadSourceDirection`),
|
|
393
|
+
* as the console's route does (AGL-3577).
|
|
394
|
+
*/ export function createCrmPicklistValuesSetupWriter(deps = {}) {
|
|
395
|
+
var _deps_store, _deps_orgIdForHost;
|
|
396
|
+
const store = (_deps_store = deps.store) != null ? _deps_store : adminCrmSetupStore();
|
|
397
|
+
const orgIdForHost = (_deps_orgIdForHost = deps.orgIdForHost) != null ? _deps_orgIdForHost : (hostId)=>resolveOrgIdForHost(hostId);
|
|
398
|
+
const gate = createCrmSetupGate(deps);
|
|
399
|
+
const scope = [
|
|
400
|
+
ORG_SCOPE_TOKEN
|
|
401
|
+
];
|
|
402
|
+
return {
|
|
403
|
+
refusal: (context)=>gate(context, scope, CRM_SETUP_PICKLIST_SCOPE_REFUSAL),
|
|
404
|
+
check: (content)=>checkCrmPicklistValuesSetup(content),
|
|
405
|
+
read: async ({ hostId, id })=>{
|
|
406
|
+
const orgId = await orgIdForHost(hostId);
|
|
407
|
+
if (!orgId) return null;
|
|
408
|
+
if (id.startsWith(CUSTOM_RECORD_PREFIX)) {
|
|
409
|
+
const field = await store.field(orgId, id.slice(CUSTOM_RECORD_PREFIX.length));
|
|
410
|
+
return field ? choiceRecord(field) : null;
|
|
411
|
+
}
|
|
412
|
+
const definition = crmPicklistDefinition(id);
|
|
413
|
+
if (!definition) return null;
|
|
414
|
+
const listId = definition.id;
|
|
415
|
+
return picklistRecord(definition, effectiveCrmPicklist(listId, await store.picklist(orgId, listId)));
|
|
416
|
+
},
|
|
417
|
+
write: async (request)=>{
|
|
418
|
+
const read = readCrmPicklistValuesSetup(request.content);
|
|
419
|
+
if (read.ok === false) return {
|
|
420
|
+
ok: false,
|
|
421
|
+
status: 400,
|
|
422
|
+
error: read.problems[0]
|
|
423
|
+
};
|
|
424
|
+
const refused = await gate(request, scope, CRM_SETUP_PICKLIST_SCOPE_REFUSAL);
|
|
425
|
+
if (refused) return _extends({
|
|
426
|
+
ok: false
|
|
427
|
+
}, refused);
|
|
428
|
+
const setup = read.value;
|
|
429
|
+
if (setup.target.kind === 'custom') {
|
|
430
|
+
const definitions = await store.fields(request.orgId);
|
|
431
|
+
const plan = planCrmCustomChoiceValues(_extends({}, setup, {
|
|
432
|
+
target: setup.target
|
|
433
|
+
}), definitions);
|
|
434
|
+
if (plan.ok === false) return {
|
|
435
|
+
ok: false,
|
|
436
|
+
status: plan.status,
|
|
437
|
+
error: plan.error
|
|
438
|
+
};
|
|
439
|
+
const field = definitions.find((one)=>one.$id === plan.id);
|
|
440
|
+
const { added, existing, inactive } = plan;
|
|
441
|
+
if (added.length) await store.setFieldOptions(request.orgId, plan.id, plan.options, request.now);
|
|
442
|
+
return _extends({
|
|
443
|
+
ok: true,
|
|
444
|
+
replayed: added.length === 0
|
|
445
|
+
}, choiceRecord(_extends({}, field, {
|
|
446
|
+
options: plan.options
|
|
447
|
+
}), {
|
|
448
|
+
added,
|
|
449
|
+
existing,
|
|
450
|
+
inactive
|
|
451
|
+
}));
|
|
452
|
+
}
|
|
453
|
+
const id = setup.target.id;
|
|
454
|
+
const definition = crmPicklistDefinition(id);
|
|
455
|
+
const planned = await store.changePicklist(request.orgId, id, (stored)=>{
|
|
456
|
+
const plan = planCrmPicklistValues(definition, effectiveCrmPicklist(id, stored), setup);
|
|
457
|
+
return {
|
|
458
|
+
write: plan.ok && plan.added.length ? plan.picklist : null,
|
|
459
|
+
result: plan
|
|
460
|
+
};
|
|
461
|
+
}, {
|
|
462
|
+
hostId: request.hostId
|
|
463
|
+
});
|
|
464
|
+
if (planned.ok === false) return {
|
|
465
|
+
ok: false,
|
|
466
|
+
status: 409,
|
|
467
|
+
error: planned.error
|
|
468
|
+
};
|
|
469
|
+
// Records that already hold an added label take its group (AGL-3577).
|
|
470
|
+
if (planned.added.length && picklistKeepsGroups(definition)) {
|
|
471
|
+
for (const target of definition.targets){
|
|
472
|
+
if (!target.groupField || target.facet || target.arrayKey) continue;
|
|
473
|
+
for (const label of planned.added){
|
|
474
|
+
await store.regroup(request.orgId, target.object, target, label, picklistGroupOf(planned.picklist, label));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const { added, existing, inactive } = planned;
|
|
479
|
+
return _extends({
|
|
480
|
+
ok: true,
|
|
481
|
+
replayed: added.length === 0
|
|
482
|
+
}, picklistRecord(definition, planned.picklist, {
|
|
483
|
+
added,
|
|
484
|
+
existing,
|
|
485
|
+
inactive
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
export const crmPicklistValuesSetupWriter = createCrmPicklistValuesSetupWriter();
|
|
491
|
+
|
|
492
|
+
//# sourceMappingURL=crm-setup-writers.server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/crm-setup-writers.server.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 { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { CONTACT_FIELDS_MAX_PER_ORG } from '@aglyn/aglyn/app-utils/contact-custom-fields'\nimport {\n CRM_COLLECTIONS,\n type CrmPicklist,\n type CrmPicklistDefinition,\n type CrmPicklistId,\n type CrmPicklistObject,\n type CrmPicklistTarget,\n crmPicklistDefinition,\n crmReadTokens,\n crmScopeTokens,\n effectiveCrmPicklist,\n normalizeCrmPicklist,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { memberCanSee } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { newResourceScopeFields, ORG_SCOPE_TOKEN } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport type {\n PluginDraftContext,\n PluginDraftRecord,\n PluginDraftRefusal,\n PluginDraftWrite,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n memberHasOrgPermission,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin/server/organizations'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport {\n checkCrmPicklistValuesSetup,\n CRM_CUSTOM_CHOICE_LIST,\n type CrmPicklistValuesOutcome,\n crmPicklistValuesName,\n planCrmCustomChoiceValues,\n planCrmPicklistValues,\n readCrmPicklistValuesSetup,\n} from '../model/crm-picklist-setup-content'\nimport {\n checkCrmFieldSetup,\n checkCrmPipelineSetup,\n type CrmFieldDefinitionDoc,\n crmPipelineSetupFacts,\n planCrmFieldSetup,\n planCrmPipelineSetup,\n readCrmFieldSetup,\n readCrmPipelineSetup,\n} from '../model/crm-setup-content'\nimport type { PipelineDoc } from '../model/deal-board-model'\nimport { picklistKeepsGroups } from '../model/picklist-values'\nimport { picklistGroupOf, regroupEverywhere } from './picklist-regroup'\nimport { crmSuiteRefusalMessage } from './suite-gate'\n\n/**\n * THE CRM'S SETUP WRITERS (AGL-3746): a deal pipeline and a custom field,\n * saved for another plugin — Aglyn AI's build — under the CRM's rules.\n *\n * Registered on the core's resource-drafts seam as `crm-pipeline` and\n * `crm-field`. Neither is a draft: a pipeline and a field have no draft\n * state, and the console saves both live, so these do too, and their AI\n * capabilities say so (`live`). What they write is exactly what the\n * console's Pipelines dialog and Fields section write\n * (`model/crm-setup-content.ts`), and they admit exactly whom the Firestore\n * rules admit for those writes, since the Admin SDK passes the rules by:\n *\n * - THE SITE is the org's, and THE PLAN carries the CRM (`crm`, Starter and\n * up — `orgCarriesCrmSuite`), refused in the suite gate's words;\n * - THE MEMBER is an owner, admin or editor (`canWriteOrgData`) whose org\n * access is not suspended, holding `data.manage`, the key the whole CRM\n * is gated on;\n * - THE SCOPE is one the member can create in (`canCreateScoped`): a\n * pipeline is stamped as the board's seed stamps it from this site\n * (`crmScopeTokens`), a field org-wide, which only an org-wide member\n * reaches.\n *\n * Idempotent: the job's attempt key is a new pipeline's or field's id, and\n * an existing pipeline, stage or field is matched by name (see the model),\n * so asking again changes nothing.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The refusals, in the console's and the routes' words. */\nexport const CRM_SETUP_ROLE_REFUSAL = 'Editing the CRM requires the editor role'\nexport const CRM_SETUP_PERMISSION_REFUSAL = 'Your organization role does not allow editing organization data'\nexport const CRM_SETUP_SCOPE_REFUSAL = 'Your access to this organization does not reach that site'\nexport const CRM_SETUP_FIELD_SCOPE_REFUSAL = 'Custom fields are organization-wide, and your access reaches only some sites'\nexport const CRM_SETUP_PICKLIST_SCOPE_REFUSAL = 'Picklists are organization-wide, and your access reaches only some sites'\n\n/** Roles allowed to write the CRM's setup — the rules' `canWriteOrgData()`. */\nconst WRITER_ROLES = new Set(['owner', 'admin', 'editor'])\n\n/** The most pipelines one site's read returns, as the board reads them. */\nconst PIPELINE_READ_LIMIT = 50\n\n/** Who the member is in the org, as the writers ask. */\nexport interface CrmSetupMember {\n role: string\n canManageData: boolean\n /** The membership itself, for its reach and whether it is suspended. */\n member: Readonly<Record<string, unknown>>\n}\n\n/** The reads and writes the writers make, over the Admin SDK unless a spec hands in its own. */\nexport interface CrmSetupStore {\n /** The pipelines readable under these scope tokens. */\n pipelines(orgId: string, readTokens: readonly string[]): Promise<PipelineDoc[]>\n pipeline(orgId: string, id: string): Promise<PipelineDoc | null>\n /** One batch: the new or changed pipeline, and the default flag off the others. */\n savePipeline(\n orgId: string,\n write: { id: string; create?: Record<string, unknown>; update?: Record<string, unknown>; defaultsOff: string[] },\n now: Date,\n ): Promise<void>\n /** Every definition the org holds, retired ones included, to the ceiling and one past it. */\n fields(orgId: string): Promise<CrmFieldDefinitionDoc[]>\n field(orgId: string, id: string): Promise<CrmFieldDefinitionDoc | null>\n createField(orgId: string, id: string, document: Record<string, unknown>): Promise<void>\n setFieldOptions(orgId: string, id: string, options: string[], now: Date): Promise<void>\n /** A picklist's stored document, or `null` for an org that never stored one (AGL-3756). */\n picklist(orgId: string, id: CrmPicklistId): Promise<Readonly<Record<string, unknown>> | null>\n /**\n * Read a picklist and write it back in one transaction, as the console's\n * route does, so a person editing the list at the same moment is not\n * overwritten. `change` answers the list to write, or `null` for none.\n */\n changePicklist<T>(\n orgId: string,\n id: CrmPicklistId,\n change: (stored: Readonly<Record<string, unknown>> | null) => { write: CrmPicklist | null; result: T },\n stamp: { hostId: string },\n ): Promise<T>\n /** A value's group onto every record of `object` holding `label` (a lead's `leadSourceDirection`). */\n regroup(orgId: string, object: CrmPicklistObject, target: CrmPicklistTarget, label: string, group: string | null): Promise<number>\n}\n\n/** The org subcollection each picklist target'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\nexport interface CrmSetupWriterDeps {\n store?: CrmSetupStore\n /** The org a site belongs to; the platform's lookup otherwise. */\n orgIdForHost?: (hostId: string) => Promise<string | null>\n /** The member, or `null` when they are not one; the platform's otherwise. */\n member?: (uid: string, orgId: string) => Promise<CrmSetupMember | null>\n}\n\nasync function platformMember(uid: string, orgId: string): Promise<CrmSetupMember | null> {\n const membership = await resolveOrgMembership(uid, orgId)\n const member = membership?.member\n if (!member) return null\n return {\n role: String(member.role ?? ''),\n canManageData: await memberHasOrgPermission(orgId, member, 'data.manage'),\n member: member as unknown as Readonly<Record<string, unknown>>,\n }\n}\n\n/** The CRM's collections over the Admin SDK. */\nexport function adminCrmSetupStore(firestore: () => Firestore = () => firebaseAdmin.app().firestore() as unknown as Firestore): CrmSetupStore {\n const org = (orgId: string) => firestore().collection('orgs').doc(orgId)\n const pipelinesOf = (orgId: string) => org(orgId).collection(CRM_COLLECTIONS.pipelines)\n const fieldsOf = (orgId: string) => org(orgId).collection(CRM_COLLECTIONS.contactFields)\n const withId = <T>(snap: FirebaseFirestore.DocumentSnapshot): T => ({ ...(snap.data() as object), $id: snap.id }) as T\n return {\n pipelines: async (orgId, readTokens) => {\n const snap = await pipelinesOf(orgId)\n .where('visibleTo', 'array-contains-any', [...readTokens])\n .orderBy(FieldPath.documentId())\n .limit(PIPELINE_READ_LIMIT)\n .get()\n return snap.docs.map((one) => withId<PipelineDoc>(one))\n },\n pipeline: async (orgId, id) => {\n const snap = await pipelinesOf(orgId).doc(id).get()\n return snap.exists ? withId<PipelineDoc>(snap) : null\n },\n savePipeline: async (orgId, write, now) => {\n const batch = firestore().batch()\n const ref = pipelinesOf(orgId).doc(write.id)\n if (write.create) batch.create(ref, write.create)\n else if (write.update) batch.update(ref, { ...write.update, updatedAt: now })\n for (const id of write.defaultsOff) {\n batch.update(pipelinesOf(orgId).doc(id), { isDefault: false, updatedAt: now })\n }\n await batch.commit()\n },\n fields: async (orgId) => {\n const snap = await fieldsOf(orgId).limit(CONTACT_FIELDS_MAX_PER_ORG + 1).get()\n return snap.docs.map((one) => withId<CrmFieldDefinitionDoc>(one))\n },\n field: async (orgId, id) => {\n const snap = await fieldsOf(orgId).doc(id).get()\n return snap.exists ? withId<CrmFieldDefinitionDoc>(snap) : null\n },\n createField: async (orgId, id, document) => {\n await fieldsOf(orgId).doc(id).create(document)\n },\n setFieldOptions: async (orgId, id, options, now) => {\n await fieldsOf(orgId).doc(id).update({ options, updatedAt: now })\n },\n picklist: async (orgId, id) => {\n const snap = await org(orgId).collection(CRM_COLLECTIONS.picklists).doc(id).get()\n return snap.exists ? ((snap.data() ?? {}) as Record<string, unknown>) : null\n },\n changePicklist: async (orgId, id, change, stamp) => {\n const ref = org(orgId).collection(CRM_COLLECTIONS.picklists).doc(id)\n return firestore().runTransaction(async (transaction) => {\n const snap = await transaction.get(ref)\n const stored = snap.exists ? ((snap.data() ?? {}) as Record<string, unknown>) : null\n const { write, result } = change(stored)\n if (write) {\n // The stamp the console's first write gives an org-wide CRM document.\n const created = normalizeCrmPicklist(stored) === null\n transaction.set(\n ref,\n {\n values: write.values,\n defaultValueId: write.defaultValueId,\n updatedAt: FieldValue.serverTimestamp(),\n ...(created\n ? { hostId: stamp.hostId, createdAt: FieldValue.serverTimestamp(), ...newResourceScopeFields([ORG_SCOPE_TOKEN]) }\n : {}),\n },\n { merge: true },\n )\n }\n return result\n })\n },\n regroup: (orgId, object, target, label, group) =>\n regroupEverywhere(firestore(), org(orgId).collection(TARGET_COLLECTIONS[object]), target, label, group),\n }\n}\n\n/** The scope a pipeline made from this site is stamped with: the board seed's. */\nfunction pipelineScope(context: Pick<PluginDraftContext, 'org' | 'hostId'>): string[] {\n const org = (context.org ?? null) as Record<string, unknown> | null\n return crmScopeTokens(org, consentGroupForHost(org, context.hostId))\n}\n\n/** What a pipeline's board at this site reads: the org's rows and the site group's. */\nfunction pipelineReadTokens(context: Pick<PluginDraftContext, 'org' | 'hostId'>): string[] {\n const org = (context.org ?? null) as Record<string, unknown> | null\n return crmReadTokens(consentGroupForHost(org, context.hostId))\n}\n\nfunction pipelineRecord(pipeline: PipelineDoc): PluginDraftRecord {\n return { id: pipeline.$id, name: String(pipeline.name ?? ''), versionId: null, facts: crmPipelineSetupFacts(pipeline.stages ?? []) }\n}\n\nfunction fieldRecord(field: CrmFieldDefinitionDoc): PluginDraftRecord {\n return {\n id: field.$id,\n name: String(field.label ?? ''),\n versionId: null,\n facts: {\n object: field.object ?? 'contact',\n key: field.key,\n type: field.type,\n ...(field.options ? { options: field.options } : {}),\n ...(field.currency ? { currency: field.currency } : {}),\n },\n }\n}\n\nfunction createCrmSetupGate(deps: CrmSetupWriterDeps) {\n const orgIdForHost = deps.orgIdForHost ?? ((hostId: string) => resolveOrgIdForHost(hostId))\n const memberOf = deps.member ?? platformMember\n /** Everything but the scope: the site is the org's, the plan, the member's role and permission. */\n return async (context: PluginDraftContext, scope: readonly string[], scopeRefusal: string): Promise<PluginDraftRefusal | null> => {\n if ((await orgIdForHost(context.hostId)) !== context.orgId) return { status: 404, error: 'Unknown site' }\n if (!checkEntitlement(context.org as never, 'crm')) {\n return { status: 403, error: crmSuiteRefusalMessage('Setting up the CRM') }\n }\n const member = await memberOf(context.uid, context.orgId)\n if (!member || !WRITER_ROLES.has(member.role) || member.member['orgSuspended'] === true) {\n return { status: 403, error: CRM_SETUP_ROLE_REFUSAL }\n }\n if (!member.canManageData) return { status: 403, error: CRM_SETUP_PERMISSION_REFUSAL }\n if (!memberCanSee(member.member as never, scope)) return { status: 403, error: scopeRefusal }\n return null\n }\n}\n\n/** The `crm-pipeline` writer: creates or brings a pipeline to the ask. */\nexport function createCrmPipelineSetupWriter(deps: CrmSetupWriterDeps = {}): PluginResourceDraftWriter {\n const store = deps.store ?? adminCrmSetupStore()\n const orgIdForHost = deps.orgIdForHost ?? ((hostId: string) => resolveOrgIdForHost(hostId))\n const gate = createCrmSetupGate(deps)\n return {\n refusal: (context) => gate(context, pipelineScope(context), CRM_SETUP_SCOPE_REFUSAL),\n check: (content) => checkCrmPipelineSetup(content),\n read: async ({ hostId, id }) => {\n const orgId = await orgIdForHost(hostId)\n const pipeline = orgId ? await store.pipeline(orgId, id) : null\n return pipeline ? pipelineRecord(pipeline) : null\n },\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readCrmPipelineSetup(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const visibleTo = pipelineScope(request)\n const refused = await gate(request, visibleTo, CRM_SETUP_SCOPE_REFUSAL)\n if (refused) return { ok: false, ...refused }\n // Asked again under the same id: the pipeline it already made.\n const made = await store.pipeline(request.orgId, request.id)\n if (made) return { ok: true, replayed: true, ...pipelineRecord(made) }\n const pipelines = await store.pipelines(request.orgId, pipelineReadTokens(request))\n const plan = planCrmPipelineSetup(read.value, pipelines, {\n visibleTo,\n hostId: request.hostId,\n uid: request.uid,\n nowMs: request.now.getTime(),\n })\n if (plan.kind === 'refuse') return { ok: false, status: 409, error: plan.error }\n if (plan.kind === 'create') {\n await store.savePipeline(request.orgId, { id: request.id, create: plan.document, defaultsOff: plan.defaultsOff }, request.now)\n return {\n ok: true,\n replayed: false,\n ...pipelineRecord({ ...(plan.document as unknown as PipelineDoc), $id: request.id }),\n }\n }\n const existing = pipelines.find((one) => one.$id === plan.id) as PipelineDoc\n const changed = Object.keys(plan.patch).length > 0 || plan.defaultsOff.length > 0\n if (changed) {\n await store.savePipeline(request.orgId, { id: plan.id, update: plan.patch, defaultsOff: plan.defaultsOff }, request.now)\n }\n return { ok: true, replayed: !changed, ...pipelineRecord({ ...existing, ...plan.patch }) }\n },\n }\n}\n\n/** The `crm-field` writer: creates a custom field, or finds the one asked for. */\nexport function createCrmFieldSetupWriter(deps: CrmSetupWriterDeps = {}): PluginResourceDraftWriter {\n const store = deps.store ?? adminCrmSetupStore()\n const orgIdForHost = deps.orgIdForHost ?? ((hostId: string) => resolveOrgIdForHost(hostId))\n const gate = createCrmSetupGate(deps)\n const scope = [ORG_SCOPE_TOKEN]\n return {\n refusal: (context) => gate(context, scope, CRM_SETUP_FIELD_SCOPE_REFUSAL),\n check: (content) => checkCrmFieldSetup(content),\n read: async ({ hostId, id }) => {\n const orgId = await orgIdForHost(hostId)\n const field = orgId ? await store.field(orgId, id) : null\n return field ? fieldRecord(field) : null\n },\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readCrmFieldSetup(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const refused = await gate(request, scope, CRM_SETUP_FIELD_SCOPE_REFUSAL)\n if (refused) return { ok: false, ...refused }\n const made = await store.field(request.orgId, request.id)\n if (made) return { ok: true, replayed: true, ...fieldRecord(made) }\n const definitions = await store.fields(request.orgId)\n const plan = planCrmFieldSetup(read.value, definitions, { hostId: request.hostId, now: request.now, visibleTo: scope })\n if (plan.kind === 'refuse') return { ok: false, status: plan.status, error: plan.error }\n if (plan.kind === 'create') {\n await store.createField(request.orgId, request.id, plan.document)\n return {\n ok: true,\n replayed: false,\n ...fieldRecord({ ...(plan.document as unknown as CrmFieldDefinitionDoc), $id: request.id }),\n }\n }\n const existing = definitions.find((one) => one.$id === plan.id) as CrmFieldDefinitionDoc\n if (!plan.options.length) return { ok: true, replayed: true, ...fieldRecord(existing) }\n const options = [...(existing.options ?? []), ...plan.options]\n await store.setFieldOptions(request.orgId, plan.id, options, request.now)\n return { ok: true, replayed: false, ...fieldRecord({ ...existing, options }) }\n },\n }\n}\n\nexport const crmPipelineSetupWriter = createCrmPipelineSetupWriter()\nexport const crmFieldSetupWriter = createCrmFieldSetupWriter()\n\n/*==========================================\n * PICKLIST VALUES (AGL-3756)\n *=========================================*/\n\n/** The record id a custom Choice field's values are reported under, beside a picklist's own id. */\nconst CUSTOM_RECORD_PREFIX = `${CRM_CUSTOM_CHOICE_LIST}:`\n\nfunction picklistRecord(definition: CrmPicklistDefinition, picklist: CrmPicklist, outcome?: CrmPicklistValuesOutcome): PluginDraftRecord {\n return {\n id: definition.id,\n name: crmPicklistValuesName(definition),\n versionId: null,\n facts: {\n list: definition.id,\n values: picklist.values.filter((value) => value.active).map((value) => value.label),\n ...(outcome ?? {}),\n },\n }\n}\n\nfunction choiceRecord(field: CrmFieldDefinitionDoc, outcome?: CrmPicklistValuesOutcome): PluginDraftRecord {\n return {\n id: `${CUSTOM_RECORD_PREFIX}${field.$id}`,\n name: String(field.label ?? ''),\n versionId: null,\n facts: { list: CRM_CUSTOM_CHOICE_LIST, object: field.object ?? 'contact', values: field.options ?? [], ...(outcome ?? {}) },\n }\n}\n\n/**\n * The `crm-picklist-values` writer: adds values to a standard CRM picklist\n * or a custom Choice field's choices, never removing, renaming, reordering\n * or reactivating one. Gated as the Fields page is — the `crm` feature, an\n * owner, admin or editor with `data.manage` — and org-wide, as every\n * picklist and field document is. A lead source added where records\n * already hold its label files them under its group (`leadSourceDirection`),\n * as the console's route does (AGL-3577).\n */\nexport function createCrmPicklistValuesSetupWriter(deps: CrmSetupWriterDeps = {}): PluginResourceDraftWriter {\n const store = deps.store ?? adminCrmSetupStore()\n const orgIdForHost = deps.orgIdForHost ?? ((hostId: string) => resolveOrgIdForHost(hostId))\n const gate = createCrmSetupGate(deps)\n const scope = [ORG_SCOPE_TOKEN]\n return {\n refusal: (context) => gate(context, scope, CRM_SETUP_PICKLIST_SCOPE_REFUSAL),\n check: (content) => checkCrmPicklistValuesSetup(content),\n read: async ({ hostId, id }) => {\n const orgId = await orgIdForHost(hostId)\n if (!orgId) return null\n if (id.startsWith(CUSTOM_RECORD_PREFIX)) {\n const field = await store.field(orgId, id.slice(CUSTOM_RECORD_PREFIX.length))\n return field ? choiceRecord(field) : null\n }\n const definition = crmPicklistDefinition(id)\n if (!definition) return null\n const listId = definition.id as CrmPicklistId\n return picklistRecord(definition, effectiveCrmPicklist(listId, await store.picklist(orgId, listId)))\n },\n write: async (request): Promise<PluginDraftWrite> => {\n const read = readCrmPicklistValuesSetup(request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const refused = await gate(request, scope, CRM_SETUP_PICKLIST_SCOPE_REFUSAL)\n if (refused) return { ok: false, ...refused }\n const setup = read.value\n\n if (setup.target.kind === 'custom') {\n const definitions = await store.fields(request.orgId)\n const plan = planCrmCustomChoiceValues({ ...setup, target: setup.target }, definitions)\n if (plan.ok === false) return { ok: false, status: plan.status, error: plan.error }\n const field = definitions.find((one) => one.$id === plan.id) as CrmFieldDefinitionDoc\n const { added, existing, inactive } = plan\n if (added.length) await store.setFieldOptions(request.orgId, plan.id, plan.options, request.now)\n return {\n ok: true,\n replayed: added.length === 0,\n ...choiceRecord({ ...field, options: plan.options }, { added, existing, inactive }),\n }\n }\n\n const id = setup.target.id\n const definition = crmPicklistDefinition(id) as CrmPicklistDefinition\n const planned = await store.changePicklist(\n request.orgId,\n id,\n (stored) => {\n const plan = planCrmPicklistValues(definition, effectiveCrmPicklist(id, stored), setup)\n return { write: plan.ok && plan.added.length ? plan.picklist : null, result: plan }\n },\n { hostId: request.hostId },\n )\n if (planned.ok === false) return { ok: false, status: 409, error: planned.error }\n // Records that already hold an added label take its group (AGL-3577).\n if (planned.added.length && picklistKeepsGroups(definition)) {\n for (const target of definition.targets) {\n if (!target.groupField || target.facet || target.arrayKey) continue\n for (const label of planned.added) {\n await store.regroup(request.orgId, target.object, target, label, picklistGroupOf(planned.picklist, label))\n }\n }\n }\n const { added, existing, inactive } = planned\n return { ok: true, replayed: added.length === 0, ...picklistRecord(definition, planned.picklist, { added, existing, inactive }) }\n },\n }\n}\n\nexport const crmPicklistValuesSetupWriter = createCrmPicklistValuesSetupWriter()\n"],"names":["consentGroupForHost","CONTACT_FIELDS_MAX_PER_ORG","CRM_COLLECTIONS","crmPicklistDefinition","crmReadTokens","crmScopeTokens","effectiveCrmPicklist","normalizeCrmPicklist","memberCanSee","checkEntitlement","newResourceScopeFields","ORG_SCOPE_TOKEN","firebaseAdmin","memberHasOrgPermission","resolveOrgIdForHost","resolveOrgMembership","FieldPath","FieldValue","checkCrmPicklistValuesSetup","CRM_CUSTOM_CHOICE_LIST","crmPicklistValuesName","planCrmCustomChoiceValues","planCrmPicklistValues","readCrmPicklistValuesSetup","checkCrmFieldSetup","checkCrmPipelineSetup","crmPipelineSetupFacts","planCrmFieldSetup","planCrmPipelineSetup","readCrmFieldSetup","readCrmPipelineSetup","picklistKeepsGroups","picklistGroupOf","regroupEverywhere","crmSuiteRefusalMessage","CRM_SETUP_ROLE_REFUSAL","CRM_SETUP_PERMISSION_REFUSAL","CRM_SETUP_SCOPE_REFUSAL","CRM_SETUP_FIELD_SCOPE_REFUSAL","CRM_SETUP_PICKLIST_SCOPE_REFUSAL","WRITER_ROLES","Set","PIPELINE_READ_LIMIT","TARGET_COLLECTIONS","contact","lead","company","companies","deal","deals","task","tasks","platformMember","uid","orgId","member","membership","role","String","canManageData","adminCrmSetupStore","firestore","app","org","collection","doc","pipelinesOf","pipelines","fieldsOf","contactFields","withId","snap","data","$id","id","readTokens","where","orderBy","documentId","limit","get","docs","map","one","pipeline","exists","savePipeline","write","now","batch","ref","create","update","updatedAt","defaultsOff","isDefault","commit","fields","field","createField","document","setFieldOptions","options","picklist","picklists","changePicklist","change","stamp","runTransaction","transaction","stored","result","created","set","values","defaultValueId","serverTimestamp","hostId","createdAt","merge","regroup","object","target","label","group","pipelineScope","context","pipelineReadTokens","pipelineRecord","name","versionId","facts","stages","fieldRecord","key","type","currency","createCrmSetupGate","deps","orgIdForHost","memberOf","scope","scopeRefusal","status","error","has","createCrmPipelineSetupWriter","store","gate","refusal","check","content","read","request","ok","problems","visibleTo","refused","made","replayed","plan","value","nowMs","getTime","kind","existing","find","changed","Object","keys","patch","length","createCrmFieldSetupWriter","definitions","crmPipelineSetupWriter","crmFieldSetupWriter","CUSTOM_RECORD_PREFIX","picklistRecord","definition","outcome","list","filter","active","choiceRecord","createCrmPicklistValuesSetupWriter","startsWith","slice","listId","setup","added","inactive","planned","targets","groupField","facet","arrayKey","crmPicklistValuesSetupWriter"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 CrmPicklist, type CrmPicklistTarget } from '@aglyn/aglyn/app-utils/crm';
|
|
18
|
+
/** The group a label is filed under in `picklist` — `null` for none, or a label it does not hold. */
|
|
19
|
+
export declare function picklistGroupOf(picklist: CrmPicklist, label: string | null): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Rewrite a target's GROUP field (AGL-3577) — a lead's
|
|
22
|
+
* `leadSourceDirection` — on every record holding `label` in any spelling
|
|
23
|
+
* (by its `keyField`) whose group is not `group` already. Answers how many
|
|
24
|
+
* were written. The field is derived, like the list keys a restamp writes,
|
|
25
|
+
* so `updatedAt` is left alone.
|
|
26
|
+
*
|
|
27
|
+
* Paged by document id rather than re-run: a record already holding the
|
|
28
|
+
* group stays in the query's answer.
|
|
29
|
+
*/
|
|
30
|
+
export declare function regroupEverywhere(firestore: FirebaseFirestore.Firestore, records: FirebaseFirestore.CollectionReference, target: CrmPicklistTarget, label: string, group: string | null): Promise<number>;
|