@aglyn/plugins-crm 1.0.0-beta.246 → 1.0.0-beta.249

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +1 -1
  2. package/package.json +14 -14
  3. package/src/lib/components/contact-custom-columns.d.ts +15 -6
  4. package/src/lib/components/contact-custom-columns.js +27 -7
  5. package/src/lib/components/contact-custom-columns.js.map +1 -1
  6. package/src/lib/components/contact-field-drawer.d.ts +2 -0
  7. package/src/lib/components/contact-field-drawer.js +25 -1
  8. package/src/lib/components/contact-field-drawer.js.map +1 -1
  9. package/src/lib/components/contacts-section.js +9 -3
  10. package/src/lib/components/contacts-section.js.map +1 -1
  11. package/src/lib/components/crm-custom-field-control.d.ts +4 -2
  12. package/src/lib/components/crm-custom-field-control.js +53 -1
  13. package/src/lib/components/crm-custom-field-control.js.map +1 -1
  14. package/src/lib/components/fields-section.js +4 -2
  15. package/src/lib/components/fields-section.js.map +1 -1
  16. package/src/lib/components/form-contact-fields-card.js +2 -0
  17. package/src/lib/components/form-contact-fields-card.js.map +1 -1
  18. package/src/lib/constants/contact-filters.d.ts +7 -0
  19. package/src/lib/constants/contact-filters.js +8 -1
  20. package/src/lib/constants/contact-filters.js.map +1 -1
  21. package/src/lib/declarations.console-server.d.ts +2 -0
  22. package/src/lib/declarations.console-server.js +5 -0
  23. package/src/lib/declarations.console-server.js.map +1 -1
  24. package/src/lib/model/crm-picklist-setup-content.d.ts +121 -0
  25. package/src/lib/model/crm-picklist-setup-content.js +262 -0
  26. package/src/lib/model/crm-picklist-setup-content.js.map +1 -0
  27. package/src/lib/model/crm-record-routes.js +6 -0
  28. package/src/lib/model/crm-record-routes.js.map +1 -1
  29. package/src/lib/model/crm-setup-content.d.ts +4 -1
  30. package/src/lib/model/crm-setup-content.js +15 -4
  31. package/src/lib/model/crm-setup-content.js.map +1 -1
  32. package/src/lib/server/crm-setup-ai-capabilities.d.ts +13 -0
  33. package/src/lib/server/crm-setup-ai-capabilities.js +94 -2
  34. package/src/lib/server/crm-setup-ai-capabilities.js.map +1 -1
  35. package/src/lib/server/crm-setup-writers.server.d.ts +28 -0
  36. package/src/lib/server/crm-setup-writers.server.js +181 -4
  37. package/src/lib/server/crm-setup-writers.server.js.map +1 -1
  38. package/src/lib/server/picklist-regroup.d.ts +30 -0
  39. package/src/lib/server/picklist-regroup.js +61 -0
  40. package/src/lib/server/picklist-regroup.js.map +1 -0
  41. package/src/lib/server/picklist-values.d.ts +2 -14
  42. package/src/lib/server/picklist-values.js +3 -40
  43. package/src/lib/server/picklist-values.js.map +1 -1
  44. package/src/lib/transfer/common.d.ts +4 -2
  45. package/src/lib/transfer/common.js +14 -3
  46. package/src/lib/transfer/common.js.map +1 -1
  47. package/src/lib/transfer/fields.js +10 -1
  48. package/src/lib/transfer/fields.js.map +1 -1
  49. package/src/lib/transfer/pipelines.js +4 -2
  50. package/src/lib/transfer/pipelines.js.map +1 -1
