@aglyn/plugins-crm 1.0.0-beta.244 → 1.0.0-beta.246
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-field-drawer.js +4 -14
- package/src/lib/components/contact-field-drawer.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +5 -0
- package/src/lib/declarations.console-server.js +34 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/model/crm-record-routes.js +12 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-setup-content.d.ts +196 -0
- package/src/lib/model/crm-setup-content.js +416 -0
- package/src/lib/model/crm-setup-content.js.map +1 -0
- package/src/lib/server/crm-setup-ai-capabilities.d.ts +55 -0
- package/src/lib/server/crm-setup-ai-capabilities.js +198 -0
- package/src/lib/server/crm-setup-ai-capabilities.js.map +1 -0
- package/src/lib/server/crm-setup-writers.server.d.ts +92 -0
- package/src/lib/server/crm-setup-writers.server.js +315 -0
- package/src/lib/server/crm-setup-writers.server.js.map +1 -0
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
*/ import { CONTACT_FIELD_TYPES } from "@aglyn/aglyn/app-utils/contact-custom-fields";
|
|
17
|
+
import { CRM_FIELD_OBJECTS } from "@aglyn/aglyn/app-utils/crm";
|
|
18
|
+
import { registerPluginAiCapability } from "@aglyn/aglyn/plugin-manager/plugin-ai-capabilities";
|
|
19
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
20
|
+
import { CRM_FIELD_LABEL_MAX_LENGTH, CRM_FIELD_OPTION_MAX_LENGTH, CRM_FIELD_OPTIONS_MAX, CRM_FIELD_SETUP_RESOURCE, CRM_PIPELINE_SETUP_RESOURCE, CRM_SETUP_OPEN_STAGES_MAX, CRM_SETUP_STAGE_NAME_MAX } from "../model/crm-setup-content.js";
|
|
21
|
+
import { PIPELINE_NAME_MAX } from "../model/deal-board-model.js";
|
|
22
|
+
/**
|
|
23
|
+
* WHAT AN AI BUILD CAN SET UP IN THE CRM (AGL-3746): a deal pipeline with its
|
|
24
|
+
* stages, and a custom field on a contact, company, deal or lead.
|
|
25
|
+
*
|
|
26
|
+
* The capabilities the AI plugin's build planner offers for "set up a sales
|
|
27
|
+
* pipeline for my agency: Discovery, Proposal, Negotiation, Won/Lost" or
|
|
28
|
+
* "add a Budget field to deals". Each item is written by this plugin's
|
|
29
|
+
* `crm-pipeline` or `crm-field` writer, so every rule is the writer's and the
|
|
30
|
+
* console's: the `crm` plan feature, an owner, admin or editor with
|
|
31
|
+
* `data.manage`, the scope, `stagesProblem`, the field types and the org's
|
|
32
|
+
* 100-field ceiling. Both are saved live, as the console saves them (`live`):
|
|
33
|
+
* neither has a draft state. Matched by name, so asking again changes
|
|
34
|
+
* nothing twice.
|
|
35
|
+
*
|
|
36
|
+
* The arguments are flat, as the contract requires, and are the setup's own
|
|
37
|
+
* fields; the planner fills them, so no model runs (`estimateCredits` 0). No
|
|
38
|
+
* page block shows either, so neither changes what a visitor sees.
|
|
39
|
+
*
|
|
40
|
+
* A Salesforce "opportunity" is a deal here and an "account" a company; the
|
|
41
|
+
* descriptions say so, since a request may use either word.
|
|
42
|
+
*/ /** The operations' names in a build plan. */ export const CRM_PIPELINE_AI_OP = 'crm-pipeline';
|
|
43
|
+
export const CRM_FIELD_AI_OP = 'crm-field';
|
|
44
|
+
/** How many of each one build may set up. */ export const CRM_PIPELINE_AI_MAX_PER_PLAN = 3;
|
|
45
|
+
export const CRM_FIELD_AI_MAX_PER_PLAN = 12;
|
|
46
|
+
/** The CRM is included from Starter (`crm`); Free has none of it. */ const CRM_FEATURE = 'crm';
|
|
47
|
+
function pick(args, keys) {
|
|
48
|
+
return Object.fromEntries(keys.filter((key)=>args[key] !== undefined && args[key] !== '').map((key)=>[
|
|
49
|
+
key,
|
|
50
|
+
args[key]
|
|
51
|
+
]));
|
|
52
|
+
}
|
|
53
|
+
/** The pipeline writer's content for an item: its arguments, as they are. Pure. */ export function crmPipelineSetupContentFromArgs(args) {
|
|
54
|
+
return pick(args, [
|
|
55
|
+
'name',
|
|
56
|
+
'renameFrom',
|
|
57
|
+
'stages',
|
|
58
|
+
'wonStage',
|
|
59
|
+
'lostStage',
|
|
60
|
+
'makeDefault'
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
/** The field writer's content for an item: its arguments, as they are. Pure. */ export function crmFieldSetupContentFromArgs(args) {
|
|
64
|
+
return pick(args, [
|
|
65
|
+
'object',
|
|
66
|
+
'label',
|
|
67
|
+
'type',
|
|
68
|
+
'options',
|
|
69
|
+
'required'
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
export const crmPipelineAiCapability = {
|
|
73
|
+
op: CRM_PIPELINE_AI_OP,
|
|
74
|
+
noun: 'sales pipeline',
|
|
75
|
+
where: 'CRM → Deals → Pipelines',
|
|
76
|
+
intents: [
|
|
77
|
+
'CRM deal pipelines and their stages — "set up a sales pipeline: Discovery, Proposal, Negotiation, Won/Lost", rename a pipeline, add or reorder stages, set a stage’s win probability, make a pipeline the default; a Salesforce opportunity pipeline is a deal pipeline'
|
|
78
|
+
],
|
|
79
|
+
argsSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
name: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'The pipeline’s name, e.g. "Agency sales". An existing pipeline of this name (any case) is updated rather than duplicated; give "renameFrom" to rename one.',
|
|
85
|
+
maxLength: PIPELINE_NAME_MAX
|
|
86
|
+
},
|
|
87
|
+
renameFrom: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'Only to rename: the current name of the pipeline that becomes "name".',
|
|
90
|
+
maxLength: PIPELINE_NAME_MAX
|
|
91
|
+
},
|
|
92
|
+
stages: {
|
|
93
|
+
type: 'array',
|
|
94
|
+
description: 'The OPEN stages a deal moves through, in order, each a name with an optional win probability in parentheses, e.g. "Discovery (20%)". Never Won or Lost: every pipeline already has one of each (name them in wonStage and lostStage). Leave out to keep the stages as they are (a new pipeline then gets the standard set). Stages already in the pipeline and not listed are kept.',
|
|
95
|
+
items: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
maxLength: CRM_SETUP_STAGE_NAME_MAX + 8
|
|
98
|
+
},
|
|
99
|
+
maxItems: CRM_SETUP_OPEN_STAGES_MAX
|
|
100
|
+
},
|
|
101
|
+
wonStage: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'What the closing Won stage is called, where the request names it, e.g. "Won". Leave out to keep "Closed Won".',
|
|
104
|
+
maxLength: CRM_SETUP_STAGE_NAME_MAX
|
|
105
|
+
},
|
|
106
|
+
lostStage: {
|
|
107
|
+
type: 'string',
|
|
108
|
+
description: 'What the closing Lost stage is called, where the request names it, e.g. "Lost". Leave out to keep "Closed Lost".',
|
|
109
|
+
maxLength: CRM_SETUP_STAGE_NAME_MAX
|
|
110
|
+
},
|
|
111
|
+
makeDefault: {
|
|
112
|
+
type: 'boolean',
|
|
113
|
+
description: 'true only when the request says new deals should land in this pipeline (its main or default pipeline).'
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
required: [
|
|
117
|
+
'name'
|
|
118
|
+
],
|
|
119
|
+
additionalProperties: false
|
|
120
|
+
},
|
|
121
|
+
maxPerPlan: CRM_PIPELINE_AI_MAX_PER_PLAN,
|
|
122
|
+
freeAllowed: false,
|
|
123
|
+
feature: CRM_FEATURE,
|
|
124
|
+
draftResource: CRM_PIPELINE_SETUP_RESOURCE,
|
|
125
|
+
live: true,
|
|
126
|
+
// Written without a model: the planner already filled the arguments.
|
|
127
|
+
estimateCredits: ()=>0,
|
|
128
|
+
degrade: 'omit',
|
|
129
|
+
draftContent: (item)=>crmPipelineSetupContentFromArgs(item.args)
|
|
130
|
+
};
|
|
131
|
+
export const crmFieldAiCapability = {
|
|
132
|
+
op: CRM_FIELD_AI_OP,
|
|
133
|
+
noun: 'CRM custom field',
|
|
134
|
+
where: 'CRM → Fields',
|
|
135
|
+
intents: [
|
|
136
|
+
'CRM custom fields on contacts, companies (accounts), deals (opportunities) or leads — "add a Budget field to opportunities", "a Choice field for Industry on companies"; types text, number, date, choice, checkbox and link only'
|
|
137
|
+
],
|
|
138
|
+
argsSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
object: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
description: 'The record it describes: contact; company (an account); deal (an opportunity); lead.',
|
|
144
|
+
enum: CRM_FIELD_OBJECTS
|
|
145
|
+
},
|
|
146
|
+
label: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: 'The field’s name as people read it, e.g. "Budget". A field of this name on the same record (any case) is left as it is rather than duplicated.',
|
|
149
|
+
maxLength: CRM_FIELD_LABEL_MAX_LENGTH
|
|
150
|
+
},
|
|
151
|
+
type: {
|
|
152
|
+
type: 'string',
|
|
153
|
+
description: 'What it holds: text; number (also an amount of money or a percentage — there is no currency type); date; select (a Choice from listed options); checkbox (yes or no); url (a link).',
|
|
154
|
+
enum: CONTACT_FIELD_TYPES
|
|
155
|
+
},
|
|
156
|
+
options: {
|
|
157
|
+
type: 'array',
|
|
158
|
+
description: 'For select only: the choices, in order. A select field that exists gains the ones it lacks.',
|
|
159
|
+
items: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
maxLength: CRM_FIELD_OPTION_MAX_LENGTH
|
|
162
|
+
},
|
|
163
|
+
maxItems: CRM_FIELD_OPTIONS_MAX
|
|
164
|
+
},
|
|
165
|
+
required: {
|
|
166
|
+
type: 'boolean',
|
|
167
|
+
description: 'true only when the request says the field must always be filled in.'
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
required: [
|
|
171
|
+
'object',
|
|
172
|
+
'label',
|
|
173
|
+
'type'
|
|
174
|
+
],
|
|
175
|
+
additionalProperties: false
|
|
176
|
+
},
|
|
177
|
+
maxPerPlan: CRM_FIELD_AI_MAX_PER_PLAN,
|
|
178
|
+
freeAllowed: false,
|
|
179
|
+
feature: CRM_FEATURE,
|
|
180
|
+
draftResource: CRM_FIELD_SETUP_RESOURCE,
|
|
181
|
+
live: true,
|
|
182
|
+
estimateCredits: ()=>0,
|
|
183
|
+
degrade: 'omit',
|
|
184
|
+
draftContent: (item)=>crmFieldSetupContentFromArgs(item.args)
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Registers both capabilities; the console-server declarations call it beside
|
|
188
|
+
* the writers, since only the console runs AI jobs (AGL-3026). Idempotent.
|
|
189
|
+
*/ export function registerCrmSetupAiCapabilities() {
|
|
190
|
+
registerPluginAiCapability(crmPipelineAiCapability, {
|
|
191
|
+
pluginId: BUNDLE_ID
|
|
192
|
+
});
|
|
193
|
+
registerPluginAiCapability(crmFieldAiCapability, {
|
|
194
|
+
pluginId: BUNDLE_ID
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
//# sourceMappingURL=crm-setup-ai-capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/crm-setup-ai-capabilities.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 { CONTACT_FIELD_TYPES } from '@aglyn/aglyn/app-utils/contact-custom-fields'\nimport { CRM_FIELD_OBJECTS } from '@aglyn/aglyn/app-utils/crm'\nimport {\n registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n CRM_FIELD_LABEL_MAX_LENGTH,\n CRM_FIELD_OPTION_MAX_LENGTH,\n CRM_FIELD_OPTIONS_MAX,\n CRM_FIELD_SETUP_RESOURCE,\n CRM_PIPELINE_SETUP_RESOURCE,\n CRM_SETUP_OPEN_STAGES_MAX,\n CRM_SETUP_STAGE_NAME_MAX,\n} from '../model/crm-setup-content'\nimport { PIPELINE_NAME_MAX } from '../model/deal-board-model'\n\n/**\n * WHAT AN AI BUILD CAN SET UP IN THE CRM (AGL-3746): a deal pipeline with its\n * stages, and a custom field on a contact, company, deal or lead.\n *\n * The capabilities the AI plugin's build planner offers for \"set up a sales\n * pipeline for my agency: Discovery, Proposal, Negotiation, Won/Lost\" or\n * \"add a Budget field to deals\". Each item is written by this plugin's\n * `crm-pipeline` or `crm-field` writer, so every rule is the writer's and the\n * console's: the `crm` plan feature, an owner, admin or editor with\n * `data.manage`, the scope, `stagesProblem`, the field types and the org's\n * 100-field ceiling. Both are saved live, as the console saves them (`live`):\n * neither has a draft state. Matched by name, so asking again changes\n * nothing twice.\n *\n * The arguments are flat, as the contract requires, and are the setup's own\n * fields; the planner fills them, so no model runs (`estimateCredits` 0). No\n * page block shows either, so neither changes what a visitor sees.\n *\n * A Salesforce \"opportunity\" is a deal here and an \"account\" a company; the\n * descriptions say so, since a request may use either word.\n */\n\n/** The operations' names in a build plan. */\nexport const CRM_PIPELINE_AI_OP = 'crm-pipeline'\nexport const CRM_FIELD_AI_OP = 'crm-field'\n\n/** How many of each one build may set up. */\nexport const CRM_PIPELINE_AI_MAX_PER_PLAN = 3\nexport const CRM_FIELD_AI_MAX_PER_PLAN = 12\n\n/** The CRM is included from Starter (`crm`); Free has none of it. */\nconst CRM_FEATURE = 'crm'\n\nfunction pick(args: PluginAiCapabilityArgs, keys: readonly string[]): Record<string, unknown> {\n return Object.fromEntries(keys.filter((key) => args[key] !== undefined && args[key] !== '').map((key) => [key, args[key]]))\n}\n\n/** The pipeline writer's content for an item: its arguments, as they are. Pure. */\nexport function crmPipelineSetupContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>> {\n return pick(args, ['name', 'renameFrom', 'stages', 'wonStage', 'lostStage', 'makeDefault'])\n}\n\n/** The field writer's content for an item: its arguments, as they are. Pure. */\nexport function crmFieldSetupContentFromArgs(args: PluginAiCapabilityArgs): Readonly<Record<string, unknown>> {\n return pick(args, ['object', 'label', 'type', 'options', 'required'])\n}\n\nexport const crmPipelineAiCapability: PluginAiCapability = {\n op: CRM_PIPELINE_AI_OP,\n noun: 'sales pipeline',\n where: 'CRM → Deals → Pipelines',\n intents: [\n 'CRM deal pipelines and their stages — \"set up a sales pipeline: Discovery, Proposal, Negotiation, Won/Lost\", rename a pipeline, add or reorder stages, set a stage’s win probability, make a pipeline the default; a Salesforce opportunity pipeline is a deal pipeline',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n name: {\n type: 'string',\n description:\n 'The pipeline’s name, e.g. \"Agency sales\". An existing pipeline of this name (any case) is updated rather than duplicated; give \"renameFrom\" to rename one.',\n maxLength: PIPELINE_NAME_MAX,\n },\n renameFrom: {\n type: 'string',\n description: 'Only to rename: the current name of the pipeline that becomes \"name\".',\n maxLength: PIPELINE_NAME_MAX,\n },\n stages: {\n type: 'array',\n description:\n 'The OPEN stages a deal moves through, in order, each a name with an optional win probability in parentheses, e.g. \"Discovery (20%)\". Never Won or Lost: every pipeline already has one of each (name them in wonStage and lostStage). Leave out to keep the stages as they are (a new pipeline then gets the standard set). Stages already in the pipeline and not listed are kept.',\n items: { type: 'string', maxLength: CRM_SETUP_STAGE_NAME_MAX + 8 },\n maxItems: CRM_SETUP_OPEN_STAGES_MAX,\n },\n wonStage: {\n type: 'string',\n description: 'What the closing Won stage is called, where the request names it, e.g. \"Won\". Leave out to keep \"Closed Won\".',\n maxLength: CRM_SETUP_STAGE_NAME_MAX,\n },\n lostStage: {\n type: 'string',\n description: 'What the closing Lost stage is called, where the request names it, e.g. \"Lost\". Leave out to keep \"Closed Lost\".',\n maxLength: CRM_SETUP_STAGE_NAME_MAX,\n },\n makeDefault: {\n type: 'boolean',\n description: 'true only when the request says new deals should land in this pipeline (its main or default pipeline).',\n },\n },\n required: ['name'],\n additionalProperties: false,\n },\n maxPerPlan: CRM_PIPELINE_AI_MAX_PER_PLAN,\n freeAllowed: false,\n feature: CRM_FEATURE,\n draftResource: CRM_PIPELINE_SETUP_RESOURCE,\n live: true,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n draftContent: (item) => crmPipelineSetupContentFromArgs(item.args),\n}\n\nexport const crmFieldAiCapability: PluginAiCapability = {\n op: CRM_FIELD_AI_OP,\n noun: 'CRM custom field',\n where: 'CRM → Fields',\n intents: [\n 'CRM custom fields on contacts, companies (accounts), deals (opportunities) or leads — \"add a Budget field to opportunities\", \"a Choice field for Industry on companies\"; types text, number, date, choice, checkbox and link only',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n object: {\n type: 'string',\n description: 'The record it describes: contact; company (an account); deal (an opportunity); lead.',\n enum: CRM_FIELD_OBJECTS,\n },\n label: {\n type: 'string',\n description:\n 'The field’s name as people read it, e.g. \"Budget\". A field of this name on the same record (any case) is left as it is rather than duplicated.',\n maxLength: CRM_FIELD_LABEL_MAX_LENGTH,\n },\n type: {\n type: 'string',\n description:\n 'What it holds: text; number (also an amount of money or a percentage — there is no currency type); date; select (a Choice from listed options); checkbox (yes or no); url (a link).',\n enum: CONTACT_FIELD_TYPES,\n },\n options: {\n type: 'array',\n description: 'For select only: the choices, in order. A select field that exists gains the ones it lacks.',\n items: { type: 'string', maxLength: CRM_FIELD_OPTION_MAX_LENGTH },\n maxItems: CRM_FIELD_OPTIONS_MAX,\n },\n required: {\n type: 'boolean',\n description: 'true only when the request says the field must always be filled in.',\n },\n },\n required: ['object', 'label', 'type'],\n additionalProperties: false,\n },\n maxPerPlan: CRM_FIELD_AI_MAX_PER_PLAN,\n freeAllowed: false,\n feature: CRM_FEATURE,\n draftResource: CRM_FIELD_SETUP_RESOURCE,\n live: true,\n estimateCredits: () => 0,\n degrade: 'omit',\n draftContent: (item) => crmFieldSetupContentFromArgs(item.args),\n}\n\n/**\n * Registers both capabilities; the console-server declarations call it beside\n * the writers, since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerCrmSetupAiCapabilities(): void {\n registerPluginAiCapability(crmPipelineAiCapability, { pluginId: BUNDLE_ID })\n registerPluginAiCapability(crmFieldAiCapability, { pluginId: BUNDLE_ID })\n}\n"],"names":["CONTACT_FIELD_TYPES","CRM_FIELD_OBJECTS","registerPluginAiCapability","BUNDLE_ID","CRM_FIELD_LABEL_MAX_LENGTH","CRM_FIELD_OPTION_MAX_LENGTH","CRM_FIELD_OPTIONS_MAX","CRM_FIELD_SETUP_RESOURCE","CRM_PIPELINE_SETUP_RESOURCE","CRM_SETUP_OPEN_STAGES_MAX","CRM_SETUP_STAGE_NAME_MAX","PIPELINE_NAME_MAX","CRM_PIPELINE_AI_OP","CRM_FIELD_AI_OP","CRM_PIPELINE_AI_MAX_PER_PLAN","CRM_FIELD_AI_MAX_PER_PLAN","CRM_FEATURE","pick","args","keys","Object","fromEntries","filter","key","undefined","map","crmPipelineSetupContentFromArgs","crmFieldSetupContentFromArgs","crmPipelineAiCapability","op","noun","where","intents","argsSchema","type","properties","name","description","maxLength","renameFrom","stages","items","maxItems","wonStage","lostStage","makeDefault","required","additionalProperties","maxPerPlan","freeAllowed","feature","draftResource","live","estimateCredits","degrade","draftContent","item","crmFieldAiCapability","object","enum","label","options","registerCrmSetupAiCapabilities","pluginId"],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
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 { PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
|
|
18
|
+
import { type CrmFieldDefinitionDoc } from '../model/crm-setup-content';
|
|
19
|
+
import type { PipelineDoc } from '../model/deal-board-model';
|
|
20
|
+
/**
|
|
21
|
+
* THE CRM'S SETUP WRITERS (AGL-3746): a deal pipeline and a custom field,
|
|
22
|
+
* saved for another plugin — Aglyn AI's build — under the CRM's rules.
|
|
23
|
+
*
|
|
24
|
+
* Registered on the core's resource-drafts seam as `crm-pipeline` and
|
|
25
|
+
* `crm-field`. Neither is a draft: a pipeline and a field have no draft
|
|
26
|
+
* state, and the console saves both live, so these do too, and their AI
|
|
27
|
+
* capabilities say so (`live`). What they write is exactly what the
|
|
28
|
+
* console's Pipelines dialog and Fields section write
|
|
29
|
+
* (`model/crm-setup-content.ts`), and they admit exactly whom the Firestore
|
|
30
|
+
* rules admit for those writes, since the Admin SDK passes the rules by:
|
|
31
|
+
*
|
|
32
|
+
* - THE SITE is the org's, and THE PLAN carries the CRM (`crm`, Starter and
|
|
33
|
+
* up — `orgCarriesCrmSuite`), refused in the suite gate's words;
|
|
34
|
+
* - THE MEMBER is an owner, admin or editor (`canWriteOrgData`) whose org
|
|
35
|
+
* access is not suspended, holding `data.manage`, the key the whole CRM
|
|
36
|
+
* is gated on;
|
|
37
|
+
* - THE SCOPE is one the member can create in (`canCreateScoped`): a
|
|
38
|
+
* pipeline is stamped as the board's seed stamps it from this site
|
|
39
|
+
* (`crmScopeTokens`), a field org-wide, which only an org-wide member
|
|
40
|
+
* reaches.
|
|
41
|
+
*
|
|
42
|
+
* Idempotent: the job's attempt key is a new pipeline's or field's id, and
|
|
43
|
+
* an existing pipeline, stage or field is matched by name (see the model),
|
|
44
|
+
* so asking again changes nothing.
|
|
45
|
+
*/
|
|
46
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
47
|
+
/** The refusals, in the console's and the routes' words. */
|
|
48
|
+
export declare const CRM_SETUP_ROLE_REFUSAL = "Editing the CRM requires the editor role";
|
|
49
|
+
export declare const CRM_SETUP_PERMISSION_REFUSAL = "Your organization role does not allow editing organization data";
|
|
50
|
+
export declare const CRM_SETUP_SCOPE_REFUSAL = "Your access to this organization does not reach that site";
|
|
51
|
+
export declare const CRM_SETUP_FIELD_SCOPE_REFUSAL = "Custom fields are organization-wide, and your access reaches only some sites";
|
|
52
|
+
/** Who the member is in the org, as the writers ask. */
|
|
53
|
+
export interface CrmSetupMember {
|
|
54
|
+
role: string;
|
|
55
|
+
canManageData: boolean;
|
|
56
|
+
/** The membership itself, for its reach and whether it is suspended. */
|
|
57
|
+
member: Readonly<Record<string, unknown>>;
|
|
58
|
+
}
|
|
59
|
+
/** The reads and writes the writers make, over the Admin SDK unless a spec hands in its own. */
|
|
60
|
+
export interface CrmSetupStore {
|
|
61
|
+
/** The pipelines readable under these scope tokens. */
|
|
62
|
+
pipelines(orgId: string, readTokens: readonly string[]): Promise<PipelineDoc[]>;
|
|
63
|
+
pipeline(orgId: string, id: string): Promise<PipelineDoc | null>;
|
|
64
|
+
/** One batch: the new or changed pipeline, and the default flag off the others. */
|
|
65
|
+
savePipeline(orgId: string, write: {
|
|
66
|
+
id: string;
|
|
67
|
+
create?: Record<string, unknown>;
|
|
68
|
+
update?: Record<string, unknown>;
|
|
69
|
+
defaultsOff: string[];
|
|
70
|
+
}, now: Date): Promise<void>;
|
|
71
|
+
/** Every definition the org holds, retired ones included, to the ceiling and one past it. */
|
|
72
|
+
fields(orgId: string): Promise<CrmFieldDefinitionDoc[]>;
|
|
73
|
+
field(orgId: string, id: string): Promise<CrmFieldDefinitionDoc | null>;
|
|
74
|
+
createField(orgId: string, id: string, document: Record<string, unknown>): Promise<void>;
|
|
75
|
+
setFieldOptions(orgId: string, id: string, options: string[], now: Date): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
export interface CrmSetupWriterDeps {
|
|
78
|
+
store?: CrmSetupStore;
|
|
79
|
+
/** The org a site belongs to; the platform's lookup otherwise. */
|
|
80
|
+
orgIdForHost?: (hostId: string) => Promise<string | null>;
|
|
81
|
+
/** The member, or `null` when they are not one; the platform's otherwise. */
|
|
82
|
+
member?: (uid: string, orgId: string) => Promise<CrmSetupMember | null>;
|
|
83
|
+
}
|
|
84
|
+
/** The CRM's collections over the Admin SDK. */
|
|
85
|
+
export declare function adminCrmSetupStore(firestore?: () => Firestore): CrmSetupStore;
|
|
86
|
+
/** The `crm-pipeline` writer: creates or brings a pipeline to the ask. */
|
|
87
|
+
export declare function createCrmPipelineSetupWriter(deps?: CrmSetupWriterDeps): PluginResourceDraftWriter;
|
|
88
|
+
/** The `crm-field` writer: creates a custom field, or finds the one asked for. */
|
|
89
|
+
export declare function createCrmFieldSetupWriter(deps?: CrmSetupWriterDeps): PluginResourceDraftWriter;
|
|
90
|
+
export declare const crmPipelineSetupWriter: PluginResourceDraftWriter;
|
|
91
|
+
export declare const crmFieldSetupWriter: PluginResourceDraftWriter;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,315 @@
|
|
|
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, crmReadTokens, crmScopeTokens } 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 { 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 } from "firebase-admin/firestore";
|
|
26
|
+
import { checkCrmFieldSetup, checkCrmPipelineSetup, crmPipelineSetupFacts, planCrmFieldSetup, planCrmPipelineSetup, readCrmFieldSetup, readCrmPipelineSetup } from "../model/crm-setup-content.js";
|
|
27
|
+
import { crmSuiteRefusalMessage } from "./suite-gate.js";
|
|
28
|
+
/** The refusals, in the console's and the routes' words. */ export const CRM_SETUP_ROLE_REFUSAL = 'Editing the CRM requires the editor role';
|
|
29
|
+
export const CRM_SETUP_PERMISSION_REFUSAL = 'Your organization role does not allow editing organization data';
|
|
30
|
+
export const CRM_SETUP_SCOPE_REFUSAL = 'Your access to this organization does not reach that site';
|
|
31
|
+
export const CRM_SETUP_FIELD_SCOPE_REFUSAL = 'Custom fields are organization-wide, and your access reaches only some sites';
|
|
32
|
+
/** Roles allowed to write the CRM's setup — the rules' `canWriteOrgData()`. */ const WRITER_ROLES = new Set([
|
|
33
|
+
'owner',
|
|
34
|
+
'admin',
|
|
35
|
+
'editor'
|
|
36
|
+
]);
|
|
37
|
+
/** The most pipelines one site's read returns, as the board reads them. */ const PIPELINE_READ_LIMIT = 50;
|
|
38
|
+
async function platformMember(uid, orgId) {
|
|
39
|
+
var _member_role;
|
|
40
|
+
const membership = await resolveOrgMembership(uid, orgId);
|
|
41
|
+
const member = membership == null ? void 0 : membership.member;
|
|
42
|
+
if (!member) return null;
|
|
43
|
+
return {
|
|
44
|
+
role: String((_member_role = member.role) != null ? _member_role : ''),
|
|
45
|
+
canManageData: await memberHasOrgPermission(orgId, member, 'data.manage'),
|
|
46
|
+
member: member
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** The CRM's collections over the Admin SDK. */ export function adminCrmSetupStore(firestore = ()=>firebaseAdmin.app().firestore()) {
|
|
50
|
+
const org = (orgId)=>firestore().collection('orgs').doc(orgId);
|
|
51
|
+
const pipelinesOf = (orgId)=>org(orgId).collection(CRM_COLLECTIONS.pipelines);
|
|
52
|
+
const fieldsOf = (orgId)=>org(orgId).collection(CRM_COLLECTIONS.contactFields);
|
|
53
|
+
const withId = (snap)=>_extends({}, snap.data(), {
|
|
54
|
+
$id: snap.id
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
pipelines: async (orgId, readTokens)=>{
|
|
58
|
+
const snap = await pipelinesOf(orgId).where('visibleTo', 'array-contains-any', [
|
|
59
|
+
...readTokens
|
|
60
|
+
]).orderBy(FieldPath.documentId()).limit(PIPELINE_READ_LIMIT).get();
|
|
61
|
+
return snap.docs.map((one)=>withId(one));
|
|
62
|
+
},
|
|
63
|
+
pipeline: async (orgId, id)=>{
|
|
64
|
+
const snap = await pipelinesOf(orgId).doc(id).get();
|
|
65
|
+
return snap.exists ? withId(snap) : null;
|
|
66
|
+
},
|
|
67
|
+
savePipeline: async (orgId, write, now)=>{
|
|
68
|
+
const batch = firestore().batch();
|
|
69
|
+
const ref = pipelinesOf(orgId).doc(write.id);
|
|
70
|
+
if (write.create) batch.create(ref, write.create);
|
|
71
|
+
else if (write.update) batch.update(ref, _extends({}, write.update, {
|
|
72
|
+
updatedAt: now
|
|
73
|
+
}));
|
|
74
|
+
for (const id of write.defaultsOff){
|
|
75
|
+
batch.update(pipelinesOf(orgId).doc(id), {
|
|
76
|
+
isDefault: false,
|
|
77
|
+
updatedAt: now
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
await batch.commit();
|
|
81
|
+
},
|
|
82
|
+
fields: async (orgId)=>{
|
|
83
|
+
const snap = await fieldsOf(orgId).limit(CONTACT_FIELDS_MAX_PER_ORG + 1).get();
|
|
84
|
+
return snap.docs.map((one)=>withId(one));
|
|
85
|
+
},
|
|
86
|
+
field: async (orgId, id)=>{
|
|
87
|
+
const snap = await fieldsOf(orgId).doc(id).get();
|
|
88
|
+
return snap.exists ? withId(snap) : null;
|
|
89
|
+
},
|
|
90
|
+
createField: async (orgId, id, document)=>{
|
|
91
|
+
await fieldsOf(orgId).doc(id).create(document);
|
|
92
|
+
},
|
|
93
|
+
setFieldOptions: async (orgId, id, options, now)=>{
|
|
94
|
+
await fieldsOf(orgId).doc(id).update({
|
|
95
|
+
options,
|
|
96
|
+
updatedAt: now
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** The scope a pipeline made from this site is stamped with: the board seed's. */ function pipelineScope(context) {
|
|
102
|
+
var _context_org;
|
|
103
|
+
const org = (_context_org = context.org) != null ? _context_org : null;
|
|
104
|
+
return crmScopeTokens(org, consentGroupForHost(org, context.hostId));
|
|
105
|
+
}
|
|
106
|
+
/** What a pipeline's board at this site reads: the org's rows and the site group's. */ function pipelineReadTokens(context) {
|
|
107
|
+
var _context_org;
|
|
108
|
+
const org = (_context_org = context.org) != null ? _context_org : null;
|
|
109
|
+
return crmReadTokens(consentGroupForHost(org, context.hostId));
|
|
110
|
+
}
|
|
111
|
+
function pipelineRecord(pipeline) {
|
|
112
|
+
var _pipeline_name, _pipeline_stages;
|
|
113
|
+
return {
|
|
114
|
+
id: pipeline.$id,
|
|
115
|
+
name: String((_pipeline_name = pipeline.name) != null ? _pipeline_name : ''),
|
|
116
|
+
versionId: null,
|
|
117
|
+
facts: crmPipelineSetupFacts((_pipeline_stages = pipeline.stages) != null ? _pipeline_stages : [])
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function fieldRecord(field) {
|
|
121
|
+
var _field_label, _field_object;
|
|
122
|
+
return {
|
|
123
|
+
id: field.$id,
|
|
124
|
+
name: String((_field_label = field.label) != null ? _field_label : ''),
|
|
125
|
+
versionId: null,
|
|
126
|
+
facts: _extends({
|
|
127
|
+
object: (_field_object = field.object) != null ? _field_object : 'contact',
|
|
128
|
+
key: field.key,
|
|
129
|
+
type: field.type
|
|
130
|
+
}, field.options ? {
|
|
131
|
+
options: field.options
|
|
132
|
+
} : {})
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function createCrmSetupGate(deps) {
|
|
136
|
+
var _deps_orgIdForHost, _deps_member;
|
|
137
|
+
const orgIdForHost = (_deps_orgIdForHost = deps.orgIdForHost) != null ? _deps_orgIdForHost : (hostId)=>resolveOrgIdForHost(hostId);
|
|
138
|
+
const memberOf = (_deps_member = deps.member) != null ? _deps_member : platformMember;
|
|
139
|
+
/** Everything but the scope: the site is the org's, the plan, the member's role and permission. */ return async (context, scope, scopeRefusal)=>{
|
|
140
|
+
if (await orgIdForHost(context.hostId) !== context.orgId) return {
|
|
141
|
+
status: 404,
|
|
142
|
+
error: 'Unknown site'
|
|
143
|
+
};
|
|
144
|
+
if (!checkEntitlement(context.org, 'crm')) {
|
|
145
|
+
return {
|
|
146
|
+
status: 403,
|
|
147
|
+
error: crmSuiteRefusalMessage('Setting up the CRM')
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const member = await memberOf(context.uid, context.orgId);
|
|
151
|
+
if (!member || !WRITER_ROLES.has(member.role) || member.member['orgSuspended'] === true) {
|
|
152
|
+
return {
|
|
153
|
+
status: 403,
|
|
154
|
+
error: CRM_SETUP_ROLE_REFUSAL
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!member.canManageData) return {
|
|
158
|
+
status: 403,
|
|
159
|
+
error: CRM_SETUP_PERMISSION_REFUSAL
|
|
160
|
+
};
|
|
161
|
+
if (!memberCanSee(member.member, scope)) return {
|
|
162
|
+
status: 403,
|
|
163
|
+
error: scopeRefusal
|
|
164
|
+
};
|
|
165
|
+
return null;
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The `crm-pipeline` writer: creates or brings a pipeline to the ask. */ export function createCrmPipelineSetupWriter(deps = {}) {
|
|
169
|
+
var _deps_store, _deps_orgIdForHost;
|
|
170
|
+
const store = (_deps_store = deps.store) != null ? _deps_store : adminCrmSetupStore();
|
|
171
|
+
const orgIdForHost = (_deps_orgIdForHost = deps.orgIdForHost) != null ? _deps_orgIdForHost : (hostId)=>resolveOrgIdForHost(hostId);
|
|
172
|
+
const gate = createCrmSetupGate(deps);
|
|
173
|
+
return {
|
|
174
|
+
refusal: (context)=>gate(context, pipelineScope(context), CRM_SETUP_SCOPE_REFUSAL),
|
|
175
|
+
check: (content)=>checkCrmPipelineSetup(content),
|
|
176
|
+
read: async ({ hostId, id })=>{
|
|
177
|
+
const orgId = await orgIdForHost(hostId);
|
|
178
|
+
const pipeline = orgId ? await store.pipeline(orgId, id) : null;
|
|
179
|
+
return pipeline ? pipelineRecord(pipeline) : null;
|
|
180
|
+
},
|
|
181
|
+
write: async (request)=>{
|
|
182
|
+
const read = readCrmPipelineSetup(request.content);
|
|
183
|
+
if (read.ok === false) return {
|
|
184
|
+
ok: false,
|
|
185
|
+
status: 400,
|
|
186
|
+
error: read.problems[0]
|
|
187
|
+
};
|
|
188
|
+
const visibleTo = pipelineScope(request);
|
|
189
|
+
const refused = await gate(request, visibleTo, CRM_SETUP_SCOPE_REFUSAL);
|
|
190
|
+
if (refused) return _extends({
|
|
191
|
+
ok: false
|
|
192
|
+
}, refused);
|
|
193
|
+
// Asked again under the same id: the pipeline it already made.
|
|
194
|
+
const made = await store.pipeline(request.orgId, request.id);
|
|
195
|
+
if (made) return _extends({
|
|
196
|
+
ok: true,
|
|
197
|
+
replayed: true
|
|
198
|
+
}, pipelineRecord(made));
|
|
199
|
+
const pipelines = await store.pipelines(request.orgId, pipelineReadTokens(request));
|
|
200
|
+
const plan = planCrmPipelineSetup(read.value, pipelines, {
|
|
201
|
+
visibleTo,
|
|
202
|
+
hostId: request.hostId,
|
|
203
|
+
uid: request.uid,
|
|
204
|
+
nowMs: request.now.getTime()
|
|
205
|
+
});
|
|
206
|
+
if (plan.kind === 'refuse') return {
|
|
207
|
+
ok: false,
|
|
208
|
+
status: 409,
|
|
209
|
+
error: plan.error
|
|
210
|
+
};
|
|
211
|
+
if (plan.kind === 'create') {
|
|
212
|
+
await store.savePipeline(request.orgId, {
|
|
213
|
+
id: request.id,
|
|
214
|
+
create: plan.document,
|
|
215
|
+
defaultsOff: plan.defaultsOff
|
|
216
|
+
}, request.now);
|
|
217
|
+
return _extends({
|
|
218
|
+
ok: true,
|
|
219
|
+
replayed: false
|
|
220
|
+
}, pipelineRecord(_extends({}, plan.document, {
|
|
221
|
+
$id: request.id
|
|
222
|
+
})));
|
|
223
|
+
}
|
|
224
|
+
const existing = pipelines.find((one)=>one.$id === plan.id);
|
|
225
|
+
const changed = Object.keys(plan.patch).length > 0 || plan.defaultsOff.length > 0;
|
|
226
|
+
if (changed) {
|
|
227
|
+
await store.savePipeline(request.orgId, {
|
|
228
|
+
id: plan.id,
|
|
229
|
+
update: plan.patch,
|
|
230
|
+
defaultsOff: plan.defaultsOff
|
|
231
|
+
}, request.now);
|
|
232
|
+
}
|
|
233
|
+
return _extends({
|
|
234
|
+
ok: true,
|
|
235
|
+
replayed: !changed
|
|
236
|
+
}, pipelineRecord(_extends({}, existing, plan.patch)));
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/** The `crm-field` writer: creates a custom field, or finds the one asked for. */ export function createCrmFieldSetupWriter(deps = {}) {
|
|
241
|
+
var _deps_store, _deps_orgIdForHost;
|
|
242
|
+
const store = (_deps_store = deps.store) != null ? _deps_store : adminCrmSetupStore();
|
|
243
|
+
const orgIdForHost = (_deps_orgIdForHost = deps.orgIdForHost) != null ? _deps_orgIdForHost : (hostId)=>resolveOrgIdForHost(hostId);
|
|
244
|
+
const gate = createCrmSetupGate(deps);
|
|
245
|
+
const scope = [
|
|
246
|
+
ORG_SCOPE_TOKEN
|
|
247
|
+
];
|
|
248
|
+
return {
|
|
249
|
+
refusal: (context)=>gate(context, scope, CRM_SETUP_FIELD_SCOPE_REFUSAL),
|
|
250
|
+
check: (content)=>checkCrmFieldSetup(content),
|
|
251
|
+
read: async ({ hostId, id })=>{
|
|
252
|
+
const orgId = await orgIdForHost(hostId);
|
|
253
|
+
const field = orgId ? await store.field(orgId, id) : null;
|
|
254
|
+
return field ? fieldRecord(field) : null;
|
|
255
|
+
},
|
|
256
|
+
write: async (request)=>{
|
|
257
|
+
var _existing_options;
|
|
258
|
+
const read = readCrmFieldSetup(request.content);
|
|
259
|
+
if (read.ok === false) return {
|
|
260
|
+
ok: false,
|
|
261
|
+
status: 400,
|
|
262
|
+
error: read.problems[0]
|
|
263
|
+
};
|
|
264
|
+
const refused = await gate(request, scope, CRM_SETUP_FIELD_SCOPE_REFUSAL);
|
|
265
|
+
if (refused) return _extends({
|
|
266
|
+
ok: false
|
|
267
|
+
}, refused);
|
|
268
|
+
const made = await store.field(request.orgId, request.id);
|
|
269
|
+
if (made) return _extends({
|
|
270
|
+
ok: true,
|
|
271
|
+
replayed: true
|
|
272
|
+
}, fieldRecord(made));
|
|
273
|
+
const definitions = await store.fields(request.orgId);
|
|
274
|
+
const plan = planCrmFieldSetup(read.value, definitions, {
|
|
275
|
+
hostId: request.hostId,
|
|
276
|
+
now: request.now,
|
|
277
|
+
visibleTo: scope
|
|
278
|
+
});
|
|
279
|
+
if (plan.kind === 'refuse') return {
|
|
280
|
+
ok: false,
|
|
281
|
+
status: plan.status,
|
|
282
|
+
error: plan.error
|
|
283
|
+
};
|
|
284
|
+
if (plan.kind === 'create') {
|
|
285
|
+
await store.createField(request.orgId, request.id, plan.document);
|
|
286
|
+
return _extends({
|
|
287
|
+
ok: true,
|
|
288
|
+
replayed: false
|
|
289
|
+
}, fieldRecord(_extends({}, plan.document, {
|
|
290
|
+
$id: request.id
|
|
291
|
+
})));
|
|
292
|
+
}
|
|
293
|
+
const existing = definitions.find((one)=>one.$id === plan.id);
|
|
294
|
+
if (!plan.options.length) return _extends({
|
|
295
|
+
ok: true,
|
|
296
|
+
replayed: true
|
|
297
|
+
}, fieldRecord(existing));
|
|
298
|
+
const options = [
|
|
299
|
+
...(_existing_options = existing.options) != null ? _existing_options : [],
|
|
300
|
+
...plan.options
|
|
301
|
+
];
|
|
302
|
+
await store.setFieldOptions(request.orgId, plan.id, options, request.now);
|
|
303
|
+
return _extends({
|
|
304
|
+
ok: true,
|
|
305
|
+
replayed: false
|
|
306
|
+
}, fieldRecord(_extends({}, existing, {
|
|
307
|
+
options
|
|
308
|
+
})));
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
export const crmPipelineSetupWriter = createCrmPipelineSetupWriter();
|
|
313
|
+
export const crmFieldSetupWriter = createCrmFieldSetupWriter();
|
|
314
|
+
|
|
315
|
+
//# sourceMappingURL=crm-setup-writers.server.js.map
|