@@ -1 +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 { CRM_COLLECTIONS, crmReadTokens, crmScopeTokens } from '@aglyn/aglyn/app-utils/crm'\nimport { memberCanSee } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { 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 } from 'firebase-admin/firestore'\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 { 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'\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}\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 }\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: { object: field.object ?? 'contact', key: field.key, type: field.type, ...(field.options ? { options: field.options } : {}) },\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"],"names":["consentGroupForHost","CONTACT_FIELDS_MAX_PER_ORG","CRM_COLLECTIONS","crmReadTokens","crmScopeTokens","memberCanSee","checkEntitlement","ORG_SCOPE_TOKEN","firebaseAdmin","memberHasOrgPermission","resolveOrgIdForHost","resolveOrgMembership","FieldPath","checkCrmFieldSetup","checkCrmPipelineSetup","crmPipelineSetupFacts","planCrmFieldSetup","planCrmPipelineSetup","readCrmFieldSetup","readCrmPipelineSetup","crmSuiteRefusalMessage","CRM_SETUP_ROLE_REFUSAL","CRM_SETUP_PERMISSION_REFUSAL","CRM_SETUP_SCOPE_REFUSAL","CRM_SETUP_FIELD_SCOPE_REFUSAL","WRITER_ROLES","Set","PIPELINE_READ_LIMIT","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","pipelineScope","context","hostId","pipelineReadTokens","pipelineRecord","name","versionId","facts","stages","fieldRecord","label","object","key","type","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"],"mappings":"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"}
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":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,mBAAmB,QAAQ,wCAAuC;AAC3E,SAASC,0BAA0B,QAAQ,+CAA8C;AACzF,SACEC,eAAe,EAMfC,qBAAqB,EACrBC,aAAa,EACbC,cAAc,EACdC,oBAAoB,EACpBC,oBAAoB,QACf,6BAA4B;AACnC,SAASC,YAAY,QAAQ,uCAAsC;AACnE,SAASC,gBAAgB,QAAQ,2CAA0C;AAC3E,SAASC,sBAAsB,EAAEC,eAAe,QAAQ,sCAAqC;AAQ7F,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SACEC,sBAAsB,EACtBC,mBAAmB,EACnBC,oBAAoB,QACf,gDAA+C;AACtD,SAASC,SAAS,EAAEC,UAAU,QAAQ,2BAA0B;AAChE,SACEC,2BAA2B,EAC3BC,sBAAsB,EAEtBC,qBAAqB,EACrBC,yBAAyB,EACzBC,qBAAqB,EACrBC,0BAA0B,QACrB,yCAAqC;AAC5C,SACEC,kBAAkB,EAClBC,qBAAqB,EAErBC,qBAAqB,EACrBC,iBAAiB,EACjBC,oBAAoB,EACpBC,iBAAiB,EACjBC,oBAAoB,QACf,gCAA4B;AAEnC,SAASC,mBAAmB,QAAQ,8BAA0B;AAC9D,SAASC,eAAe,EAAEC,iBAAiB,QAAQ,wBAAoB;AACvE,SAASC,sBAAsB,QAAQ,kBAAc;AA+BrD,0DAA0D,GAC1D,OAAO,MAAMC,yBAAyB,2CAA0C;AAChF,OAAO,MAAMC,+BAA+B,kEAAiE;AAC7G,OAAO,MAAMC,0BAA0B,4DAA2D;AAClG,OAAO,MAAMC,gCAAgC,+EAA8E;AAC3H,OAAO,MAAMC,mCAAmC,2EAA0E;AAE1H,6EAA6E,GAC7E,MAAMC,eAAe,IAAIC,IAAI;IAAC;IAAS;IAAS;CAAS;AAEzD,yEAAyE,GACzE,MAAMC,sBAAsB;AA2C5B,kEAAkE,GAClE,MAAMC,qBAAwD;IAC5DC,SAAS;IACTC,MAAM;IACNC,SAAS5C,gBAAgB6C,SAAS;IAClCC,MAAM9C,gBAAgB+C,KAAK;IAC3BC,MAAMhD,gBAAgBiD,KAAK;AAC7B;AAUA,eAAeC,eAAeC,GAAW,EAAEC,KAAa;QAKvCC;IAJf,MAAMC,aAAa,MAAMzC,qBAAqBsC,KAAKC;IACnD,MAAMC,SAASC,8BAAAA,WAAYD,MAAM;IACjC,IAAI,CAACA,QAAQ,OAAO;IACpB,OAAO;QACLE,MAAMC,QAAOH,eAAAA,OAAOE,IAAI,YAAXF,eAAe;QAC5BI,eAAe,MAAM9C,uBAAuByC,OAAOC,QAAQ;QAC3DA,QAAQA;IACV;AACF;AAEA,8CAA8C,GAC9C,OAAO,SAASK,mBAAmBC,YAA6B,IAAMjD,cAAckD,GAAG,GAAGD,SAAS,EAA0B;IAC3H,MAAME,MAAM,CAACT,QAAkBO,YAAYG,UAAU,CAAC,QAAQC,GAAG,CAACX;IAClE,MAAMY,cAAc,CAACZ,QAAkBS,IAAIT,OAAOU,UAAU,CAAC9D,gBAAgBiE,SAAS;IACtF,MAAMC,WAAW,CAACd,QAAkBS,IAAIT,OAAOU,UAAU,CAAC9D,gBAAgBmE,aAAa;IACvF,MAAMC,SAAS,CAAIC,OAAiD,aAAMA,KAAKC,IAAI;YAAeC,KAAKF,KAAKG,EAAE;;IAC9G,OAAO;QACLP,WAAW,OAAOb,OAAOqB;YACvB,MAAMJ,OAAO,MAAML,YAAYZ,OAC5BsB,KAAK,CAAC,aAAa,sBAAsB;mBAAID;aAAW,EACxDE,OAAO,CAAC7D,UAAU8D,UAAU,IAC5BC,KAAK,CAACrC,qBACNsC,GAAG;YACN,OAAOT,KAAKU,IAAI,CAACC,GAAG,CAAC,CAACC,MAAQb,OAAoBa;QACpD;QACAC,UAAU,OAAO9B,OAAOoB;YACtB,MAAMH,OAAO,MAAML,YAAYZ,OAAOW,GAAG,CAACS,IAAIM,GAAG;YACjD,OAAOT,KAAKc,MAAM,GAAGf,OAAoBC,QAAQ;QACnD;QACAe,cAAc,OAAOhC,OAAOiC,OAAOC;YACjC,MAAMC,QAAQ5B,YAAY4B,KAAK;YAC/B,MAAMC,MAAMxB,YAAYZ,OAAOW,GAAG,CAACsB,MAAMb,EAAE;YAC3C,IAAIa,MAAMI,MAAM,EAAEF,MAAME,MAAM,CAACD,KAAKH,MAAMI,MAAM;iBAC3C,IAAIJ,MAAMK,MAAM,EAAEH,MAAMG,MAAM,CAACF,KAAK,aAAKH,MAAMK,MAAM;gBAAEC,WAAWL;;YACvE,KAAK,MAAMd,MAAMa,MAAMO,WAAW,CAAE;gBAClCL,MAAMG,MAAM,CAAC1B,YAAYZ,OAAOW,GAAG,CAACS,KAAK;oBAAEqB,WAAW;oBAAOF,WAAWL;gBAAI;YAC9E;YACA,MAAMC,MAAMO,MAAM;QACpB;QACAC,QAAQ,OAAO3C;YACb,MAAMiB,OAAO,MAAMH,SAASd,OAAOyB,KAAK,CAAC9E,6BAA6B,GAAG+E,GAAG;YAC5E,OAAOT,KAAKU,IAAI,CAACC,GAAG,CAAC,CAACC,MAAQb,OAA8Ba;QAC9D;QACAe,OAAO,OAAO5C,OAAOoB;YACnB,MAAMH,OAAO,MAAMH,SAASd,OAAOW,GAAG,CAACS,IAAIM,GAAG;YAC9C,OAAOT,KAAKc,MAAM,GAAGf,OAA8BC,QAAQ;QAC7D;QACA4B,aAAa,OAAO7C,OAAOoB,IAAI0B;YAC7B,MAAMhC,SAASd,OAAOW,GAAG,CAACS,IAAIiB,MAAM,CAACS;QACvC;QACAC,iBAAiB,OAAO/C,OAAOoB,IAAI4B,SAASd;YAC1C,MAAMpB,SAASd,OAAOW,GAAG,CAACS,IAAIkB,MAAM,CAAC;gBAAEU;gBAAST,WAAWL;YAAI;QACjE;QACAe,UAAU,OAAOjD,OAAOoB;gBAECH;YADvB,MAAMA,OAAO,MAAMR,IAAIT,OAAOU,UAAU,CAAC9D,gBAAgBsG,SAAS,EAAEvC,GAAG,CAACS,IAAIM,GAAG;YAC/E,OAAOT,KAAKc,MAAM,IAAKd,aAAAA,KAAKC,IAAI,cAATD,aAAe,CAAC,IAAiC;QAC1E;QACAkC,gBAAgB,OAAOnD,OAAOoB,IAAIgC,QAAQC;YACxC,MAAMjB,MAAM3B,IAAIT,OAAOU,UAAU,CAAC9D,gBAAgBsG,SAAS,EAAEvC,GAAG,CAACS;YACjE,OAAOb,YAAY+C,cAAc,CAAC,OAAOC;oBAERtC;gBAD/B,MAAMA,OAAO,MAAMsC,YAAY7B,GAAG,CAACU;gBACnC,MAAMoB,SAASvC,KAAKc,MAAM,IAAKd,aAAAA,KAAKC,IAAI,cAATD,aAAe,CAAC,IAAiC;gBAChF,MAAM,EAAEgB,KAAK,EAAEwB,MAAM,EAAE,GAAGL,OAAOI;gBACjC,IAAIvB,OAAO;oBACT,sEAAsE;oBACtE,MAAMyB,UAAUzG,qBAAqBuG,YAAY;oBACjDD,YAAYI,GAAG,CACbvB,KACA;wBACEwB,QAAQ3B,MAAM2B,MAAM;wBACpBC,gBAAgB5B,MAAM4B,cAAc;wBACpCtB,WAAW5E,WAAWmG,eAAe;uBACjCJ,UACA;wBAAEK,QAAQV,MAAMU,MAAM;wBAAEC,WAAWrG,WAAWmG,eAAe;uBAAO1G,uBAAuB;wBAACC;qBAAgB,KAC5G,CAAC,IAEP;wBAAE4G,OAAO;oBAAK;gBAElB;gBACA,OAAOR;YACT;QACF;QACAS,SAAS,CAAClE,OAAOmE,QAAQC,QAAQC,OAAOC,QACtC3F,kBAAkB4B,aAAaE,IAAIT,OAAOU,UAAU,CAACrB,kBAAkB,CAAC8E,OAAO,GAAGC,QAAQC,OAAOC;IACrG;AACF;AAEA,gFAAgF,GAChF,SAASC,cAAcC,OAAmD;QAC3DA;IAAb,MAAM/D,OAAO+D,eAAAA,QAAQ/D,GAAG,YAAX+D,eAAe;IAC5B,OAAOzH,eAAe0D,KAAK/D,oBAAoB+D,KAAK+D,QAAQT,MAAM;AACpE;AAEA,qFAAqF,GACrF,SAASU,mBAAmBD,OAAmD;QAChEA;IAAb,MAAM/D,OAAO+D,eAAAA,QAAQ/D,GAAG,YAAX+D,eAAe;IAC5B,OAAO1H,cAAcJ,oBAAoB+D,KAAK+D,QAAQT,MAAM;AAC9D;AAEA,SAASW,eAAe5C,QAAqB;QACHA,gBAAoEA;IAA5G,OAAO;QAAEV,IAAIU,SAASX,GAAG;QAAEwD,MAAMvE,QAAO0B,iBAAAA,SAAS6C,IAAI,YAAb7C,iBAAiB;QAAK8C,WAAW;QAAMC,OAAOzG,uBAAsB0D,mBAAAA,SAASgD,MAAM,YAAfhD,mBAAmB,EAAE;IAAE;AACrI;AAEA,SAASiD,YAAYnC,KAA4B;QAGhCA,cAGHA;IALZ,OAAO;QACLxB,IAAIwB,MAAMzB,GAAG;QACbwD,MAAMvE,QAAOwC,eAAAA,MAAMyB,KAAK,YAAXzB,eAAe;QAC5BgC,WAAW;QACXC,OAAO;YACLV,MAAM,GAAEvB,gBAAAA,MAAMuB,MAAM,YAAZvB,gBAAgB;YACxBoC,KAAKpC,MAAMoC,GAAG;YACdC,MAAMrC,MAAMqC,IAAI;WACZrC,MAAMI,OAAO,GAAG;YAAEA,SAASJ,MAAMI,OAAO;QAAC,IAAI,CAAC,GAC9CJ,MAAMsC,QAAQ,GAAG;YAAEA,UAAUtC,MAAMsC,QAAQ;QAAC,IAAI,CAAC;IAEzD;AACF;AAEA,SAASC,mBAAmBC,IAAwB;QAC7BA,oBACJA;IADjB,MAAMC,gBAAeD,qBAAAA,KAAKC,YAAY,YAAjBD,qBAAsB,CAACrB,SAAmBvG,oBAAoBuG;IACnF,MAAMuB,YAAWF,eAAAA,KAAKnF,MAAM,YAAXmF,eAAetF;IAChC,iGAAiG,GACjG,OAAO,OAAO0E,SAA6Be,OAA0BC;QACnE,IAAI,AAAC,MAAMH,aAAab,QAAQT,MAAM,MAAOS,QAAQxE,KAAK,EAAE,OAAO;YAAEyF,QAAQ;YAAKC,OAAO;QAAe;QACxG,IAAI,CAACvI,iBAAiBqH,QAAQ/D,GAAG,EAAW,QAAQ;YAClD,OAAO;gBAAEgF,QAAQ;gBAAKC,OAAO9G,uBAAuB;YAAsB;QAC5E;QACA,MAAMqB,SAAS,MAAMqF,SAASd,QAAQzE,GAAG,EAAEyE,QAAQxE,KAAK;QACxD,IAAI,CAACC,UAAU,CAACf,aAAayG,GAAG,CAAC1F,OAAOE,IAAI,KAAKF,OAAOA,MAAM,CAAC,eAAe,KAAK,MAAM;YACvF,OAAO;gBAAEwF,QAAQ;gBAAKC,OAAO7G;YAAuB;QACtD;QACA,IAAI,CAACoB,OAAOI,aAAa,EAAE,OAAO;YAAEoF,QAAQ;YAAKC,OAAO5G;QAA6B;QACrF,IAAI,CAAC5B,aAAa+C,OAAOA,MAAM,EAAWsF,QAAQ,OAAO;YAAEE,QAAQ;YAAKC,OAAOF;QAAa;QAC5F,OAAO;IACT;AACF;AAEA,wEAAwE,GACxE,OAAO,SAASI,6BAA6BR,OAA2B,CAAC,CAAC;QAC1DA,aACOA;IADrB,MAAMS,SAAQT,cAAAA,KAAKS,KAAK,YAAVT,cAAc9E;IAC5B,MAAM+E,gBAAeD,qBAAAA,KAAKC,YAAY,YAAjBD,qBAAsB,CAACrB,SAAmBvG,oBAAoBuG;IACnF,MAAM+B,OAAOX,mBAAmBC;IAChC,OAAO;QACLW,SAAS,CAACvB,UAAYsB,KAAKtB,SAASD,cAAcC,UAAUzF;QAC5DiH,OAAO,CAACC,UAAY9H,sBAAsB8H;QAC1CC,MAAM,OAAO,EAAEnC,MAAM,EAAE3C,EAAE,EAAE;YACzB,MAAMpB,QAAQ,MAAMqF,aAAatB;YACjC,MAAMjC,WAAW9B,QAAQ,MAAM6F,MAAM/D,QAAQ,CAAC9B,OAAOoB,MAAM;YAC3D,OAAOU,WAAW4C,eAAe5C,YAAY;QAC/C;QACAG,OAAO,OAAOkE;YACZ,MAAMD,OAAO1H,qBAAqB2H,QAAQF,OAAO;YACjD,IAAIC,KAAKE,EAAE,KAAK,OAAO,OAAO;gBAAEA,IAAI;gBAAOX,QAAQ;gBAAKC,OAAOQ,KAAKG,QAAQ,CAAC,EAAE;YAAC;YAChF,MAAMC,YAAY/B,cAAc4B;YAChC,MAAMI,UAAU,MAAMT,KAAKK,SAASG,WAAWvH;YAC/C,IAAIwH,SAAS,OAAO;gBAAEH,IAAI;eAAUG;YACpC,+DAA+D;YAC/D,MAAMC,OAAO,MAAMX,MAAM/D,QAAQ,CAACqE,QAAQnG,KAAK,EAAEmG,QAAQ/E,EAAE;YAC3D,IAAIoF,MAAM,OAAO;gBAAEJ,IAAI;gBAAMK,UAAU;eAAS/B,eAAe8B;YAC/D,MAAM3F,YAAY,MAAMgF,MAAMhF,SAAS,CAACsF,QAAQnG,KAAK,EAAEyE,mBAAmB0B;YAC1E,MAAMO,OAAOpI,qBAAqB4H,KAAKS,KAAK,EAAE9F,WAAW;gBACvDyF;gBACAvC,QAAQoC,QAAQpC,MAAM;gBACtBhE,KAAKoG,QAAQpG,GAAG;gBAChB6G,OAAOT,QAAQjE,GAAG,CAAC2E,OAAO;YAC5B;YACA,IAAIH,KAAKI,IAAI,KAAK,UAAU,OAAO;gBAAEV,IAAI;gBAAOX,QAAQ;gBAAKC,OAAOgB,KAAKhB,KAAK;YAAC;YAC/E,IAAIgB,KAAKI,IAAI,KAAK,UAAU;gBAC1B,MAAMjB,MAAM7D,YAAY,CAACmE,QAAQnG,KAAK,EAAE;oBAAEoB,IAAI+E,QAAQ/E,EAAE;oBAAEiB,QAAQqE,KAAK5D,QAAQ;oBAAEN,aAAakE,KAAKlE,WAAW;gBAAC,GAAG2D,QAAQjE,GAAG;gBAC7H,OAAO;oBACLkE,IAAI;oBACJK,UAAU;mBACP/B,eAAe,aAAMgC,KAAK5D,QAAQ;oBAA6B3B,KAAKgF,QAAQ/E,EAAE;;YAErF;YACA,MAAM2F,WAAWlG,UAAUmG,IAAI,CAAC,CAACnF,MAAQA,IAAIV,GAAG,KAAKuF,KAAKtF,EAAE;YAC5D,MAAM6F,UAAUC,OAAOC,IAAI,CAACT,KAAKU,KAAK,EAAEC,MAAM,GAAG,KAAKX,KAAKlE,WAAW,CAAC6E,MAAM,GAAG;YAChF,IAAIJ,SAAS;gBACX,MAAMpB,MAAM7D,YAAY,CAACmE,QAAQnG,KAAK,EAAE;oBAAEoB,IAAIsF,KAAKtF,EAAE;oBAAEkB,QAAQoE,KAAKU,KAAK;oBAAE5E,aAAakE,KAAKlE,WAAW;gBAAC,GAAG2D,QAAQjE,GAAG;YACzH;YACA,OAAO;gBAAEkE,IAAI;gBAAMK,UAAU,CAACQ;eAAYvC,eAAe,aAAKqC,UAAaL,KAAKU,KAAK;QACvF;IACF;AACF;AAEA,gFAAgF,GAChF,OAAO,SAASE,0BAA0BlC,OAA2B,CAAC,CAAC;QACvDA,aACOA;IADrB,MAAMS,SAAQT,cAAAA,KAAKS,KAAK,YAAVT,cAAc9E;IAC5B,MAAM+E,gBAAeD,qBAAAA,KAAKC,YAAY,YAAjBD,qBAAsB,CAACrB,SAAmBvG,oBAAoBuG;IACnF,MAAM+B,OAAOX,mBAAmBC;IAChC,MAAMG,QAAQ;QAAClI;KAAgB;IAC/B,OAAO;QACL0I,SAAS,CAACvB,UAAYsB,KAAKtB,SAASe,OAAOvG;QAC3CgH,OAAO,CAACC,UAAY/H,mBAAmB+H;QACvCC,MAAM,OAAO,EAAEnC,MAAM,EAAE3C,EAAE,EAAE;YACzB,MAAMpB,QAAQ,MAAMqF,aAAatB;YACjC,MAAMnB,QAAQ5C,QAAQ,MAAM6F,MAAMjD,KAAK,CAAC5C,OAAOoB,MAAM;YACrD,OAAOwB,QAAQmC,YAAYnC,SAAS;QACtC;QACAX,OAAO,OAAOkE;gBAoBSY;YAnBrB,MAAMb,OAAO3H,kBAAkB4H,QAAQF,OAAO;YAC9C,IAAIC,KAAKE,EAAE,KAAK,OAAO,OAAO;gBAAEA,IAAI;gBAAOX,QAAQ;gBAAKC,OAAOQ,KAAKG,QAAQ,CAAC,EAAE;YAAC;YAChF,MAAME,UAAU,MAAMT,KAAKK,SAASZ,OAAOvG;YAC3C,IAAIuH,SAAS,OAAO;gBAAEH,IAAI;eAAUG;YACpC,MAAMC,OAAO,MAAMX,MAAMjD,KAAK,CAACuD,QAAQnG,KAAK,EAAEmG,QAAQ/E,EAAE;YACxD,IAAIoF,MAAM,OAAO;gBAAEJ,IAAI;gBAAMK,UAAU;eAAS1B,YAAYyB;YAC5D,MAAMe,cAAc,MAAM1B,MAAMlD,MAAM,CAACwD,QAAQnG,KAAK;YACpD,MAAM0G,OAAOrI,kBAAkB6H,KAAKS,KAAK,EAAEY,aAAa;gBAAExD,QAAQoC,QAAQpC,MAAM;gBAAE7B,KAAKiE,QAAQjE,GAAG;gBAAEoE,WAAWf;YAAM;YACrH,IAAImB,KAAKI,IAAI,KAAK,UAAU,OAAO;gBAAEV,IAAI;gBAAOX,QAAQiB,KAAKjB,MAAM;gBAAEC,OAAOgB,KAAKhB,KAAK;YAAC;YACvF,IAAIgB,KAAKI,IAAI,KAAK,UAAU;gBAC1B,MAAMjB,MAAMhD,WAAW,CAACsD,QAAQnG,KAAK,EAAEmG,QAAQ/E,EAAE,EAAEsF,KAAK5D,QAAQ;gBAChE,OAAO;oBACLsD,IAAI;oBACJK,UAAU;mBACP1B,YAAY,aAAM2B,KAAK5D,QAAQ;oBAAuC3B,KAAKgF,QAAQ/E,EAAE;;YAE5F;YACA,MAAM2F,WAAWQ,YAAYP,IAAI,CAAC,CAACnF,MAAQA,IAAIV,GAAG,KAAKuF,KAAKtF,EAAE;YAC9D,IAAI,CAACsF,KAAK1D,OAAO,CAACqE,MAAM,EAAE,OAAO;gBAAEjB,IAAI;gBAAMK,UAAU;eAAS1B,YAAYgC;YAC5E,MAAM/D,UAAU;oBAAK+D,oBAAAA,SAAS/D,OAAO,YAAhB+D,oBAAoB,EAAE;mBAAML,KAAK1D,OAAO;aAAC;YAC9D,MAAM6C,MAAM9C,eAAe,CAACoD,QAAQnG,KAAK,EAAE0G,KAAKtF,EAAE,EAAE4B,SAASmD,QAAQjE,GAAG;YACxE,OAAO;gBAAEkE,IAAI;gBAAMK,UAAU;eAAU1B,YAAY,aAAKgC;gBAAU/D;;QACpE;IACF;AACF;AAEA,OAAO,MAAMwE,yBAAyB5B,+BAA8B;AACpE,OAAO,MAAM6B,sBAAsBH,4BAA2B;AAE9D;;2CAE2C,GAE3C,iGAAiG,GACjG,MAAMI,uBAAuB,GAAG7J,uBAAuB,CAAC,CAAC;AAEzD,SAAS8J,eAAeC,UAAiC,EAAE3E,QAAqB,EAAE4E,OAAkC;IAClH,OAAO;QACLzG,IAAIwG,WAAWxG,EAAE;QACjBuD,MAAM7G,sBAAsB8J;QAC5BhD,WAAW;QACXC,OAAO;YACLiD,MAAMF,WAAWxG,EAAE;YACnBwC,QAAQX,SAASW,MAAM,CAACmE,MAAM,CAAC,CAACpB,QAAUA,MAAMqB,MAAM,EAAEpG,GAAG,CAAC,CAAC+E,QAAUA,MAAMtC,KAAK;WAC9EwD,kBAAAA,UAAW,CAAC;IAEpB;AACF;AAEA,SAASI,aAAarF,KAA4B,EAAEiF,OAAkC;QAGrEjF,cAEkCA,eAAmCA;IAJpF,OAAO;QACLxB,IAAI,GAAGsG,uBAAuB9E,MAAMzB,GAAG,EAAE;QACzCwD,MAAMvE,QAAOwC,eAAAA,MAAMyB,KAAK,YAAXzB,eAAe;QAC5BgC,WAAW;QACXC,OAAO;YAAEiD,MAAMjK;YAAwBsG,MAAM,GAAEvB,gBAAAA,MAAMuB,MAAM,YAAZvB,gBAAgB;YAAWgB,MAAM,GAAEhB,iBAAAA,MAAMI,OAAO,YAAbJ,iBAAiB,EAAE;WAAMiF,kBAAAA,UAAW,CAAC;IACzH;AACF;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASK,mCAAmC9C,OAA2B,CAAC,CAAC;QAChEA,aACOA;IADrB,MAAMS,SAAQT,cAAAA,KAAKS,KAAK,YAAVT,cAAc9E;IAC5B,MAAM+E,gBAAeD,qBAAAA,KAAKC,YAAY,YAAjBD,qBAAsB,CAACrB,SAAmBvG,oBAAoBuG;IACnF,MAAM+B,OAAOX,mBAAmBC;IAChC,MAAMG,QAAQ;QAAClI;KAAgB;IAC/B,OAAO;QACL0I,SAAS,CAACvB,UAAYsB,KAAKtB,SAASe,OAAOtG;QAC3C+G,OAAO,CAACC,UAAYrI,4BAA4BqI;QAChDC,MAAM,OAAO,EAAEnC,MAAM,EAAE3C,EAAE,EAAE;YACzB,MAAMpB,QAAQ,MAAMqF,aAAatB;YACjC,IAAI,CAAC/D,OAAO,OAAO;YACnB,IAAIoB,GAAG+G,UAAU,CAACT,uBAAuB;gBACvC,MAAM9E,QAAQ,MAAMiD,MAAMjD,KAAK,CAAC5C,OAAOoB,GAAGgH,KAAK,CAACV,qBAAqBL,MAAM;gBAC3E,OAAOzE,QAAQqF,aAAarF,SAAS;YACvC;YACA,MAAMgF,aAAa/K,sBAAsBuE;YACzC,IAAI,CAACwG,YAAY,OAAO;YACxB,MAAMS,SAAST,WAAWxG,EAAE;YAC5B,OAAOuG,eAAeC,YAAY5K,qBAAqBqL,QAAQ,MAAMxC,MAAM5C,QAAQ,CAACjD,OAAOqI;QAC7F;QACApG,OAAO,OAAOkE;YACZ,MAAMD,OAAOjI,2BAA2BkI,QAAQF,OAAO;YACvD,IAAIC,KAAKE,EAAE,KAAK,OAAO,OAAO;gBAAEA,IAAI;gBAAOX,QAAQ;gBAAKC,OAAOQ,KAAKG,QAAQ,CAAC,EAAE;YAAC;YAChF,MAAME,UAAU,MAAMT,KAAKK,SAASZ,OAAOtG;YAC3C,IAAIsH,SAAS,OAAO;gBAAEH,IAAI;eAAUG;YACpC,MAAM+B,QAAQpC,KAAKS,KAAK;YAExB,IAAI2B,MAAMlE,MAAM,CAAC0C,IAAI,KAAK,UAAU;gBAClC,MAAMS,cAAc,MAAM1B,MAAMlD,MAAM,CAACwD,QAAQnG,KAAK;gBACpD,MAAM0G,OAAO3I,0BAA0B,aAAKuK;oBAAOlE,QAAQkE,MAAMlE,MAAM;oBAAImD;gBAC3E,IAAIb,KAAKN,EAAE,KAAK,OAAO,OAAO;oBAAEA,IAAI;oBAAOX,QAAQiB,KAAKjB,MAAM;oBAAEC,OAAOgB,KAAKhB,KAAK;gBAAC;gBAClF,MAAM9C,QAAQ2E,YAAYP,IAAI,CAAC,CAACnF,MAAQA,IAAIV,GAAG,KAAKuF,KAAKtF,EAAE;gBAC3D,MAAM,EAAEmH,KAAK,EAAExB,QAAQ,EAAEyB,QAAQ,EAAE,GAAG9B;gBACtC,IAAI6B,MAAMlB,MAAM,EAAE,MAAMxB,MAAM9C,eAAe,CAACoD,QAAQnG,KAAK,EAAE0G,KAAKtF,EAAE,EAAEsF,KAAK1D,OAAO,EAAEmD,QAAQjE,GAAG;gBAC/F,OAAO;oBACLkE,IAAI;oBACJK,UAAU8B,MAAMlB,MAAM,KAAK;mBACxBY,aAAa,aAAKrF;oBAAOI,SAAS0D,KAAK1D,OAAO;oBAAI;oBAAEuF;oBAAOxB;oBAAUyB;gBAAS;YAErF;YAEA,MAAMpH,KAAKkH,MAAMlE,MAAM,CAAChD,EAAE;YAC1B,MAAMwG,aAAa/K,sBAAsBuE;YACzC,MAAMqH,UAAU,MAAM5C,MAAM1C,cAAc,CACxCgD,QAAQnG,KAAK,EACboB,IACA,CAACoC;gBACC,MAAMkD,OAAO1I,sBAAsB4J,YAAY5K,qBAAqBoE,IAAIoC,SAAS8E;gBACjF,OAAO;oBAAErG,OAAOyE,KAAKN,EAAE,IAAIM,KAAK6B,KAAK,CAAClB,MAAM,GAAGX,KAAKzD,QAAQ,GAAG;oBAAMQ,QAAQiD;gBAAK;YACpF,GACA;gBAAE3C,QAAQoC,QAAQpC,MAAM;YAAC;YAE3B,IAAI0E,QAAQrC,EAAE,KAAK,OAAO,OAAO;gBAAEA,IAAI;gBAAOX,QAAQ;gBAAKC,OAAO+C,QAAQ/C,KAAK;YAAC;YAChF,sEAAsE;YACtE,IAAI+C,QAAQF,KAAK,CAAClB,MAAM,IAAI5I,oBAAoBmJ,aAAa;gBAC3D,KAAK,MAAMxD,UAAUwD,WAAWc,OAAO,CAAE;oBACvC,IAAI,CAACtE,OAAOuE,UAAU,IAAIvE,OAAOwE,KAAK,IAAIxE,OAAOyE,QAAQ,EAAE;oBAC3D,KAAK,MAAMxE,SAASoE,QAAQF,KAAK,CAAE;wBACjC,MAAM1C,MAAM3B,OAAO,CAACiC,QAAQnG,KAAK,EAAEoE,OAAOD,MAAM,EAAEC,QAAQC,OAAO3F,gBAAgB+J,QAAQxF,QAAQ,EAAEoB;oBACrG;gBACF;YACF;YACA,MAAM,EAAEkE,KAAK,EAAExB,QAAQ,EAAEyB,QAAQ,EAAE,GAAGC;YACtC,OAAO;gBAAErC,IAAI;gBAAMK,UAAU8B,MAAMlB,MAAM,KAAK;eAAMM,eAAeC,YAAYa,QAAQxF,QAAQ,EAAE;gBAAEsF;gBAAOxB;gBAAUyB;YAAS;QAC/H;IACF;AACF;AAEA,OAAO,MAAMM,+BAA+BZ,qCAAoC"}
@@ -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>;
@@ -0,0 +1,61 @@
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
+ */ // Modules by their own paths, never the `@aglyn/aglyn` barrel: the CRM's
17
+ // setup writers (AGL-3756) reach this file as well as the picklist route.
18
+ import { crmPicklistKey, crmPicklistValueByLabel } from "@aglyn/aglyn/app-utils/crm";
19
+ import { FieldPath } from "firebase-admin/firestore";
20
+ /** Writes per batch — under Firestore's 500, with room to spare. */ const BATCH_SIZE = 400;
21
+ /** The group a label is filed under in `picklist` — `null` for none, or a label it does not hold. */ export function picklistGroupOf(picklist, label) {
22
+ var _ref;
23
+ var _crmPicklistValueByLabel;
24
+ return (_ref = label === null ? null : (_crmPicklistValueByLabel = crmPicklistValueByLabel(picklist, label)) == null ? void 0 : _crmPicklistValueByLabel.group) != null ? _ref : null;
25
+ }
26
+ /**
27
+ * Rewrite a target's GROUP field (AGL-3577) — a lead's
28
+ * `leadSourceDirection` — on every record holding `label` in any spelling
29
+ * (by its `keyField`) whose group is not `group` already. Answers how many
30
+ * were written. The field is derived, like the list keys a restamp writes,
31
+ * so `updatedAt` is left alone.
32
+ *
33
+ * Paged by document id rather than re-run: a record already holding the
34
+ * group stays in the query's answer.
35
+ */ export async function regroupEverywhere(firestore, records, target, label, group) {
36
+ const key = crmPicklistKey(label);
37
+ if (!key || !target.keyField || !target.groupField) return 0;
38
+ const query = records.where(target.keyField, '==', key).orderBy(FieldPath.documentId()).limit(BATCH_SIZE);
39
+ let written = 0;
40
+ let after = null;
41
+ for(;;){
42
+ var _page_docs_;
43
+ const page = await (after ? query.startAfter(after) : query).get();
44
+ if (page.empty) return written;
45
+ const batch = firestore.batch();
46
+ let changed = 0;
47
+ for (const snapshot of page.docs){
48
+ if (snapshot.get(target.groupField) === group) continue;
49
+ batch.update(snapshot.ref, {
50
+ [target.groupField]: group
51
+ });
52
+ changed += 1;
53
+ }
54
+ if (changed) await batch.commit();
55
+ written += changed;
56
+ if (page.size < BATCH_SIZE) return written;
57
+ after = (_page_docs_ = page.docs[page.docs.length - 1]) != null ? _page_docs_ : null;
58
+ }
59
+ }
60
+
61
+ //# sourceMappingURL=picklist-regroup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/picklist-regroup.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\n// Modules by their own paths, never the `@aglyn/aglyn` barrel: the CRM's\n// setup writers (AGL-3756) reach this file as well as the picklist route.\nimport {\n type CrmPicklist,\n type CrmPicklistTarget,\n crmPicklistKey,\n crmPicklistValueByLabel,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { FieldPath } from 'firebase-admin/firestore'\n\n/** Writes per batch — under Firestore's 500, with room to spare. */\nconst BATCH_SIZE = 400\n\n/** The group a label is filed under in `picklist` — `null` for none, or a label it does not hold. */\nexport function picklistGroupOf(picklist: CrmPicklist, label: string | null): string | null {\n return (label === null ? null : crmPicklistValueByLabel(picklist, label)?.group) ?? null\n}\n\n/**\n * Rewrite a target's GROUP field (AGL-3577) — a lead's\n * `leadSourceDirection` — on every record holding `label` in any spelling\n * (by its `keyField`) whose group is not `group` already. Answers how many\n * were written. The field is derived, like the list keys a restamp writes,\n * so `updatedAt` is left alone.\n *\n * Paged by document id rather than re-run: a record already holding the\n * group stays in the query's answer.\n */\nexport async function regroupEverywhere(\n firestore: FirebaseFirestore.Firestore,\n records: FirebaseFirestore.CollectionReference,\n target: CrmPicklistTarget,\n label: string,\n group: string | null,\n): Promise<number> {\n const key = crmPicklistKey(label)\n if (!key || !target.keyField || !target.groupField) return 0\n const query = records\n .where(target.keyField, '==', key)\n .orderBy(FieldPath.documentId())\n .limit(BATCH_SIZE)\n let written = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n const page: FirebaseFirestore.QuerySnapshot = await (after ? query.startAfter(after) : query).get()\n if (page.empty) return written\n const batch = firestore.batch()\n let changed = 0\n for (const snapshot of page.docs) {\n if (snapshot.get(target.groupField) === group) continue\n batch.update(snapshot.ref, { [target.groupField]: group })\n changed += 1\n }\n if (changed) await batch.commit()\n written += changed\n if (page.size < BATCH_SIZE) return written\n after = page.docs[page.docs.length - 1] ?? null\n }\n}\n"],"names":["crmPicklistKey","crmPicklistValueByLabel","FieldPath","BATCH_SIZE","picklistGroupOf","picklist","label","group","regroupEverywhere","firestore","records","target","key","keyField","groupField","query","where","orderBy","documentId","limit","written","after","page","startAfter","get","empty","batch","changed","snapshot","docs","update","ref","commit","size","length"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,yEAAyE;AACzE,0EAA0E;AAC1E,SAGEA,cAAc,EACdC,uBAAuB,QAClB,6BAA4B;AACnC,SAASC,SAAS,QAAQ,2BAA0B;AAEpD,kEAAkE,GAClE,MAAMC,aAAa;AAEnB,mGAAmG,GACnG,OAAO,SAASC,gBAAgBC,QAAqB,EAAEC,KAAoB;QACjEA;QAAwBL;IAAhC,QAAQK,OAAAA,UAAU,OAAO,QAAOL,2BAAAA,wBAAwBI,UAAUC,2BAAlCL,yBAA0CM,KAAK,YAAvED,OAA4E;AACtF;AAEA;;;;;;;;;CASC,GACD,OAAO,eAAeE,kBACpBC,SAAsC,EACtCC,OAA8C,EAC9CC,MAAyB,EACzBL,KAAa,EACbC,KAAoB;IAEpB,MAAMK,MAAMZ,eAAeM;IAC3B,IAAI,CAACM,OAAO,CAACD,OAAOE,QAAQ,IAAI,CAACF,OAAOG,UAAU,EAAE,OAAO;IAC3D,MAAMC,QAAQL,QACXM,KAAK,CAACL,OAAOE,QAAQ,EAAE,MAAMD,KAC7BK,OAAO,CAACf,UAAUgB,UAAU,IAC5BC,KAAK,CAAChB;IACT,IAAIiB,UAAU;IACd,IAAIC,QAAwD;IAC5D,OAAS;YAaCC;QAZR,MAAMA,OAAwC,MAAM,AAACD,CAAAA,QAAQN,MAAMQ,UAAU,CAACF,SAASN,KAAI,EAAGS,GAAG;QACjG,IAAIF,KAAKG,KAAK,EAAE,OAAOL;QACvB,MAAMM,QAAQjB,UAAUiB,KAAK;QAC7B,IAAIC,UAAU;QACd,KAAK,MAAMC,YAAYN,KAAKO,IAAI,CAAE;YAChC,IAAID,SAASJ,GAAG,CAACb,OAAOG,UAAU,MAAMP,OAAO;YAC/CmB,MAAMI,MAAM,CAACF,SAASG,GAAG,EAAE;gBAAE,CAACpB,OAAOG,UAAU,CAAC,EAAEP;YAAM;YACxDoB,WAAW;QACb;QACA,IAAIA,SAAS,MAAMD,MAAMM,MAAM;QAC/BZ,WAAWO;QACX,IAAIL,KAAKW,IAAI,GAAG9B,YAAY,OAAOiB;QACnCC,SAAQC,cAAAA,KAAKO,IAAI,CAACP,KAAKO,IAAI,CAACK,MAAM,GAAG,EAAE,YAA/BZ,cAAmC;IAC7C;AACF"}
@@ -14,20 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type CrmPicklist, type CrmPicklistTarget, type PluginApiHandler } from '@aglyn/aglyn/server';
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>;
17
+ import { type PluginApiHandler } from '@aglyn/aglyn/server';
18
+ export { picklistGroupOf, regroupEverywhere } from './picklist-regroup';
31
19
  /** `POST crm/picklist-values` — any registered picklist, named by `picklistId`. */
32
20
  export declare const crmPicklistValuesHandler: PluginApiHandler;
33
21
  /** `POST crm/lead-source-values` — the same route with the picklist fixed to the lead source. */
@@ -14,12 +14,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { consentGroupForHost, CONTACT_FACETS_FIELD, CRM_COLLECTIONS, CRM_CONTACT_FACET_KEYS_FIELD, CRM_LEAD_SOURCE_PICKLIST, crmContactFacetKeys, crmPicklistDefinition, crmPicklistKey, crmPicklistValueByLabel, effectiveCrmPicklist, newResourceScopeFields, normalizeCrmPicklist, ORG_SCOPE_TOKEN } from "@aglyn/aglyn/server";
17
+ */ import { consentGroupForHost, CONTACT_FACETS_FIELD, CRM_COLLECTIONS, CRM_CONTACT_FACET_KEYS_FIELD, CRM_LEAD_SOURCE_PICKLIST, crmContactFacetKeys, crmPicklistDefinition, crmPicklistKey, effectiveCrmPicklist, newResourceScopeFields, normalizeCrmPicklist, ORG_SCOPE_TOKEN } from "@aglyn/aglyn/server";
18
18
  import { firebaseAdmin } from "@aglyn/tenant-data-admin";
19
19
  import { FieldPath, FieldValue } from "firebase-admin/firestore";
20
20
  import { addPicklistValue, deletePicklistValue, picklistKeepsGroups, renamePicklistValue, setPicklistValueGroup } from "../model/picklist-values.js";
21
21
  import { orgHostIds, readCrmRouteScope } from "./org-caller.js";
22
+ import { picklistGroupOf, regroupEverywhere } from "./picklist-regroup.js";
22
23
  import { authorizeCrmWriter } from "./task-routes.js";
24
+ export { picklistGroupOf, regroupEverywhere } from "./picklist-regroup.js";
23
25
  /** What the suite gate names when a plan without the CRM asks. */ const suiteAct = (definition)=>`Editing the organization's ${definition.plural}`;
24
26
  /** The org subcollection each target object's records live in. */ const TARGET_COLLECTIONS = {
25
27
  contact: 'contacts',
@@ -110,45 +112,6 @@ import { authorizeCrmWriter } from "./task-routes.js";
110
112
  after = (_page_docs_ = page.docs[page.docs.length - 1]) != null ? _page_docs_ : null;
111
113
  }
112
114
  }
113
- /** The group a label is filed under in `picklist` — `null` for none, or a label it does not hold. */ export function picklistGroupOf(picklist, label) {
114
- var _ref;
115
- var _crmPicklistValueByLabel;
116
- return (_ref = label === null ? null : (_crmPicklistValueByLabel = crmPicklistValueByLabel(picklist, label)) == null ? void 0 : _crmPicklistValueByLabel.group) != null ? _ref : null;
117
- }
118
- /**
119
- * Rewrite a target's GROUP field (AGL-3577) — a lead's
120
- * `leadSourceDirection` — on every record holding `label` in any spelling
121
- * (by its `keyField`) whose group is not `group` already. Answers how many
122
- * were written. The field is derived, like the list keys a restamp writes,
123
- * so `updatedAt` is left alone.
124
- *
125
- * Paged by document id rather than re-run: a record already holding the
126
- * group stays in the query's answer.
127
- */ export async function regroupEverywhere(firestore, records, target, label, group) {
128
- const key = crmPicklistKey(label);
129
- if (!key || !target.keyField || !target.groupField) return 0;
130
- const query = records.where(target.keyField, '==', key).orderBy(FieldPath.documentId()).limit(BATCH_SIZE);
131
- let written = 0;
132
- let after = null;
133
- for(;;){
134
- var _page_docs_;
135
- const page = await (after ? query.startAfter(after) : query).get();
136
- if (page.empty) return written;
137
- const batch = firestore.batch();
138
- let changed = 0;
139
- for (const snapshot of page.docs){
140
- if (snapshot.get(target.groupField) === group) continue;
141
- batch.update(snapshot.ref, {
142
- [target.groupField]: group
143
- });
144
- changed += 1;
145
- }
146
- if (changed) await batch.commit();
147
- written += changed;
148
- if (page.size < BATCH_SIZE) return written;
149
- after = (_page_docs_ = page.docs[page.docs.length - 1]) != null ? _page_docs_ : null;
150
- }
151
- }
152
115
  /** A contact as it reads once one holder's facet field holds `to` (or nothing, for `null`). */ function withFacetValue(data, groupId, field, to) {
153
116
  var _data_CONTACT_FACETS_FIELD, _facets_groupId;
154
117
  const facets = _extends({}, (_data_CONTACT_FACETS_FIELD = data[CONTACT_FACETS_FIELD]) != null ? _data_CONTACT_FACETS_FIELD : {});
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/picklist-values.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n consentGroupForHost,\n CONTACT_FACETS_FIELD,\n CRM_COLLECTIONS,\n CRM_CONTACT_FACET_KEYS_FIELD,\n CRM_LEAD_SOURCE_PICKLIST,\n type CrmPicklist,\n type CrmPicklistDefinition,\n type CrmPicklistId,\n type CrmPicklistObject,\n type CrmPicklistTarget,\n crmContactFacetKeys,\n crmPicklistDefinition,\n crmPicklistKey,\n crmPicklistValueByLabel,\n effectiveCrmPicklist,\n newResourceScopeFields,\n normalizeCrmPicklist,\n ORG_SCOPE_TOKEN,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport {\n addPicklistValue,\n deletePicklistValue,\n picklistKeepsGroups,\n type PicklistMove,\n type PicklistValuesResponse,\n renamePicklistValue,\n setPicklistValueGroup,\n} from '../model/picklist-values'\nimport { orgHostIds, readCrmRouteScope } from './org-caller'\nimport { authorizeCrmWriter } from './task-routes'\n\n/** What the suite gate names when a plan without the CRM asks. */\nconst suiteAct = (definition: CrmPicklistDefinition) =>\n `Editing the organization's ${definition.plural}`\n\n/** The org subcollection each target object's records live in. */\nconst TARGET_COLLECTIONS: Record<CrmPicklistObject, string> = {\n contact: 'contacts',\n lead: 'leads',\n company: CRM_COLLECTIONS.companies,\n deal: CRM_COLLECTIONS.deals,\n task: CRM_COLLECTIONS.tasks,\n}\n\n/** Writes per batch — under Firestore's 500, with room to spare. */\nconst BATCH_SIZE = 400\n\n/**\n * Set `field` to `to` (or remove it, for `null`) on every document `query`\n * finds holding `from`, a batch at a time, with `beside` written alongside.\n * Answers how many were written.\n *\n * The query is re-run after each batch rather than paged, because each\n * batch moves its documents OUT of the query's answer — the next run finds\n * only what is left, and a run that finds nothing is the end.\n */\nasync function replaceEverywhere(\n firestore: FirebaseFirestore.Firestore,\n query: FirebaseFirestore.Query,\n field: string | FirebaseFirestore.FieldPath,\n to: string | null,\n beside:\n | Record<string, unknown>\n | ((snapshot: FirebaseFirestore.QueryDocumentSnapshot) => Record<string, unknown>) = {},\n): Promise<number> {\n let written = 0\n for (;;) {\n const page = await query.limit(BATCH_SIZE).get()\n if (page.empty) return written\n const batch = firestore.batch()\n for (const snapshot of page.docs) {\n const alongside = typeof beside === 'function' ? beside(snapshot) : beside\n batch.update(\n snapshot.ref,\n field,\n to ?? FieldValue.delete(),\n 'updatedAt',\n FieldValue.serverTimestamp(),\n ...Object.entries(alongside).flat(),\n )\n }\n await batch.commit()\n written += page.size\n if (page.size < BATCH_SIZE) return written\n }\n}\n\n/**\n * Rewrite an ARRAY target (AGL-3521): every record whose `keyField` lists\n * the old label's key has each entry of `field` holding `from` at\n * `arrayKey` moved to `to` — or the entry's label removed, for `null` —\n * and its `keyField` restamped from the entries as moved. Answers how many\n * records were written.\n *\n * Paged by document id rather than re-run, because a rename that changes\n * only the label's case keeps its key, so a written record can stay in the\n * query's answer.\n */\nasync function replaceInArrayEverywhere(\n firestore: FirebaseFirestore.Firestore,\n records: FirebaseFirestore.CollectionReference,\n target: CrmPicklistTarget & { arrayKey: string; keyField: string },\n from: string,\n to: string | null,\n): Promise<number> {\n const fromKey = crmPicklistKey(from)\n if (!fromKey) return 0\n const query = records\n .where(target.keyField, 'array-contains', fromKey)\n .orderBy(FieldPath.documentId())\n .limit(BATCH_SIZE)\n let written = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n const page: FirebaseFirestore.QuerySnapshot = await (after ? query.startAfter(after) : query).get()\n if (page.empty) return written\n const batch = firestore.batch()\n let changed = 0\n for (const snapshot of page.docs) {\n const entries = snapshot.get(target.field)\n if (!Array.isArray(entries)) continue\n let moved = false\n const next = entries.map((entry: unknown) => {\n if (!entry || typeof entry !== 'object') return entry\n const row = entry as Record<string, unknown>\n if (row[target.arrayKey] !== from) return entry\n moved = true\n const rest = { ...row }\n delete rest[target.arrayKey]\n return to === null ? rest : { ...rest, [target.arrayKey]: to }\n })\n if (!moved) continue\n const keys = new Set<string>()\n for (const entry of next) {\n const key =\n entry && typeof entry === 'object'\n ? crmPicklistKey((entry as Record<string, unknown>)[target.arrayKey])\n : null\n if (key) keys.add(key)\n }\n batch.update(snapshot.ref, {\n [target.field]: next,\n [target.keyField]: [...keys],\n updatedAt: FieldValue.serverTimestamp(),\n })\n changed += 1\n }\n if (changed) await batch.commit()\n written += changed\n if (page.size < BATCH_SIZE) return written\n after = page.docs[page.docs.length - 1] ?? null\n }\n}\n\n/** The group a label is filed under in `picklist` — `null` for none, or a label it does not hold. */\nexport function picklistGroupOf(picklist: CrmPicklist, label: string | null): string | null {\n return (label === null ? null : crmPicklistValueByLabel(picklist, label)?.group) ?? null\n}\n\n/**\n * Rewrite a target's GROUP field (AGL-3577) — a lead's\n * `leadSourceDirection` — on every record holding `label` in any spelling\n * (by its `keyField`) whose group is not `group` already. Answers how many\n * were written. The field is derived, like the list keys a restamp writes,\n * so `updatedAt` is left alone.\n *\n * Paged by document id rather than re-run: a record already holding the\n * group stays in the query's answer.\n */\nexport async function regroupEverywhere(\n firestore: FirebaseFirestore.Firestore,\n records: FirebaseFirestore.CollectionReference,\n target: CrmPicklistTarget,\n label: string,\n group: string | null,\n): Promise<number> {\n const key = crmPicklistKey(label)\n if (!key || !target.keyField || !target.groupField) return 0\n const query = records\n .where(target.keyField, '==', key)\n .orderBy(FieldPath.documentId())\n .limit(BATCH_SIZE)\n let written = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n const page: FirebaseFirestore.QuerySnapshot = await (after ? query.startAfter(after) : query).get()\n if (page.empty) return written\n const batch = firestore.batch()\n let changed = 0\n for (const snapshot of page.docs) {\n if (snapshot.get(target.groupField) === group) continue\n batch.update(snapshot.ref, { [target.groupField]: group })\n changed += 1\n }\n if (changed) await batch.commit()\n written += changed\n if (page.size < BATCH_SIZE) return written\n after = page.docs[page.docs.length - 1] ?? null\n }\n}\n\n/** A contact as it reads once one holder's facet field holds `to` (or nothing, for `null`). */\nfunction withFacetValue(\n data: Record<string, unknown>,\n groupId: string,\n field: string,\n to: string | null,\n): Record<string, unknown> {\n const facets = { ...((data[CONTACT_FACETS_FIELD] as Record<string, unknown> | undefined) ?? {}) }\n const facet = { ...((facets[groupId] as Record<string, unknown> | undefined) ?? {}) }\n if (to === null) delete facet[field]\n else facet[field] = to\n facets[groupId] = facet\n return { ...data, [CONTACT_FACETS_FIELD]: facets }\n}\n\n/**\n * `POST crm/picklist-values` — rename a value of one of the CRM's standard\n * picklists, or delete an added one and move its records to another\n * (AGL-3298, AGL-3510). `crm/lead-source-values` is the same route with the\n * picklist fixed to the lead source.\n *\n * Body: `{ hostId | orgId, picklistId, action: 'rename', valueId, label }` or\n * `{ hostId | orgId, picklistId, action: 'delete', valueId, replaceWith: label | null }`,\n * and, on a list whose records keep the value's group (`picklistKeepsGroups`,\n * AGL-3577), `{ …, action: 'group', valueId, group: id | null }` or\n * `{ …, action: 'add', label, group?, meaning? }`.\n * `picklistId` is one of `CRM_PICKLIST_DEFINITIONS`; anything else is refused.\n *\n * ## A value's group on its records\n *\n * A lead keeps its lead source's group as `leadSourceDirection` — the field\n * the Leads list's direction filter asks. Any move can change the group a\n * label has: a regroup, an add of a value records already hold, a rename\n * onto such a label, a delete. So after the move, every target naming a\n * `groupField` has it rewritten on the records holding the old label and\n * the new one, from the list as it now stands.\n *\n * Records store the value's LABEL (see the picklist block in `crm.ts`), so\n * both moves change records as well as the list: every record the\n * definition's `targets` name that holds the old label is rewritten to the\n * new one — or, for a delete with no replacement, cleared. That is\n * Salesforce's Replace, done as part of the move, so a report grouped by\n * the field follows a rename instead of splitting into the old name and\n * the new. A standard value cannot be deleted, and on a definition with\n * meanings a delete's replacement must mean the same (see\n * `deletePicklistValue`).\n *\n * ## The list first, then the records\n *\n * The list is written in a transaction before any record: from that moment\n * no picker offers the old label and no import or API write accepts it, so\n * the sweep that follows cannot be outrun by a new record taking the name\n * it is removing. A sweep interrupted part-way leaves records holding a\n * label the list no longer has, which they show as \"(not in the list)\"; the\n * same request sent again finishes it, because the rename of a value to its\n * own current label is a no-op on the list and still sweeps.\n *\n * ## Who may\n *\n * Whoever may write the CRM — `authorizeCrmWriter`, the question every CRM\n * route asks — which is who the rules let edit the list itself. The sweep\n * reaches every record in the org whatever the caller's scope: it changes\n * one label to another that the same caller could already have picked, and\n * reveals nothing about the records but their count.\n */\nfunction picklistValuesHandler(fixed?: CrmPicklistId): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const definition = crmPicklistDefinition(fixed ?? body['picklistId'])\n if (!definition) {\n res.status(400).json({ error: 'Name one of the CRM’s picklists.' })\n return\n }\n const picklistId = definition.id as CrmPicklistId\n const action = body['action']\n const valueId = String(body['valueId'] ?? '').trim().slice(0, 64)\n const regrouping = action === 'group' || action === 'add'\n if (regrouping && !picklistKeepsGroups(definition)) {\n res.status(400).json({ error: `The ${definition.plural} are added and grouped on the Fields page.` })\n return\n }\n if (\n !(action === 'rename' || action === 'delete' || regrouping) ||\n (action === 'add' ? !String(body['label'] ?? '').trim() : !valueId)\n ) {\n res.status(400).json({ error: 'Name a value, and whether to rename or delete it.' })\n return\n }\n const asGroup = (raw: unknown) => (typeof raw === 'string' && raw ? raw : null)\n const replaceWith =\n body['replaceWith'] === null || body['replaceWith'] === undefined\n ? null\n : String(body['replaceWith'])\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: suiteAct(definition) })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const listRef = orgRef.collection(CRM_COLLECTIONS.picklists).doc(picklistId)\n\n /*\n * THE LIST. Read and written in one transaction, so two admins editing\n * at once cannot each write a list missing the other's change.\n */\n const moved = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(listRef)\n const before: CrmPicklist = effectiveCrmPicklist(picklistId, snapshot.data())\n const value =\n action === 'add' ? null : before.values.find((entry) => entry.id === valueId)\n if (action !== 'add' && !value) {\n return { ok: false as const, error: 'That value is no longer in the list.' }\n }\n const move: PicklistMove =\n action === 'add'\n ? addPicklistValue(definition, before, String(body['label'] ?? ''), {\n group: asGroup(body['group']),\n meaning: asGroup(body['meaning']),\n })\n : action === 'group'\n ? { ok: true, picklist: setPicklistValueGroup(definition, before, valueId, asGroup(body['group'])) }\n : action === 'rename'\n ? renamePicklistValue(before, valueId, String(body['label'] ?? ''))\n : deletePicklistValue(definition, before, valueId, replaceWith)\n if (move.ok === false) return move\n const created = normalizeCrmPicklist(snapshot.data()) === null\n transaction.set(\n listRef,\n {\n values: move.picklist.values,\n defaultValueId: move.picklist.defaultValueId,\n updatedAt: FieldValue.serverTimestamp(),\n // The stamp every org-wide CRM document carries (see the Fields\n // page), written when this move is what creates the document.\n ...(created\n ? {\n hostId: scope.hostId || null,\n createdAt: FieldValue.serverTimestamp(),\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n }\n : {}),\n },\n { merge: true },\n )\n // A regroup and an add move no label: `from` and `to` are the value's own.\n const own =\n action === 'add'\n ? (move.picklist.values[move.picklist.values.length - 1]?.label ?? '')\n : (value?.label ?? '')\n const to =\n action === 'rename'\n ? (move.picklist.values.find((entry) => entry.id === valueId)?.label ?? null)\n : regrouping\n ? own\n : replaceWith === null\n ? null\n : (move.picklist.values.find(\n (entry) => entry.label.toLowerCase() === replaceWith.trim().toLowerCase(),\n )?.label ?? null)\n return { ok: true as const, from: regrouping ? own : (value?.label ?? ''), to, after: move.picklist }\n })\n if (moved.ok === false) {\n res.status(400).json({ error: moved.error })\n return\n }\n\n /*\n * THE RECORDS, target by target: a record's own field, with its query\n * key moved beside it; for an array target, each entry of a record's\n * list (a deal's contact roles); or, for a facet target, each holder's\n * facet on a contact — one query per consent group the org's sites\n * belong to.\n * A rename to the same label moves nothing, and says so.\n */\n const updated: PicklistValuesResponse['updated'] = {}\n let groupIds: string[] | null = null\n for (const target of definition.targets) {\n updated[target.object] ??= 0\n if (target.groupField && !target.facet && !target.arrayKey) {\n // The group each label now has, on every record holding either (AGL-3577).\n const labels = new Map<string, string>()\n for (const label of [moved.from, moved.to]) {\n const key = label === null ? null : crmPicklistKey(label)\n if (key && label !== null) labels.set(key, label)\n }\n const regroup = async () => {\n for (const label of labels.values()) {\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await regroupEverywhere(\n firestore,\n orgRef.collection(TARGET_COLLECTIONS[target.object]),\n target,\n label,\n picklistGroupOf(moved.after, label),\n ))\n }\n }\n if (moved.from === moved.to) {\n await regroup()\n continue\n }\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n orgRef.collection(TARGET_COLLECTIONS[target.object]).where(target.field, '==', moved.from),\n target.field,\n moved.to,\n {\n ...(target.keyField ? { [target.keyField]: crmPicklistKey(moved.to) } : {}),\n [target.groupField]: picklistGroupOf(moved.after, moved.to),\n },\n ))\n await regroup()\n continue\n }\n if (moved.from === moved.to) continue\n const records = orgRef.collection(TARGET_COLLECTIONS[target.object])\n if (target.arrayKey && target.keyField) {\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceInArrayEverywhere(\n firestore,\n records,\n { ...target, arrayKey: target.arrayKey, keyField: target.keyField },\n moved.from,\n moved.to,\n ))\n continue\n }\n if (target.facet) {\n if (!groupIds) {\n const hostIds = await orgHostIds(firestore, writer.orgId)\n groupIds = [\n ...new Set(hostIds.map((hostId) => consentGroupForHost(writer.org, hostId).groupId)),\n ].filter(Boolean)\n }\n for (const groupId of groupIds) {\n const path = new FieldPath(CONTACT_FACETS_FIELD, groupId, target.field)\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(path, '==', moved.from),\n path,\n moved.to,\n // A facet value a contact list filters by is keyed in\n // `facetKeys` (AGL-3511), restamped from the record as moved.\n (snapshot) => ({\n [CRM_CONTACT_FACET_KEYS_FIELD]: crmContactFacetKeys(\n withFacetValue(snapshot.data(), groupId, target.field, moved.to),\n ),\n }),\n ))\n }\n continue\n }\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(target.field, '==', moved.from),\n target.field,\n moved.to,\n target.keyField ? { [target.keyField]: crmPicklistKey(moved.to) } : {},\n ))\n }\n const answer: PicklistValuesResponse = { ok: true, updated }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] picklist-values failed', error)\n res.status(500).json({ error: `The ${definition.plural} could not be saved.` })\n }\n }\n}\n\n/** `POST crm/picklist-values` — any registered picklist, named by `picklistId`. */\nexport const crmPicklistValuesHandler: PluginApiHandler = picklistValuesHandler()\n\n/** `POST crm/lead-source-values` — the same route with the picklist fixed to the lead source. */\nexport const crmLeadSourceValuesHandler: PluginApiHandler =\n picklistValuesHandler(CRM_LEAD_SOURCE_PICKLIST)\n"],"names":["consentGroupForHost","CONTACT_FACETS_FIELD","CRM_COLLECTIONS","CRM_CONTACT_FACET_KEYS_FIELD","CRM_LEAD_SOURCE_PICKLIST","crmContactFacetKeys","crmPicklistDefinition","crmPicklistKey","crmPicklistValueByLabel","effectiveCrmPicklist","newResourceScopeFields","normalizeCrmPicklist","ORG_SCOPE_TOKEN","firebaseAdmin","FieldPath","FieldValue","addPicklistValue","deletePicklistValue","picklistKeepsGroups","renamePicklistValue","setPicklistValueGroup","orgHostIds","readCrmRouteScope","authorizeCrmWriter","suiteAct","definition","plural","TARGET_COLLECTIONS","contact","lead","company","companies","deal","deals","task","tasks","BATCH_SIZE","replaceEverywhere","firestore","query","field","to","beside","written","page","limit","get","empty","batch","snapshot","docs","alongside","update","ref","delete","serverTimestamp","Object","entries","flat","commit","size","replaceInArrayEverywhere","records","target","from","fromKey","where","keyField","orderBy","documentId","after","startAfter","changed","Array","isArray","moved","next","map","entry","row","arrayKey","rest","keys","Set","key","add","updatedAt","length","picklistGroupOf","picklist","label","group","regroupEverywhere","groupField","withFacetValue","data","groupId","facets","facet","picklistValuesHandler","fixed","req","res","body","method","setHeader","status","json","error","scope","picklistId","id","action","valueId","String","trim","slice","regrouping","asGroup","raw","replaceWith","undefined","writer","ok","app","orgRef","collection","doc","orgId","listRef","picklists","runTransaction","transaction","move","before","value","values","find","meaning","created","set","defaultValueId","hostId","createdAt","merge","own","toLowerCase","updated","groupIds","targets","object","labels","Map","regroup","hostIds","org","filter","Boolean","path","answer","console","crmPicklistValuesHandler","crmLeadSourceValuesHandler"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/picklist-values.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n consentGroupForHost,\n CONTACT_FACETS_FIELD,\n CRM_COLLECTIONS,\n CRM_CONTACT_FACET_KEYS_FIELD,\n CRM_LEAD_SOURCE_PICKLIST,\n type CrmPicklist,\n type CrmPicklistDefinition,\n type CrmPicklistId,\n type CrmPicklistObject,\n type CrmPicklistTarget,\n crmContactFacetKeys,\n crmPicklistDefinition,\n crmPicklistKey,\n effectiveCrmPicklist,\n newResourceScopeFields,\n normalizeCrmPicklist,\n ORG_SCOPE_TOKEN,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport {\n addPicklistValue,\n deletePicklistValue,\n picklistKeepsGroups,\n type PicklistMove,\n type PicklistValuesResponse,\n renamePicklistValue,\n setPicklistValueGroup,\n} from '../model/picklist-values'\nimport { orgHostIds, readCrmRouteScope } from './org-caller'\nimport { picklistGroupOf, regroupEverywhere } from './picklist-regroup'\nimport { authorizeCrmWriter } from './task-routes'\n\nexport { picklistGroupOf, regroupEverywhere } from './picklist-regroup'\n\n/** What the suite gate names when a plan without the CRM asks. */\nconst suiteAct = (definition: CrmPicklistDefinition) =>\n `Editing the organization's ${definition.plural}`\n\n/** The org subcollection each target object's records live in. */\nconst TARGET_COLLECTIONS: Record<CrmPicklistObject, string> = {\n contact: 'contacts',\n lead: 'leads',\n company: CRM_COLLECTIONS.companies,\n deal: CRM_COLLECTIONS.deals,\n task: CRM_COLLECTIONS.tasks,\n}\n\n/** Writes per batch — under Firestore's 500, with room to spare. */\nconst BATCH_SIZE = 400\n\n/**\n * Set `field` to `to` (or remove it, for `null`) on every document `query`\n * finds holding `from`, a batch at a time, with `beside` written alongside.\n * Answers how many were written.\n *\n * The query is re-run after each batch rather than paged, because each\n * batch moves its documents OUT of the query's answer — the next run finds\n * only what is left, and a run that finds nothing is the end.\n */\nasync function replaceEverywhere(\n firestore: FirebaseFirestore.Firestore,\n query: FirebaseFirestore.Query,\n field: string | FirebaseFirestore.FieldPath,\n to: string | null,\n beside:\n | Record<string, unknown>\n | ((snapshot: FirebaseFirestore.QueryDocumentSnapshot) => Record<string, unknown>) = {},\n): Promise<number> {\n let written = 0\n for (;;) {\n const page = await query.limit(BATCH_SIZE).get()\n if (page.empty) return written\n const batch = firestore.batch()\n for (const snapshot of page.docs) {\n const alongside = typeof beside === 'function' ? beside(snapshot) : beside\n batch.update(\n snapshot.ref,\n field,\n to ?? FieldValue.delete(),\n 'updatedAt',\n FieldValue.serverTimestamp(),\n ...Object.entries(alongside).flat(),\n )\n }\n await batch.commit()\n written += page.size\n if (page.size < BATCH_SIZE) return written\n }\n}\n\n/**\n * Rewrite an ARRAY target (AGL-3521): every record whose `keyField` lists\n * the old label's key has each entry of `field` holding `from` at\n * `arrayKey` moved to `to` — or the entry's label removed, for `null` —\n * and its `keyField` restamped from the entries as moved. Answers how many\n * records were written.\n *\n * Paged by document id rather than re-run, because a rename that changes\n * only the label's case keeps its key, so a written record can stay in the\n * query's answer.\n */\nasync function replaceInArrayEverywhere(\n firestore: FirebaseFirestore.Firestore,\n records: FirebaseFirestore.CollectionReference,\n target: CrmPicklistTarget & { arrayKey: string; keyField: string },\n from: string,\n to: string | null,\n): Promise<number> {\n const fromKey = crmPicklistKey(from)\n if (!fromKey) return 0\n const query = records\n .where(target.keyField, 'array-contains', fromKey)\n .orderBy(FieldPath.documentId())\n .limit(BATCH_SIZE)\n let written = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n const page: FirebaseFirestore.QuerySnapshot = await (after ? query.startAfter(after) : query).get()\n if (page.empty) return written\n const batch = firestore.batch()\n let changed = 0\n for (const snapshot of page.docs) {\n const entries = snapshot.get(target.field)\n if (!Array.isArray(entries)) continue\n let moved = false\n const next = entries.map((entry: unknown) => {\n if (!entry || typeof entry !== 'object') return entry\n const row = entry as Record<string, unknown>\n if (row[target.arrayKey] !== from) return entry\n moved = true\n const rest = { ...row }\n delete rest[target.arrayKey]\n return to === null ? rest : { ...rest, [target.arrayKey]: to }\n })\n if (!moved) continue\n const keys = new Set<string>()\n for (const entry of next) {\n const key =\n entry && typeof entry === 'object'\n ? crmPicklistKey((entry as Record<string, unknown>)[target.arrayKey])\n : null\n if (key) keys.add(key)\n }\n batch.update(snapshot.ref, {\n [target.field]: next,\n [target.keyField]: [...keys],\n updatedAt: FieldValue.serverTimestamp(),\n })\n changed += 1\n }\n if (changed) await batch.commit()\n written += changed\n if (page.size < BATCH_SIZE) return written\n after = page.docs[page.docs.length - 1] ?? null\n }\n}\n\n/** A contact as it reads once one holder's facet field holds `to` (or nothing, for `null`). */\nfunction withFacetValue(\n data: Record<string, unknown>,\n groupId: string,\n field: string,\n to: string | null,\n): Record<string, unknown> {\n const facets = { ...((data[CONTACT_FACETS_FIELD] as Record<string, unknown> | undefined) ?? {}) }\n const facet = { ...((facets[groupId] as Record<string, unknown> | undefined) ?? {}) }\n if (to === null) delete facet[field]\n else facet[field] = to\n facets[groupId] = facet\n return { ...data, [CONTACT_FACETS_FIELD]: facets }\n}\n\n/**\n * `POST crm/picklist-values` — rename a value of one of the CRM's standard\n * picklists, or delete an added one and move its records to another\n * (AGL-3298, AGL-3510). `crm/lead-source-values` is the same route with the\n * picklist fixed to the lead source.\n *\n * Body: `{ hostId | orgId, picklistId, action: 'rename', valueId, label }` or\n * `{ hostId | orgId, picklistId, action: 'delete', valueId, replaceWith: label | null }`,\n * and, on a list whose records keep the value's group (`picklistKeepsGroups`,\n * AGL-3577), `{ …, action: 'group', valueId, group: id | null }` or\n * `{ …, action: 'add', label, group?, meaning? }`.\n * `picklistId` is one of `CRM_PICKLIST_DEFINITIONS`; anything else is refused.\n *\n * ## A value's group on its records\n *\n * A lead keeps its lead source's group as `leadSourceDirection` — the field\n * the Leads list's direction filter asks. Any move can change the group a\n * label has: a regroup, an add of a value records already hold, a rename\n * onto such a label, a delete. So after the move, every target naming a\n * `groupField` has it rewritten on the records holding the old label and\n * the new one, from the list as it now stands.\n *\n * Records store the value's LABEL (see the picklist block in `crm.ts`), so\n * both moves change records as well as the list: every record the\n * definition's `targets` name that holds the old label is rewritten to the\n * new one — or, for a delete with no replacement, cleared. That is\n * Salesforce's Replace, done as part of the move, so a report grouped by\n * the field follows a rename instead of splitting into the old name and\n * the new. A standard value cannot be deleted, and on a definition with\n * meanings a delete's replacement must mean the same (see\n * `deletePicklistValue`).\n *\n * ## The list first, then the records\n *\n * The list is written in a transaction before any record: from that moment\n * no picker offers the old label and no import or API write accepts it, so\n * the sweep that follows cannot be outrun by a new record taking the name\n * it is removing. A sweep interrupted part-way leaves records holding a\n * label the list no longer has, which they show as \"(not in the list)\"; the\n * same request sent again finishes it, because the rename of a value to its\n * own current label is a no-op on the list and still sweeps.\n *\n * ## Who may\n *\n * Whoever may write the CRM — `authorizeCrmWriter`, the question every CRM\n * route asks — which is who the rules let edit the list itself. The sweep\n * reaches every record in the org whatever the caller's scope: it changes\n * one label to another that the same caller could already have picked, and\n * reveals nothing about the records but their count.\n */\nfunction picklistValuesHandler(fixed?: CrmPicklistId): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const definition = crmPicklistDefinition(fixed ?? body['picklistId'])\n if (!definition) {\n res.status(400).json({ error: 'Name one of the CRM’s picklists.' })\n return\n }\n const picklistId = definition.id as CrmPicklistId\n const action = body['action']\n const valueId = String(body['valueId'] ?? '').trim().slice(0, 64)\n const regrouping = action === 'group' || action === 'add'\n if (regrouping && !picklistKeepsGroups(definition)) {\n res.status(400).json({ error: `The ${definition.plural} are added and grouped on the Fields page.` })\n return\n }\n if (\n !(action === 'rename' || action === 'delete' || regrouping) ||\n (action === 'add' ? !String(body['label'] ?? '').trim() : !valueId)\n ) {\n res.status(400).json({ error: 'Name a value, and whether to rename or delete it.' })\n return\n }\n const asGroup = (raw: unknown) => (typeof raw === 'string' && raw ? raw : null)\n const replaceWith =\n body['replaceWith'] === null || body['replaceWith'] === undefined\n ? null\n : String(body['replaceWith'])\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: suiteAct(definition) })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const listRef = orgRef.collection(CRM_COLLECTIONS.picklists).doc(picklistId)\n\n /*\n * THE LIST. Read and written in one transaction, so two admins editing\n * at once cannot each write a list missing the other's change.\n */\n const moved = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(listRef)\n const before: CrmPicklist = effectiveCrmPicklist(picklistId, snapshot.data())\n const value =\n action === 'add' ? null : before.values.find((entry) => entry.id === valueId)\n if (action !== 'add' && !value) {\n return { ok: false as const, error: 'That value is no longer in the list.' }\n }\n const move: PicklistMove =\n action === 'add'\n ? addPicklistValue(definition, before, String(body['label'] ?? ''), {\n group: asGroup(body['group']),\n meaning: asGroup(body['meaning']),\n })\n : action === 'group'\n ? { ok: true, picklist: setPicklistValueGroup(definition, before, valueId, asGroup(body['group'])) }\n : action === 'rename'\n ? renamePicklistValue(before, valueId, String(body['label'] ?? ''))\n : deletePicklistValue(definition, before, valueId, replaceWith)\n if (move.ok === false) return move\n const created = normalizeCrmPicklist(snapshot.data()) === null\n transaction.set(\n listRef,\n {\n values: move.picklist.values,\n defaultValueId: move.picklist.defaultValueId,\n updatedAt: FieldValue.serverTimestamp(),\n // The stamp every org-wide CRM document carries (see the Fields\n // page), written when this move is what creates the document.\n ...(created\n ? {\n hostId: scope.hostId || null,\n createdAt: FieldValue.serverTimestamp(),\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n }\n : {}),\n },\n { merge: true },\n )\n // A regroup and an add move no label: `from` and `to` are the value's own.\n const own =\n action === 'add'\n ? (move.picklist.values[move.picklist.values.length - 1]?.label ?? '')\n : (value?.label ?? '')\n const to =\n action === 'rename'\n ? (move.picklist.values.find((entry) => entry.id === valueId)?.label ?? null)\n : regrouping\n ? own\n : replaceWith === null\n ? null\n : (move.picklist.values.find(\n (entry) => entry.label.toLowerCase() === replaceWith.trim().toLowerCase(),\n )?.label ?? null)\n return { ok: true as const, from: regrouping ? own : (value?.label ?? ''), to, after: move.picklist }\n })\n if (moved.ok === false) {\n res.status(400).json({ error: moved.error })\n return\n }\n\n /*\n * THE RECORDS, target by target: a record's own field, with its query\n * key moved beside it; for an array target, each entry of a record's\n * list (a deal's contact roles); or, for a facet target, each holder's\n * facet on a contact — one query per consent group the org's sites\n * belong to.\n * A rename to the same label moves nothing, and says so.\n */\n const updated: PicklistValuesResponse['updated'] = {}\n let groupIds: string[] | null = null\n for (const target of definition.targets) {\n updated[target.object] ??= 0\n if (target.groupField && !target.facet && !target.arrayKey) {\n // The group each label now has, on every record holding either (AGL-3577).\n const labels = new Map<string, string>()\n for (const label of [moved.from, moved.to]) {\n const key = label === null ? null : crmPicklistKey(label)\n if (key && label !== null) labels.set(key, label)\n }\n const regroup = async () => {\n for (const label of labels.values()) {\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await regroupEverywhere(\n firestore,\n orgRef.collection(TARGET_COLLECTIONS[target.object]),\n target,\n label,\n picklistGroupOf(moved.after, label),\n ))\n }\n }\n if (moved.from === moved.to) {\n await regroup()\n continue\n }\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n orgRef.collection(TARGET_COLLECTIONS[target.object]).where(target.field, '==', moved.from),\n target.field,\n moved.to,\n {\n ...(target.keyField ? { [target.keyField]: crmPicklistKey(moved.to) } : {}),\n [target.groupField]: picklistGroupOf(moved.after, moved.to),\n },\n ))\n await regroup()\n continue\n }\n if (moved.from === moved.to) continue\n const records = orgRef.collection(TARGET_COLLECTIONS[target.object])\n if (target.arrayKey && target.keyField) {\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceInArrayEverywhere(\n firestore,\n records,\n { ...target, arrayKey: target.arrayKey, keyField: target.keyField },\n moved.from,\n moved.to,\n ))\n continue\n }\n if (target.facet) {\n if (!groupIds) {\n const hostIds = await orgHostIds(firestore, writer.orgId)\n groupIds = [\n ...new Set(hostIds.map((hostId) => consentGroupForHost(writer.org, hostId).groupId)),\n ].filter(Boolean)\n }\n for (const groupId of groupIds) {\n const path = new FieldPath(CONTACT_FACETS_FIELD, groupId, target.field)\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(path, '==', moved.from),\n path,\n moved.to,\n // A facet value a contact list filters by is keyed in\n // `facetKeys` (AGL-3511), restamped from the record as moved.\n (snapshot) => ({\n [CRM_CONTACT_FACET_KEYS_FIELD]: crmContactFacetKeys(\n withFacetValue(snapshot.data(), groupId, target.field, moved.to),\n ),\n }),\n ))\n }\n continue\n }\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(target.field, '==', moved.from),\n target.field,\n moved.to,\n target.keyField ? { [target.keyField]: crmPicklistKey(moved.to) } : {},\n ))\n }\n const answer: PicklistValuesResponse = { ok: true, updated }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] picklist-values failed', error)\n res.status(500).json({ error: `The ${definition.plural} could not be saved.` })\n }\n }\n}\n\n/** `POST crm/picklist-values` — any registered picklist, named by `picklistId`. */\nexport const crmPicklistValuesHandler: PluginApiHandler = picklistValuesHandler()\n\n/** `POST crm/lead-source-values` — the same route with the picklist fixed to the lead source. */\nexport const crmLeadSourceValuesHandler: PluginApiHandler =\n picklistValuesHandler(CRM_LEAD_SOURCE_PICKLIST)\n"],"names":["consentGroupForHost","CONTACT_FACETS_FIELD","CRM_COLLECTIONS","CRM_CONTACT_FACET_KEYS_FIELD","CRM_LEAD_SOURCE_PICKLIST","crmContactFacetKeys","crmPicklistDefinition","crmPicklistKey","effectiveCrmPicklist","newResourceScopeFields","normalizeCrmPicklist","ORG_SCOPE_TOKEN","firebaseAdmin","FieldPath","FieldValue","addPicklistValue","deletePicklistValue","picklistKeepsGroups","renamePicklistValue","setPicklistValueGroup","orgHostIds","readCrmRouteScope","picklistGroupOf","regroupEverywhere","authorizeCrmWriter","suiteAct","definition","plural","TARGET_COLLECTIONS","contact","lead","company","companies","deal","deals","task","tasks","BATCH_SIZE","replaceEverywhere","firestore","query","field","to","beside","written","page","limit","get","empty","batch","snapshot","docs","alongside","update","ref","delete","serverTimestamp","Object","entries","flat","commit","size","replaceInArrayEverywhere","records","target","from","fromKey","where","keyField","orderBy","documentId","after","startAfter","changed","Array","isArray","moved","next","map","entry","row","arrayKey","rest","keys","Set","key","add","updatedAt","length","withFacetValue","data","groupId","facets","facet","picklistValuesHandler","fixed","req","res","body","method","setHeader","status","json","error","scope","picklistId","id","action","valueId","String","trim","slice","regrouping","asGroup","raw","replaceWith","undefined","writer","ok","app","orgRef","collection","doc","orgId","listRef","picklists","runTransaction","transaction","move","before","value","values","find","group","meaning","picklist","created","set","defaultValueId","hostId","createdAt","merge","own","label","toLowerCase","updated","groupIds","targets","object","groupField","labels","Map","regroup","hostIds","org","filter","Boolean","path","answer","console","crmPicklistValuesHandler","crmLeadSourceValuesHandler"],"mappings":"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"}
@@ -181,9 +181,11 @@ export declare function nameKey(value: string): string;
181
181
  export declare function customKeyOf(fieldId: string): string | null;
182
182
  /**
183
183
  * A stored custom value as the file reads it: a date (stored as epoch
184
- * milliseconds) as its day, `YYYY-MM-DD`; anything else as stored.
184
+ * milliseconds) as its day, `YYYY-MM-DD`; an amount (stored as minor units)
185
+ * as major units in the field's currency, `1250.5` (AGL-3756); anything
186
+ * else as stored.
185
187
  */
186
- export declare function customTransferValue(definition: Pick<ContactFieldDefinition, 'type'> | undefined, value: unknown): unknown;
188
+ export declare function customTransferValue(definition: Pick<ContactFieldDefinition, 'type' | 'currency'> | undefined, value: unknown): unknown;
187
189
  /** Every custom field a record holds, as `custom:<key>` values. */
188
190
  export declare function customTransferValues(definitions: readonly ContactFieldDefinition[], stored: unknown): Record<string, unknown>;
189
191
  /**
@@ -35,7 +35,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
35
35
  * (`TransferPlanRefusedError` carrying `suite-gate.ts`'s answer, else
36
36
  * `TransferEngineError`), so the wizard shows the sentence a CRM route
37
37
  * would have answered.
38
- *=========================================*/ import { checkCrmRecordsQuota, consentGroupForHost, CRM_COLLECTIONS, crmPicklistDefinition, crmReadTokens, crmScopeTokens, effectiveCrmPicklist, fieldDefinitionsForObject, isCrmPicklistId, newResourceScopeFields, normalizeCrmPicklist, ORG_SCOPE_TOKEN, readCrmCustomInput, visibleToTokens } from "@aglyn/aglyn/server";
38
+ *=========================================*/ import { checkCrmRecordsQuota, consentGroupForHost, CRM_COLLECTIONS, crmPicklistDefinition, crmReadTokens, crmScopeTokens, effectiveCrmPicklist, fieldDefinitionsForObject, isCrmPicklistId, newResourceScopeFields, normalizeCrmPicklist, ORG_SCOPE_TOKEN, readCrmCustomInput, customFieldCurrency, currencyMinorToMajor, visibleToTokens } from "@aglyn/aglyn/server";
39
39
  import { CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_LIFECYCLE_STAGES, CRM_FORECAST_CATEGORIES, CRM_FORECAST_CATEGORY_LABELS } from "@aglyn/aglyn/app-utils/crm";
40
40
  import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
41
41
  import { listOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
@@ -646,9 +646,14 @@ const FILTER_OPS = new Set([
646
646
  }
647
647
  /**
648
648
  * A stored custom value as the file reads it: a date (stored as epoch
649
- * milliseconds) as its day, `YYYY-MM-DD`; anything else as stored.
649
+ * milliseconds) as its day, `YYYY-MM-DD`; an amount (stored as minor units)
650
+ * as major units in the field's currency, `1250.5` (AGL-3756); anything
651
+ * else as stored.
650
652
  */ export function customTransferValue(definition, value) {
651
653
  if (value === null || value === undefined) return null;
654
+ if ((definition == null ? void 0 : definition.type) === 'currency') {
655
+ return typeof value === 'number' && Number.isFinite(value) ? currencyMinorToMajor(value, customFieldCurrency(definition)) : null;
656
+ }
652
657
  if ((definition == null ? void 0 : definition.type) === 'date') {
653
658
  const ms = storedMs(value);
654
659
  return ms === null ? null : new Date(ms).toISOString().slice(0, 10);
@@ -670,8 +675,14 @@ const FILTER_OPS = new Set([
670
675
  var _Object_values_;
671
676
  const raw = {};
672
677
  for (const [fieldId, value] of Object.entries(values)){
678
+ var _definitions_find;
673
679
  const key = customKeyOf(fieldId);
674
- if (key) raw[key] = value === undefined || value === '' ? null : value;
680
+ if (!key) continue;
681
+ const blank = value === undefined || value === '';
682
+ // A file's amount is major units, which the reader takes as text; a
683
+ // number it is handed would be read as the minor units it stores.
684
+ const currency = ((_definitions_find = definitions.find((one)=>one.key === key)) == null ? void 0 : _definitions_find.type) === 'currency';
685
+ raw[key] = blank ? null : currency && typeof value === 'number' ? String(value) : value;
675
686
  }
676
687
  if (!Object.keys(raw).length) return {
677
688
  values: {}