@aglyn/aglyn 1.0.0-beta.219 → 1.0.0-beta.220

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 (175) hide show
  1. package/package.json +12 -12
  2. package/src/lib/app-utils/actions.d.ts +1 -59
  3. package/src/lib/app-utils/actions.js +7 -72
  4. package/src/lib/app-utils/actions.js.map +1 -1
  5. package/src/lib/app-utils/consent-groups.d.ts +1 -1
  6. package/src/lib/app-utils/consent-groups.js.map +1 -1
  7. package/src/lib/app-utils/console-routes.d.ts +0 -36
  8. package/src/lib/app-utils/console-routes.js +2 -37
  9. package/src/lib/app-utils/console-routes.js.map +1 -1
  10. package/src/lib/app-utils/contacts.d.ts +1 -1
  11. package/src/lib/app-utils/contacts.js.map +1 -1
  12. package/src/lib/app-utils/container-membership.d.ts +93 -0
  13. package/src/lib/app-utils/container-membership.js +165 -0
  14. package/src/lib/app-utils/container-membership.js.map +1 -0
  15. package/src/lib/app-utils/crm-task-reminders.d.ts +23 -6
  16. package/src/lib/app-utils/crm-task-reminders.js +32 -11
  17. package/src/lib/app-utils/crm-task-reminders.js.map +1 -1
  18. package/src/lib/app-utils/crm.d.ts +24 -4
  19. package/src/lib/app-utils/crm.js +23 -0
  20. package/src/lib/app-utils/crm.js.map +1 -1
  21. package/src/lib/app-utils/csv-upload.d.ts +1 -1
  22. package/src/lib/app-utils/csv-upload.js +1 -1
  23. package/src/lib/app-utils/csv-upload.js.map +1 -1
  24. package/src/lib/app-utils/{dataset-csv.d.ts → csv.d.ts} +10 -43
  25. package/src/lib/app-utils/{dataset-csv.js → csv.js} +10 -81
  26. package/src/lib/app-utils/csv.js.map +1 -0
  27. package/src/lib/app-utils/datasets.d.ts +0 -19
  28. package/src/lib/app-utils/datasets.js +0 -13
  29. package/src/lib/app-utils/datasets.js.map +1 -1
  30. package/src/lib/app-utils/docs-help.generated.d.ts +2 -2
  31. package/src/lib/app-utils/docs-help.generated.js +2 -0
  32. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  33. package/src/lib/app-utils/docs-index.generated.js +63 -35
  34. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  35. package/src/lib/app-utils/dynamic-list-rule.js +3 -3
  36. package/src/lib/app-utils/dynamic-list-rule.js.map +1 -1
  37. package/src/lib/app-utils/email-text.d.ts +80 -0
  38. package/src/lib/app-utils/email-text.js +189 -0
  39. package/src/lib/app-utils/email-text.js.map +1 -0
  40. package/src/lib/app-utils/{list-assignment-policy.d.ts → enrollment-basis.d.ts} +4 -0
  41. package/src/lib/app-utils/{list-assignment-policy.js → enrollment-basis.js} +5 -1
  42. package/src/lib/app-utils/enrollment-basis.js.map +1 -0
  43. package/src/lib/app-utils/expand-repeatables.js +4 -4
  44. package/src/lib/app-utils/expand-repeatables.js.map +1 -1
  45. package/src/lib/app-utils/forms.js +2 -2
  46. package/src/lib/app-utils/forms.js.map +1 -1
  47. package/src/lib/app-utils/health-report.d.ts +2 -3
  48. package/src/lib/app-utils/health-report.js +8 -6
  49. package/src/lib/app-utils/health-report.js.map +1 -1
  50. package/src/lib/app-utils/inbound-mail-domain.d.ts +27 -0
  51. package/src/lib/app-utils/inbound-mail-domain.js +31 -0
  52. package/src/lib/app-utils/inbound-mail-domain.js.map +1 -0
  53. package/src/lib/app-utils/lockdown.d.ts +11 -0
  54. package/src/lib/app-utils/lockdown.js +33 -3
  55. package/src/lib/app-utils/lockdown.js.map +1 -1
  56. package/src/lib/app-utils/member-email-aliases.d.ts +25 -0
  57. package/src/lib/app-utils/member-email-aliases.js +29 -0
  58. package/src/lib/app-utils/member-email-aliases.js.map +1 -1
  59. package/src/lib/app-utils/notifications.d.ts +69 -0
  60. package/src/lib/app-utils/notifications.js +119 -0
  61. package/src/lib/app-utils/notifications.js.map +1 -1
  62. package/src/lib/app-utils/operator-alerts.d.ts +15 -1
  63. package/src/lib/app-utils/operator-alerts.js +47 -10
  64. package/src/lib/app-utils/operator-alerts.js.map +1 -1
  65. package/src/lib/app-utils/plan-entitlements.d.ts +164 -178
  66. package/src/lib/app-utils/plan-entitlements.js +200 -224
  67. package/src/lib/app-utils/plan-entitlements.js.map +1 -1
  68. package/src/lib/app-utils/platform-marketing-consent.d.ts +56 -2
  69. package/src/lib/app-utils/platform-marketing-consent.js +6 -5
  70. package/src/lib/app-utils/platform-marketing-consent.js.map +1 -1
  71. package/src/lib/app-utils/plugin-starter-templates.generated.d.ts +12 -0
  72. package/src/lib/app-utils/plugin-starter-templates.generated.js +711 -0
  73. package/src/lib/app-utils/plugin-starter-templates.generated.js.map +1 -0
  74. package/src/lib/app-utils/repeat-query.d.ts +56 -0
  75. package/src/lib/app-utils/{dataset-query.js → repeat-query.js} +16 -16
  76. package/src/lib/app-utils/repeat-query.js.map +1 -0
  77. package/src/lib/app-utils/server.d.ts +7 -9
  78. package/src/lib/app-utils/server.js +18 -26
  79. package/src/lib/app-utils/server.js.map +1 -1
  80. package/src/lib/app-utils/starter-template-nodes.d.ts +137 -0
  81. package/src/lib/app-utils/starter-template-nodes.js +129 -0
  82. package/src/lib/app-utils/starter-template-nodes.js.map +1 -0
  83. package/src/lib/app-utils/starter-templates.d.ts +11 -34
  84. package/src/lib/app-utils/starter-templates.js +19 -230
  85. package/src/lib/app-utils/starter-templates.js.map +1 -1
  86. package/src/lib/app-utils/{email-topics.d.ts → subscription-topics.d.ts} +64 -48
  87. package/src/lib/app-utils/{email-topics.js → subscription-topics.js} +39 -109
  88. package/src/lib/app-utils/subscription-topics.js.map +1 -0
  89. package/src/lib/app-utils/usage-budget.d.ts +73 -188
  90. package/src/lib/app-utils/usage-budget.js +58 -194
  91. package/src/lib/app-utils/usage-budget.js.map +1 -1
  92. package/src/lib/app-utils/utm-attribution.d.ts +1 -1
  93. package/src/lib/app-utils/utm-attribution.js +1 -1
  94. package/src/lib/app-utils/utm-attribution.js.map +1 -1
  95. package/src/lib/app-utils/{campaign-touch.d.ts → utm-touch.d.ts} +21 -13
  96. package/src/lib/app-utils/{campaign-touch.js → utm-touch.js} +30 -22
  97. package/src/lib/app-utils/utm-touch.js.map +1 -0
  98. package/src/lib/app-utils/variables.d.ts +9 -9
  99. package/src/lib/app-utils/variables.js.map +1 -1
  100. package/src/lib/app-utils/visitor-record-ceiling.d.ts +32 -0
  101. package/src/lib/app-utils/visitor-record-ceiling.js +39 -0
  102. package/src/lib/app-utils/visitor-record-ceiling.js.map +1 -1
  103. package/src/lib/foundation/definitions/org-billing.types.d.ts +2 -2
  104. package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
  105. package/src/lib/plugin-manager/feature-plugins.d.ts +80 -79
  106. package/src/lib/plugin-manager/feature-plugins.js +22 -35
  107. package/src/lib/plugin-manager/feature-plugins.js.map +1 -1
  108. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +12 -1
  109. package/src/lib/plugin-manager/first-party-plugins.generated.js +58 -2
  110. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  111. package/src/lib/plugin-manager/index.d.ts +0 -1
  112. package/src/lib/plugin-manager/index.js +0 -1
  113. package/src/lib/plugin-manager/index.js.map +1 -1
  114. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +9 -4
  115. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  116. package/src/lib/plugin-manager/plugin-containers.d.ts +68 -0
  117. package/src/lib/plugin-manager/plugin-containers.generated.d.ts +10 -0
  118. package/src/lib/plugin-manager/plugin-containers.generated.js +20 -0
  119. package/src/lib/plugin-manager/plugin-containers.generated.js.map +1 -0
  120. package/src/lib/plugin-manager/plugin-containers.js +27 -0
  121. package/src/lib/plugin-manager/plugin-containers.js.map +1 -0
  122. package/src/lib/plugin-manager/plugin-entitlements.d.ts +8 -0
  123. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  124. package/src/lib/plugin-manager/plugin-membership-detach.js +1 -1
  125. package/src/lib/plugin-manager/plugin-membership-detach.js.map +1 -1
  126. package/src/lib/plugin-manager/plugin-record-email-state.d.ts +69 -0
  127. package/src/lib/plugin-manager/plugin-record-email-state.js +32 -0
  128. package/src/lib/plugin-manager/plugin-record-email-state.js.map +1 -1
  129. package/src/lib/plugin-manager/plugin-record-lists.d.ts +115 -0
  130. package/src/lib/plugin-manager/plugin-record-lists.js +75 -0
  131. package/src/lib/plugin-manager/plugin-record-lists.js.map +1 -0
  132. package/src/lib/plugin-manager/plugin-record-timeline.d.ts +42 -8
  133. package/src/lib/plugin-manager/plugin-record-timeline.js.map +1 -1
  134. package/src/lib/plugin-manager/plugin-site-beacons.d.ts +71 -0
  135. package/src/lib/plugin-manager/plugin-site-beacons.js +54 -0
  136. package/src/lib/plugin-manager/plugin-site-beacons.js.map +1 -0
  137. package/src/lib/plugin-manager/plugin-subscription-topics.generated.d.ts +11 -0
  138. package/src/lib/plugin-manager/plugin-subscription-topics.generated.js +41 -0
  139. package/src/lib/plugin-manager/plugin-subscription-topics.generated.js.map +1 -0
  140. package/src/lib/plugin-manager/plugin-usage-axes.d.ts +212 -0
  141. package/src/lib/plugin-manager/plugin-usage-axes.js +82 -3
  142. package/src/lib/plugin-manager/plugin-usage-axes.js.map +1 -1
  143. package/src/lib/plugin-manager/plugin-usage-meters.d.ts +108 -0
  144. package/src/lib/plugin-manager/plugin-usage-meters.js +105 -0
  145. package/src/lib/plugin-manager/plugin-usage-meters.js.map +1 -0
  146. package/src/lib/plugin-manager/usage-alert-contributors.d.ts +76 -6
  147. package/src/lib/plugin-manager/usage-alert-contributors.js +23 -10
  148. package/src/lib/plugin-manager/usage-alert-contributors.js.map +1 -1
  149. package/src/lib/app-utils/assist-credits.d.ts +0 -482
  150. package/src/lib/app-utils/assist-credits.js +0 -510
  151. package/src/lib/app-utils/assist-credits.js.map +0 -1
  152. package/src/lib/app-utils/campaign-membership.d.ts +0 -121
  153. package/src/lib/app-utils/campaign-membership.js +0 -189
  154. package/src/lib/app-utils/campaign-membership.js.map +0 -1
  155. package/src/lib/app-utils/campaign-touch.js.map +0 -1
  156. package/src/lib/app-utils/console-record-links.d.ts +0 -84
  157. package/src/lib/app-utils/console-record-links.js +0 -76
  158. package/src/lib/app-utils/console-record-links.js.map +0 -1
  159. package/src/lib/app-utils/crm-booking.d.ts +0 -75
  160. package/src/lib/app-utils/crm-booking.js +0 -133
  161. package/src/lib/app-utils/crm-booking.js.map +0 -1
  162. package/src/lib/app-utils/crm-inbound.d.ts +0 -245
  163. package/src/lib/app-utils/crm-inbound.js +0 -407
  164. package/src/lib/app-utils/crm-inbound.js.map +0 -1
  165. package/src/lib/app-utils/crm-reports.d.ts +0 -479
  166. package/src/lib/app-utils/crm-reports.js +0 -680
  167. package/src/lib/app-utils/crm-reports.js.map +0 -1
  168. package/src/lib/app-utils/dataset-csv.js.map +0 -1
  169. package/src/lib/app-utils/dataset-query.d.ts +0 -57
  170. package/src/lib/app-utils/dataset-query.js.map +0 -1
  171. package/src/lib/app-utils/email-topics.js.map +0 -1
  172. package/src/lib/app-utils/list-assignment-policy.js.map +0 -1
  173. package/src/lib/plugin-manager/plugin-metered-lines.d.ts +0 -97
  174. package/src/lib/plugin-manager/plugin-metered-lines.js +0 -123
  175. package/src/lib/plugin-manager/plugin-metered-lines.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/crm.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/**\n * Contacts CRM v2 (AGL-2595): the records that sit BESIDE a contact.\n *\n * v1 is one collection — `contacts` — and everything a holder knows about a\n * person lives in that holder's facet on the shared row. That shape is right\n * for the person and wrong for everything a sales team keeps AROUND the\n * person: a company is known by several contacts, a deal moves through\n * stages on its own clock, a task is due whether or not anybody opens the\n * contact it hangs off, and an activity log grows past anything a facet\n * should carry. So each of those is a collection of its own under\n * `orgs/{orgId}/`, pointing back at the contact by id.\n *\n * ## One scope model, not a second one\n *\n * Every collection here carries `visibleTo`, the same array `contacts`\n * carries, and is stamped the same way — {@link crmScopeTokens} is the\n * contact create path's expression, exported so that no creator computes its\n * own. The rules gate all six on the contacts predicate (`canReadScopedPeople`,\n * `data.manage`), because a deal or a call log is a fact about a person and\n * discloses exactly what the contact row would.\n *\n * What these collections do NOT have is the facet map. A contact is shared\n * across holders because one human is one row; a company, deal or task is\n * one holder's record from the moment it is created, so `visibleTo` alone\n * says who may see it and there is nothing to split per holder.\n *\n * Pure data module: types, constants and the small helpers every surface\n * would otherwise write for itself. No Firestore, no React.\n */\n\nimport {\n type AglynPostalAddress,\n normalizeAddress,\n normalizePhone,\n type OrgCrmAssignmentRule,\n} from '../foundation'\nimport { type ConsentGroup, consentGroupScope } from './consent-groups'\nimport {\n CONTACT_LIFECYCLE_STAGES,\n type ContactLifecycleStage,\n type CrmActivityKind,\n type CrmTaskKind,\n isContactLifecycleStage,\n} from './crm-kinds'\nimport {\n CONTACT_ALTERNATE_EMAILS_FIELD,\n CONTACT_FACETS_FIELD,\n CONTACT_SOURCE_LABELS,\n type ContactInteraction,\n type ContactSegment,\n type ContactSource,\n normalizeContactEmail,\n readContactFacet,\n} from './contacts'\nimport { type EmailState, isEmailStateStatus } from './email-state'\nimport {\n NAME_TOKEN_MAX_PREFIX,\n nameSearchKey,\n nameSearchTokens,\n SCOPED_SEARCH_JOIN,\n scopedSearchTokens,\n} from './name-search'\nimport { MAX_SCOPE_HOSTS, ORG_SCOPE_TOKEN, type ScopeToken } from './scope-tokens'\n\n// The fixed vocabularies and their guards live in a leaf module; every name\n// stays importable from here.\nexport * from './crm-kinds'\n\n/**\n * The CRM's collections, every one under `orgs/{orgId}/`.\n *\n * Named here rather than spelled at each call site because the rules, the\n * indexes and the console must agree on the string, and the three prefixed\n * ones are prefixed on purpose: `tasks`, `activities` and `views` are words\n * the org document will want for something else one day, and a collection\n * name is persisted in every document path that uses it.\n */\nexport const CRM_COLLECTIONS = {\n companies: 'companies',\n pipelines: 'pipelines',\n deals: 'deals',\n tasks: 'crmTasks',\n activities: 'crmActivities',\n contactFields: 'contactFields',\n views: 'crmViews',\n emailTemplates: 'crmEmailTemplates',\n /** An org's value sets for its standard picklist fields (AGL-3298). */\n picklists: 'crmPicklists',\n} as const\n\nexport type CrmCollection = (typeof CRM_COLLECTIONS)[keyof typeof CRM_COLLECTIONS]\n\n/**\n * The three collections the CRM RECORDS band counts (AGL-2611), in the order\n * the billing caption lists them. `contacts` is not in `CRM_COLLECTIONS`\n * because it predates the hub and is addressed through `orgDataCollection`,\n * so the band's own list has to name all three itself. Tasks, pipelines,\n * activities and field definitions are deliberately absent: a band that\n * counted what a rep does every hour would price the team's effort, not the\n * audience it holds.\n */\nexport const CRM_RECORD_COLLECTIONS = [\n 'contacts',\n CRM_COLLECTIONS.companies,\n CRM_COLLECTIONS.deals,\n] as const\n\n/**\n * What a create says when the records band refused it, on every surface —\n * the contacts list's alert, the company and deal drawers, the plugin's\n * create routes and the lead conversion (AGL-2596, widened in AGL-2611).\n *\n * One sentence in one place, because a reader who is refused in the drawer\n * and then reads the list must be told the same thing, and the remedy is the\n * same wherever the refusal lands: a band refuses only on a plan that puts\n * no overage rate past it, so \"upgrade\" is the whole of the answer.\n */\nexport const CRM_RECORDS_BAND_FULL_MESSAGE =\n 'CRM records limit reached — this record was not added. Upgrade in ' +\n 'Billing to keep collecting.'\n\n/**\n * The most logged activities ONE record may carry (AGL-2611) — a contact's,\n * a company's or a deal's own log, counted on the link the activity was\n * filed under.\n *\n * A platform ceiling and not a plan dimension, in the family of\n * `WEBHOOK_MAX_PER_HOST` and `NON_PAGE_SCREEN_MAX_PER_HOST`: activities are\n * not in the records band because they are bounded by human effort, and\n * this is the bound. Five thousand is a call a day for fourteen years on one\n * person, so nobody working a real relationship reaches it — what does is\n * an automation logging on every event, or an import replaying a history,\n * and either of those past this line is a document cost with no reader.\n *\n * Enforced where an activity is written: the console's log dialog, the\n * `logCrmActivity` automation step and `POST /v1/activities`, each with one\n * aggregate read on the record's link before the create. The timeline reads\n * a hundred at a time and is untouched by the number.\n */\nexport const CRM_ACTIVITIES_PER_RECORD_CEILING = 5_000\n\n/**\n * What every writer says when a record's log is full — the console's log\n * dialog, the automation step's run history and `POST /v1/activities`.\n */\nexport const CRM_ACTIVITY_LOG_FULL_MESSAGE =\n `This record already has ${CRM_ACTIVITIES_PER_RECORD_CEILING.toLocaleString(\n 'en-US',\n )} activities, which is the most one record can carry.`\n\n/** The record an activity is filed under: its links, of which one leads. */\nexport interface CrmActivityLink {\n contactId?: string | null\n companyId?: string | null\n dealId?: string | null\n /**\n * `hosts/{hostId}/leads/{leadId}` — a person not yet converted (AGL-2615).\n * A lead is host-scoped by path and carries no `visibleTo` of its own, so\n * an activity filed under one is stamped with the site's scope like any\n * other record created from that site.\n */\n leadId?: string | null\n}\n\n/**\n * The field the per-record activity ceiling is counted on, or `null` for an\n * activity that names no record at all.\n *\n * The contact leads, then the company, then the deal: an activity logged\n * from a contact's page carries the company beside it (the automation step\n * copies the facet's `companyId` onto every record it creates), and a\n * ceiling counted on the company would let one busy account exhaust every\n * contact filed under it. The record whose PAGE the log is read on is the\n * one whose log has the limit. Shared by the client dialog and the two\n * server writers so they count the same thing.\n */\nexport function crmActivityCeilingLink(\n link: CrmActivityLink,\n): { field: 'contactId' | 'companyId' | 'dealId' | 'leadId'; id: string } | null {\n if (link.contactId) return { field: 'contactId', id: String(link.contactId) }\n if (link.companyId) return { field: 'companyId', id: String(link.companyId) }\n if (link.dealId) return { field: 'dealId', id: String(link.dealId) }\n // Last, because a converted lead's activities carry the contact beside it\n // and the contact is the record whose page they are read on.\n if (link.leadId) return { field: 'leadId', id: String(link.leadId) }\n return null\n}\n\n/**\n * Whether ONE MORE activity fits under `CRM_ACTIVITIES_PER_RECORD_CEILING`.\n * Both halves of the boundary live here so no writer re-derives `>=`.\n */\nexport function crmActivityLogHasRoom(existing: number): boolean {\n const count = Number(existing)\n return !(Number.isFinite(count) && count >= CRM_ACTIVITIES_PER_RECORD_CEILING)\n}\n\n/** How a lifecycle stage reads on screen — typed so a stage cannot ship unlabeled. */\nexport const CONTACT_LIFECYCLE_STAGE_LABELS: Record<ContactLifecycleStage, string> = {\n subscriber: 'Subscriber',\n lead: 'Lead',\n 'marketing-qualified': 'Marketing qualified',\n 'sales-qualified': 'Sales qualified',\n opportunity: 'Opportunity',\n customer: 'Customer',\n evangelist: 'Evangelist',\n other: 'Other',\n}\n\n/**\n * The stage a person is in once a capture has happened that implies at least\n * `floor` (AGL-2612).\n *\n * The one ordering rule every capture door shares: a door names the EARLIEST\n * stage that describes what just happened — a form submission is a lead, a\n * newsletter opt-in is a subscriber, a purchase is a customer — and the\n * result is that stage for a person who had none or an earlier one, and the\n * stage they already had otherwise. \"Never downgrades\" is the whole contract:\n * a customer who fills in a contact form is still a customer, and `other` —\n * the deliberate stage a business picked for a funnel step none of the names\n * fit — sits after `customer` in the list precisely so no capture can\n * overwrite it. A stored value that is not a stage at all reads as absent,\n * because a capture door is not the place to preserve a typo.\n *\n * With no floor the answer is the current stage as it stands, or `undefined`\n * for one that is absent or unusable — so a writer can apply this\n * unconditionally and write only what comes back.\n */\nexport function advanceContactLifecycleStage(\n current: unknown,\n floor: ContactLifecycleStage | undefined,\n): ContactLifecycleStage | undefined {\n const held = isContactLifecycleStage(current) ? current : undefined\n if (!floor) return held\n if (!held) return floor\n const order: readonly string[] = CONTACT_LIFECYCLE_STAGES\n return order.indexOf(held) < order.indexOf(floor) ? floor : held\n}\n\n/**\n * The stage a person is in once they have BOUGHT something (AGL-2596).\n *\n * `customer` when they had no stage or an earlier one; whatever they already\n * had otherwise — {@link advanceContactLifecycleStage} with `customer` as the\n * floor, kept under its own name because \"after a purchase\" is the question\n * the order paths and the reports ask.\n */\nexport function contactLifecycleStageAfterPurchase(\n current: unknown,\n): ContactLifecycleStage {\n return advanceContactLifecycleStage(current, 'customer') ?? 'customer'\n}\n\n/**\n * What a custom contact field may hold.\n *\n * Scalars only. A field is something a merchant filters and exports on, and\n * a nested value is neither; `null` is the explicit \"cleared\" that a form\n * writes so the key stays present for a `where` clause to find.\n */\nexport type ContactCustomValue = string | number | boolean | null\n\n/**\n * The same scalar, under the name the other objects use (AGL-2661).\n *\n * A company's and a deal's `custom` map hold exactly what a contact's\n * does — one definition, one coercion rule, one stored shape — so the\n * value type is one type with two names: the contact name it was born\n * with, and this one for code that is not about contacts.\n */\nexport type CrmCustomValue = ContactCustomValue\n\n/**\n * The fields every CRM document carries.\n *\n * `visibleTo` is the scope both enforcement layers evaluate — see\n * `scope-tokens.ts` for why an absent one is seen by NOBODY. `hostId` is\n * provenance: the site whose console created the record, never rewritten,\n * so a report can say where a deal came from after the scope has widened.\n * The timestamps are `unknown` because a client write carries a `Date` and a\n * read hands back a Firestore `Timestamp`; callers narrow at the edge.\n */\nexport interface CrmScoped {\n visibleTo: string[]\n /** The site that created it. */\n hostId: string\n createdAt?: unknown\n updatedAt?: unknown\n}\n\n/** `orgs/{orgId}/companies/{companyId}`. */\nexport interface CrmCompany extends CrmScoped {\n name: string\n /** `nameSearchFields` twins, for the prefix search the list runs. */\n nameLower?: string\n nameTokens?: string[]\n /** Lowercase hostname, no protocol — see {@link normalizeCompanyDomain}. */\n domain?: string\n website?: string\n /** E.164 — `normalizePhone` before writing. */\n phone?: string\n address?: AglynPostalAddress | null\n industry?: string\n ownerUid?: string\n notes?: string\n /**\n * Lowercased, deduplicated, capped at twenty — the same shape a contact's\n * tags take, so a bulk \"Add tag\" over companies and one over contacts\n * write the same kind of value (AGL-2621).\n */\n tags?: string[]\n createdByUid?: string\n /**\n * How many contacts name this company in their {@link CONTACT_COMPANY_IDS_FIELD}\n * mirror — see {@link COMPANY_CONTACTS_COUNT_FIELD}. Absent on a company\n * nobody has linked since the counter existed, which reads as zero.\n */\n contactsCount?: number\n /**\n * Custom field values, keyed by the key of a definition whose `object`\n * is `company` — see {@link fieldDefinitionObject} (AGL-2661).\n */\n custom?: Record<string, CrmCustomValue>\n /**\n * When the earliest OPEN task filed against this company is due, epoch\n * ms, or `null` when none is — see {@link CrmDeal.nextTaskAtMs}.\n */\n nextTaskAtMs?: number | null\n /** Org-library files attached to the company (AGL-2662) — see {@link CRM_MEDIA_IDS_MAX}. */\n mediaIds?: string[]\n}\n\n/** One step of a pipeline. */\nexport interface CrmDealStage {\n id: string\n name: string\n /** Position in the pipeline, ascending. */\n order: number\n /** Chance of closing from here, 0–100 — what a weighted forecast multiplies by. */\n probability: number\n /**\n * Whether landing in this stage closes the deal. A pipeline has exactly one\n * `won` and one `lost` stage in the default set; `open` is everything in\n * between.\n */\n kind: 'open' | 'won' | 'lost'\n}\n\n/** `orgs/{orgId}/pipelines/{pipelineId}`. */\nexport interface CrmPipeline extends CrmScoped {\n name: string\n stages: CrmDealStage[]\n /** The pipeline a new deal lands in when nobody picks one. */\n isDefault?: boolean\n /**\n * When the pipeline was retired, epoch ms. An archived pipeline takes no\n * new deal and is offered by no picker, but it is never deleted: the deals\n * it closed still name it, and a report that could not resolve their\n * stages would forecast them as orphans. Absent or `null` while active.\n */\n archivedAt?: number | null\n}\n\n/** Whether a pipeline has been retired — see {@link CrmPipeline.archivedAt}. */\nexport function isPipelineArchived(\n pipeline: Pick<CrmPipeline, 'archivedAt'> | null | undefined,\n): boolean {\n return typeof pipeline?.archivedAt === 'number' && pipeline.archivedAt > 0\n}\n\n/**\n * The stages a fresh pipeline starts with.\n *\n * Readonly on purpose: a pipeline document stores its own COPY (`[...]`), so\n * a merchant editing their stages must not be editing the module's default,\n * and a second pipeline seeded later must start from the original set.\n */\nexport const DEFAULT_DEAL_STAGES: readonly CrmDealStage[] = [\n { id: 'qualified', name: 'Qualified', order: 0, probability: 10, kind: 'open' },\n { id: 'contact-made', name: 'Contact made', order: 1, probability: 20, kind: 'open' },\n { id: 'proposal-sent', name: 'Proposal sent', order: 2, probability: 40, kind: 'open' },\n { id: 'negotiation', name: 'Negotiation', order: 3, probability: 60, kind: 'open' },\n { id: 'won', name: 'Won', order: 4, probability: 100, kind: 'won' },\n { id: 'lost', name: 'Lost', order: 5, probability: 0, kind: 'lost' },\n]\n\nexport type CrmDealStatus = 'open' | 'won' | 'lost'\n\n/**\n * One product on a deal — what is being sold, how many, at what.\n *\n * `productId` names a catalog product when the line came from one; a line\n * typed by hand has none. The name is copied rather than joined, the way a\n * deal copies its contact's name: a catalog product can be renamed or\n * deleted after the deal was priced, and the deal has to keep saying what\n * it was for. `currency` is the deal's — every line on a deal is in one\n * currency, because their sum is the deal's amount and a sum across\n * currencies is a number with no unit.\n */\nexport interface CrmDealLineItem {\n productId?: string\n name: string\n /** A whole number of units, one or more. */\n quantity: number\n /** Per unit, in the currency's minor unit, zero or more. */\n unitAmountCents: number\n /** Lowercase ISO 4217. */\n currency: string\n}\n\n/** The most lines one deal carries — a quote, not a catalog. */\nexport const DEAL_LINE_ITEMS_MAX = 50\nexport const DEAL_LINE_ITEM_NAME_MAX = 120\n/** Units per line; past this the number is a data-entry slip. */\nexport const DEAL_LINE_ITEM_QUANTITY_MAX = 1_000_000\n\n/**\n * `orgs/{orgId}/deals/{dealId}`.\n *\n * A WON DEAL MAKES ITS CONTACT A CUSTOMER (AGL-2641). A win is the same\n * fact as a purchase — the business has decided this person bought — and an\n * order already floors a contact's lifecycle stage at `customer` on\n * capture. So every writer of `status` — the console's stage route at both\n * levels and the REST resource — applies the same floor to the contact\n * `contactId` names on the transition into `won`, in the facet of the site\n * the deal was made on (`hostId`), with `advanceContactLifecycleStage`'s\n * rule: an empty stage is filled, an earlier one advanced, and nobody is\n * ever moved back. Behind no setting, because a won deal is a customer by\n * definition; an automation on `dealWon` is for what happens NEXT.\n */\nexport interface CrmDeal extends CrmScoped {\n title: string\n titleLower?: string\n pipelineId: string\n stageId: string\n /**\n * Denormalized from the stage's `kind` at every stage move, because it is\n * what the list filters and the indexes sort on — a query cannot join the\n * pipeline to ask what the stage means.\n */\n status: CrmDealStatus\n /**\n * What the deal is worth. Typed by hand on a deal with no line items;\n * on a deal WITH them it is their sum, stored beside them by every\n * writer (`lineItemsTotalCents`), because the board, the reports and the\n * REST list all read this one field and none of them can afford to add\n * up fifty lines per row. A deal with line items refuses a typed amount.\n */\n amountCents?: number\n /** Lowercase ISO 4217; `'usd'` when absent. */\n currency?: string\n /** The products behind the amount — see {@link CrmDealLineItem}. */\n lineItems?: CrmDealLineItem[]\n expectedCloseAtMs?: number | null\n closedAtMs?: number | null\n /** When the deal last moved — what \"stuck in stage\" reports read. */\n stageChangedAtMs?: number\n ownerUid?: string\n contactId?: string\n companyId?: string\n lostReason?: string\n notes?: string\n createdByUid?: string\n /**\n * Custom field values, keyed by the key of a definition whose `object`\n * is `deal` — see {@link fieldDefinitionObject} (AGL-2661).\n */\n custom?: Record<string, CrmCustomValue>\n /**\n * When the earliest OPEN task filed against this deal is due, epoch ms,\n * or `null` when no open task names it (AGL-2661).\n *\n * DENORMALIZED from `crmTasks`, because the question \"which deals have\n * nothing scheduled\" is asked of a LIST — a column, a filter, a report\n * tile — and a list cannot afford a task query per row. Every server\n * writer of a task recomputes it for the records the task names\n * (`recomputeCrmNextTaskAt` in the admin library), and a client-direct\n * task write asks the console route to do the same; the Fields section\n * offers a one-off recompute for the records written before the field\n * existed. Absent on such a record, which every reader treats as `null`.\n */\n nextTaskAtMs?: number | null\n /** Org-library files attached to the deal (AGL-2662) — see {@link CRM_MEDIA_IDS_MAX}. */\n mediaIds?: string[]\n}\n\nexport const CRM_TASK_KIND_LABELS: Record<CrmTaskKind, string> = {\n call: 'Call',\n email: 'Email',\n meeting: 'Meeting',\n todo: 'To-do',\n}\n\nexport type CrmTaskPriority = 'low' | 'normal' | 'high'\nexport type CrmTaskStatus = 'open' | 'done'\n\n/**\n * `orgs/{orgId}/crmTasks/{taskId}`.\n *\n * The one CRM record that may belong to NO site (AGL-2637): a task filed\n * from the organization's own hub carries `hostId: null` and the org scope\n * token alone, because a to-do owed by the organization — renew the\n * insurance, chase the agency's own invoice — is not captured by any brand.\n * Every other record is a fact about a person some site met, so\n * `CrmScoped` keeps its site required.\n */\nexport interface CrmTask extends Omit<CrmScoped, 'hostId'> {\n /** The site that created it, or `null` for the organization's own task. */\n hostId: string | null\n title: string\n notes?: string\n kind: CrmTaskKind\n priority: CrmTaskPriority\n status: CrmTaskStatus\n dueAtMs?: number | null\n completedAtMs?: number | null\n /**\n * Who ticked it off, which is not always the assignee: a manager closing\n * out a departed teammate's list completes tasks that were never theirs.\n * Stamped by the `crm/task-complete` route beside `completedAtMs`.\n */\n completedByUid?: string\n assigneeUid?: string\n /**\n * The person who made it, or `''` when no person did.\n *\n * An automation has no uid, and inventing one — the action's id, a\n * sentinel — would put a value into a field every reader resolves as a\n * member. The empty string says \"nobody\", and {@link sourceActionId}\n * beside it says what.\n */\n createdByUid: string\n /** The automation that created it (AGL-2605), when a person did not. */\n sourceActionId?: string\n /**\n * The plugin that filed it on the record-timeline seam (AGL-2981) — a\n * sequence's call step, a reply to answer — when no person did.\n */\n sourcePluginId?: string\n contactId?: string\n companyId?: string\n dealId?: string\n /**\n * `hosts/{hostId}/leads/{leadId}` — a task filed on a lead not yet\n * converted (AGL-3233): a sequence's call step or a reply to answer. The\n * conversion stamps the contact beside it, so the task is the contact's\n * from then on and the lead's page still lists it.\n */\n leadId?: string\n /**\n * When the assignee is reminded (AGL-2659): the due time unless a person\n * moved it, `null` for no reminder. The hourly `/api/crm/task-reminders`\n * runner reads every open task whose reminder has come due, so a task\n * that should never remind carries `null` rather than no field — a\n * missing key and a null both fall outside the range, but the null says\n * it was decided.\n */\n remindAtMs?: number | null\n /**\n * When the runner handled the reminder — sent it, or found nobody to\n * send it to. Absent while the reminder is still owed, which is how a\n * rerun over the same hour sends nothing twice; cleared again when the\n * reminder moves, because a moved reminder is a new one.\n */\n reminderSentAtMs?: number\n}\n\n/** How an activity kind reads on screen — typed so a kind cannot ship unlabeled. */\nexport const CRM_ACTIVITY_KIND_LABELS: Record<CrmActivityKind, string> = {\n call: 'Call',\n email: 'Email',\n meeting: 'Meeting',\n note: 'Note',\n other: 'Other',\n}\n\n/**\n * Whether a kind takes an outcome and a duration.\n *\n * A call and a meeting are conversations: they end somewhere (\"left a\n * voicemail\", \"agreed to a trial\") and they take a measurable amount of\n * time, and both are what a manager reading the log wants to know. An email\n * has neither in any useful sense, a note is not an event at all, and\n * `other` is unknowable — so the dialog hides the two fields for those\n * rather than offering boxes that mean nothing.\n */\nexport function activityKindHasOutcome(kind: CrmActivityKind): boolean {\n return kind === 'call' || kind === 'meeting'\n}\n\n/**\n * Where a one-to-one email got to (AGL-2615), in the order it normally\n * happens — the vocabulary the delivery webhook maps its events onto an\n * `email` activity in, and the chip the timeline shows beside the entry.\n *\n * The first four are a progression: a message is sent, then delivered, then\n * opened, then clicked, and a later state implies the earlier ones. The last\n * two are terminal failures. A message the mailbox provider bounced or that\n * the recipient reported is never \"opened\" in any sense the timeline should\n * report, whatever a tracking pixel says afterwards.\n */\nexport const CRM_EMAIL_DELIVERY_STATES = [\n 'sent',\n 'delivered',\n 'opened',\n 'clicked',\n 'bounced',\n 'complained',\n] as const\n\nexport type CrmEmailDeliveryState = (typeof CRM_EMAIL_DELIVERY_STATES)[number]\n\n/** How a delivery state reads on the chip — typed so a state cannot ship unlabeled. */\nexport const CRM_EMAIL_DELIVERY_STATE_LABELS: Record<CrmEmailDeliveryState, string> = {\n sent: 'Sent',\n delivered: 'Delivered',\n opened: 'Opened',\n clicked: 'Clicked',\n bounced: 'Bounced',\n complained: 'Marked as spam',\n}\n\nexport function isCrmEmailDeliveryState(\n value: unknown,\n): value is CrmEmailDeliveryState {\n return (\n typeof value === 'string' &&\n (CRM_EMAIL_DELIVERY_STATES as readonly string[]).includes(value)\n )\n}\n\n/** The two states that mean the message did not land. */\nexport function isCrmEmailDeliveryFailure(state: unknown): boolean {\n return state === 'bounced' || state === 'complained'\n}\n\n/**\n * The rank the webhook advances by. Higher wins; a failure outranks every\n * progression state, and a complaint outranks a bounce because it is the\n * one a sender is scored on.\n */\nconst CRM_EMAIL_DELIVERY_RANK: Record<CrmEmailDeliveryState, number> = {\n sent: 1,\n delivered: 2,\n opened: 3,\n clicked: 4,\n bounced: 5,\n complained: 6,\n}\n\n/**\n * The state an activity holds after one more delivery event, from the state\n * it held before.\n *\n * MONOTONIC. Provider events arrive at least once and in no promised order\n * — an `opened` can reach the webhook before the `delivered` it implies, and\n * a replay can hand back yesterday's `delivered` after today's `clicked` —\n * so the row keeps whichever state is further along, and an event that says\n * less than the row already knows changes nothing. A stored value the\n * vocabulary does not name reads as nothing, so the incoming event stands.\n */\nexport function nextCrmEmailDeliveryState(\n current: unknown,\n incoming: CrmEmailDeliveryState,\n): CrmEmailDeliveryState {\n if (!isCrmEmailDeliveryState(current)) return incoming\n return CRM_EMAIL_DELIVERY_RANK[incoming] > CRM_EMAIL_DELIVERY_RANK[current]\n ? incoming\n : current\n}\n\n/** The most a one-to-one email's subject may hold. */\nexport const CRM_EMAIL_SUBJECT_MAX = 200\n/** The most a one-to-one email's body may hold — a letter, not a document. */\nexport const CRM_EMAIL_BODY_MAX = 10_000\n\n/**\n * The `context` a one-to-one email is sent under, which `sendEmail` stamps\n * as a provider tag on the message and the delivery log files it by.\n */\nexport const CRM_EMAIL_CONTEXT = 'crm'\n/** The provider tag naming the activity row a one-to-one email belongs to. */\nexport const CRM_EMAIL_ACTIVITY_TAG = 'activityId'\n/** The provider tag naming the org whose `crmActivities` holds that row. */\nexport const CRM_EMAIL_ORG_TAG = 'orgId'\n\n/** What a provider tag value may be — anything else fails the whole send. */\nconst PROVIDER_TAG_VALUE = /^[A-Za-z0-9_-]{1,256}$/\n\n/**\n * The tags a one-to-one email carries so the delivery webhook can find its\n * activity row (AGL-2615): the org, the activity, and the site for the\n * per-site suppression list a bounce lands on.\n *\n * Every value is checked against the provider's alphabet rather than\n * trusted, for the reason `contextTag` gives — a value the provider rejects\n * fails the send, and a failed send is worse than an untracked one. The\n * org and the activity are the pair the webhook needs, so an unusable\n * value for EITHER yields no tags at all: a tag set that named a row it\n * could not locate would be a promise the timeline cannot keep. The site\n * is stamped when it can be and dropped alone when it cannot.\n */\nexport function crmEmailDeliveryTags(input: {\n orgId: string\n hostId: string\n activityId: string\n}): { name: string; value: string }[] {\n const orgId = String(input.orgId ?? '')\n const activityId = String(input.activityId ?? '')\n const hostId = String(input.hostId ?? '')\n if (!PROVIDER_TAG_VALUE.test(orgId) || !PROVIDER_TAG_VALUE.test(activityId)) {\n return []\n }\n return [\n { name: CRM_EMAIL_ORG_TAG, value: orgId },\n { name: CRM_EMAIL_ACTIVITY_TAG, value: activityId },\n ...(PROVIDER_TAG_VALUE.test(hostId) ? [{ name: 'hostId', value: hostId }] : []),\n ]\n}\n\n/**\n * Which way an `email` activity's message traveled. `outbound` is every\n * message the workspace wrote — sent by the platform, or copied to the\n * capture address from a mailbox; `inbound` is one a correspondent wrote,\n * which only the capture route files (AGL-2657).\n */\nexport type CrmEmailDirection = 'outbound' | 'inbound'\n\n/**\n * `orgs/{orgId}/crmActivities/{activityId}` — one thing that happened.\n *\n * Distinct from a contact's `interactions`: those are what the PLATFORM\n * recorded (a form, an order, a booking) and live capped on the row. An\n * activity is what a PERSON logged — a call made, a meeting held — and can\n * hang off a company or a deal with no contact at all.\n */\nexport interface CrmActivity extends CrmScoped {\n kind: CrmActivityKind\n body: string\n /** When it happened, which is not when it was logged. */\n atMs: number\n /** Who logged it, or `''` for an automation — see `CrmTask.createdByUid`. */\n byUid: string\n /**\n * The author's display name as it read when the activity was logged.\n *\n * Denormalized because there is no lookup that could answer it later: a\n * member document is readable by its own subject and by org-wide members\n * only, so a scoped editor reading a colleague's call log could not\n * resolve the `byUid` beside it into a name. Stamped from the signed-in\n * user's resolved name at log time and never rewritten, so it can drift\n * from a later rename — the way a signed letter keeps the name it was\n * signed with.\n */\n byName?: string\n /** The automation that logged it (AGL-2605), when a person did not. */\n sourceActionId?: string\n /**\n * The plugin that filed it on the record-timeline seam (AGL-2981) — an\n * email a sequence sent, a reply it read — or the CRM's own id on an\n * entry it wrote about a filing (AGL-3274), so a reader can tell the\n * bookkeeping entries from what a person logged.\n */\n sourcePluginId?: string\n /**\n * The campaign a filing entry is about (AGL-3274): \"Filed under\" and\n * \"Removed from\" name it in the body and carry the container's id here,\n * so a campaign's own page can one day list what happened under it\n * without parsing a sentence.\n */\n campaignId?: string\n contactId?: string\n companyId?: string\n dealId?: string\n /** The lead it was filed under (AGL-2615) — see {@link CrmActivityLink.leadId}. */\n leadId?: string\n outcome?: string\n durationMinutes?: number\n /**\n * An `email` the platform SENT (AGL-2615), as against one a person logged\n * by hand: the subject line, who it went to, and where delivery got to.\n * Absent on a hand-logged email, which has a body and nothing else.\n */\n subject?: string\n /** The address the message left for. */\n to?: string\n /**\n * `outbound` for a message the platform sent or a teammate copied to the\n * capture address; `inbound` for one a correspondent wrote (AGL-2657).\n */\n direction?: CrmEmailDirection\n /** See {@link CrmEmailDeliveryState}; advanced by the delivery webhook. */\n deliveryState?: CrmEmailDeliveryState\n /** When the delivery state last moved, epoch ms. */\n deliveryAtMs?: number\n /**\n * What the receiving server said when the state moved to a failure\n * (AGL-3245): a bounce's diagnostic, scrubbed of the address. Absent on a\n * progression state.\n */\n deliveryDetail?: string\n /**\n * `hosts/{hostId}/bookings/{bookingId}` — the booking a `meeting` was\n * filed from (AGL-2660), so a row the Bookings plugin wrote can be told\n * from one a person logged, and a booking is never filed twice.\n */\n bookingId?: string\n /**\n * An email CAPTURED from a mailbox (AGL-2657) — forwarded or copied to\n * the workspace's capture address — as against one the platform sent:\n * who wrote it, the provider's `Message-ID` the row is deduplicated by,\n * the message it answered, and the subject with its reply and forward\n * prefixes removed, which is what groups a thread. Absent on every other\n * email row.\n */\n from?: string\n messageId?: string\n inReplyTo?: string\n threadSubject?: string\n}\n\n/** An activity as a listener hands it back: the document plus its id. */\nexport type CrmActivityRow = CrmActivity & { $id: string }\n\n/** What a sent one-to-one email is logged from — see {@link buildCrmEmailActivity}. */\nexport interface CrmEmailActivityInput {\n subject: string\n body: string\n /** The recipient, as the message was addressed. */\n to: string\n /** When it was sent. */\n atMs: number\n /** Who sent it, or `''` for an automation — see `CrmActivity.byUid`. */\n byUid: string\n byName?: string\n sourceActionId?: string\n link: CrmActivityLink\n hostId: string\n visibleTo: string[]\n}\n\n/**\n * The activity row a sent one-to-one email is logged as (AGL-2615).\n *\n * ONE builder for the two writers — the console's send route and the\n * `sendEmail` automation step — so a message a rep wrote and a message a\n * flow sent are the same kind of entry on the timeline: `kind: 'email'`,\n * outbound, starting at `sent` for the delivery webhook to advance. A\n * writer that assembled its own would be the one whose rows the chip did\n * not know how to read. Timestamps are the writer's: a server stamps\n * `serverTimestamp()` and this module has no Firestore.\n */\nexport function buildCrmEmailActivity(input: CrmEmailActivityInput): CrmActivity {\n const { link } = input\n return {\n kind: 'email',\n subject: String(input.subject ?? '').slice(0, CRM_EMAIL_SUBJECT_MAX),\n body: String(input.body ?? '').slice(0, CRM_EMAIL_BODY_MAX),\n to: input.to,\n direction: 'outbound',\n deliveryState: 'sent',\n deliveryAtMs: input.atMs,\n atMs: input.atMs,\n byUid: input.byUid,\n ...(input.byName ? { byName: input.byName } : {}),\n ...(input.sourceActionId ? { sourceActionId: input.sourceActionId } : {}),\n // Only the links the caller fixed: a key with no value is `undefined`,\n // which Firestore refuses.\n ...(link.contactId ? { contactId: String(link.contactId) } : {}),\n ...(link.companyId ? { companyId: String(link.companyId) } : {}),\n ...(link.dealId ? { dealId: String(link.dealId) } : {}),\n ...(link.leadId ? { leadId: String(link.leadId) } : {}),\n hostId: input.hostId,\n visibleTo: [...input.visibleTo],\n }\n}\n\n/**\n * One campaign email this person was sent, and what became of it (AGL-2616).\n *\n * The per-recipient delivery log — `emailDeliveries/{key}/messages` — keeps\n * one document per MESSAGE with a timestamp per lifecycle state and a count\n * of opens and clicks, and this is that document as a timeline reads it:\n * which email, when it went out, and how far it got. A message that bounced\n * or was complained about carries that state too, because \"we mailed them\n * and it bounced\" is part of the history of a relationship and a timeline\n * that showed only the successes would read as if nothing had been tried.\n *\n * The server answers with this shape, never the log record itself: the log\n * carries the recipient's address, the provider and the links they followed,\n * and none of that is the timeline's to show.\n */\nexport interface ContactCampaignEmail {\n /** The provider's message id — distinct per send, and the entry's key. */\n messageId: string\n /** The site the campaign went out from — the send's `hostId`. */\n hostId: string\n /** `orgs/{orgId}/campaigns/{campaignId}` — the email, whose report the entry links to. */\n campaignId: string\n /**\n * The email as the team named it in the Emails console — its display name,\n * or its subject when it has none. `null` when the email has since been\n * deleted, in which case the entry still has the subject it was sent with.\n */\n campaignName: string | null\n /** The subject line the person received. */\n subject: string | null\n /** When it went out — the provider's `sent` instant, or the first event seen. */\n sentAtMs: number\n deliveredAtMs?: number\n openedAtMs?: number\n clickedAtMs?: number\n bouncedAtMs?: number\n complainedAtMs?: number\n openCount: number\n clickCount: number\n}\n\n/**\n * One entry of a contact's timeline (AGL-2600): something the platform\n * CAPTURED on the contact's facet, something a person LOGGED beside it, or\n * — since AGL-2616 — a CAMPAIGN email the person was sent.\n *\n * A tagged union rather than a flattened row, because the three are\n * different facts with different affordances — a captured interaction is\n * read-only and names the door it came through, a logged activity has an\n * author who may edit it, a campaign email is read-only and links to the\n * campaign's report — and a surface drawing the stream has to say which is\n * which. A logged activity of kind `email` and a campaign entry are two\n * different things on purpose: the first is a message a person on the team\n * sent or recorded, the second is a mailing the platform delivered, and the\n * two must never share a kind id or a glyph.\n */\nexport type ContactTimelineEntry =\n | {\n kind: 'captured'\n /** Distinct across the merged list, for a React key. */\n key: string\n atMs: number\n interaction: ContactInteraction\n }\n | {\n kind: 'logged'\n key: string\n atMs: number\n activity: CrmActivityRow\n }\n | {\n kind: 'campaign'\n key: string\n atMs: number\n email: ContactCampaignEmail\n }\n\n/**\n * What became of one campaign email, as the words a timeline row prints\n * after the email's name: `sent · delivered · opened ×2 · clicked`.\n *\n * In lifecycle order, and only the states that happened. `sent` is always\n * first because the row exists; the counts appear only past one, because\n * \"opened ×1\" says nothing \"opened\" does not. A bounce or a complaint is\n * printed where it falls, after whatever succeeded before it — a message\n * delivered and then complained about reads as both, which is what\n * happened.\n */\nexport function campaignEmailSummary(email: ContactCampaignEmail): string[] {\n const counted = (word: string, count: number): string =>\n count > 1 ? `${word} ×${count}` : word\n const parts = ['sent']\n if (email.deliveredAtMs) parts.push('delivered')\n if (email.openedAtMs) parts.push(counted('opened', email.openCount))\n if (email.clickedAtMs) parts.push(counted('clicked', email.clickCount))\n if (email.bouncedAtMs) parts.push('bounced')\n if (email.complainedAtMs) parts.push('marked as spam')\n return parts\n}\n\n/** A time that can be sorted on; anything that is not one sinks to the bottom. */\nconst sortableMs = (value: unknown): number =>\n typeof value === 'number' && Number.isFinite(value)\n ? value\n : Number.NEGATIVE_INFINITY\n\n/**\n * ONE newest-first stream from a contact's two histories.\n *\n * The captured interactions live on the contact's facet and the logged\n * activities in their own collection, and a page showing them as two lists\n * asks the reader to do the interleaving in their head — \"did the call come\n * before or after the order?\" is the question a timeline exists to answer.\n *\n * STABLE on purpose. The sort is on `atMs` alone, and at a tie the captured\n * entries keep their place ahead of the logged ones, each in the order it\n * arrived: a facet's interactions are already newest-first and a listener's\n * rows are already ordered, so the merge must not reshuffle what its inputs\n * settled. An entry with no usable time goes LAST, never first — a row that\n * cannot say when it happened must not read as the most recent thing.\n *\n * The key is what a list renders by. A captured interaction has no id of its\n * own, so its key is built from what it does carry; a logged activity's is\n * its document id; a campaign email's is the provider's message id.\n *\n * A campaign email is placed at the instant it was SENT, not at its latest\n * open. The row tells the whole story of that message in one line, and a\n * message that moved up the stream every time its reader re-opened it would\n * be a timeline that reorders itself under the reader.\n */\nexport function mergeContactTimeline(\n interactions: readonly ContactInteraction[] | null | undefined,\n activities: readonly CrmActivityRow[] | null | undefined,\n campaignEmails?: readonly ContactCampaignEmail[] | null,\n): ContactTimelineEntry[] {\n const entries: ContactTimelineEntry[] = [\n ...(interactions ?? []).map(\n (interaction, index): ContactTimelineEntry => ({\n kind: 'captured',\n key: `captured:${interaction.type}:${interaction.refId ?? index}:${interaction.atMs}:${index}`,\n atMs: interaction.atMs,\n interaction,\n }),\n ),\n ...(activities ?? []).map(\n (activity): ContactTimelineEntry => ({\n kind: 'logged',\n key: `logged:${activity.$id}`,\n atMs: activity.atMs,\n activity,\n }),\n ),\n ...(campaignEmails ?? []).map(\n (email): ContactTimelineEntry => ({\n kind: 'campaign',\n key: `campaign:${email.messageId}`,\n atMs: email.sentAtMs,\n email,\n }),\n ),\n ]\n // `Array.prototype.sort` is stable, which is what keeps the tie rule above\n // true without a secondary comparison on the entry's kind or position.\n return entries.sort((a, b) => sortableMs(b.atMs) - sortableMs(a.atMs))\n}\n\n/**\n * Which record an activity is ABOUT, for a surface that links to it, or\n * `null` when it was filed against nothing.\n *\n * A contact outranks a deal outranks a company. An activity can name all\n * three — a call with a person about a deal at their company — and the\n * contact is the one a reader means when they ask \"who was this with\"; the\n * deal is next because it is the thing with a clock on it; the company is\n * the widest and so the last resort.\n */\nexport function crmActivityRecordLink(\n activity: Pick<CrmActivity, 'contactId' | 'companyId' | 'dealId' | 'leadId'>,\n): { record: 'contact' | 'deal' | 'company' | 'lead'; id: string } | null {\n if (activity.contactId) return { record: 'contact', id: activity.contactId }\n if (activity.dealId) return { record: 'deal', id: activity.dealId }\n if (activity.companyId) return { record: 'company', id: activity.companyId }\n // A lead is the narrowest: once converted, the contact it became leads.\n if (activity.leadId) return { record: 'lead', id: activity.leadId }\n return null\n}\n\nconst MINUTE_MS = 60_000\nconst HOUR_MS = 60 * MINUTE_MS\nconst DAY_MS = 24 * HOUR_MS\n\n/**\n * How long ago something happened, in the words a list row uses.\n *\n * Coarse on purpose. A row reads \"3 days ago\" and carries the full timestamp\n * in its tooltip; the sentence exists so that a reader scanning a log can\n * tell this week from last month without parsing dates. Past a week the\n * relative form stops helping — \"412 days ago\" is a date the reader has to\n * compute — so it becomes one. A time AHEAD of now is a date too: a call\n * logged for tomorrow is a scheduling mistake the page should show plainly\n * rather than dress as \"in 14 hours\". An unusable time reads as nothing.\n *\n * English rather than `Intl.RelativeTimeFormat` because the console is\n * English-only, and the thresholds are what the spec pins.\n */\nexport function activityTimeLabel(atMs: number, nowMs: number): string {\n if (!Number.isFinite(atMs) || !Number.isFinite(nowMs)) return ''\n const elapsed = nowMs - atMs\n if (elapsed < -MINUTE_MS || elapsed >= 7 * DAY_MS) {\n return new Date(atMs).toLocaleDateString()\n }\n if (elapsed < MINUTE_MS) return 'just now'\n if (elapsed < HOUR_MS) return `${Math.floor(elapsed / MINUTE_MS)} min ago`\n if (elapsed < DAY_MS) return `${Math.floor(elapsed / HOUR_MS)} h ago`\n if (elapsed < 2 * DAY_MS) return 'yesterday'\n return `${Math.floor(elapsed / DAY_MS)} days ago`\n}\n\nexport type ContactFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'select'\n | 'checkbox'\n | 'url'\n\n/**\n * `orgs/{orgId}/contactFields/{fieldId}` — a custom field a holder defined.\n *\n * The `key` is the map key under a facet's `custom`, so it is IMMUTABLE once\n * a value has been written under it; a rename is a new field and a retire.\n * `retiredAt` rather than a delete for the same reason: values written under\n * the key survive, and a retired field still has to be able to read them\n * back on an export.\n */\nexport interface ContactFieldDefinition extends CrmScoped {\n /** `^[a-z][a-z0-9_]{0,39}$` — see {@link normalizeContactFieldKey}. */\n key: string\n label: string\n type: ContactFieldType\n /** The choices, for `select`. */\n options?: string[]\n required?: boolean\n /** Position in the form and the export, ascending. */\n order: number\n retiredAt?: number | null\n /**\n * Which record the field describes (AGL-2661). ABSENT means `contact`,\n * because every definition written before companies and deals could carry\n * custom fields described a contact, and a backfill that stamped them\n * would touch every org for a fact the reader can infer. Read it through\n * {@link fieldDefinitionObject}, never directly, so that inference lives\n * in one place. Keys are unique PER OBJECT: a company field and a contact\n * field may both be called `region`.\n */\n object?: CrmFieldObject\n}\n\n/**\n * The records a custom field may describe, in the order the Fields\n * section tabs them (AGL-2661), with `lead` since AGL-3272.\n *\n * A lead is last because it is the record a person leaves: the three\n * before it are what a lead becomes, and the tabs read in that order.\n */\nexport const CRM_FIELD_OBJECTS = ['contact', 'company', 'deal', 'lead'] as const\n\nexport type CrmFieldObject = (typeof CRM_FIELD_OBJECTS)[number]\n\n/** How each object reads on the Fields section's tabs and in a refusal. */\nexport const CRM_FIELD_OBJECT_LABELS: Record<CrmFieldObject, string> = {\n contact: 'Contacts',\n company: 'Companies',\n deal: 'Deals',\n lead: 'Leads',\n}\n\nexport function isCrmFieldObject(value: unknown): value is CrmFieldObject {\n return (\n typeof value === 'string' &&\n (CRM_FIELD_OBJECTS as readonly string[]).includes(value)\n )\n}\n\n/**\n * Which record a definition describes — `contact` when the document says\n * nothing, or says something no reader understands.\n *\n * The one reader of {@link ContactFieldDefinition.object}. A stored value\n * outside the list is read as `contact` rather than refused, for the\n * reason an absent one is: the definition predates the field, or was\n * written by something that never learned it, and either way the values\n * under its key sit on contacts.\n */\nexport function fieldDefinitionObject(\n definition: Pick<ContactFieldDefinition, 'object'> | null | undefined,\n): CrmFieldObject {\n return isCrmFieldObject(definition?.object) ? definition.object : 'contact'\n}\n\n/**\n * What the CRM › Fields table's query reads on a definition (AGL-3335):\n *\n * object which tab it belongs to, stored even for a contact's, because\n * a query cannot find a field's absence;\n * required a boolean on every definition, for the Required filter;\n * searchTokens every word prefix of its name and of its key, the key read\n * whole and split at `_`, `-` and `.`, so \"plan\" finds\n * `plan_interest` and \"interest\" does too.\n *\n * Its script-side twin is `crmFieldListFields` in\n * `tools/scripts/lib/org-record-list-fields.mjs`; both answer the\n * `fieldDefinitions` of `org-record-list-fields.fixtures.json`.\n */\nexport function crmFieldListFields(definition: Record<string, unknown> | null | undefined): {\n object: CrmFieldObject\n required: boolean\n searchTokens: string[]\n} {\n const label = typeof definition?.['label'] === 'string' ? definition['label'] : ''\n const key = typeof definition?.['key'] === 'string' ? definition['key'] : ''\n return {\n object: fieldDefinitionObject(definition as Pick<ContactFieldDefinition, 'object'>),\n required: definition?.['required'] === true,\n searchTokens: nameSearchTokens([label, key, key.replace(/[_.-]+/g, ' ')].join(' ')),\n }\n}\n\n/**\n * The definition type under the name new code uses (AGL-2661). The type,\n * the collection (`contactFields`) and the rules match keep the contact\n * name: renaming them would ripple through the rules, the REST resources\n * and the docs for no change in what is stored.\n */\nexport type CrmFieldDefinition = ContactFieldDefinition\n\n/** What a stored field key must look like — a letter, then up to 39 of `[a-z0-9_]`. */\nexport const CONTACT_FIELD_KEY_PATTERN = /^[a-z][a-z0-9_]{0,39}$/\n\n/**\n * A typed label, as the key it would be stored under, or `null` when nothing\n * usable survives.\n *\n * Lowercased and snake-cased rather than refused, because the key is derived\n * from the label a merchant typed (\"Annual revenue\" → `annual_revenue`) and\n * they should not have to learn the grammar. Anything outside `[a-z0-9_]` is\n * dropped after separators become underscores, runs collapse, and a leading\n * run of digits or underscores goes — a key has to start with a letter so it\n * can never collide with an array index or read as a number in a filter.\n */\nexport function normalizeContactFieldKey(input: unknown): string | null {\n const key = String(input ?? '')\n .trim()\n .toLowerCase()\n .replace(/[\\s-]+/g, '_')\n .replace(/[^a-z0-9_]/g, '')\n .replace(/_+/g, '_')\n .replace(/^[^a-z]+/, '')\n .replace(/_+$/, '')\n .slice(0, 40)\n return CONTACT_FIELD_KEY_PATTERN.test(key) ? key : null\n}\n\n/** The stage a deal is in, or `null` when the pipeline no longer has it. */\nexport function dealStageById(\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n stageId: string,\n): CrmDealStage | null {\n return pipeline?.stages?.find((stage) => stage.id === stageId) ?? null\n}\n\n/**\n * What a deal is worth to a forecast, in cents.\n *\n * STATUS wins over the stage. A deal marked won is worth its full amount\n * whatever stage it happens to sit in, and a lost one is worth nothing —\n * the stage's probability is the odds of an OPEN deal, and applying it to a\n * closed one would forecast a sale that has already happened as a fraction\n * of itself. An open deal with no resolvable stage is worth nothing rather\n * than everything: the pipeline lost the stage, and a forecast that filled\n * the gap with 100% would be the most optimistic number available.\n */\nexport function weightedDealAmountCents(\n deal: Pick<CrmDeal, 'amountCents' | 'status'>,\n stage: Pick<CrmDealStage, 'probability'> | null | undefined,\n): number {\n const amount = Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0))\n if (deal.status === 'won') return amount\n if (deal.status === 'lost' || !stage) return 0\n const probability = Math.min(100, Math.max(0, Number(stage.probability) || 0))\n return Math.round((amount * probability) / 100)\n}\n\n/** Whether a deal's amount is derived — it has at least one line item. */\nexport function dealHasLineItems(\n deal: Pick<CrmDeal, 'lineItems'> | null | undefined,\n): boolean {\n return Array.isArray(deal?.lineItems) && deal.lineItems.length > 0\n}\n\n/**\n * What a set of line items adds up to, in cents — the amount a deal with\n * line items stores. Whole cents, never negative: a line's quantity and\n * unit amount have both been through `readDealLineItems`, but a stored\n * document from an older writer is trusted no further than that.\n */\nexport function lineItemsTotalCents(\n items: readonly Pick<CrmDealLineItem, 'quantity' | 'unitAmountCents'>[] | null | undefined,\n): number {\n let total = 0\n for (const item of items ?? []) {\n const quantity = Math.max(0, Math.round(Number(item.quantity) || 0))\n const unit = Math.max(0, Math.round(Number(item.unitAmountCents) || 0))\n total += quantity * unit\n }\n return total\n}\n\n/**\n * Line items as a client typed or sent them, validated into the stored\n * shape, or the one reason they were refused.\n *\n * The one reader for both writers — the products card on a deal's page\n * and `POST`/`PATCH /v1/deals` — so a line the console accepts is a line\n * the API accepts and the other way round. The rules: at most\n * {@link DEAL_LINE_ITEMS_MAX} lines; every line a name, a whole quantity\n * of one or more, a whole unit amount of zero or more; every line in the\n * DEAL's currency (`currency`), which a line may omit and may not\n * contradict. An empty list is valid and means \"no line items\" — the\n * amount goes back to being typed.\n */\nexport function readDealLineItems(\n input: unknown,\n currency: string,\n): { items: CrmDealLineItem[] } | { error: string } {\n if (input === undefined || input === null) return { items: [] }\n if (!Array.isArray(input)) return { error: 'Line items must be a list' }\n if (input.length > DEAL_LINE_ITEMS_MAX) {\n return { error: `A deal carries at most ${DEAL_LINE_ITEMS_MAX} line items` }\n }\n const dealCurrency = String(currency || 'usd').trim().toLowerCase()\n const items: CrmDealLineItem[] = []\n for (const [index, raw] of input.entries()) {\n const at = `Line ${index + 1}`\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n return { error: `${at} must be an object` }\n }\n const line = raw as Record<string, unknown>\n const name = String(line.name ?? '')\n .trim()\n .slice(0, DEAL_LINE_ITEM_NAME_MAX)\n if (!name) return { error: `${at} needs a name` }\n const quantity = Number(line.quantity)\n if (!Number.isInteger(quantity) || quantity < 1 || quantity > DEAL_LINE_ITEM_QUANTITY_MAX) {\n return {\n error: `${at}: the quantity must be a whole number from 1 to ${DEAL_LINE_ITEM_QUANTITY_MAX.toLocaleString()}`,\n }\n }\n const unitAmountCents = Number(line.unitAmountCents)\n if (!Number.isInteger(unitAmountCents) || unitAmountCents < 0) {\n return { error: `${at}: the unit amount must be a whole number of cents, 0 or more` }\n }\n const lineCurrency =\n line.currency === undefined || line.currency === null || line.currency === ''\n ? dealCurrency\n : String(line.currency).trim().toLowerCase()\n if (lineCurrency !== dealCurrency) {\n return { error: `${at} is in ${lineCurrency.toUpperCase()}; every line must be in the deal's currency, ${dealCurrency.toUpperCase()}` }\n }\n const productId =\n typeof line.productId === 'string' && line.productId.trim()\n ? line.productId.trim().slice(0, 200)\n : undefined\n items.push({\n ...(productId ? { productId } : {}),\n name,\n quantity,\n unitAmountCents,\n currency: dealCurrency,\n })\n }\n return { items }\n}\n\nexport type TaskDueState = 'overdue' | 'today' | 'upcoming' | 'none' | 'done'\n\n/**\n * Where a task stands against the clock.\n *\n * `today` is decided on the LOCAL calendar day and beats `overdue`: a task\n * due at nine this morning is today's work until midnight, not something the\n * list should paint red at 9:01. Overdue is yesterday or earlier. `done`\n * comes first because a completed task's due date is history whichever side\n * of now it fell on.\n */\nexport function taskDueState(\n task: Pick<CrmTask, 'status' | 'dueAtMs'>,\n nowMs: number,\n): TaskDueState {\n if (task.status === 'done') return 'done'\n const dueAtMs = task.dueAtMs\n if (typeof dueAtMs !== 'number' || !Number.isFinite(dueAtMs)) return 'none'\n const due = new Date(dueAtMs)\n const now = new Date(nowMs)\n const sameDay =\n due.getFullYear() === now.getFullYear() &&\n due.getMonth() === now.getMonth() &&\n due.getDate() === now.getDate()\n if (sameDay) return 'today'\n return dueAtMs < nowMs ? 'overdue' : 'upcoming'\n}\n\n/**\n * A hostname label: `[a-z0-9-]`, not starting or ending with a hyphen, at\n * most 63 characters — RFC 1123 as far as a company domain needs it.\n */\nconst DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/\n\n/**\n * A typed or pasted domain, reduced to the bare lowercase hostname a company\n * is keyed by, or `null` when what is left is not one.\n *\n * Strips what people paste along with a domain — the protocol, a `www.`, a\n * path, a query, a port — because the value is a KEY: two contacts at\n * `https://www.acme.com/about` and `acme.com` work for one company, and the\n * match is only findable if both reduce to the same string. The last label\n * has to be letters because a company's domain has a TLD; a bare IP address\n * or a single word is not one and answers `null` rather than being stored as\n * something a later match will never hit.\n */\nexport function normalizeCompanyDomain(input: unknown): string | null {\n let value = String(input ?? '')\n .trim()\n .toLowerCase()\n if (!value) return null\n value = value.replace(/^[a-z][a-z0-9+.-]*:\\/\\//, '')\n value = value.replace(/^www\\./, '')\n value = value.split(/[/?#]/, 1)[0]\n value = value.split(':', 1)[0]\n value = value.replace(/\\.+$/, '')\n if (!value || value.length > 253) return null\n const labels = value.split('.')\n if (labels.length < 2) return null\n if (!labels.every((label) => DOMAIN_LABEL.test(label))) return null\n if (!/^[a-z]{2,}$/.test(labels[labels.length - 1])) return null\n return value\n}\n\n/** The longest website URL a company stores. */\nconst COMPANY_WEBSITE_MAX = 500\n\n/**\n * The typed website, as a URL; `''` for a blank; `null` when it cannot be\n * one.\n *\n * People type `acme.com` where a URL is asked for, and refusing that is\n * pedantry — it becomes `https://acme.com`. What IS refused is anything the\n * URL parser cannot read or a scheme other than http(s): a `javascript:` link\n * on a record that renders as an anchor is not a website. One function for\n * the company drawer and the companies import, so a website typed and a\n * website imported are stored the same way.\n */\nexport function normalizeCompanyWebsite(input: unknown): string | null {\n const raw = String(input ?? '').trim()\n if (!raw) return ''\n const candidate = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(raw) ? raw : `https://${raw}`\n try {\n const url = new URL(candidate)\n if (url.protocol !== 'http:' && url.protocol !== 'https:') return null\n return url.href.length > COMPANY_WEBSITE_MAX ? null : url.href\n } catch {\n return null\n }\n}\n\n/**\n * Mailbox domains that belong to people rather than to a business: the\n * providers anybody can sign up with, and the ones an internet provider\n * hands its subscribers.\n *\n * Auto-associating a contact with a company by email domain is the whole\n * reason {@link companyDomainForEmail} exists, and it is wrong for every\n * address at a public mailbox: `gmail.com` is not a company, and a rule\n * that treated it as one would file half of a consumer list under a single\n * phantom account. It is also the fact a cold email has to know — an\n * address here is a person's own, not their work address — which is why the\n * list is exported with {@link isPublicMailboxDomain}: a second list kept\n * for that question would drift from this one.\n *\n * Exact domains, not suffixes: a subscriber's regional host (`nc.rr.com`)\n * is not listed, and neither is a company that happens to end in `mail.com`.\n */\nexport const PUBLIC_MAILBOX_DOMAINS: ReadonlySet<string> = new Set([\n // Google, Yahoo, Microsoft, Apple, AOL.\n 'gmail.com',\n 'googlemail.com',\n 'yahoo.com',\n 'yahoo.co.uk',\n 'yahoo.ca',\n 'yahoo.com.au',\n 'yahoo.co.in',\n 'yahoo.fr',\n 'yahoo.de',\n 'ymail.com',\n 'rocketmail.com',\n 'hotmail.com',\n 'hotmail.co.uk',\n 'hotmail.fr',\n 'hotmail.de',\n 'hotmail.it',\n 'outlook.com',\n 'live.com',\n 'live.co.uk',\n 'live.ca',\n 'msn.com',\n 'icloud.com',\n 'me.com',\n 'mac.com',\n 'aol.com',\n 'aim.com',\n // Independent and privacy-first providers.\n 'proton.me',\n 'protonmail.com',\n 'protonmail.ch',\n 'pm.me',\n 'tutanota.com',\n 'tuta.io',\n 'hushmail.com',\n 'gmx.com',\n 'gmx.net',\n 'gmx.de',\n 'web.de',\n 'mail.com',\n 'yandex.com',\n 'yandex.ru',\n 'zoho.com',\n 'zohomail.com',\n 'fastmail.com',\n 'hey.com',\n // The largest consumer providers outside the US and Europe.\n 'qq.com',\n '163.com',\n '126.com',\n 'sina.com',\n 'naver.com',\n 'hanmail.net',\n 'rediffmail.com',\n // US internet providers' subscriber mailboxes.\n 'comcast.net',\n 'att.net',\n 'sbcglobal.net',\n 'bellsouth.net',\n 'pacbell.net',\n 'verizon.net',\n 'cox.net',\n 'charter.net',\n 'earthlink.net',\n 'optonline.net',\n 'frontier.com',\n 'windstream.net',\n 'centurylink.net',\n 'juno.com',\n 'netzero.net',\n // Subscriber mailboxes at the largest providers in Canada, the UK, France,\n // Germany, Italy and Australia.\n 'shaw.ca',\n 'sympatico.ca',\n 'btinternet.com',\n 'sky.com',\n 'virginmedia.com',\n 'orange.fr',\n 'free.fr',\n 'laposte.net',\n 't-online.de',\n 'libero.it',\n 'bigpond.com',\n])\n\n/**\n * Whether a domain — or the domain of an address — is one of the\n * {@link PUBLIC_MAILBOX_DOMAINS}. Read the way a company domain is, so case,\n * a trailing dot and a pasted `www.` do not hide one.\n */\nexport function isPublicMailboxDomain(domainOrEmail: unknown): boolean {\n const value = String(domainOrEmail ?? '').trim()\n const domain = normalizeCompanyDomain(value.slice(value.lastIndexOf('@') + 1))\n return domain !== null && PUBLIC_MAILBOX_DOMAINS.has(domain)\n}\n\n/**\n * The company domain an email address implies, or `null` when it implies\n * none — a malformed address, or one at a public mailbox provider.\n */\nexport function companyDomainForEmail(email: unknown): string | null {\n const value = String(email ?? '')\n .trim()\n .toLowerCase()\n const at = value.lastIndexOf('@')\n if (at < 1 || at === value.length - 1) return null\n if (/[\\s@]/.test(value.slice(0, at))) return null\n const domain = normalizeCompanyDomain(value.slice(at + 1))\n if (!domain || PUBLIC_MAILBOX_DOMAINS.has(domain)) return null\n return domain\n}\n\n/**\n * The name a company minted from a domain starts with: the first label with\n * a capital — `acme.com` → `Acme`, `initech.co.uk` → `Initech`.\n *\n * A starting point and not a claim about what the business is called: the\n * lead-convert dialog offers it for editing, and a company the capture door\n * creates on its own carries it until somebody renames the record. The\n * domain itself would be an honest name too, but a list of companies that\n * reads `acme.com`, `globex.example` looks like a list of websites, and the\n * domain is on the row beside the name anyway.\n */\nexport function companyNameForDomain(domain: string): string {\n const label = domain.split('.')[0] || domain\n return label.charAt(0).toUpperCase() + label.slice(1)\n}\n\n/*==========================================\n * THE CONTACT–COMPANY LINK (AGL-2597, AGL-2613).\n *\n * The association proper is `facets.{groupId}.companyId` — one company per\n * holder, inside that holder's facet like the notes and the tags, and for\n * the same reason: which account a person belongs to is one business's\n * knowledge of them. But a facet path is per group, and Firestore cannot\n * answer \"every contact whose facet, whichever group's, names company X\" —\n * a `where` needs one field path, and the group id is part of the path. So\n * the facet is MIRRORED into a top-level array an `array-contains` can\n * query, and the company carries a COUNT of the contacts whose mirror names\n * it, so a list of companies can say how many people are at each without\n * reading a person.\n *\n * Three fields, kept in step by ONE planner. Every writer — the picker on a\n * contact's page, the company page's link control, the bulk bar, the server\n * doors that link on capture — asks {@link planContactCompanyLink} what\n * changes and applies the answer with its own SDK's sentinels. None of them\n * decides for itself whether the old id leaves the mirror, which is the rule\n * a second copy would get wrong: another business filing the same person\n * under the same account is THEIR link, and this holder letting go must not\n * take it away.\n *=========================================*/\n\n/**\n * The top-level field on a CONTACT naming every company it is linked to.\n *\n * It carries the union of every holder's link, so removing an id from it is\n * only correct when no other holder's facet still names that id — the rule\n * the planner enforces, and the reason nothing writes this field without\n * going through it. The facet is the truth and this is its index: a reader\n * answering \"which company is this person at\" reads the facet; only a QUERY\n * reads this.\n */\nexport const CONTACT_COMPANY_IDS_FIELD = 'companyIds'\n\n/**\n * The field on a COMPANY counting the contacts whose mirror names it.\n *\n * Denormalized because the honest figure is an aggregate over every contact\n * in the org, which a list of two hundred companies cannot afford to take\n * per row. Moved by `increment` in the same batch as the link that changes\n * it, so the two cannot disagree by a failed second write; a company's own\n * page still takes the live aggregate, which is what corrects a count that\n * predates the counter.\n */\nexport const COMPANY_CONTACTS_COUNT_FIELD = 'contactsCount'\n\n/** What the planner needs to know about a contact's links, and nothing else. */\nexport interface ContactCompanyLinkState {\n /** This holder's link, or `null` when the facet names no company. */\n companyId: string | null\n /** The mirror as stored, in no order. */\n companyIds: string[]\n /**\n * Ids named by OTHER holders' facets — what keeps an id in the mirror\n * when this holder lets go of it.\n */\n heldElsewhere: string[]\n}\n\n/** The link state of a stored contact, read for one holder. */\nexport function readContactCompanyLink(\n contact: Record<string, unknown> | null | undefined,\n groupId: string,\n): ContactCompanyLinkState {\n const document = contact ?? {}\n const facets = document[CONTACT_FACETS_FIELD]\n const heldElsewhere = new Set<string>()\n if (facets && typeof facets === 'object' && !Array.isArray(facets)) {\n for (const [holder, facet] of Object.entries(\n facets as Record<string, unknown>,\n )) {\n if (holder === groupId) continue\n const named =\n facet && typeof facet === 'object' && !Array.isArray(facet)\n ? (facet as Record<string, unknown>)['companyId']\n : undefined\n if (typeof named === 'string' && named) heldElsewhere.add(named)\n }\n }\n const mirror = document[CONTACT_COMPANY_IDS_FIELD]\n return {\n companyId: readContactFacet(document, groupId).companyId ?? null,\n companyIds: Array.isArray(mirror)\n ? mirror.filter((id): id is string => typeof id === 'string' && !!id)\n : [],\n heldElsewhere: [...heldElsewhere],\n }\n}\n\n/**\n * Every holder whose facet names this company, in no order.\n *\n * What a company's DELETION clears. Unlike a link change, which is one\n * holder's, a deletion takes the record away from every holder at once: a\n * facet still naming it would be a link to nothing, on a surface its holder\n * has no reason to revisit. Nothing else about another holder's facet is\n * read.\n */\nexport function contactGroupsNamingCompany(\n contact: Record<string, unknown> | null | undefined,\n companyId: string,\n): string[] {\n const facets = (contact ?? {})[CONTACT_FACETS_FIELD]\n if (!companyId || !facets || typeof facets !== 'object' || Array.isArray(facets)) {\n return []\n }\n return Object.entries(facets as Record<string, unknown>)\n .filter(([, facet]) =>\n Boolean(\n facet &&\n typeof facet === 'object' &&\n !Array.isArray(facet) &&\n (facet as Record<string, unknown>)['companyId'] === companyId,\n ),\n )\n .map(([groupId]) => groupId)\n}\n\n/**\n * How the mirror changes. Three shapes because Firestore takes ONE transform\n * per field per write: an `arrayUnion` and an `arrayRemove` on the same\n * field cannot share an update, so a move rewrites the array whole.\n */\nexport type ContactCompanyMirrorChange =\n | { op: 'union'; companyId: string }\n | { op: 'remove'; companyId: string }\n | { op: 'set'; companyIds: string[] }\n\nexport interface ContactCompanyLinkPlan {\n /** The facet's new value: the id, or `null` meaning delete the field. */\n companyId: string | null\n /** What happens to the mirror, or `null` when it already carries the right ids. */\n mirror: ContactCompanyMirrorChange | null\n /**\n * Per company, how its contacts count moves. A company enters the list\n * only when the mirror actually gains or loses it, so a link some other\n * holder already made is not counted twice and a mirror that never carried\n * an id is not decremented for it.\n */\n counts: Array<{ companyId: string; delta: 1 | -1 }>\n}\n\n/**\n * What linking a contact to a company FOR ONE HOLDER changes — or unlinking\n * them, with `null`. `null` when the document already says what was asked.\n *\n * Three cases, and the mirror is handled differently in each because it is\n * shared across holders while the facet is not:\n *\n * - A first link `union`s the id in, which is safe against a concurrent\n * writer adding another holder's id.\n * - A MOVE from one company to another rewrites the mirror as a whole. The\n * old id is dropped only if no other holder's facet still names it.\n * - An unlink `remove`s the old id, on the same condition, and leaves the\n * mirror alone when another holder still needs it there.\n *\n * The counts follow the mirror, not the facet: the company's figure is \"how\n * many contacts name me in the mirror\", and that is the quantity the\n * company page's live aggregate measures.\n */\nexport function planContactCompanyLink(\n state: ContactCompanyLinkState,\n companyId: string | null,\n): ContactCompanyLinkPlan | null {\n const previous = state.companyId\n if (previous === companyId) return null\n const mirror = new Set(state.companyIds)\n const previousLeaves =\n previous !== null && !state.heldElsewhere.includes(previous)\n const counts: ContactCompanyLinkPlan['counts'] = []\n let change: ContactCompanyMirrorChange | null = null\n if (companyId && !previous) {\n change = { op: 'union', companyId }\n if (!mirror.has(companyId)) counts.push({ companyId, delta: 1 })\n } else if (companyId && previous) {\n const kept = state.companyIds.filter(\n (id) => id !== previous || !previousLeaves,\n )\n change = { op: 'set', companyIds: [...new Set([...kept, companyId])] }\n if (previousLeaves && mirror.has(previous)) {\n counts.push({ companyId: previous, delta: -1 })\n }\n if (!mirror.has(companyId)) counts.push({ companyId, delta: 1 })\n } else if (previous && previousLeaves) {\n change = { op: 'remove', companyId: previous }\n if (mirror.has(previous)) counts.push({ companyId: previous, delta: -1 })\n }\n return { companyId, mirror: change, counts }\n}\n\n/*==========================================\n * THE CRM's ORGANIZATION SETTINGS (AGL-2613).\n *\n * One map under `crm` on the org document, so the CRM → Settings section\n * can grow a key per setting without a rules change each time: the org\n * document's client branch is a deny-list, and `crm` is declared\n * client-writable in `ORG_CLIENT_WRITABLE_FIELDS` with its reason.\n *=========================================*/\n\n/** The org-document key the CRM's settings live under. */\nexport const ORG_CRM_SETTINGS_FIELD = 'crm'\n\n/** The dotted path an `update()` writes the auto-create switch by. */\nexport const CRM_AUTO_CREATE_COMPANIES_PATH = `${ORG_CRM_SETTINGS_FIELD}.autoCreateCompanies`\n\n/**\n * Whether a capture from a work email domain no visible company carries\n * should CREATE the company. Off unless the org document says `true`: a\n * company minted from every domain that ever submitted a form is a list\n * nobody asked for, so the default is the quiet one.\n *\n * Read off the raw document rather than a typed field, because the capture\n * door holds a `Partial<AglynOrganization>` and the console a\n * `Partial<AglynOrgBilling>`, and one reader has to answer both.\n */\nexport function orgAutoCreatesCompanies(\n orgDocument: Record<string, unknown> | null | undefined,\n): boolean {\n const settings = (orgDocument ?? {})[ORG_CRM_SETTINGS_FIELD]\n return Boolean(\n settings &&\n typeof settings === 'object' &&\n !Array.isArray(settings) &&\n (settings as Record<string, unknown>)['autoCreateCompanies'] === true,\n )\n}\n\n/*==========================================\n * WHO A NEW RECORD BELONGS TO (AGL-2618).\n *\n * Salesforce calls these assignment rules and HubSpot rotates to an owner;\n * either way they are the first thing a sales team configures, because a\n * record with no owner is one nobody follows up. The shape here is what the\n * CRM → Settings section writes onto the org document under `crm`, what the\n * capture door's assignment pass reads, and what the `assignContactOwner`\n * automation step's round-robin mode reads — one vocabulary, three readers.\n *\n * ## The order of decision\n *\n * The rules are tried in their stored order and the FIRST match assigns; a\n * rule that names a member the roster no longer has, or a round-robin rule\n * over an empty pool, is passed over rather than stopping the pass. When no\n * rule claims the capture, the capturing site's default owner does; when\n * the site has none, the record stays unassigned, which is what the product\n * did before any of this existed and is the honest answer to \"nobody has\n * said\".\n *\n * ## Only a record with no owner\n *\n * The capture pass runs when a record is CREATED and never overwrites an\n * owner: a door that named one (the console's drawer, an import column, a\n * conversion with a picked owner) has expressed a person's choice, and a\n * returning visitor's contact already belongs to somebody. The automation\n * step is the deliberate exception — an author who put \"assign an owner\"\n * on a stage change means to reassign.\n *\n * ## Why the pointer is a uid, and why the server moves it\n *\n * See `OrgCrmRoundRobin`. The pool is edited in the console and the pointer\n * is advanced by the Admin SDK inside the transaction that writes the owner,\n * so concurrent captures take distinct turns and an edit to the pool never\n * skips or repeats a member.\n *=========================================*/\n\n/** Where the ordered rules live on the org document. */\nexport const CRM_ASSIGNMENT_RULES_PATH = `${ORG_CRM_SETTINGS_FIELD}.assignmentRules`\n\n/** The round-robin pool's member list, in rotation order. */\nexport const CRM_ROUND_ROBIN_POOL_PATH = `${ORG_CRM_SETTINGS_FIELD}.roundRobin.memberUids`\n\n/** The member handed the most recent round-robin record. */\nexport const CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH = `${ORG_CRM_SETTINGS_FIELD}.roundRobin.lastAssignedUid`\n\n/**\n * The most rules an org may keep. A first-match list longer than this is\n * one nobody can reason about, and the section refuses the fifty-first\n * rather than letting the document grow unbounded.\n */\nexport const CRM_ASSIGNMENT_RULES_MAX = 50\n\n/** The most members a round-robin pool may hold. */\nexport const CRM_ROUND_ROBIN_POOL_MAX = 50\n\n/**\n * The map of per-site CRM settings under `crm` on the ORG document, keyed\n * by host id. A field on the org, not a subcollection of the host: the\n * host-subcollection guards read a quoted `'hosts'` beside a site id as a\n * client path under `hosts/{hostId}`, which this is not, so the key is\n * named here and spelled nowhere else.\n */\nconst CRM_HOST_SETTINGS_KEY = 'hosts'\n\n/**\n * The field-path SEGMENTS of a site's default owner on the org document.\n *\n * Segments rather than a dotted string, because the host id is a document\n * id and a document id may contain a dot; joined with dots it would be read\n * as two path elements and the write would land beside the setting rather\n * than in it. A caller builds a `FieldPath` from these on either SDK.\n */\nexport function crmHostDefaultOwnerSegments(hostId: string): string[] {\n if (!hostId) throw new Error('a site default owner must name a site')\n return [ORG_CRM_SETTINGS_FIELD, CRM_HOST_SETTINGS_KEY, hostId, 'defaultOwnerUid']\n}\n\n/** The conditions a rule may name — every one present must hold. */\nexport interface CrmAssignmentRuleWhen {\n /** The capture door: `form`, `booking`, `order`, `manual`… */\n source?: ContactSource\n /** The form the capture came through, by its document id. */\n formId?: string\n /** The captured address's domain, lowercased, without a leading `@`. */\n emailDomain?: string\n /** A tag the capture carries or the contact already wears. */\n tag?: string\n}\n\n/** How a matching rule assigns: one member, or the next in the pool. */\nexport type CrmAssignmentTarget = { memberUid: string } | { roundRobin: true }\n\n/**\n * One rule as the CRM reads it — `OrgCrmAssignmentRule` with the source\n * narrowed to the capture vocabulary.\n */\nexport interface CrmAssignmentRule extends OrgCrmAssignmentRule {\n when: CrmAssignmentRuleWhen\n assign: CrmAssignmentTarget\n}\n\n/** The pool as the CRM reads it: never absent, possibly empty. */\nexport interface CrmRoundRobinPool {\n memberUids: string[]\n lastAssignedUid: string | null\n}\n\n/**\n * The org's assignment settings, read tolerantly off the raw document.\n *\n * Every field is optional on the document and the section writes each on\n * its own, so a reader that trusted the shape would throw on the first org\n * that has set the pool but never a rule. Malformed entries are dropped\n * rather than refused wholesale: one hand-edited rule must not stop the\n * others from assigning.\n */\nexport interface CrmAssignmentSettings {\n rules: CrmAssignmentRule[]\n pool: CrmRoundRobinPool\n /** Site id → default owner uid, for the sites that set one. */\n hostDefaultOwners: Record<string, string>\n}\n\nfunction isContactSourceValue(value: unknown): value is ContactSource {\n return typeof value === 'string' && value in CONTACT_SOURCE_LABELS\n}\n\nfunction cleanText(value: unknown, max: number): string | undefined {\n const text = typeof value === 'string' ? value.trim().slice(0, max) : ''\n return text || undefined\n}\n\n/** A rule as the document holds it, or `null` for one that cannot assign. */\nexport function readCrmAssignmentRule(raw: unknown): CrmAssignmentRule | null {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null\n const entry = raw as Record<string, unknown>\n const id = cleanText(entry['id'], 64)\n if (!id) return null\n const whenRaw =\n entry['when'] && typeof entry['when'] === 'object' && !Array.isArray(entry['when'])\n ? (entry['when'] as Record<string, unknown>)\n : {}\n const when: CrmAssignmentRuleWhen = {}\n if (isContactSourceValue(whenRaw['source'])) when.source = whenRaw['source']\n const formId = cleanText(whenRaw['formId'], 128)\n if (formId) when.formId = formId\n // A person types a domain the way they see one in an address — `@acme.com`\n // — so the `@` is what the field means, not part of the domain.\n const emailDomain = normalizeCompanyDomain(\n String(whenRaw['emailDomain'] ?? '')\n .trim()\n .replace(/^@/, ''),\n )\n if (emailDomain) when.emailDomain = emailDomain\n const tag = cleanText(whenRaw['tag'], 60)?.toLowerCase()\n if (tag) when.tag = tag\n const assignRaw =\n entry['assign'] &&\n typeof entry['assign'] === 'object' &&\n !Array.isArray(entry['assign'])\n ? (entry['assign'] as Record<string, unknown>)\n : null\n if (!assignRaw) return null\n if (assignRaw['roundRobin'] === true) {\n return { id, when, assign: { roundRobin: true } }\n }\n const memberUid = cleanText(assignRaw['memberUid'], 128)\n return memberUid ? { id, when, assign: { memberUid } } : null\n}\n\nexport function readCrmAssignmentSettings(\n orgDocument: Record<string, unknown> | null | undefined,\n): CrmAssignmentSettings {\n const settings = (orgDocument ?? {})[ORG_CRM_SETTINGS_FIELD]\n const crm =\n settings && typeof settings === 'object' && !Array.isArray(settings)\n ? (settings as Record<string, unknown>)\n : {}\n const rules = (Array.isArray(crm['assignmentRules']) ? crm['assignmentRules'] : [])\n .map(readCrmAssignmentRule)\n .filter((rule): rule is CrmAssignmentRule => rule !== null)\n .slice(0, CRM_ASSIGNMENT_RULES_MAX)\n const roundRobin =\n crm['roundRobin'] && typeof crm['roundRobin'] === 'object'\n ? (crm['roundRobin'] as Record<string, unknown>)\n : {}\n const memberUids = [\n ...new Set(\n (Array.isArray(roundRobin['memberUids']) ? roundRobin['memberUids'] : [])\n .map((uid) => cleanText(uid, 128))\n .filter((uid): uid is string => Boolean(uid)),\n ),\n ].slice(0, CRM_ROUND_ROBIN_POOL_MAX)\n const hostSettings = crm[CRM_HOST_SETTINGS_KEY]\n const hosts =\n hostSettings && typeof hostSettings === 'object' && !Array.isArray(hostSettings)\n ? (hostSettings as Record<string, unknown>)\n : {}\n const hostDefaultOwners: Record<string, string> = {}\n for (const [hostId, value] of Object.entries(hosts)) {\n const owner =\n value && typeof value === 'object'\n ? cleanText((value as Record<string, unknown>)['defaultOwnerUid'], 128)\n : undefined\n if (owner) hostDefaultOwners[hostId] = owner\n }\n return {\n rules,\n pool: {\n memberUids,\n lastAssignedUid: cleanText(roundRobin['lastAssignedUid'], 128) ?? null,\n },\n hostDefaultOwners,\n }\n}\n\n/** The member a site hands unclaimed captures to, or `null` for nobody. */\nexport function crmHostDefaultOwner(\n orgDocument: Record<string, unknown> | null | undefined,\n hostId: string,\n): string | null {\n return readCrmAssignmentSettings(orgDocument).hostDefaultOwners[hostId] ?? null\n}\n\n/** What a rule is matched against: the capture, as its door described it. */\nexport interface CrmAssignmentCapture {\n source: ContactSource\n email: string\n formId?: string | null\n /** The capture's own tags and whatever the contact already wears. */\n tags?: readonly string[]\n}\n\n/**\n * The domain a rule's `emailDomain` is compared with — the address's own,\n * lowercased. Not `companyDomainForEmail`, which answers `null` for a public\n * mailbox: a team may well route every `gmail.com` sign-up to one rep, and\n * a rule that could not name a consumer domain could not say so.\n */\nexport function assignmentEmailDomain(email: string): string | null {\n const at = email.lastIndexOf('@')\n if (at < 0) return null\n return normalizeCompanyDomain(email.slice(at + 1))\n}\n\n/**\n * Whether every condition the rule names holds for this capture. A rule\n * with no condition matches every capture — the catch-all.\n */\nexport function assignmentRuleMatches(\n when: CrmAssignmentRuleWhen,\n capture: CrmAssignmentCapture,\n): boolean {\n if (when.source && when.source !== capture.source) return false\n if (when.formId && when.formId !== (capture.formId ?? '')) return false\n if (when.emailDomain && when.emailDomain !== assignmentEmailDomain(capture.email)) {\n return false\n }\n if (when.tag) {\n const worn = (capture.tags ?? []).map((tag) => String(tag).trim().toLowerCase())\n if (!worn.includes(when.tag)) return false\n }\n return true\n}\n\n/**\n * The pool in the order the next record should try it: the member after\n * the last recipient first, wrapping round, and the last recipient\n * themselves at the end — so a pool of one still assigns, and a pointer\n * naming somebody no longer in the pool starts from the top.\n */\nexport function roundRobinOrder(\n memberUids: readonly string[],\n lastAssignedUid: string | null | undefined,\n): string[] {\n if (!memberUids.length) return []\n const at = lastAssignedUid ? memberUids.indexOf(lastAssignedUid) : -1\n if (at < 0) return [...memberUids]\n return [...memberUids.slice(at + 1), ...memberUids.slice(0, at + 1)]\n}\n\n/**\n * How a rule reads in the settings list — its conditions in words and\n * where it sends the record — so a reader can tell two rules apart without\n * opening either. `memberLabel` turns a uid into a name; the section passes\n * the roster's.\n */\nexport function describeAssignmentRule(\n rule: CrmAssignmentRule,\n memberLabel: (uid: string) => string,\n): { when: string; assign: string } {\n const parts: string[] = []\n if (rule.when.source) parts.push(`source is ${CONTACT_SOURCE_LABELS[rule.when.source]}`)\n if (rule.when.formId) parts.push(`form is ${rule.when.formId}`)\n if (rule.when.emailDomain) parts.push(`email domain is ${rule.when.emailDomain}`)\n if (rule.when.tag) parts.push(`tagged ${rule.when.tag}`)\n return {\n when: parts.length ? parts.join(' and ') : 'Every capture',\n assign:\n 'roundRobin' in rule.assign ? 'Round robin' : memberLabel(rule.assign.memberUid),\n }\n}\n\n/**\n * A fresh rule id, unique among the ones the org already holds. Time and\n * entropy rather than a counter, so two admins adding a rule in two tabs\n * do not mint the same id and have one reorder clobber the other's rule.\n */\nexport function newAssignmentRuleId(existing: readonly string[]): string {\n for (;;) {\n const id = `rule-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`\n if (!existing.includes(id)) return id\n }\n}\n\n/**\n * The `visibleTo` every CRM creator stamps — client or server, whichever\n * door the record comes in through.\n *\n * The contact create path's own expression (`upsertHostContact`), lifted\n * here so that a company, a deal or a task created from a site's console\n * lands in exactly the scope a contact captured on that site would: the\n * whole org when the org has chosen `defaultResourceScope: 'org'`, and\n * otherwise the sites that present as one sender — which, undeclared, is\n * this site alone. That is the agency's isolation, arrived at with nothing\n * configured, and the reason this is a function rather than a convention\n * eight creators are asked to remember.\n *\n * Widening past the group is an ACT — an org-wide member editing the scope\n * on the record — never a default. The rules refuse a scoped member creating\n * a record outside their own tokens, so a creator that bypassed this could\n * only make its record invisible to itself, never wider.\n */\nexport function crmScopeTokens(\n org: Record<string, unknown> | null | undefined,\n group: ConsentGroup,\n): ScopeToken[] {\n return (org ?? {})['defaultResourceScope'] === 'org'\n ? [ORG_SCOPE_TOKEN]\n : consentGroupScope(group)\n}\n\n/**\n * The `visibleTo` tokens a reader of this group may LIST, for the\n * `array-contains-any` every CRM listener filters by.\n *\n * The other half of {@link crmScopeTokens}: that is what a creator stamps,\n * this is what a reader asks for, and the two differ by exactly one token.\n * `'org'` leads because an org-wide record is visible to every site — an org\n * that widened its default deliberately still sees its own rows — and the\n * group's sites follow. The contacts list computes this inline; every other\n * CRM reader takes it from here so that none of them can drop the org token\n * and lose the org-wide rows, or forget the cap and have Firestore refuse\n * the query outright.\n *\n * Capped at {@link MAX_SCOPE_HOSTS}: a group wider than the operator's limit\n * is listed as far as the limit reaches, which is the contacts list's\n * behavior and the only one short of an error.\n */\nexport function crmReadTokens(group: ConsentGroup): ScopeToken[] {\n return [ORG_SCOPE_TOKEN, ...consentGroupScope(group)].slice(0, MAX_SCOPE_HOSTS)\n}\n\n/*==========================================\n * THE TEAM, AS A RECORD NAMES THEM (AGL-2614).\n *\n * An owner and an assignee are stored by uid — `orgs/{orgId}/members/{uid}`\n * is keyed by it, and a uid outlives every address change. But the people\n * who NAME an owner do not think in uids: an automation step is typed, a\n * CSV column is an address, and a picker lists names. So a reference\n * arrives as one of two things, and every reader that turns a reference\n * into a person has to accept both or the roster splits into the members\n * who can be named and the members who cannot.\n *\n * The member documents make the second case real. Two production paths\n * create a member document WITHOUT its `email` — a host-access re-grant,\n * and an add whose auth record carried none — and such a member was\n * unnameable everywhere a surface asked for an address: an automation\n * could not assign to them, and a roster mapper that dropped a row with no\n * address could not even list them. A name they still have (the roster's\n * `displayName`), and a uid they always have, so a label falls through to\n * the uid rather than to nothing, and a reference is resolved by uid first\n * and by address second.\n *\n * Roster-only, deliberately. The project's Auth records could resolve an\n * address the roster cannot, and would resolve people who are not on this\n * organization at all (AGL-1122); a reference that names nobody on the\n * roster names nobody.\n *=========================================*/\n\n/** One person on the team, as a picker lists them and a reference resolves to them. */\nexport interface CrmMemberOption {\n /** The account uid — what `ownerUid` and `assigneeUid` store. */\n uid: string\n /** The name a colleague recognizes: display name, else address, else the uid. */\n label: string\n /** The roster's address, when the member document carries one. */\n email?: string\n}\n\n/**\n * How a step or a column names somebody on the team: an address when the\n * text is one, otherwise a uid. `null` for a blank, which every caller\n * treats as \"nobody named\" rather than as a member called \"\".\n */\nexport type CrmMemberRef =\n | { kind: 'uid'; uid: string }\n | { kind: 'email'; email: string }\n\nexport function parseCrmMemberRef(value: unknown): CrmMemberRef | null {\n const text = String(value ?? '').trim()\n if (!text) return null\n if (text.includes('@')) {\n const email = normalizeContactEmail(text)\n return email ? { kind: 'email', email } : null\n }\n return { kind: 'uid', uid: text }\n}\n\n/**\n * A roster row as every CRM picker and column shows it.\n *\n * One mapping rather than four, because four is what the CRM had — and one\n * of them dropped a member whose document had neither a display name nor an\n * address, which is precisely the member the uid fallback exists for.\n */\nexport function crmMemberOption(\n member: Record<string, unknown>,\n): CrmMemberOption | null {\n const uid = String(member['$id'] ?? member['uid'] ?? '').trim()\n if (!uid) return null\n const displayName = String(member['displayName'] ?? '').trim()\n const email = String(member['email'] ?? '').trim()\n return {\n uid,\n label: displayName || email || uid,\n ...(email ? { email } : {}),\n }\n}\n\n/**\n * A roster row as a PICKER lists it: the name, and the address in\n * parentheses when the roster holds one that the name does not already\n * spell. Two teammates can share a display name — a workspace with two\n * \"Zach Gover\" rows cannot tell them apart by name alone, and the address is\n * the one thing the roster guarantees is unique. A column keeps `label`,\n * because a column is read at a glance and a picker is chosen from.\n */\nexport function crmMemberPickerLabel(\n member: Pick<CrmMemberOption, 'label' | 'email'>,\n): string {\n const label = String(member.label ?? '').trim()\n const email = String(member.email ?? '').trim()\n if (!email || email.toLowerCase() === label.toLowerCase()) return label || email\n return `${label} (${email})`\n}\n\n/**\n * The member a stored reference names: by uid first — the stored shape —\n * and by address second, so a record that carries an address where a uid\n * belongs (an older import, a hand edit) still names the person the roster\n * has under it. Undefined for nobody, which the caller renders honestly as\n * \"former member\" or as the reference itself rather than as unassigned.\n */\nexport function findOrgMember<T extends { uid: string; email?: string | null }>(\n members: readonly T[],\n ref: string | null | undefined,\n): T | undefined {\n const parsed = parseCrmMemberRef(ref)\n if (!parsed) return undefined\n if (parsed.kind === 'uid') {\n return members.find((member) => member.uid === parsed.uid)\n }\n return members.find(\n (member) => normalizeContactEmail(member.email) === parsed.email,\n )\n}\n\n/*==========================================\n * LEADS (AGL-2608).\n *\n * A lead is NOT one of the six collections above. It lives at\n * `hosts/{hostId}/leads/{personKey}`, written by `addHostLead` when a\n * visitor signs up, books or submits a form, and it is host-scoped by PATH:\n * no `visibleTo`, no facet map, private to the site that captured it. What\n * the CRM adds is the working state a sales team keeps on such a capture —\n * a status, an owner, notes — and the record of its conversion into the\n * contact, company and deal that live in the org collections.\n *\n * These fields are typed here rather than beside `addHostLead` because the\n * writer of the capture and the reader of the working state are different\n * programs: the capture door stamps none of them, and a lead that predates\n * this block carries none of them, which is why every field is optional and\n * {@link crmLeadStatus} reads an absent status as `new`.\n *=========================================*/\n\n/**\n * Where a lead stands, in the order a person works one.\n *\n * `qualified` is the CONVERTED state — a lead becomes a contact by being\n * qualified, and the conversion stamps `convertedContactId` beside it — and\n * `unqualified` is the closed-without-conversion state with its reason. A\n * fixed list rather than free text for the reason the lifecycle stages are:\n * the section filters on it and a report counts by it.\n */\nexport const CRM_LEAD_STATUSES = [\n 'new',\n 'working',\n 'qualified',\n 'unqualified',\n] as const\n\nexport type CrmLeadStatus = (typeof CRM_LEAD_STATUSES)[number]\n\n/** How a lead status reads on screen — typed so a status cannot ship unlabeled. */\nexport const CRM_LEAD_STATUS_LABELS: Record<CrmLeadStatus, string> = {\n new: 'New',\n working: 'Working',\n qualified: 'Qualified',\n unqualified: 'Unqualified',\n}\n\n/**\n * The statuses a lead still needs somebody's attention in — what the Leads\n * section shows by default, so the list opens on the work rather than on\n * the history.\n */\nexport const CRM_LEAD_OPEN_STATUSES: readonly CrmLeadStatus[] = ['new', 'working']\n\n/**\n * The statuses that mean nobody needs to work the lead any more — the\n * operand of the `in` clause an open-lead figure subtracts with. Subtracts,\n * because an untouched lead carries no status field at all and Firestore\n * cannot select on a field's absence; the closed statuses are always\n * written, so they can be counted, and what remains is open. See\n * `openLeadsFromCounts`.\n */\nexport const CRM_LEAD_CLOSED_STATUSES: readonly CrmLeadStatus[] =\n CRM_LEAD_STATUSES.filter((status) => !CRM_LEAD_OPEN_STATUSES.includes(status))\n\nexport function isCrmLeadStatus(value: unknown): value is CrmLeadStatus {\n return (\n typeof value === 'string' &&\n (CRM_LEAD_STATUSES as readonly string[]).includes(value)\n )\n}\n\n/**\n * The working state the CRM writes onto a lead document, beside what the\n * capture door wrote (`email`, `name`, `sources`, `submissionCount`,\n * `firstSeenAtMs`, `lastSeenAtMs`, the consent map).\n *\n * The four `converted*`/`dealId`/`companyId` fields are stamped ONLY by the\n * `crm/lead-convert` server route, in one write after the contact exists, so\n * a lead that carries `convertedContactId` names a contact that was really\n * created and a lead without it was never converted, whatever its status\n * says. `unqualifiedReason` travels with `status: 'unqualified'` and is the\n * one free-text field a report will want to read back.\n */\nexport interface CrmLeadFields extends CrmLeadProfile {\n status?: CrmLeadStatus\n /** The team member working the lead. */\n ownerUid?: string\n notes?: string\n unqualifiedReason?: string\n /** `orgs/{orgId}/contacts/{contactId}` — the person this lead became. */\n convertedContactId?: string\n convertedAtMs?: number\n /** The deal the conversion opened, when the converter asked for one. */\n dealId?: string\n /** The company the conversion created or linked, when it named one. */\n companyId?: string\n /**\n * The last verdict on the lead's address (AGL-3245) — bounced, blocked,\n * unsubscribed, do-not-contact — written by the platform's senders only,\n * never by a form or the lead's editor. Absent means nothing is known.\n */\n emailState?: EmailState\n /**\n * The campaigns the lead is filed under (AGL-3254): container ids from\n * `orgs/{orgId}/emailCampaigns`, at the top of the document the way a\n * form carries them (`campaign-membership.ts`), never names. Written by\n * the New lead drawer, the import, the bulk bar and a sequence's enroll;\n * handed to the contact's facet when the lead converts.\n */\n campaignIds?: string[]\n /**\n * The org's custom LEAD fields, keyed by each definition's `key`\n * (AGL-3272) — the same map a contact facet, a company and a deal keep,\n * judged against the definitions whose `object` is `lead`.\n *\n * It does NOT travel to the contact on convert. Keys are unique per\n * object, so a lead's `budget` and a contact's `budget` are two\n * definitions that may hold two types, and copying one map into the\n * other would write a value the receiving definition never agreed to.\n * Salesforce answers this with an explicit lead field mapping; until\n * there is one here the values stay on the lead, which a converted lead\n * is still read back for.\n */\n custom?: Record<string, CrmCustomValue>\n}\n\n/*==========================================\n * THE LEAD'S OWN PROFILE (AGL-3231).\n *\n * A Salesforce lead is a record of its own: it carries the person AND\n * their company as text — company, title, phone, website, address, the\n * source that produced it — and none of it is a contact until the lead\n * converts, when the convert step hands every field to the contact and the\n * account it creates. A lead that held only an address and a name could\n * not be worked without a contact beside it, which is how one person came\n * to sit in both lists; these fields are what let the lead stand alone.\n *\n * ## Text, not links\n *\n * `company` is the company's NAME, never a company id. A lead has not been\n * qualified, so the account it names may not deserve a record yet — a\n * thousand imported leads must not mint a thousand companies — and the\n * conversion is where the text becomes a link: it finds the company by\n * that name or by the address's domain, or creates it. The `companyId` on\n * `CrmLeadFields` is the conversion's stamp, written once, and never what\n * a lead carries while it is open.\n *\n * ## The same shapes the contact keeps\n *\n * The phone is E.164 through `normalizePhone`, the address is\n * `AglynPostalAddress` through `normalizeAddress`, tags are lower-cased and\n * capped as the contact's are, and the website is normalized as a\n * company's is — so the conversion copies values rather than translating\n * them, and a lead typed in the drawer and a contact typed in its drawer\n * refuse the same phone number in the same sentence.\n *\n * ## `leadSource` beside `sources`\n *\n * `sources` is the capture door's record of which SURFACES met the person\n * — `signup`, `booking`, `form:{id}`, `import` — and no file may rewrite\n * it. `leadSource` is what Salesforce calls Lead Source: one of the org's\n * own picklist values (\"Trade show\", \"Referral\" — see LEAD SOURCE IS A\n * PICKLIST below), reported on and filtered by, and never derived. Two\n * fields because they answer two questions, and a file that could write\n * the first would be rewriting the site's own history.\n *=========================================*/\n\n/** The longest text one profile field holds — the contact's own cap. */\nexport const CRM_LEAD_TEXT_MAX = 120\n/** The most tags one lead keeps — the contact's own cap. */\nexport const CRM_LEAD_TAGS_MAX = 20\n/** The longest note a lead keeps — the lead page's own cap. */\nexport const CRM_LEAD_NOTES_MAX = 4000\n\nexport interface CrmLeadProfile {\n /** The company's name, as text — see the block header. */\n company?: string\n jobTitle?: string\n /** E.164 — `normalizePhone` before writing. */\n phone?: string\n /** An http(s) URL — `normalizeCompanyWebsite` before writing. */\n website?: string\n address?: AglynPostalAddress | null\n /** Lower-cased, deduplicated, at most {@link CRM_LEAD_TAGS_MAX}. */\n tags?: string[]\n /**\n * Where the lead came from — Salesforce's Lead Source: the LABEL of one\n * of the org's lead source values (see {@link CrmPicklist}).\n */\n leadSource?: string\n}\n\n/** The profile's keys, in the order a card lists them. */\nexport const CRM_LEAD_PROFILE_KEYS = [\n 'company',\n 'jobTitle',\n 'phone',\n 'website',\n 'address',\n 'tags',\n 'leadSource',\n] as const satisfies readonly (keyof CrmLeadProfile)[]\n\nexport type CrmLeadProfileKey = (typeof CRM_LEAD_PROFILE_KEYS)[number]\n\n/**\n * A profile as a writer receives it: a key present is a value to store, a\n * key present and `null` is a field to clear, a key absent is left alone.\n * The PATCH semantics every CRM write has.\n */\nexport type CrmLeadProfilePatch = {\n [K in CrmLeadProfileKey]?: NonNullable<CrmLeadProfile[K]> | null\n}\n\n/** Why one field of a profile could not be read, under the field. */\nexport type CrmLeadProfileErrors = Partial<Record<CrmLeadProfileKey, string>>\n\n/** The sentence an unreadable phone number is refused with, under the field. */\nexport const CRM_LEAD_PHONE_REFUSAL =\n 'That phone number could not be read. Enter it with its country code, like ' +\n '+1 512 555 0107.'\n\n/** The sentence an unreadable website is refused with, under the field. */\nexport const CRM_LEAD_WEBSITE_REFUSAL = 'Enter a web address, like acme.com'\n\n/**\n * Tags as a lead stores them: lower-cased, trimmed, deduplicated, capped —\n * from an array or a comma-separated string, so a file cell and a form\n * field are one input.\n */\nexport function normalizeCrmLeadTags(value: unknown): string[] {\n const items = Array.isArray(value)\n ? value\n : typeof value === 'string'\n ? value.split(',')\n : []\n return [\n ...new Set(\n items\n .map((tag) =>\n String(tag ?? '')\n .trim()\n .slice(0, 40)\n .toLowerCase(),\n )\n .filter(Boolean),\n ),\n ].slice(0, CRM_LEAD_TAGS_MAX)\n}\n\n/**\n * Read a profile off an untrusted body, one field at a time.\n *\n * Every key the body names is answered: as a normalized value, as `null`\n * when the body cleared it (an empty string, `null`, an empty list, a blank\n * address), or as an error under the field when the value cannot be\n * stored as typed. Keys the body does not name are not in the answer, so\n * a caller can write exactly what was asked. A `website` is normalized the\n * way a company's is; a phone the way a contact's is; both refuse rather\n * than store a value the record would then render as a link to nowhere.\n */\nexport function normalizeCrmLeadProfile(\n input: Record<string, unknown> | null | undefined,\n): { patch: CrmLeadProfilePatch; errors: CrmLeadProfileErrors } {\n const body = input ?? {}\n const patch: CrmLeadProfilePatch = {}\n const errors: CrmLeadProfileErrors = {}\n const text = (key: 'company' | 'jobTitle' | 'leadSource') => {\n if (!(key in body)) return\n const value = String(body[key] ?? '')\n .trim()\n .replace(/\\s+/g, ' ')\n .slice(0, CRM_LEAD_TEXT_MAX)\n patch[key] = value || null\n }\n text('company')\n text('jobTitle')\n text('leadSource')\n if ('phone' in body) {\n const raw = String(body['phone'] ?? '').trim()\n if (!raw) patch.phone = null\n else {\n const phone = normalizePhone(raw)\n if (phone) patch.phone = phone\n else errors.phone = CRM_LEAD_PHONE_REFUSAL\n }\n }\n if ('website' in body) {\n const raw = String(body['website'] ?? '').trim()\n if (!raw) patch.website = null\n else {\n const website = normalizeCompanyWebsite(raw)\n if (website) patch.website = website\n else errors.website = CRM_LEAD_WEBSITE_REFUSAL\n }\n }\n if ('address' in body) {\n const raw = body['address']\n patch.address =\n raw && typeof raw === 'object' && !Array.isArray(raw)\n ? normalizeAddress(raw as AglynPostalAddress)\n : null\n }\n if ('tags' in body) {\n const tags = normalizeCrmLeadTags(body['tags'])\n patch.tags = tags.length ? tags : null\n }\n return { patch, errors }\n}\n\n/*==========================================\n * LEAD SOURCE IS A PICKLIST (AGL-3298).\n *\n * Salesforce's Lead Source is a restricted picklist: an admin keeps the\n * list of values, a rep picks one, and an import or an API write naming\n * anything else is refused. Here the list is one org document,\n * `orgs/{orgId}/crmPicklists/leadSource`, holding the values in order as\n * `{ id, label, active }` and an optional default for new records.\n *\n * ## Records store the LABEL\n *\n * A lead carries `leadSource: \"Trade show\"`, not an id. Every reader that\n * existed before the picklist — the CSV export, the REST resource, the\n * record facts, a saved view's filter, a report grouped by the field —\n * reads the text and keeps working, and a file exported from here\n * re-imports as is. What an id would have bought is a free rename; here a\n * rename (and a delete) REPLACES the old label on every record in the same\n * operation, the way Salesforce's own Replace does, so a report grouped\n * by the field follows the rename rather than splitting in two. The value's\n * `id` is what the list itself is keyed by — the default, a reorder, the\n * manage page's rows — and it never changes.\n *\n * ## Inactive, not gone\n *\n * A deactivated value leaves every picker but stays on the records that\n * hold it, shown with an \"(inactive)\" hint, and a write that keeps a\n * record's current value is never refused for it. A value is removed only\n * by Delete, which names the value its records move to.\n *\n * ## An org that never edited the list\n *\n * Has no document, and reads {@link CRM_LEAD_SOURCE_STARTER_LABELS} — a\n * small Salesforce-like set. The first edit on the manage page writes that\n * set down as the org's own, so a new org starts with a usable list and no\n * org-creation step has to seed it.\n *=========================================*/\n\n/** The picklists an org keeps, by document id — one per standard field. */\nexport const CRM_PICKLIST_IDS = ['leadSource'] as const\n\nexport type CrmPicklistId = (typeof CRM_PICKLIST_IDS)[number]\n\n/** The lead source value set's document id. */\nexport const CRM_LEAD_SOURCE_PICKLIST: CrmPicklistId = 'leadSource'\n\n/** The most values one picklist holds — a menu, not a table. */\nexport const CRM_PICKLIST_VALUES_MAX = 200\n\n/** One value of a picklist. `id` never changes; `label` is what records store. */\nexport interface CrmPicklistValue {\n id: string\n label: string\n active: boolean\n}\n\n/** A picklist as stored and as every reader takes it. */\nexport interface CrmPicklist {\n /** In the order every picker lists them. */\n values: CrmPicklistValue[]\n /** The value a new record starts with, by id — `null` for none. */\n defaultValueId: string | null\n}\n\n/**\n * The values an org that has never edited its list reads — see the block\n * header. Salesforce's own standard set, trimmed to what a small team\n * recognizes.\n */\nexport const CRM_LEAD_SOURCE_STARTER_LABELS: readonly string[] = [\n 'Web',\n 'Phone inquiry',\n 'Referral',\n 'Partner',\n 'Purchased list',\n 'Trade show',\n 'Other',\n]\n\n/** A label as a picklist stores it: trimmed, single-spaced, capped. */\nexport function normalizeCrmPicklistLabel(value: unknown): string {\n return String(value ?? '')\n .trim()\n .replace(/\\s+/g, ' ')\n .slice(0, CRM_LEAD_TEXT_MAX)\n}\n\n/** The key two labels are compared by: case and spacing do not make a new value. */\nfunction picklistKey(label: string): string {\n return normalizeCrmPicklistLabel(label).toLowerCase()\n}\n\n/**\n * A new value's id: the label as a slug, suffixed until it is not one of\n * `taken`. Derived rather than random so a seeded list reads the same in\n * every org, and never reused, because a default names a value by it.\n */\nexport function crmPicklistValueId(label: string, taken: Iterable<string>): string {\n const used = new Set(taken)\n const base =\n normalizeCrmPicklistLabel(label)\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40) || 'value'\n let id = base\n for (let n = 2; used.has(id); n += 1) id = `${base}-${n}`\n return id\n}\n\n/** A picklist of active values from labels, in order, with no default. */\nexport function crmPicklistFromLabels(labels: readonly string[]): CrmPicklist {\n const values: CrmPicklistValue[] = []\n const seen = new Set<string>()\n for (const raw of labels) {\n const label = normalizeCrmPicklistLabel(raw)\n if (!label || seen.has(picklistKey(label))) continue\n seen.add(picklistKey(label))\n values.push({ id: crmPicklistValueId(label, values.map((value) => value.id)), label, active: true })\n }\n return { values: values.slice(0, CRM_PICKLIST_VALUES_MAX), defaultValueId: null }\n}\n\n/**\n * A stored document as a picklist, or `null` for one that holds none.\n *\n * Tolerant, because the document is client-written: a value with no label\n * is dropped, a second value with the same label (in any case) or the same\n * id is dropped, `active` is true unless it is `false`, and a default that\n * names no value is no default.\n */\nexport function normalizeCrmPicklist(raw: unknown): CrmPicklist | null {\n if (!raw || typeof raw !== 'object') return null\n const stored = raw as Record<string, unknown>\n if (!Array.isArray(stored['values'])) return null\n const values: CrmPicklistValue[] = []\n const labels = new Set<string>()\n const ids = new Set<string>()\n for (const entry of stored['values']) {\n if (!entry || typeof entry !== 'object') continue\n const value = entry as Record<string, unknown>\n const label = normalizeCrmPicklistLabel(value['label'])\n if (!label || labels.has(picklistKey(label))) continue\n let id = String(value['id'] ?? '').trim().slice(0, 64)\n if (!id || id.includes('/') || ids.has(id)) id = crmPicklistValueId(label, ids)\n labels.add(picklistKey(label))\n ids.add(id)\n values.push({ id, label, active: value['active'] !== false })\n if (values.length >= CRM_PICKLIST_VALUES_MAX) break\n }\n const defaultId = String(stored['defaultValueId'] ?? '')\n return {\n values,\n defaultValueId: ids.has(defaultId) ? defaultId : null,\n }\n}\n\n/** The org's lead source list as every reader should take it — see the block header. */\nexport function effectiveCrmLeadSourcePicklist(raw: unknown): CrmPicklist {\n return normalizeCrmPicklist(raw) ?? crmPicklistFromLabels(CRM_LEAD_SOURCE_STARTER_LABELS)\n}\n\n/** The value a label names, in any case or spacing — `null` for none. */\nexport function crmPicklistValueByLabel(\n picklist: CrmPicklist,\n label: unknown,\n): CrmPicklistValue | null {\n const key = picklistKey(String(label ?? ''))\n if (!key) return null\n return picklist.values.find((value) => picklistKey(value.label) === key) ?? null\n}\n\n/** The values a picker offers. */\nexport function crmPicklistActiveValues(picklist: CrmPicklist): CrmPicklistValue[] {\n return picklist.values.filter((value) => value.active)\n}\n\n/** The label a new record starts with — only while the default value is active. */\nexport function crmPicklistDefaultLabel(picklist: CrmPicklist): string | null {\n const value = picklist.values.find((entry) => entry.id === picklist.defaultValueId)\n return value?.active ? value.label : null\n}\n\n/**\n * The sentence a lead source outside the list is refused with, naming what\n * the list allows. A long list is cut at twenty names so the sentence stays\n * one a person can read.\n */\nexport function crmLeadSourceRefusal(picklist: CrmPicklist): string {\n const active = crmPicklistActiveValues(picklist).map((value) => value.label)\n if (!active.length) {\n return 'This organization has no active lead sources. Add one under CRM › Fields › Leads.'\n }\n const shown = active.slice(0, 20).join(', ')\n const more = active.length > 20 ? `, and ${active.length - 20} more` : ''\n return `Lead source must be one of: ${shown}${more}.`\n}\n\n/**\n * Whether a write may store `value` as a record's lead source, and the\n * label it stores.\n *\n * - Blank or `null` clears the field — lead source is never required.\n * - An ACTIVE value, in any case or spacing, stores that value's label.\n * - The record's `current` value is kept even when it is inactive or no\n * longer listed: re-saving a record must never be refused for a value\n * somebody else deactivated.\n * - Anything else is refused with {@link crmLeadSourceRefusal}.\n */\nexport function judgeCrmLeadSource(\n picklist: CrmPicklist,\n value: unknown,\n current?: unknown,\n): { ok: true; value: string | null } | { ok: false; error: string } {\n const label = normalizeCrmPicklistLabel(value)\n if (!label) return { ok: true, value: null }\n const held = normalizeCrmPicklistLabel(current)\n if (held && picklistKey(held) === picklistKey(label)) return { ok: true, value: held }\n const match = crmPicklistValueByLabel(picklist, label)\n if (match?.active) return { ok: true, value: match.label }\n return { ok: false, error: crmLeadSourceRefusal(picklist) }\n}\n\n/** One option of a lead source select. */\nexport interface CrmPicklistOption {\n label: string\n /** Listed but deactivated — shown with an \"(inactive)\" hint. */\n inactive: boolean\n /** Not in the list at all — a value written before the list existed. */\n unlisted: boolean\n}\n\n/**\n * What a select offers for a record holding `current`: every active value\n * in order, then the record's own value when the list would not otherwise\n * show it, so the select keeps what the record holds until it is changed.\n */\nexport function crmPicklistOptions(\n picklist: CrmPicklist,\n current?: unknown,\n): CrmPicklistOption[] {\n const options: CrmPicklistOption[] = crmPicklistActiveValues(picklist).map((value) => ({\n label: value.label,\n inactive: false,\n unlisted: false,\n }))\n const held = normalizeCrmPicklistLabel(current)\n if (held && !options.some((option) => picklistKey(option.label) === picklistKey(held))) {\n const listed = crmPicklistValueByLabel(picklist, held)\n options.push({ label: held, inactive: Boolean(listed), unlisted: !listed })\n }\n return options\n}\n\n/**\n * Where a label sorts: its value's position in the list, unlisted values\n * after every listed one, and a record with none last. The order the admin\n * chose is the order a sort by the field reads in, as Salesforce's does.\n */\nexport function crmPicklistRank(picklist: CrmPicklist, label: unknown): number {\n const text = normalizeCrmPicklistLabel(label)\n if (!text) return Number.MAX_SAFE_INTEGER\n const at = picklist.values.findIndex((value) => picklistKey(value.label) === picklistKey(text))\n return at < 0 ? CRM_PICKLIST_VALUES_MAX : at\n}\n\n/** The name a lead is listed under: the name it carries, else its address. */\nexport function crmLeadDisplayName(\n lead: Record<string, unknown> | null | undefined,\n): string {\n const name = String(lead?.['name'] ?? '').trim()\n return name || String(lead?.['email'] ?? '').trim()\n}\n\n/**\n * A lead's status as the list and the filter should read it.\n *\n * `new` for a lead that carries no status at all — every lead captured\n * before the CRM existed, and every lead the capture door writes today,\n * because `addHostLead` stamps none. Reading the absence as `new` is what\n * lets the section open on the leads a site already holds rather than on an\n * empty list until each one has been touched once. A stored value the union\n * does not name also reads as `new`: it is a document some other writer\n * produced, and refusing to list it would hide a person.\n */\nexport function crmLeadStatus(\n lead: Pick<CrmLeadFields, 'status'> | null | undefined,\n): CrmLeadStatus {\n const status = lead?.status\n return isCrmLeadStatus(status) ? status : 'new'\n}\n\n/** Whether a lead still needs working — see {@link CRM_LEAD_OPEN_STATUSES}. */\nexport function isCrmLeadOpen(\n lead: Pick<CrmLeadFields, 'status'> | null | undefined,\n): boolean {\n return CRM_LEAD_OPEN_STATUSES.includes(crmLeadStatus(lead))\n}\n\n/*==========================================\n * SAVED VIEWS (AGL-2617) — a list the way one person works it.\n *\n * \"My open leads in Texas\" is a list a rep opens every morning, and it is\n * three things at once: the filters that pick the people, the columns they\n * want to see about them, and the order they want them in. A `ContactSegment`\n * kept the first of those for two dimensions — tags and capture sources —\n * and nothing kept the rest, so every reload put the reader back at the\n * whole list. A view keeps all three, for every list in the CRM, under a\n * name, and is addressable: `?view=<id>` on the section's own URL opens it.\n *\n * ## The filters are the LIST'S grammar, not a second one\n *\n * A clause here is `{ field, op, value }` — structurally the\n * `ListFilterRequest` the shared list-filter grammar already speaks, the\n * request a grid's filter panel produces and the one the query translator\n * and the in-memory matcher both read. A view stores a list of them and\n * nothing more, so a view can only ask what the list can already answer,\n * and a list that gains a filterable field gains it for its views at no\n * cost. The type is restated here rather than imported because this module\n * is pure data and the grammar's home is a UI library; the shape is the\n * contract, and `normalizeCrmViewFilters` is what holds a stored document\n * to it.\n *\n * ## Mine, and shared\n *\n * A view is one person's working arrangement until they say otherwise, so\n * it lists only for its owner; `shared` puts it in front of everyone who\n * can read the section. Both are stamped `visibleTo` like every CRM row,\n * because a view is a description of the people it selects and answers to\n * the same authority they do — a private view is hidden by the LISTING, not\n * by the rules, and the rules let the creator or an org-wide member change\n * or remove it.\n *\n * ## The default is the reader's, not the view's\n *\n * Which view a section opens on is a preference of one person for one\n * organization, so it lives on the reader's own profile document beside\n * their notification mutes rather than on the view — a shared view somebody\n * else made their default must not become everybody's.\n *=========================================*/\n\n/** The lists a view can be saved for — every CRM section that has one. */\nexport const CRM_VIEW_SECTIONS = [\n 'contacts',\n 'companies',\n 'deals',\n 'tasks',\n 'leads',\n] as const\n\nexport type CrmViewSection = (typeof CRM_VIEW_SECTIONS)[number]\n\nexport function isCrmViewSection(value: unknown): value is CrmViewSection {\n return (\n typeof value === 'string' &&\n (CRM_VIEW_SECTIONS as readonly string[]).includes(value)\n )\n}\n\n/**\n * One filter a view carries: a field, an operator and a value, exactly as a\n * list's own filter panel would send them to its query.\n *\n * `label` is the human reading of an OPAQUE value — a team member's name\n * beside their uid, a company's name beside its id — kept on the clause so\n * the chip that shows it can say \"Owner is Dana\" without a read. Display\n * only: nothing matches on it, and a clause without one shows its value.\n */\nexport interface CrmViewFilterClause {\n field: string\n op: string\n value: string\n label?: string\n}\n\n/** The column a view orders by, and which way. */\nexport interface CrmViewSort {\n field: string\n direction: 'asc' | 'desc'\n}\n\n/**\n * What a view holds about a list — the part a section reads and writes,\n * without the name and the ownership around it.\n *\n * `columns` is the VISIBLE set, by column field, or empty for the list's\n * own default; `sort` is one column or none, because a table sorts by one.\n */\nexport interface CrmViewState {\n filters: CrmViewFilterClause[]\n columns: string[]\n sort: CrmViewSort | null\n}\n\n/** `orgs/{orgId}/crmViews/{viewId}`. */\nexport interface CrmSavedView extends CrmScoped, CrmViewState {\n section: CrmViewSection\n name: string\n /** Whose working arrangement this is — the creator, and the one member the rules let change it. */\n ownerUid: string\n createdByUid: string\n /** Listed for everybody who can read the section, rather than the owner alone. */\n shared: boolean\n}\n\nexport const CRM_VIEW_NAME_MAX = 60\n/**\n * The most clauses one view carries. A bound on the predicate, not the\n * audience: the list matches most of them in memory over a bounded window,\n * and a stored array has no natural end.\n */\nexport const CRM_VIEW_MAX_FILTERS = 20\nexport const CRM_VIEW_MAX_COLUMNS = 60\n/** Filter operators that carry no value, so an empty `value` is not a dropped clause. */\nconst VALUELESS_VIEW_OPS: ReadonlySet<string> = new Set(['isEmpty', 'isNotEmpty'])\n\nexport const EMPTY_CRM_VIEW_STATE: CrmViewState = Object.freeze({\n filters: [],\n columns: [],\n sort: null,\n}) as CrmViewState\n\nconst asViewText = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * A stored view's filters, held to the clause shape.\n *\n * Strict about what it keeps: a clause with no field or no operator is not\n * a filter, and a valued operator with no value would read as a filter\n * that matches nothing — so both are dropped rather than kept, for the same\n * reason the dynamic-list rule drops an empty branch: a view must not\n * quietly select a different population than the one it reads as.\n */\nexport function normalizeCrmViewFilters(value: unknown): CrmViewFilterClause[] {\n if (!Array.isArray(value)) return []\n const clauses: CrmViewFilterClause[] = []\n for (const raw of value) {\n const entry = (raw ?? {}) as Record<string, unknown>\n const field = asViewText(entry['field'], 80)\n const op = asViewText(entry['op'], 40)\n const text = asViewText(entry['value'], 500)\n if (!field || !op) continue\n if (!text && !VALUELESS_VIEW_OPS.has(op)) continue\n const label = asViewText(entry['label'], 120)\n clauses.push({ field, op, value: text, ...(label ? { label } : {}) })\n if (clauses.length >= CRM_VIEW_MAX_FILTERS) break\n }\n return clauses\n}\n\nexport function normalizeCrmViewColumns(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n for (const raw of value) {\n const column = asViewText(raw, 80)\n if (column) seen.add(column)\n if (seen.size >= CRM_VIEW_MAX_COLUMNS) break\n }\n return [...seen]\n}\n\nexport function normalizeCrmViewSort(value: unknown): CrmViewSort | null {\n const entry = (value ?? null) as Record<string, unknown> | null\n const field = entry ? asViewText(entry['field'], 80) : ''\n if (!field) return null\n return { field, direction: entry?.['direction'] === 'desc' ? 'desc' : 'asc' }\n}\n\n/** A stored document's three list-facing fields, held to their shapes. */\nexport function normalizeCrmViewState(value: unknown): CrmViewState {\n const entry = (value ?? {}) as Record<string, unknown>\n return {\n filters: normalizeCrmViewFilters(entry['filters']),\n columns: normalizeCrmViewColumns(entry['columns']),\n sort: normalizeCrmViewSort(entry['sort']),\n }\n}\n\n/**\n * Whether two states describe the same list — what \"unsaved changes\" means.\n *\n * Clause order matters (a list applies them in order, and the first one the\n * query can serve is the one it serves); the label on a clause does not,\n * because nothing matches on it.\n */\nexport function crmViewStateEquals(a: CrmViewState, b: CrmViewState): boolean {\n if (a.filters.length !== b.filters.length) return false\n for (let index = 0; index < a.filters.length; index += 1) {\n const left = a.filters[index]\n const right = b.filters[index]\n if (\n left.field !== right.field ||\n left.op !== right.op ||\n left.value !== right.value\n ) {\n return false\n }\n }\n if (a.columns.length !== b.columns.length) return false\n if (a.columns.some((column, index) => column !== b.columns[index])) {\n return false\n }\n if (!a.sort || !b.sort) return a.sort === b.sort\n return a.sort.field === b.sort.field && a.sort.direction === b.sort.direction\n}\n\n/**\n * Whether a view belongs in this reader's menu: their own, or one somebody\n * shared. The rules admit the read either way — see the module note — so\n * this is what keeps one person's private arrangement out of another's\n * list.\n */\nexport function crmViewIsListed(\n view: Pick<CrmSavedView, 'shared' | 'ownerUid'>,\n uid: string | null | undefined,\n): boolean {\n return view.shared === true || (Boolean(uid) && view.ownerUid === uid)\n}\n\n/**\n * The Contacts list's filterable fields, by the names a view clause carries.\n *\n * A contract between three readers that cannot import one another: the\n * list's own filter grammar (which declares these as its columns), a saved\n * view (which stores them), and the dynamic-list translator below in\n * `dynamic-list-rule.ts` (which turns them into audience dimensions). Named\n * once so a rename on the list cannot silently stop an audience matching.\n * `custom` is a PREFIX: a custom field's column is the prefix and its key.\n */\nexport const CRM_CONTACT_VIEW_FIELDS = {\n tags: 'tags',\n source: 'source',\n owner: 'ownerUid',\n stage: 'lifecycleStage',\n company: 'companyId',\n createdAt: 'createdAt',\n updatedAt: 'updatedAt',\n orders: 'ordersCount',\n ltv: 'ltvCents',\n custom: 'custom_',\n} as const\n\n/** The column a custom contact field filters and shows as. */\nexport const crmContactCustomColumn = (key: string): string =>\n `${CRM_CONTACT_VIEW_FIELDS.custom}${key}`\n\n/** The field key a custom column stands for, or `null` for any other column. */\nexport function crmContactCustomKey(column: string): string | null {\n const prefix = CRM_CONTACT_VIEW_FIELDS.custom\n return column.startsWith(prefix) && column.length > prefix.length\n ? column.slice(prefix.length)\n : null\n}\n\n/**\n * A stored phone number as the `href` of a click-to-call link, or `null`\n * when the value is not one (AGL-2661).\n *\n * The console stores E.164 (`normalizePhone` runs before every write), and\n * a `tel:` URL wants exactly that: digits with a leading `+`, nothing else.\n * Spaces, dots, dashes and parentheses are dropped rather than refused\n * because a value written before normalization existed may carry them;\n * anything that is not a run of digits after that — a word, an extension\n * typed as `x123` — is refused, because a dialer handed it would ring\n * nothing and the link would be a lie.\n */\nexport function crmTelHref(phone: unknown): string | null {\n const text = String(phone ?? '').trim()\n if (!text) return null\n const compact = text.replace(/[\\s().-]/g, '')\n return /^\\+?\\d{3,20}$/.test(compact) ? `tel:${compact}` : null\n}\n\nconst CRM_VIEW_TAGS_FIELD = CRM_CONTACT_VIEW_FIELDS.tags\nconst CRM_VIEW_SOURCE_FIELD = CRM_CONTACT_VIEW_FIELDS.source\n\n/**\n * A saved segment, as the filters of a contacts view.\n *\n * A segment is \"any of these tags AND any of these sources\", so each\n * becomes one `isAnyOf` clause — OR within, AND across, which is the\n * reading `contactMatchesSegment` gives it. A single tag is emitted as\n * `contains` instead: that operator is the one the contacts query can serve\n * from the index, so a one-tag segment opened as a view reaches the whole\n * collection rather than the loaded window. The values are the segment's\n * own, joined the way the grammar's `isAnyOf` splits them.\n */\nexport function crmViewFiltersFromSegment(\n segment: Pick<ContactSegment, 'tags' | 'sources'>,\n): CrmViewFilterClause[] {\n const tags = (segment.tags ?? []).map((tag) => tag.trim()).filter(Boolean)\n const sources = (segment.sources ?? []).filter(\n (source) => source in CONTACT_SOURCE_LABELS,\n )\n return [\n ...(tags.length === 1\n ? [{ field: CRM_VIEW_TAGS_FIELD, op: 'contains', value: tags[0] }]\n : tags.length\n ? [{ field: CRM_VIEW_TAGS_FIELD, op: 'isAnyOf', value: tags.join(',') }]\n : []),\n ...(sources.length === 1\n ? [{ field: CRM_VIEW_SOURCE_FIELD, op: 'equals', value: sources[0] }]\n : sources.length\n ? [\n {\n field: CRM_VIEW_SOURCE_FIELD,\n op: 'isAnyOf',\n value: sources.join(','),\n },\n ]\n : []),\n ]\n}\n\n/**\n * The other direction: the segment a view's filters describe, or `null`\n * when the view carries no tag or source clause a segment could hold.\n *\n * Only the two dimensions a segment has are read, and only through the\n * operators that mean \"has one of these\"; a `name startsWith` or an owner\n * clause is not a segment's to keep. What comes back is what \"Save as\n * segment\" writes, so a campaign audience built from it selects exactly the\n * tags and sources the reader could see on the chips.\n */\nexport function crmViewSegmentFilters(\n filters: readonly CrmViewFilterClause[],\n): Pick<ContactSegment, 'tags' | 'sources'> | null {\n const tags = new Set<string>()\n const sources = new Set<ContactSource>()\n const split = (value: string) =>\n value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n for (const clause of filters) {\n if (clause.field === CRM_VIEW_TAGS_FIELD) {\n if (clause.op === 'contains') tags.add(clause.value.trim().toLowerCase())\n else if (clause.op === 'isAnyOf') {\n for (const tag of split(clause.value)) tags.add(tag.toLowerCase())\n }\n } else if (clause.field === CRM_VIEW_SOURCE_FIELD) {\n const values = clause.op === 'equals' ? [clause.value.trim()] : clause.op === 'isAnyOf' ? split(clause.value) : []\n for (const source of values) {\n if (source in CONTACT_SOURCE_LABELS) sources.add(source as ContactSource)\n }\n }\n }\n tags.delete('')\n if (!tags.size && !sources.size) return null\n return {\n ...(tags.size ? { tags: [...tags] } : {}),\n ...(sources.size ? { sources: [...sources] } : {}),\n }\n}\n\n/**\n * `users/{uid}.crmDefaultViews` — `{ [orgId]: { [section]: viewId } }`.\n *\n * On the reader's own profile document, beside `notificationPrefs`, because\n * it is the same kind of fact: how one person wants their console to\n * behave, owned and written by them alone. Keyed by org first because one\n * account sits on several organizations and a view id is only meaningful\n * inside the one that holds it.\n */\nexport const CRM_DEFAULT_VIEWS_FIELD = 'crmDefaultViews'\n\n/** The view a section opens on for this reader in this org, or none. */\nexport function crmDefaultViewId(\n profile: Record<string, unknown> | null | undefined,\n orgId: string | null | undefined,\n section: CrmViewSection,\n): string | null {\n if (!profile || !orgId) return null\n const byOrg = profile[CRM_DEFAULT_VIEWS_FIELD]\n if (!byOrg || typeof byOrg !== 'object' || Array.isArray(byOrg)) return null\n const bySection = (byOrg as Record<string, unknown>)[orgId]\n if (!bySection || typeof bySection !== 'object' || Array.isArray(bySection)) {\n return null\n }\n const viewId = (bySection as Record<string, unknown>)[section]\n return typeof viewId === 'string' && viewId.trim() ? viewId : null\n}\n\n/**\n * The merge patch that sets — or, with `null`, clears — one default.\n *\n * Shaped for a merged write so the other organizations' and sections'\n * defaults on the same document survive it; `null` rather than a delete\n * sentinel because this module carries no Firestore, and the reader above\n * treats a null as no default.\n */\nexport function crmDefaultViewPatch(\n orgId: string,\n section: CrmViewSection,\n viewId: string | null,\n): Record<string, unknown> {\n return { [CRM_DEFAULT_VIEWS_FIELD]: { [orgId]: { [section]: viewId } } }\n}\n\n/*==========================================\n * FILES ON A RECORD (AGL-2662)\n *=========================================*/\n\n/** The field a record's attached files are stored under, on all three. */\nexport const CRM_MEDIA_IDS_FIELD = 'mediaIds'\n\n/**\n * The most files one record may carry.\n *\n * A platform ceiling in the family of {@link CRM_ACTIVITIES_PER_RECORD_CEILING}\n * rather than a plan dimension: attachments are bounded by human effort, and\n * this is the bound. It is also what the Firestore rules can actually check\n * — a rule can assert a list and its size, and cannot walk one — so the\n * number is enforced rather than merely documented.\n */\nexport const CRM_MEDIA_IDS_MAX = 20\n\n/**\n * A stored or submitted attachment list, cleaned.\n *\n * Ids rather than URLs, deduplicated, trimmed, non-empty and capped. Written\n * through by every path that sets the field — the console card, the REST\n * write — so a record cannot hold a list one of them would refuse.\n *\n * An id is a media DOCUMENT id, resolved against the organization's library\n * (`orgs/{orgId}/media/{mediaId}`) at read time. Storing the id and not the\n * URL is what lets a file move between folders, and what keeps a private\n * asset behind the signed CDN door rather than pinned to a raw storage URL\n * that names its current location.\n */\nexport function normalizeCrmMediaIds(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return [\n ...new Set(\n value\n .map((id) => String(id ?? '').trim())\n .filter((id) => id.length > 0 && id.length <= 200),\n ),\n ].slice(0, CRM_MEDIA_IDS_MAX)\n}\n\n/*==========================================\n * WHAT A CRM LIST QUERIES BY (AGL-3321).\n *\n * Every filter and every search word on the Leads, Contacts, Companies,\n * Deals and Tasks lists is a predicate on the list's Firestore query, and a\n * query can only ask about a field a WRITER stored in the shape the query\n * asks for. Three kinds of field exist for that and for nothing else:\n *\n * `searchTokens` word prefixes of what the list's search box reads\n * (a lead's name, address, company, title and tags;\n * a deal's title), for `array-contains` on one typed\n * word — see `nameSearchTokens`.\n * `scopedSearchTokens` the same tokens behind each of the record's\n * `visibleTo` tokens (`host:abc~acme`). A list under\n * a site already spends its one array clause on\n * `visibleTo array-contains-any`, so its search asks\n * `array-contains-any` over the site's tokens joined\n * to the word — one clause answering both.\n * keys a normalized scalar where the stored value cannot\n * be asked as it is: a lead's status (absent on a\n * capture), its lead source (compared the way the\n * picklist compares labels), a verdict on the\n * address (`none` when there is none) and a contact's\n * per-holder facet values (`facetKeys`).\n *\n * All of them are DERIVED: a pure function of the document, computed here\n * and nowhere else, and written by every path that writes one of its inputs\n * — see `CRM_LIST_FIELD_INPUTS` — so a record a list cannot find is a writer\n * that forgot, never a second formula that disagrees. A server writer that\n * holds the whole document spreads `crmListFields`; one that patched a field\n * restamps the record from what was stored (`restampCrmListFields` in the\n * admin library); the one-time backfill (`backfill-crm-list-fields.mjs`)\n * restates them for a plain Node script and is held to the same worked\n * examples (`tools/scripts/lib/org-record-list-fields.fixtures.json`), which this\n * library's spec asserts too.\n *=========================================*/\n\n/** The search box's word-prefix tokens, on every CRM record a list searches. */\nexport const CRM_SEARCH_TOKENS_FIELD = 'searchTokens'\n/** The same tokens behind each `visibleTo` token — see the block header. */\nexport const CRM_SCOPED_SEARCH_TOKENS_FIELD = 'scopedSearchTokens'\n/** What joins a scope token to a search token — the platform's one join (`SCOPED_SEARCH_JOIN`). */\nexport const CRM_SCOPED_SEARCH_JOIN = SCOPED_SEARCH_JOIN\n/**\n * The most search tokens one record keeps. A name, an address, a company\n * and a title are a few dozen prefixes; the cap is what keeps a record with\n * twenty long tags from spending hundreds of index entries on them.\n */\nexport const CRM_SEARCH_TOKENS_MAX = 200\n\n/** The collections whose lists query the fields below. */\nexport type CrmListCollection = 'leads' | 'contacts' | 'companies' | 'deals' | 'crmTasks'\n\nconst SEARCH_KEY = nameSearchKey\n\n/**\n * The word-prefix tokens of several values, merged: every prefix (up to\n * twelve characters) of every whitespace-separated word, lower-cased —\n * exactly `nameSearchTokens` per value — plus, for an email address, the\n * words it is made of, so `acme` finds `dana@acme.com` and `dana` does too.\n */\nexport function crmSearchTokens(values: readonly unknown[]): string[] {\n const tokens = new Set<string>()\n const addWord = (word: string) => {\n const capped = word.slice(0, NAME_TOKEN_MAX_PREFIX)\n for (let end = 1; end <= capped.length; end += 1) {\n if (tokens.size >= CRM_SEARCH_TOKENS_MAX) return\n tokens.add(capped.slice(0, end))\n }\n }\n const addText = (text: string) => {\n const key = SEARCH_KEY(text)\n if (!key) return\n for (const word of key.split(' ')) {\n if (!word) continue\n addWord(word)\n // An address, a domain or a hyphenated name is also its parts.\n if (/[@.+_-]/.test(word)) {\n for (const part of word.split(/[@.+_-]+/)) if (part) addWord(part)\n }\n }\n }\n for (const value of values) {\n if (Array.isArray(value)) {\n for (const entry of value) if (typeof entry === 'string') addText(entry)\n } else if (typeof value === 'string') {\n addText(value)\n }\n }\n return [...tokens]\n}\n\n/**\n * The two search fields for a record whose search reads `values`: the\n * tokens, and the same tokens behind each of its scope tokens through the\n * platform's one scoped writer (`scopedSearchTokens`).\n */\nexport function crmSearchFields(\n visibleTo: unknown,\n values: readonly unknown[],\n): { searchTokens: string[]; scopedSearchTokens: string[] } {\n const searchTokens = crmSearchTokens(values)\n return { searchTokens, scopedSearchTokens: scopedSearchTokens(visibleTo, searchTokens) }\n}\n\n/**\n * The address verdict as a list asks it (AGL-3245): the state's status, or\n * `none` when nothing is known — stored rather than left absent, because a\n * query cannot find a field's absence and \"Nothing known\" is a filter.\n */\nexport const CRM_EMAIL_STATUS_FIELD = 'emailStatus'\nexport const CRM_EMAIL_STATUS_NONE = 'none'\n\nexport function crmEmailStatusKey(record: Record<string, unknown> | null | undefined): string {\n const raw = record?.['emailState']\n const status =\n raw && typeof raw === 'object' ? (raw as Record<string, unknown>)['status'] : undefined\n // A status outside the vocabulary reads as nothing known, as `readEmailState` reads it.\n return isEmailStateStatus(status) ? status : CRM_EMAIL_STATUS_NONE\n}\n\n/** A lead's lead source as the list compares it: the picklist's key, `null` for none. */\nexport const CRM_LEAD_SOURCE_KEY_FIELD = 'leadSourceKey'\n\nexport function crmLeadSourceKey(value: unknown): string | null {\n const key = SEARCH_KEY(normalizeCrmPicklistLabel(value))\n return key || null\n}\n\n/** What a lead's search box reads (AGL-3246). */\nexport const CRM_LEAD_SEARCH_SOURCES = ['name', 'email', 'company', 'jobTitle', 'tags'] as const\n\n/**\n * A lead's campaigns behind each of its `visibleTo` tokens\n * (`host:abc~{campaignId}`), for the Leads list's Campaign filter under a\n * site: there the scope clause is the query's one array clause, and a\n * campaign asked of these keys answers both at once — the way the search\n * folds into the scope through `scopedSearchTokens`.\n */\nexport const CRM_LEAD_SCOPED_CAMPAIGNS_FIELD = 'scopedCampaignIds'\n\nfunction leadCampaignIds(lead: Record<string, unknown>): string[] {\n const raw = lead['campaignIds']\n return Array.isArray(raw)\n ? raw.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n : []\n}\n\n/**\n * Every field the Leads list queries by, from the lead as stored (or as it\n * will be once written): the search tokens, its campaigns behind its scope,\n * the status — `new` for a lead nobody has touched, written so `status in\n * [new, working]` finds it — the lead source key and the address verdict.\n */\nexport function crmLeadListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n scopedCampaignIds: string[]\n status: CrmLeadStatus\n leadSourceKey: string | null\n emailStatus: string\n} {\n const lead = record as Record<string, unknown>\n return {\n ...crmSearchFields(\n lead['visibleTo'],\n CRM_LEAD_SEARCH_SOURCES.map((field) => lead[field]),\n ),\n scopedCampaignIds: scopedSearchTokens(lead['visibleTo'], leadCampaignIds(lead)),\n status: crmLeadStatus(lead as Pick<CrmLeadFields, 'status'>),\n leadSourceKey: crmLeadSourceKey(lead['leadSource']),\n emailStatus: crmEmailStatusKey(lead),\n }\n}\n\n/** What a company's search box reads: its name and its domain. */\nexport const CRM_COMPANY_SEARCH_SOURCES = ['name', 'domain'] as const\n\nexport function crmCompanyListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n} {\n const company = record as Record<string, unknown>\n return crmSearchFields(\n company['visibleTo'],\n CRM_COMPANY_SEARCH_SOURCES.map((field) => company[field]),\n )\n}\n\n/**\n * What a deal's search box reads: its title (AGL-3315) — as word prefixes,\n * and as `titleLower`, the whole title's key, which a reader whose access\n * is some sites searches by its start.\n */\nexport function crmDealListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n titleLower: string\n} {\n const deal = record as Record<string, unknown>\n return {\n ...crmSearchFields(deal['visibleTo'], [deal['title']]),\n titleLower: SEARCH_KEY(typeof deal['title'] === 'string' ? deal['title'] : ''),\n }\n}\n\n/** What a task's search box reads: its title. */\nexport function crmTaskListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n} {\n const task = record as Record<string, unknown>\n return crmSearchFields(task['visibleTo'], [task['title']])\n}\n\n/*------------------------------------------\n * A CONTACT'S FACET KEYS.\n *\n * An owner, a stage, a source, a company, a tag and a custom value live on\n * a holder's facet — `facets.{groupId}` — so a `where` on one would be a path\n * per holder and an index per holder. `facetKeys` flattens them into one\n * array a query can ask `array-contains` of:\n *\n * `{groupId}:{field}={value}` this holder's value\n * `{groupId}:{field}` this holder has one (for \"is set\")\n * `*:{field}={value}`, `*:{field}` any holder's, for the organization level\n *\n * `field` is `owner`, `stage`, `source`, `company`, `tag` or `custom.{key}`,\n * plus two presence-only fields: `orders` for a holder the person has bought\n * from, and `ltv` for one they are worth something to — the per-holder\n * figures a range cannot reach, asked only whether there are any.\n * A text value is keyed lower-cased and single-spaced; a number or a flag\n * as its string. Under a site the viewing group's keys are asked; at the\n * organization level, where no group is viewing, the `*` keys are.\n *-----------------------------------------*/\n\nexport const CRM_CONTACT_FACET_KEYS_FIELD = 'facetKeys'\n/** The group a key names at the organization level: any holder. */\nexport const CRM_FACET_KEY_ANY_GROUP = '*'\n/** The most facet keys one contact keeps. */\nexport const CRM_FACET_KEYS_MAX = 600\n\n/** The facet fields a contact list filters by, as their keys name them. */\nexport type CrmFacetKeyField =\n | 'owner'\n | 'stage'\n | 'source'\n | 'company'\n | 'tag'\n | 'orders'\n | 'ltv'\n | `custom.${string}`\n\n/** A facet value as its key spells it, or `null` for a value a key cannot hold. */\nexport function crmFacetKeyValue(value: unknown): string | null {\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null\n if (typeof value !== 'string') return null\n const key = SEARCH_KEY(value).slice(0, 120)\n return key || null\n}\n\n/** One facet key: `{group}:{field}` for presence, `{group}:{field}={value}` for a value. */\nexport function crmFacetKey(group: string, field: CrmFacetKeyField, value?: string): string {\n return value === undefined ? `${group}:${field}` : `${group}:${field}=${value}`\n}\n\n/** The keys one holder's facet contributes, under `group`. */\nfunction facetKeysOf(group: string, facet: Record<string, unknown>, into: Set<string>): void {\n const add = (field: CrmFacetKeyField, raw: unknown) => {\n const value = crmFacetKeyValue(raw)\n if (value === null) return\n into.add(crmFacetKey(group, field))\n into.add(crmFacetKey(group, field, value))\n }\n add('owner', facet['ownerUid'])\n if (isContactLifecycleStage(facet['lifecycleStage'])) add('stage', facet['lifecycleStage'])\n add('company', facet['companyId'])\n const sources = facet['sources']\n if (sources && typeof sources === 'object' && !Array.isArray(sources)) {\n for (const [source, on] of Object.entries(sources as Record<string, unknown>)) {\n if (on) add('source', source)\n }\n }\n if (Array.isArray(facet['tags'])) {\n for (const tag of facet['tags']) add('tag', tag)\n }\n // Presence only: a holder's commercial figures are asked \"any?\", never ranged.\n if (typeof facet['ordersCount'] === 'number' && facet['ordersCount'] > 0) {\n into.add(crmFacetKey(group, 'orders'))\n }\n if (typeof facet['ltvCents'] === 'number' && facet['ltvCents'] > 0) {\n into.add(crmFacetKey(group, 'ltv'))\n }\n const custom = facet['custom']\n if (custom && typeof custom === 'object' && !Array.isArray(custom)) {\n for (const [key, value] of Object.entries(custom as Record<string, unknown>)) {\n if (/^[A-Za-z0-9_-]{1,64}$/.test(key)) add(`custom.${key}`, value)\n }\n }\n}\n\n/** Every facet key a contact carries — see the block above. */\nexport function crmContactFacetKeys(record: object): string[] {\n const facets = (record as Record<string, unknown>)[CONTACT_FACETS_FIELD]\n if (!facets || typeof facets !== 'object' || Array.isArray(facets)) return []\n const groups = new Set<string>()\n const any = new Set<string>()\n for (const [groupId, facet] of Object.entries(facets as Record<string, unknown>)) {\n if (!groupId || !facet || typeof facet !== 'object' || Array.isArray(facet)) continue\n facetKeysOf(groupId, facet as Record<string, unknown>, groups)\n facetKeysOf(CRM_FACET_KEY_ANY_GROUP, facet as Record<string, unknown>, any)\n }\n return [...any, ...groups].slice(0, CRM_FACET_KEYS_MAX)\n}\n\n/**\n * What a contact's search box reads: the shared identity — the canonical\n * name and every address — and the two profile values a person is looked\n * up by, through their top-level search echoes (`HostContact.phone`,\n * `HostContact.companyName`): the company's words, and the phone's digits.\n */\nexport const CRM_CONTACT_SEARCH_SOURCES = [\n 'name',\n 'email',\n CONTACT_ALTERNATE_EMAILS_FIELD,\n 'companyName',\n] as const\n\n/**\n * A phone number as the words a search box finds it by: its digits whole,\n * without a leading country code of one to three digits, and its last seven\n * and last four — so `5551234`, `555123` and `4567` all find +1 555 123 4567.\n * Typed as digits: a search reads one word, and `(555)` is not one of these.\n */\nexport function crmPhoneSearchWords(phone: unknown): string[] {\n const digits = typeof phone === 'string' ? phone.replace(/\\D/g, '') : ''\n if (digits.length < 4) return []\n const words = new Set<string>([digits])\n for (let strip = 1; strip <= 3; strip += 1) {\n if (digits.length - strip >= 7) words.add(digits.slice(strip))\n }\n if (digits.length > 7) words.add(digits.slice(-7))\n words.add(digits.slice(-4))\n return [...words]\n}\n\nexport function crmContactListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n facetKeys: string[]\n emailStatus: string\n} {\n const contact = record as Record<string, unknown>\n return {\n ...crmSearchFields(contact['visibleTo'], [\n ...CRM_CONTACT_SEARCH_SOURCES.map((field) => contact[field]),\n crmPhoneSearchWords(contact['phone']),\n ]),\n facetKeys: crmContactFacetKeys(contact),\n emailStatus: crmEmailStatusKey(contact),\n }\n}\n\n/**\n * The stored fields each collection's list fields are computed from. A\n * write that sets one of them must restamp the record — or spread\n * {@link crmListFields} over the document it wrote.\n */\nexport const CRM_LIST_FIELD_INPUTS: Readonly<Record<CrmListCollection, readonly string[]>> = {\n leads: ['visibleTo', ...CRM_LEAD_SEARCH_SOURCES, 'status', 'leadSource', 'emailState', 'campaignIds'],\n contacts: ['visibleTo', ...CRM_CONTACT_SEARCH_SOURCES, 'phone', CONTACT_FACETS_FIELD, 'emailState'],\n companies: ['visibleTo', ...CRM_COMPANY_SEARCH_SOURCES],\n deals: ['visibleTo', 'title'],\n crmTasks: ['visibleTo', 'title'],\n}\n\n/** Every list field of one record, from the record as stored. */\nexport function crmListFields(\n collection: CrmListCollection,\n record: object,\n): Record<string, unknown> {\n switch (collection) {\n case 'leads':\n return crmLeadListFields(record)\n case 'contacts':\n return crmContactListFields(record)\n case 'companies':\n return crmCompanyListFields(record)\n case 'deals':\n return crmDealListFields(record)\n case 'crmTasks':\n return crmTaskListFields(record)\n }\n}\n\nconst sameValue = (a: unknown, b: unknown): boolean => {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((entry, at) => entry === b[at])\n }\n return a === b\n}\n\n/**\n * The list fields a stored record carries WRONGLY — the patch that brings\n * it level, `{}` when it already is. What a restamp and a backfill write.\n */\nexport function crmListFieldsPatch(\n collection: CrmListCollection,\n record: object,\n): Record<string, unknown> {\n const fields = crmListFields(collection, record)\n const stored = record as Record<string, unknown>\n const patch: Record<string, unknown> = {}\n for (const [field, value] of Object.entries(fields)) {\n if (!sameValue(stored[field], value)) patch[field] = value\n }\n return patch\n}\n\n/**\n * The field each collection's list ORDERS or asks `null` of, which a record\n * must therefore carry even when it has no value: a contact, a company and a\n * deal carry `nextTaskAtMs` (\"No next activity\" asks `== null`), and a task\n * carries `dueAtMs` (every task view orders by it, and Firestore leaves a\n * document without the ordered field out of the answer). An absent field is\n * neither `null` nor anything else a query can ask for.\n */\nexport const CRM_LIST_NULLABLE_FIELD: Readonly<Record<CrmListCollection, string | null>> = {\n leads: null,\n contacts: 'nextTaskAtMs',\n companies: 'nextTaskAtMs',\n deals: 'nextTaskAtMs',\n crmTasks: 'dueAtMs',\n}\n\n/**\n * The list fields a NEW record is written with: {@link crmListFields}, and\n * the collection's {@link CRM_LIST_NULLABLE_FIELD} as `null` when the record\n * does not set it — nothing is scheduled against a record that did not\n * exist, and a task created undated is still one its views must reach.\n */\nexport function crmNewRecordListFields(\n collection: CrmListCollection,\n record: object,\n): Record<string, unknown> {\n const fields = crmListFields(collection, record)\n const nullable = CRM_LIST_NULLABLE_FIELD[collection]\n const stored = (record as Record<string, unknown>)[nullable ?? '']\n if (!nullable || (nullable in record && stored !== undefined)) return fields\n return { ...fields, [nullable]: null }\n}\n\n/** Whether a patch touches a field the collection's list fields are computed from. */\nexport function crmListFieldsTouched(\n collection: CrmListCollection,\n patch: Record<string, unknown>,\n): boolean {\n const inputs = CRM_LIST_FIELD_INPUTS[collection]\n return Object.keys(patch).some((key) =>\n inputs.some((input) => key === input || key.startsWith(`${input}.`)),\n )\n}\n"],"names":["normalizeAddress","normalizePhone","consentGroupScope","CONTACT_LIFECYCLE_STAGES","isContactLifecycleStage","CONTACT_ALTERNATE_EMAILS_FIELD","CONTACT_FACETS_FIELD","CONTACT_SOURCE_LABELS","normalizeContactEmail","readContactFacet","isEmailStateStatus","NAME_TOKEN_MAX_PREFIX","nameSearchKey","nameSearchTokens","SCOPED_SEARCH_JOIN","scopedSearchTokens","MAX_SCOPE_HOSTS","ORG_SCOPE_TOKEN","CRM_COLLECTIONS","companies","pipelines","deals","tasks","activities","contactFields","views","emailTemplates","picklists","CRM_RECORD_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","CRM_ACTIVITIES_PER_RECORD_CEILING","CRM_ACTIVITY_LOG_FULL_MESSAGE","toLocaleString","crmActivityCeilingLink","link","contactId","field","id","String","companyId","dealId","leadId","crmActivityLogHasRoom","existing","count","Number","isFinite","CONTACT_LIFECYCLE_STAGE_LABELS","subscriber","lead","opportunity","customer","evangelist","other","advanceContactLifecycleStage","current","floor","held","undefined","order","indexOf","contactLifecycleStageAfterPurchase","isPipelineArchived","pipeline","archivedAt","DEFAULT_DEAL_STAGES","name","probability","kind","DEAL_LINE_ITEMS_MAX","DEAL_LINE_ITEM_NAME_MAX","DEAL_LINE_ITEM_QUANTITY_MAX","CRM_TASK_KIND_LABELS","call","email","meeting","todo","CRM_ACTIVITY_KIND_LABELS","note","activityKindHasOutcome","CRM_EMAIL_DELIVERY_STATES","CRM_EMAIL_DELIVERY_STATE_LABELS","sent","delivered","opened","clicked","bounced","complained","isCrmEmailDeliveryState","value","includes","isCrmEmailDeliveryFailure","state","CRM_EMAIL_DELIVERY_RANK","nextCrmEmailDeliveryState","incoming","CRM_EMAIL_SUBJECT_MAX","CRM_EMAIL_BODY_MAX","CRM_EMAIL_CONTEXT","CRM_EMAIL_ACTIVITY_TAG","CRM_EMAIL_ORG_TAG","PROVIDER_TAG_VALUE","crmEmailDeliveryTags","input","orgId","activityId","hostId","test","buildCrmEmailActivity","subject","slice","body","to","direction","deliveryState","deliveryAtMs","atMs","byUid","byName","sourceActionId","visibleTo","campaignEmailSummary","counted","word","parts","deliveredAtMs","push","openedAtMs","openCount","clickedAtMs","clickCount","bouncedAtMs","complainedAtMs","sortableMs","NEGATIVE_INFINITY","mergeContactTimeline","interactions","campaignEmails","entries","map","interaction","index","key","type","refId","activity","$id","messageId","sentAtMs","sort","a","b","crmActivityRecordLink","record","MINUTE_MS","HOUR_MS","DAY_MS","activityTimeLabel","nowMs","elapsed","Date","toLocaleDateString","Math","CRM_FIELD_OBJECTS","CRM_FIELD_OBJECT_LABELS","contact","company","deal","isCrmFieldObject","fieldDefinitionObject","definition","object","crmFieldListFields","label","required","searchTokens","replace","join","CONTACT_FIELD_KEY_PATTERN","normalizeContactFieldKey","trim","toLowerCase","dealStageById","stageId","stages","find","stage","weightedDealAmountCents","amount","max","round","amountCents","status","min","dealHasLineItems","Array","isArray","lineItems","length","lineItemsTotalCents","items","total","item","quantity","unit","unitAmountCents","readDealLineItems","currency","error","dealCurrency","raw","line","at","isInteger","lineCurrency","toUpperCase","productId","taskDueState","task","dueAtMs","due","now","sameDay","getFullYear","getMonth","getDate","DOMAIN_LABEL","normalizeCompanyDomain","split","labels","every","COMPANY_WEBSITE_MAX","normalizeCompanyWebsite","candidate","url","URL","protocol","href","PUBLIC_MAILBOX_DOMAINS","Set","isPublicMailboxDomain","domainOrEmail","domain","lastIndexOf","has","companyDomainForEmail","companyNameForDomain","charAt","CONTACT_COMPANY_IDS_FIELD","COMPANY_CONTACTS_COUNT_FIELD","readContactCompanyLink","groupId","document","facets","heldElsewhere","holder","facet","Object","named","add","mirror","companyIds","filter","contactGroupsNamingCompany","Boolean","planContactCompanyLink","previous","previousLeaves","counts","change","op","delta","kept","ORG_CRM_SETTINGS_FIELD","CRM_AUTO_CREATE_COMPANIES_PATH","orgAutoCreatesCompanies","orgDocument","settings","CRM_ASSIGNMENT_RULES_PATH","CRM_ROUND_ROBIN_POOL_PATH","CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH","CRM_ASSIGNMENT_RULES_MAX","CRM_ROUND_ROBIN_POOL_MAX","CRM_HOST_SETTINGS_KEY","crmHostDefaultOwnerSegments","Error","isContactSourceValue","cleanText","text","readCrmAssignmentRule","whenRaw","entry","when","source","formId","emailDomain","tag","assignRaw","assign","roundRobin","memberUid","readCrmAssignmentSettings","crm","rules","rule","memberUids","uid","hostSettings","hosts","hostDefaultOwners","owner","pool","lastAssignedUid","crmHostDefaultOwner","assignmentEmailDomain","assignmentRuleMatches","capture","worn","tags","roundRobinOrder","describeAssignmentRule","memberLabel","newAssignmentRuleId","toString","random","crmScopeTokens","org","group","crmReadTokens","parseCrmMemberRef","crmMemberOption","member","displayName","crmMemberPickerLabel","findOrgMember","members","ref","parsed","CRM_LEAD_STATUSES","CRM_LEAD_STATUS_LABELS","new","working","qualified","unqualified","CRM_LEAD_OPEN_STATUSES","CRM_LEAD_CLOSED_STATUSES","isCrmLeadStatus","CRM_LEAD_TEXT_MAX","CRM_LEAD_TAGS_MAX","CRM_LEAD_NOTES_MAX","CRM_LEAD_PROFILE_KEYS","CRM_LEAD_PHONE_REFUSAL","CRM_LEAD_WEBSITE_REFUSAL","normalizeCrmLeadTags","normalizeCrmLeadProfile","patch","errors","phone","website","address","CRM_PICKLIST_IDS","CRM_LEAD_SOURCE_PICKLIST","CRM_PICKLIST_VALUES_MAX","CRM_LEAD_SOURCE_STARTER_LABELS","normalizeCrmPicklistLabel","picklistKey","crmPicklistValueId","taken","used","base","normalize","n","crmPicklistFromLabels","values","seen","active","defaultValueId","normalizeCrmPicklist","stored","ids","defaultId","effectiveCrmLeadSourcePicklist","crmPicklistValueByLabel","picklist","crmPicklistActiveValues","crmPicklistDefaultLabel","crmLeadSourceRefusal","shown","more","judgeCrmLeadSource","ok","match","crmPicklistOptions","options","inactive","unlisted","some","option","listed","crmPicklistRank","MAX_SAFE_INTEGER","findIndex","crmLeadDisplayName","crmLeadStatus","isCrmLeadOpen","CRM_VIEW_SECTIONS","isCrmViewSection","CRM_VIEW_NAME_MAX","CRM_VIEW_MAX_FILTERS","CRM_VIEW_MAX_COLUMNS","VALUELESS_VIEW_OPS","EMPTY_CRM_VIEW_STATE","freeze","filters","columns","asViewText","normalizeCrmViewFilters","clauses","normalizeCrmViewColumns","column","size","normalizeCrmViewSort","normalizeCrmViewState","crmViewStateEquals","left","right","crmViewIsListed","view","shared","ownerUid","CRM_CONTACT_VIEW_FIELDS","createdAt","updatedAt","orders","ltv","custom","crmContactCustomColumn","crmContactCustomKey","prefix","startsWith","crmTelHref","compact","CRM_VIEW_TAGS_FIELD","CRM_VIEW_SOURCE_FIELD","crmViewFiltersFromSegment","segment","sources","crmViewSegmentFilters","clause","delete","CRM_DEFAULT_VIEWS_FIELD","crmDefaultViewId","profile","section","byOrg","bySection","viewId","crmDefaultViewPatch","CRM_MEDIA_IDS_FIELD","CRM_MEDIA_IDS_MAX","normalizeCrmMediaIds","CRM_SEARCH_TOKENS_FIELD","CRM_SCOPED_SEARCH_TOKENS_FIELD","CRM_SCOPED_SEARCH_JOIN","CRM_SEARCH_TOKENS_MAX","SEARCH_KEY","crmSearchTokens","tokens","addWord","capped","end","addText","part","crmSearchFields","CRM_EMAIL_STATUS_FIELD","CRM_EMAIL_STATUS_NONE","crmEmailStatusKey","CRM_LEAD_SOURCE_KEY_FIELD","crmLeadSourceKey","CRM_LEAD_SEARCH_SOURCES","CRM_LEAD_SCOPED_CAMPAIGNS_FIELD","leadCampaignIds","crmLeadListFields","scopedCampaignIds","leadSourceKey","emailStatus","CRM_COMPANY_SEARCH_SOURCES","crmCompanyListFields","crmDealListFields","titleLower","crmTaskListFields","CRM_CONTACT_FACET_KEYS_FIELD","CRM_FACET_KEY_ANY_GROUP","CRM_FACET_KEYS_MAX","crmFacetKeyValue","crmFacetKey","facetKeysOf","into","on","crmContactFacetKeys","groups","any","CRM_CONTACT_SEARCH_SOURCES","crmPhoneSearchWords","digits","words","strip","crmContactListFields","facetKeys","CRM_LIST_FIELD_INPUTS","leads","contacts","crmTasks","crmListFields","collection","sameValue","crmListFieldsPatch","fields","CRM_LIST_NULLABLE_FIELD","crmNewRecordListFields","nullable","crmListFieldsTouched","inputs","keys"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/crm.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/**\n * Contacts CRM v2 (AGL-2595): the records that sit BESIDE a contact.\n *\n * v1 is one collection — `contacts` — and everything a holder knows about a\n * person lives in that holder's facet on the shared row. That shape is right\n * for the person and wrong for everything a sales team keeps AROUND the\n * person: a company is known by several contacts, a deal moves through\n * stages on its own clock, a task is due whether or not anybody opens the\n * contact it hangs off, and an activity log grows past anything a facet\n * should carry. So each of those is a collection of its own under\n * `orgs/{orgId}/`, pointing back at the contact by id.\n *\n * ## One scope model, not a second one\n *\n * Every collection here carries `visibleTo`, the same array `contacts`\n * carries, and is stamped the same way — {@link crmScopeTokens} is the\n * contact create path's expression, exported so that no creator computes its\n * own. The rules gate all six on the contacts predicate (`canReadScopedPeople`,\n * `data.manage`), because a deal or a call log is a fact about a person and\n * discloses exactly what the contact row would.\n *\n * What these collections do NOT have is the facet map. A contact is shared\n * across holders because one human is one row; a company, deal or task is\n * one holder's record from the moment it is created, so `visibleTo` alone\n * says who may see it and there is nothing to split per holder.\n *\n * Pure data module: types, constants and the small helpers every surface\n * would otherwise write for itself. No Firestore, no React.\n */\n\nimport {\n type AglynPostalAddress,\n normalizeAddress,\n normalizePhone,\n type OrgCrmAssignmentRule,\n} from '../foundation'\nimport { type ConsentGroup, consentGroupScope } from './consent-groups'\nimport {\n CONTACT_LIFECYCLE_STAGES,\n type ContactLifecycleStage,\n type CrmActivityKind,\n type CrmTaskKind,\n isContactLifecycleStage,\n} from './crm-kinds'\nimport {\n CONTACT_ALTERNATE_EMAILS_FIELD,\n CONTACT_FACETS_FIELD,\n CONTACT_SOURCE_LABELS,\n type ContactInteraction,\n type ContactSegment,\n type ContactSource,\n normalizeContactEmail,\n readContactFacet,\n} from './contacts'\nimport { type EmailState, isEmailStateStatus } from './email-state'\nimport {\n NAME_TOKEN_MAX_PREFIX,\n nameSearchKey,\n nameSearchTokens,\n SCOPED_SEARCH_JOIN,\n scopedSearchTokens,\n} from './name-search'\nimport { MAX_SCOPE_HOSTS, ORG_SCOPE_TOKEN, type ScopeToken } from './scope-tokens'\n\n// The fixed vocabularies and their guards live in a leaf module; every name\n// stays importable from here.\nexport * from './crm-kinds'\n\n/**\n * The CRM's collections, every one under `orgs/{orgId}/`.\n *\n * Named here rather than spelled at each call site because the rules, the\n * indexes and the console must agree on the string, and the three prefixed\n * ones are prefixed on purpose: `tasks`, `activities` and `views` are words\n * the org document will want for something else one day, and a collection\n * name is persisted in every document path that uses it.\n */\nexport const CRM_COLLECTIONS = {\n companies: 'companies',\n pipelines: 'pipelines',\n deals: 'deals',\n tasks: 'crmTasks',\n activities: 'crmActivities',\n contactFields: 'contactFields',\n views: 'crmViews',\n emailTemplates: 'crmEmailTemplates',\n /** An org's value sets for its standard picklist fields (AGL-3298). */\n picklists: 'crmPicklists',\n} as const\n\nexport type CrmCollection = (typeof CRM_COLLECTIONS)[keyof typeof CRM_COLLECTIONS]\n\n/**\n * The three collections the CRM RECORDS band counts (AGL-2611), in the order\n * the billing caption lists them. `contacts` is not in `CRM_COLLECTIONS`\n * because it predates the hub and is addressed through `orgDataCollection`,\n * so the band's own list has to name all three itself. Tasks, pipelines,\n * activities and field definitions are deliberately absent: a band that\n * counted what a rep does every hour would price the team's effort, not the\n * audience it holds.\n */\nexport const CRM_RECORD_COLLECTIONS = [\n 'contacts',\n CRM_COLLECTIONS.companies,\n CRM_COLLECTIONS.deals,\n] as const\n\n/**\n * What a create says when the records band refused it, on every surface —\n * the contacts list's alert, the company and deal drawers, the plugin's\n * create routes and the lead conversion (AGL-2596, widened in AGL-2611).\n *\n * One sentence in one place, because a reader who is refused in the drawer\n * and then reads the list must be told the same thing, and the remedy is the\n * same wherever the refusal lands: a band refuses only on a plan that puts\n * no overage rate past it, so \"upgrade\" is the whole of the answer.\n */\nexport const CRM_RECORDS_BAND_FULL_MESSAGE =\n 'CRM records limit reached — this record was not added. Upgrade in ' +\n 'Billing to keep collecting.'\n\n/**\n * The most logged activities ONE record may carry (AGL-2611) — a contact's,\n * a company's or a deal's own log, counted on the link the activity was\n * filed under.\n *\n * A platform ceiling and not a plan dimension, in the family of\n * `WEBHOOK_MAX_PER_HOST` and `NON_PAGE_SCREEN_MAX_PER_HOST`: activities are\n * not in the records band because they are bounded by human effort, and\n * this is the bound. Five thousand is a call a day for fourteen years on one\n * person, so nobody working a real relationship reaches it — what does is\n * an automation logging on every event, or an import replaying a history,\n * and either of those past this line is a document cost with no reader.\n *\n * Enforced where an activity is written: the console's log dialog, the\n * `logCrmActivity` automation step and `POST /v1/activities`, each with one\n * aggregate read on the record's link before the create. The timeline reads\n * a hundred at a time and is untouched by the number.\n */\nexport const CRM_ACTIVITIES_PER_RECORD_CEILING = 5_000\n\n/**\n * What every writer says when a record's log is full — the console's log\n * dialog, the automation step's run history and `POST /v1/activities`.\n */\nexport const CRM_ACTIVITY_LOG_FULL_MESSAGE =\n `This record already has ${CRM_ACTIVITIES_PER_RECORD_CEILING.toLocaleString(\n 'en-US',\n )} activities, which is the most one record can carry.`\n\n/** The record an activity is filed under: its links, of which one leads. */\nexport interface CrmActivityLink {\n contactId?: string | null\n companyId?: string | null\n dealId?: string | null\n /**\n * `hosts/{hostId}/leads/{leadId}` — a person not yet converted (AGL-2615).\n * A lead is host-scoped by path and carries no `visibleTo` of its own, so\n * an activity filed under one is stamped with the site's scope like any\n * other record created from that site.\n */\n leadId?: string | null\n}\n\n/**\n * The field the per-record activity ceiling is counted on, or `null` for an\n * activity that names no record at all.\n *\n * The contact leads, then the company, then the deal: an activity logged\n * from a contact's page carries the company beside it (the automation step\n * copies the facet's `companyId` onto every record it creates), and a\n * ceiling counted on the company would let one busy account exhaust every\n * contact filed under it. The record whose PAGE the log is read on is the\n * one whose log has the limit. Shared by the client dialog and the two\n * server writers so they count the same thing.\n */\nexport function crmActivityCeilingLink(\n link: CrmActivityLink,\n): { field: 'contactId' | 'companyId' | 'dealId' | 'leadId'; id: string } | null {\n if (link.contactId) return { field: 'contactId', id: String(link.contactId) }\n if (link.companyId) return { field: 'companyId', id: String(link.companyId) }\n if (link.dealId) return { field: 'dealId', id: String(link.dealId) }\n // Last, because a converted lead's activities carry the contact beside it\n // and the contact is the record whose page they are read on.\n if (link.leadId) return { field: 'leadId', id: String(link.leadId) }\n return null\n}\n\n/**\n * Whether ONE MORE activity fits under `CRM_ACTIVITIES_PER_RECORD_CEILING`.\n * Both halves of the boundary live here so no writer re-derives `>=`.\n */\nexport function crmActivityLogHasRoom(existing: number): boolean {\n const count = Number(existing)\n return !(Number.isFinite(count) && count >= CRM_ACTIVITIES_PER_RECORD_CEILING)\n}\n\n/** How a lifecycle stage reads on screen — typed so a stage cannot ship unlabeled. */\nexport const CONTACT_LIFECYCLE_STAGE_LABELS: Record<ContactLifecycleStage, string> = {\n subscriber: 'Subscriber',\n lead: 'Lead',\n 'marketing-qualified': 'Marketing qualified',\n 'sales-qualified': 'Sales qualified',\n opportunity: 'Opportunity',\n customer: 'Customer',\n evangelist: 'Evangelist',\n other: 'Other',\n}\n\n/**\n * The stage a person is in once a capture has happened that implies at least\n * `floor` (AGL-2612).\n *\n * The one ordering rule every capture door shares: a door names the EARLIEST\n * stage that describes what just happened — a form submission is a lead, a\n * newsletter opt-in is a subscriber, a purchase is a customer — and the\n * result is that stage for a person who had none or an earlier one, and the\n * stage they already had otherwise. \"Never downgrades\" is the whole contract:\n * a customer who fills in a contact form is still a customer, and `other` —\n * the deliberate stage a business picked for a funnel step none of the names\n * fit — sits after `customer` in the list precisely so no capture can\n * overwrite it. A stored value that is not a stage at all reads as absent,\n * because a capture door is not the place to preserve a typo.\n *\n * With no floor the answer is the current stage as it stands, or `undefined`\n * for one that is absent or unusable — so a writer can apply this\n * unconditionally and write only what comes back.\n */\nexport function advanceContactLifecycleStage(\n current: unknown,\n floor: ContactLifecycleStage | undefined,\n): ContactLifecycleStage | undefined {\n const held = isContactLifecycleStage(current) ? current : undefined\n if (!floor) return held\n if (!held) return floor\n const order: readonly string[] = CONTACT_LIFECYCLE_STAGES\n return order.indexOf(held) < order.indexOf(floor) ? floor : held\n}\n\n/**\n * The stage a person is in once they have BOUGHT something (AGL-2596).\n *\n * `customer` when they had no stage or an earlier one; whatever they already\n * had otherwise — {@link advanceContactLifecycleStage} with `customer` as the\n * floor, kept under its own name because \"after a purchase\" is the question\n * the order paths and the reports ask.\n */\nexport function contactLifecycleStageAfterPurchase(\n current: unknown,\n): ContactLifecycleStage {\n return advanceContactLifecycleStage(current, 'customer') ?? 'customer'\n}\n\n/**\n * What a custom contact field may hold.\n *\n * Scalars only. A field is something a merchant filters and exports on, and\n * a nested value is neither; `null` is the explicit \"cleared\" that a form\n * writes so the key stays present for a `where` clause to find.\n */\nexport type ContactCustomValue = string | number | boolean | null\n\n/**\n * The same scalar, under the name the other objects use (AGL-2661).\n *\n * A company's and a deal's `custom` map hold exactly what a contact's\n * does — one definition, one coercion rule, one stored shape — so the\n * value type is one type with two names: the contact name it was born\n * with, and this one for code that is not about contacts.\n */\nexport type CrmCustomValue = ContactCustomValue\n\n/**\n * The fields every CRM document carries.\n *\n * `visibleTo` is the scope both enforcement layers evaluate — see\n * `scope-tokens.ts` for why an absent one is seen by NOBODY. `hostId` is\n * provenance: the site whose console created the record, never rewritten,\n * so a report can say where a deal came from after the scope has widened.\n * The timestamps are `unknown` because a client write carries a `Date` and a\n * read hands back a Firestore `Timestamp`; callers narrow at the edge.\n */\nexport interface CrmScoped {\n visibleTo: string[]\n /** The site that created it. */\n hostId: string\n createdAt?: unknown\n updatedAt?: unknown\n}\n\n/** `orgs/{orgId}/companies/{companyId}`. */\nexport interface CrmCompany extends CrmScoped {\n name: string\n /** `nameSearchFields` twins, for the prefix search the list runs. */\n nameLower?: string\n nameTokens?: string[]\n /** Lowercase hostname, no protocol — see {@link normalizeCompanyDomain}. */\n domain?: string\n website?: string\n /** E.164 — `normalizePhone` before writing. */\n phone?: string\n address?: AglynPostalAddress | null\n industry?: string\n ownerUid?: string\n notes?: string\n /**\n * Lowercased, deduplicated, capped at twenty — the same shape a contact's\n * tags take, so a bulk \"Add tag\" over companies and one over contacts\n * write the same kind of value (AGL-2621).\n */\n tags?: string[]\n createdByUid?: string\n /**\n * How many contacts name this company in their {@link CONTACT_COMPANY_IDS_FIELD}\n * mirror — see {@link COMPANY_CONTACTS_COUNT_FIELD}. Absent on a company\n * nobody has linked since the counter existed, which reads as zero.\n */\n contactsCount?: number\n /**\n * Custom field values, keyed by the key of a definition whose `object`\n * is `company` — see {@link fieldDefinitionObject} (AGL-2661).\n */\n custom?: Record<string, CrmCustomValue>\n /**\n * When the earliest OPEN task filed against this company is due, epoch\n * ms, or `null` when none is — see {@link CrmDeal.nextTaskAtMs}.\n */\n nextTaskAtMs?: number | null\n /** Org-library files attached to the company (AGL-2662) — see {@link CRM_MEDIA_IDS_MAX}. */\n mediaIds?: string[]\n}\n\n/** One step of a pipeline. */\nexport interface CrmDealStage {\n id: string\n name: string\n /** Position in the pipeline, ascending. */\n order: number\n /** Chance of closing from here, 0–100 — what a weighted forecast multiplies by. */\n probability: number\n /**\n * Whether landing in this stage closes the deal. A pipeline has exactly one\n * `won` and one `lost` stage in the default set; `open` is everything in\n * between.\n */\n kind: 'open' | 'won' | 'lost'\n}\n\n/** `orgs/{orgId}/pipelines/{pipelineId}`. */\nexport interface CrmPipeline extends CrmScoped {\n name: string\n stages: CrmDealStage[]\n /** The pipeline a new deal lands in when nobody picks one. */\n isDefault?: boolean\n /**\n * When the pipeline was retired, epoch ms. An archived pipeline takes no\n * new deal and is offered by no picker, but it is never deleted: the deals\n * it closed still name it, and a report that could not resolve their\n * stages would forecast them as orphans. Absent or `null` while active.\n */\n archivedAt?: number | null\n}\n\n/** Whether a pipeline has been retired — see {@link CrmPipeline.archivedAt}. */\nexport function isPipelineArchived(\n pipeline: Pick<CrmPipeline, 'archivedAt'> | null | undefined,\n): boolean {\n return typeof pipeline?.archivedAt === 'number' && pipeline.archivedAt > 0\n}\n\n/**\n * The stages a fresh pipeline starts with.\n *\n * Readonly on purpose: a pipeline document stores its own COPY (`[...]`), so\n * a merchant editing their stages must not be editing the module's default,\n * and a second pipeline seeded later must start from the original set.\n */\nexport const DEFAULT_DEAL_STAGES: readonly CrmDealStage[] = [\n { id: 'qualified', name: 'Qualified', order: 0, probability: 10, kind: 'open' },\n { id: 'contact-made', name: 'Contact made', order: 1, probability: 20, kind: 'open' },\n { id: 'proposal-sent', name: 'Proposal sent', order: 2, probability: 40, kind: 'open' },\n { id: 'negotiation', name: 'Negotiation', order: 3, probability: 60, kind: 'open' },\n { id: 'won', name: 'Won', order: 4, probability: 100, kind: 'won' },\n { id: 'lost', name: 'Lost', order: 5, probability: 0, kind: 'lost' },\n]\n\nexport type CrmDealStatus = 'open' | 'won' | 'lost'\n\n/**\n * One product on a deal — what is being sold, how many, at what.\n *\n * `productId` names a catalog product when the line came from one; a line\n * typed by hand has none. The name is copied rather than joined, the way a\n * deal copies its contact's name: a catalog product can be renamed or\n * deleted after the deal was priced, and the deal has to keep saying what\n * it was for. `currency` is the deal's — every line on a deal is in one\n * currency, because their sum is the deal's amount and a sum across\n * currencies is a number with no unit.\n */\nexport interface CrmDealLineItem {\n productId?: string\n name: string\n /** A whole number of units, one or more. */\n quantity: number\n /** Per unit, in the currency's minor unit, zero or more. */\n unitAmountCents: number\n /** Lowercase ISO 4217. */\n currency: string\n}\n\n/** The most lines one deal carries — a quote, not a catalog. */\nexport const DEAL_LINE_ITEMS_MAX = 50\nexport const DEAL_LINE_ITEM_NAME_MAX = 120\n/** Units per line; past this the number is a data-entry slip. */\nexport const DEAL_LINE_ITEM_QUANTITY_MAX = 1_000_000\n\n/**\n * `orgs/{orgId}/deals/{dealId}`.\n *\n * A WON DEAL MAKES ITS CONTACT A CUSTOMER (AGL-2641). A win is the same\n * fact as a purchase — the business has decided this person bought — and an\n * order already floors a contact's lifecycle stage at `customer` on\n * capture. So every writer of `status` — the console's stage route at both\n * levels and the REST resource — applies the same floor to the contact\n * `contactId` names on the transition into `won`, in the facet of the site\n * the deal was made on (`hostId`), with `advanceContactLifecycleStage`'s\n * rule: an empty stage is filled, an earlier one advanced, and nobody is\n * ever moved back. Behind no setting, because a won deal is a customer by\n * definition; an automation on `dealWon` is for what happens NEXT.\n */\nexport interface CrmDeal extends CrmScoped {\n title: string\n titleLower?: string\n pipelineId: string\n stageId: string\n /**\n * Denormalized from the stage's `kind` at every stage move, because it is\n * what the list filters and the indexes sort on — a query cannot join the\n * pipeline to ask what the stage means.\n */\n status: CrmDealStatus\n /**\n * What the deal is worth. Typed by hand on a deal with no line items;\n * on a deal WITH them it is their sum, stored beside them by every\n * writer (`lineItemsTotalCents`), because the board, the reports and the\n * REST list all read this one field and none of them can afford to add\n * up fifty lines per row. A deal with line items refuses a typed amount.\n */\n amountCents?: number\n /** Lowercase ISO 4217; `'usd'` when absent. */\n currency?: string\n /** The products behind the amount — see {@link CrmDealLineItem}. */\n lineItems?: CrmDealLineItem[]\n expectedCloseAtMs?: number | null\n closedAtMs?: number | null\n /** When the deal last moved — what \"stuck in stage\" reports read. */\n stageChangedAtMs?: number\n ownerUid?: string\n contactId?: string\n companyId?: string\n lostReason?: string\n notes?: string\n createdByUid?: string\n /**\n * Custom field values, keyed by the key of a definition whose `object`\n * is `deal` — see {@link fieldDefinitionObject} (AGL-2661).\n */\n custom?: Record<string, CrmCustomValue>\n /**\n * When the earliest OPEN task filed against this deal is due, epoch ms,\n * or `null` when no open task names it (AGL-2661).\n *\n * DENORMALIZED from `crmTasks`, because the question \"which deals have\n * nothing scheduled\" is asked of a LIST — a column, a filter, a report\n * tile — and a list cannot afford a task query per row. Every server\n * writer of a task recomputes it for the records the task names\n * (`recomputeCrmNextTaskAt` in the admin library), and a client-direct\n * task write asks the console route to do the same; the Fields section\n * offers a one-off recompute for the records written before the field\n * existed. Absent on such a record, which every reader treats as `null`.\n */\n nextTaskAtMs?: number | null\n /** Org-library files attached to the deal (AGL-2662) — see {@link CRM_MEDIA_IDS_MAX}. */\n mediaIds?: string[]\n}\n\nexport const CRM_TASK_KIND_LABELS: Record<CrmTaskKind, string> = {\n call: 'Call',\n email: 'Email',\n meeting: 'Meeting',\n todo: 'To-do',\n}\n\nexport type CrmTaskPriority = 'low' | 'normal' | 'high'\nexport type CrmTaskStatus = 'open' | 'done'\n\n/**\n * `orgs/{orgId}/crmTasks/{taskId}`.\n *\n * The one CRM record that may belong to NO site (AGL-2637): a task filed\n * from the organization's own hub carries `hostId: null` and the org scope\n * token alone, because a to-do owed by the organization — renew the\n * insurance, chase the agency's own invoice — is not captured by any brand.\n * Every other record is a fact about a person some site met, so\n * `CrmScoped` keeps its site required.\n */\nexport interface CrmTask extends Omit<CrmScoped, 'hostId'> {\n /** The site that created it, or `null` for the organization's own task. */\n hostId: string | null\n title: string\n notes?: string\n kind: CrmTaskKind\n priority: CrmTaskPriority\n status: CrmTaskStatus\n dueAtMs?: number | null\n completedAtMs?: number | null\n /**\n * Who ticked it off, which is not always the assignee: a manager closing\n * out a departed teammate's list completes tasks that were never theirs.\n * Stamped by the `crm/task-complete` route beside `completedAtMs`.\n */\n completedByUid?: string\n assigneeUid?: string\n /**\n * The person who made it, or `''` when no person did.\n *\n * An automation has no uid, and inventing one — the action's id, a\n * sentinel — would put a value into a field every reader resolves as a\n * member. The empty string says \"nobody\", and {@link sourceActionId}\n * beside it says what.\n */\n createdByUid: string\n /** The automation that created it (AGL-2605), when a person did not. */\n sourceActionId?: string\n /**\n * The plugin that filed it on the record-timeline seam (AGL-2981) — a\n * sequence's call step, a reply to answer — when no person did.\n */\n sourcePluginId?: string\n contactId?: string\n companyId?: string\n dealId?: string\n /**\n * `hosts/{hostId}/leads/{leadId}` — a task filed on a lead not yet\n * converted (AGL-3233): a sequence's call step or a reply to answer. The\n * conversion stamps the contact beside it, so the task is the contact's\n * from then on and the lead's page still lists it.\n */\n leadId?: string\n /**\n * When the assignee is reminded (AGL-2659): the due time unless a person\n * moved it, `null` for no reminder. The hourly `/api/crm/task-reminders`\n * runner reads every open task whose reminder has come due, so a task\n * that should never remind carries `null` rather than no field — a\n * missing key and a null both fall outside the range, but the null says\n * it was decided.\n */\n remindAtMs?: number | null\n /**\n * When the runner handled the reminder — sent it, or found nobody to\n * send it to. Absent while the reminder is still owed, which is how a\n * rerun over the same hour sends nothing twice; cleared again when the\n * reminder moves, because a moved reminder is a new one.\n */\n reminderSentAtMs?: number\n}\n\n/** How an activity kind reads on screen — typed so a kind cannot ship unlabeled. */\nexport const CRM_ACTIVITY_KIND_LABELS: Record<CrmActivityKind, string> = {\n call: 'Call',\n email: 'Email',\n meeting: 'Meeting',\n note: 'Note',\n other: 'Other',\n}\n\n/**\n * Whether a kind takes an outcome and a duration.\n *\n * A call and a meeting are conversations: they end somewhere (\"left a\n * voicemail\", \"agreed to a trial\") and they take a measurable amount of\n * time, and both are what a manager reading the log wants to know. An email\n * has neither in any useful sense, a note is not an event at all, and\n * `other` is unknowable — so the dialog hides the two fields for those\n * rather than offering boxes that mean nothing.\n */\nexport function activityKindHasOutcome(kind: CrmActivityKind): boolean {\n return kind === 'call' || kind === 'meeting'\n}\n\n/**\n * Where a one-to-one email got to (AGL-2615), in the order it normally\n * happens — the vocabulary the delivery webhook maps its events onto an\n * `email` activity in, and the chip the timeline shows beside the entry.\n *\n * The first four are a progression: a message is sent, then delivered, then\n * opened, then clicked, and a later state implies the earlier ones. The last\n * two are terminal failures. A message the mailbox provider bounced or that\n * the recipient reported is never \"opened\" in any sense the timeline should\n * report, whatever a tracking pixel says afterwards.\n */\nexport const CRM_EMAIL_DELIVERY_STATES = [\n 'sent',\n 'delivered',\n 'opened',\n 'clicked',\n 'bounced',\n 'complained',\n] as const\n\nexport type CrmEmailDeliveryState = (typeof CRM_EMAIL_DELIVERY_STATES)[number]\n\n/** How a delivery state reads on the chip — typed so a state cannot ship unlabeled. */\nexport const CRM_EMAIL_DELIVERY_STATE_LABELS: Record<CrmEmailDeliveryState, string> = {\n sent: 'Sent',\n delivered: 'Delivered',\n opened: 'Opened',\n clicked: 'Clicked',\n bounced: 'Bounced',\n complained: 'Marked as spam',\n}\n\nexport function isCrmEmailDeliveryState(\n value: unknown,\n): value is CrmEmailDeliveryState {\n return (\n typeof value === 'string' &&\n (CRM_EMAIL_DELIVERY_STATES as readonly string[]).includes(value)\n )\n}\n\n/** The two states that mean the message did not land. */\nexport function isCrmEmailDeliveryFailure(state: unknown): boolean {\n return state === 'bounced' || state === 'complained'\n}\n\n/**\n * The rank the webhook advances by. Higher wins; a failure outranks every\n * progression state, and a complaint outranks a bounce because it is the\n * one a sender is scored on.\n */\nconst CRM_EMAIL_DELIVERY_RANK: Record<CrmEmailDeliveryState, number> = {\n sent: 1,\n delivered: 2,\n opened: 3,\n clicked: 4,\n bounced: 5,\n complained: 6,\n}\n\n/**\n * The state an activity holds after one more delivery event, from the state\n * it held before.\n *\n * MONOTONIC. Provider events arrive at least once and in no promised order\n * — an `opened` can reach the webhook before the `delivered` it implies, and\n * a replay can hand back yesterday's `delivered` after today's `clicked` —\n * so the row keeps whichever state is further along, and an event that says\n * less than the row already knows changes nothing. A stored value the\n * vocabulary does not name reads as nothing, so the incoming event stands.\n */\nexport function nextCrmEmailDeliveryState(\n current: unknown,\n incoming: CrmEmailDeliveryState,\n): CrmEmailDeliveryState {\n if (!isCrmEmailDeliveryState(current)) return incoming\n return CRM_EMAIL_DELIVERY_RANK[incoming] > CRM_EMAIL_DELIVERY_RANK[current]\n ? incoming\n : current\n}\n\n/** The most a one-to-one email's subject may hold. */\nexport const CRM_EMAIL_SUBJECT_MAX = 200\n/** The most a one-to-one email's body may hold — a letter, not a document. */\nexport const CRM_EMAIL_BODY_MAX = 10_000\n\n/**\n * The `context` a one-to-one email is sent under, which `sendEmail` stamps\n * as a provider tag on the message and the delivery log files it by.\n */\nexport const CRM_EMAIL_CONTEXT = 'crm'\n/** The provider tag naming the activity row a one-to-one email belongs to. */\nexport const CRM_EMAIL_ACTIVITY_TAG = 'activityId'\n/** The provider tag naming the org whose `crmActivities` holds that row. */\nexport const CRM_EMAIL_ORG_TAG = 'orgId'\n\n/** What a provider tag value may be — anything else fails the whole send. */\nconst PROVIDER_TAG_VALUE = /^[A-Za-z0-9_-]{1,256}$/\n\n/**\n * The tags a one-to-one email carries so the delivery webhook can find its\n * activity row (AGL-2615): the org, the activity, and the site for the\n * per-site suppression list a bounce lands on.\n *\n * Every value is checked against the provider's alphabet rather than\n * trusted, for the reason `contextTag` gives — a value the provider rejects\n * fails the send, and a failed send is worse than an untracked one. The\n * org and the activity are the pair the webhook needs, so an unusable\n * value for EITHER yields no tags at all: a tag set that named a row it\n * could not locate would be a promise the timeline cannot keep. The site\n * is stamped when it can be and dropped alone when it cannot.\n */\nexport function crmEmailDeliveryTags(input: {\n orgId: string\n hostId: string\n activityId: string\n}): { name: string; value: string }[] {\n const orgId = String(input.orgId ?? '')\n const activityId = String(input.activityId ?? '')\n const hostId = String(input.hostId ?? '')\n if (!PROVIDER_TAG_VALUE.test(orgId) || !PROVIDER_TAG_VALUE.test(activityId)) {\n return []\n }\n return [\n { name: CRM_EMAIL_ORG_TAG, value: orgId },\n { name: CRM_EMAIL_ACTIVITY_TAG, value: activityId },\n ...(PROVIDER_TAG_VALUE.test(hostId) ? [{ name: 'hostId', value: hostId }] : []),\n ]\n}\n\n/**\n * Which way an `email` activity's message traveled. `outbound` is every\n * message the workspace wrote — sent by the platform, or copied to the\n * capture address from a mailbox; `inbound` is one a correspondent wrote,\n * which only the capture route files (AGL-2657).\n */\nexport type CrmEmailDirection = 'outbound' | 'inbound'\n\n/**\n * `orgs/{orgId}/crmActivities/{activityId}` — one thing that happened.\n *\n * Distinct from a contact's `interactions`: those are what the PLATFORM\n * recorded (a form, an order, a booking) and live capped on the row. An\n * activity is what a PERSON logged — a call made, a meeting held — and can\n * hang off a company or a deal with no contact at all.\n */\nexport interface CrmActivity extends CrmScoped {\n kind: CrmActivityKind\n body: string\n /** When it happened, which is not when it was logged. */\n atMs: number\n /** Who logged it, or `''` for an automation — see `CrmTask.createdByUid`. */\n byUid: string\n /**\n * The author's display name as it read when the activity was logged.\n *\n * Denormalized because there is no lookup that could answer it later: a\n * member document is readable by its own subject and by org-wide members\n * only, so a scoped editor reading a colleague's call log could not\n * resolve the `byUid` beside it into a name. Stamped from the signed-in\n * user's resolved name at log time and never rewritten, so it can drift\n * from a later rename — the way a signed letter keeps the name it was\n * signed with.\n */\n byName?: string\n /** The automation that logged it (AGL-2605), when a person did not. */\n sourceActionId?: string\n /**\n * The plugin that filed it on the record-timeline seam (AGL-2981) — an\n * email a sequence sent, a reply it read — or the CRM's own id on an\n * entry it wrote about a filing (AGL-3274), so a reader can tell the\n * bookkeeping entries from what a person logged.\n */\n sourcePluginId?: string\n /**\n * The campaign a filing entry is about (AGL-3274): \"Filed under\" and\n * \"Removed from\" name it in the body and carry the container's id here,\n * so a campaign's own page can one day list what happened under it\n * without parsing a sentence.\n */\n campaignId?: string\n contactId?: string\n companyId?: string\n dealId?: string\n /** The lead it was filed under (AGL-2615) — see {@link CrmActivityLink.leadId}. */\n leadId?: string\n outcome?: string\n durationMinutes?: number\n /**\n * An `email` the platform SENT (AGL-2615), as against one a person logged\n * by hand: the subject line, who it went to, and where delivery got to.\n * Absent on a hand-logged email, which has a body and nothing else.\n */\n subject?: string\n /** The address the message left for. */\n to?: string\n /**\n * `outbound` for a message the platform sent or a teammate copied to the\n * capture address; `inbound` for one a correspondent wrote (AGL-2657).\n */\n direction?: CrmEmailDirection\n /** See {@link CrmEmailDeliveryState}; advanced by the delivery webhook. */\n deliveryState?: CrmEmailDeliveryState\n /** When the delivery state last moved, epoch ms. */\n deliveryAtMs?: number\n /**\n * What the receiving server said when the state moved to a failure\n * (AGL-3245): a bounce's diagnostic, scrubbed of the address. Absent on a\n * progression state.\n */\n deliveryDetail?: string\n /**\n * `hosts/{hostId}/bookings/{bookingId}` — the booking a `meeting` was\n * filed from (AGL-2660), on the rows filed before a booking reached the\n * record through the record-timeline seam. A booking's meeting is now an\n * entry keyed by the booking, and `sourcePluginId` names who filed it.\n */\n bookingId?: string\n /**\n * An email CAPTURED from a mailbox (AGL-2657) — forwarded or copied to\n * the workspace's capture address — as against one the platform sent:\n * who wrote it, the provider's `Message-ID` the row is deduplicated by,\n * the message it answered, and the subject with its reply and forward\n * prefixes removed, which is what groups a thread. Absent on every other\n * email row.\n */\n from?: string\n messageId?: string\n inReplyTo?: string\n threadSubject?: string\n}\n\n/** An activity as a listener hands it back: the document plus its id. */\nexport type CrmActivityRow = CrmActivity & { $id: string }\n\n/** What a sent one-to-one email is logged from — see {@link buildCrmEmailActivity}. */\nexport interface CrmEmailActivityInput {\n subject: string\n body: string\n /** The recipient, as the message was addressed. */\n to: string\n /** When it was sent. */\n atMs: number\n /** Who sent it, or `''` for an automation — see `CrmActivity.byUid`. */\n byUid: string\n byName?: string\n sourceActionId?: string\n link: CrmActivityLink\n hostId: string\n visibleTo: string[]\n}\n\n/**\n * The activity row a sent one-to-one email is logged as (AGL-2615).\n *\n * ONE builder for the two writers — the console's send route and the\n * `sendEmail` automation step — so a message a rep wrote and a message a\n * flow sent are the same kind of entry on the timeline: `kind: 'email'`,\n * outbound, starting at `sent` for the delivery webhook to advance. A\n * writer that assembled its own would be the one whose rows the chip did\n * not know how to read. Timestamps are the writer's: a server stamps\n * `serverTimestamp()` and this module has no Firestore.\n */\nexport function buildCrmEmailActivity(input: CrmEmailActivityInput): CrmActivity {\n const { link } = input\n return {\n kind: 'email',\n subject: String(input.subject ?? '').slice(0, CRM_EMAIL_SUBJECT_MAX),\n body: String(input.body ?? '').slice(0, CRM_EMAIL_BODY_MAX),\n to: input.to,\n direction: 'outbound',\n deliveryState: 'sent',\n deliveryAtMs: input.atMs,\n atMs: input.atMs,\n byUid: input.byUid,\n ...(input.byName ? { byName: input.byName } : {}),\n ...(input.sourceActionId ? { sourceActionId: input.sourceActionId } : {}),\n // Only the links the caller fixed: a key with no value is `undefined`,\n // which Firestore refuses.\n ...(link.contactId ? { contactId: String(link.contactId) } : {}),\n ...(link.companyId ? { companyId: String(link.companyId) } : {}),\n ...(link.dealId ? { dealId: String(link.dealId) } : {}),\n ...(link.leadId ? { leadId: String(link.leadId) } : {}),\n hostId: input.hostId,\n visibleTo: [...input.visibleTo],\n }\n}\n\n/**\n * One campaign email this person was sent, and what became of it (AGL-2616).\n *\n * The per-recipient delivery log — `emailDeliveries/{key}/messages` — keeps\n * one document per MESSAGE with a timestamp per lifecycle state and a count\n * of opens and clicks, and this is that document as a timeline reads it:\n * which email, when it went out, and how far it got. A message that bounced\n * or was complained about carries that state too, because \"we mailed them\n * and it bounced\" is part of the history of a relationship and a timeline\n * that showed only the successes would read as if nothing had been tried.\n *\n * The server answers with this shape, never the log record itself: the log\n * carries the recipient's address, the provider and the links they followed,\n * and none of that is the timeline's to show.\n */\nexport interface ContactCampaignEmail {\n /** The provider's message id — distinct per send, and the entry's key. */\n messageId: string\n /** The site the campaign went out from — the send's `hostId`. */\n hostId: string\n /** `orgs/{orgId}/campaigns/{campaignId}` — the email, whose report the entry links to. */\n campaignId: string\n /**\n * The email as the team named it in the Emails console — its display name,\n * or its subject when it has none. `null` when the email has since been\n * deleted, in which case the entry still has the subject it was sent with.\n */\n campaignName: string | null\n /** The subject line the person received. */\n subject: string | null\n /** When it went out — the provider's `sent` instant, or the first event seen. */\n sentAtMs: number\n deliveredAtMs?: number\n openedAtMs?: number\n clickedAtMs?: number\n bouncedAtMs?: number\n complainedAtMs?: number\n openCount: number\n clickCount: number\n}\n\n/**\n * One entry of a contact's timeline (AGL-2600): something the platform\n * CAPTURED on the contact's facet, something a person LOGGED beside it, or\n * — since AGL-2616 — a CAMPAIGN email the person was sent.\n *\n * A tagged union rather than a flattened row, because the three are\n * different facts with different affordances — a captured interaction is\n * read-only and names the door it came through, a logged activity has an\n * author who may edit it, a campaign email is read-only and links to the\n * campaign's report — and a surface drawing the stream has to say which is\n * which. A logged activity of kind `email` and a campaign entry are two\n * different things on purpose: the first is a message a person on the team\n * sent or recorded, the second is a mailing the platform delivered, and the\n * two must never share a kind id or a glyph.\n */\nexport type ContactTimelineEntry =\n | {\n kind: 'captured'\n /** Distinct across the merged list, for a React key. */\n key: string\n atMs: number\n interaction: ContactInteraction\n }\n | {\n kind: 'logged'\n key: string\n atMs: number\n activity: CrmActivityRow\n }\n | {\n kind: 'campaign'\n key: string\n atMs: number\n email: ContactCampaignEmail\n }\n\n/**\n * What became of one campaign email, as the words a timeline row prints\n * after the email's name: `sent · delivered · opened ×2 · clicked`.\n *\n * In lifecycle order, and only the states that happened. `sent` is always\n * first because the row exists; the counts appear only past one, because\n * \"opened ×1\" says nothing \"opened\" does not. A bounce or a complaint is\n * printed where it falls, after whatever succeeded before it — a message\n * delivered and then complained about reads as both, which is what\n * happened.\n */\nexport function campaignEmailSummary(email: ContactCampaignEmail): string[] {\n const counted = (word: string, count: number): string =>\n count > 1 ? `${word} ×${count}` : word\n const parts = ['sent']\n if (email.deliveredAtMs) parts.push('delivered')\n if (email.openedAtMs) parts.push(counted('opened', email.openCount))\n if (email.clickedAtMs) parts.push(counted('clicked', email.clickCount))\n if (email.bouncedAtMs) parts.push('bounced')\n if (email.complainedAtMs) parts.push('marked as spam')\n return parts\n}\n\n/** A time that can be sorted on; anything that is not one sinks to the bottom. */\nconst sortableMs = (value: unknown): number =>\n typeof value === 'number' && Number.isFinite(value)\n ? value\n : Number.NEGATIVE_INFINITY\n\n/**\n * ONE newest-first stream from a contact's two histories.\n *\n * The captured interactions live on the contact's facet and the logged\n * activities in their own collection, and a page showing them as two lists\n * asks the reader to do the interleaving in their head — \"did the call come\n * before or after the order?\" is the question a timeline exists to answer.\n *\n * STABLE on purpose. The sort is on `atMs` alone, and at a tie the captured\n * entries keep their place ahead of the logged ones, each in the order it\n * arrived: a facet's interactions are already newest-first and a listener's\n * rows are already ordered, so the merge must not reshuffle what its inputs\n * settled. An entry with no usable time goes LAST, never first — a row that\n * cannot say when it happened must not read as the most recent thing.\n *\n * The key is what a list renders by. A captured interaction has no id of its\n * own, so its key is built from what it does carry; a logged activity's is\n * its document id; a campaign email's is the provider's message id.\n *\n * A campaign email is placed at the instant it was SENT, not at its latest\n * open. The row tells the whole story of that message in one line, and a\n * message that moved up the stream every time its reader re-opened it would\n * be a timeline that reorders itself under the reader.\n */\nexport function mergeContactTimeline(\n interactions: readonly ContactInteraction[] | null | undefined,\n activities: readonly CrmActivityRow[] | null | undefined,\n campaignEmails?: readonly ContactCampaignEmail[] | null,\n): ContactTimelineEntry[] {\n const entries: ContactTimelineEntry[] = [\n ...(interactions ?? []).map(\n (interaction, index): ContactTimelineEntry => ({\n kind: 'captured',\n key: `captured:${interaction.type}:${interaction.refId ?? index}:${interaction.atMs}:${index}`,\n atMs: interaction.atMs,\n interaction,\n }),\n ),\n ...(activities ?? []).map(\n (activity): ContactTimelineEntry => ({\n kind: 'logged',\n key: `logged:${activity.$id}`,\n atMs: activity.atMs,\n activity,\n }),\n ),\n ...(campaignEmails ?? []).map(\n (email): ContactTimelineEntry => ({\n kind: 'campaign',\n key: `campaign:${email.messageId}`,\n atMs: email.sentAtMs,\n email,\n }),\n ),\n ]\n // `Array.prototype.sort` is stable, which is what keeps the tie rule above\n // true without a secondary comparison on the entry's kind or position.\n return entries.sort((a, b) => sortableMs(b.atMs) - sortableMs(a.atMs))\n}\n\n/**\n * Which record an activity is ABOUT, for a surface that links to it, or\n * `null` when it was filed against nothing.\n *\n * A contact outranks a deal outranks a company. An activity can name all\n * three — a call with a person about a deal at their company — and the\n * contact is the one a reader means when they ask \"who was this with\"; the\n * deal is next because it is the thing with a clock on it; the company is\n * the widest and so the last resort.\n */\nexport function crmActivityRecordLink(\n activity: Pick<CrmActivity, 'contactId' | 'companyId' | 'dealId' | 'leadId'>,\n): { record: 'contact' | 'deal' | 'company' | 'lead'; id: string } | null {\n if (activity.contactId) return { record: 'contact', id: activity.contactId }\n if (activity.dealId) return { record: 'deal', id: activity.dealId }\n if (activity.companyId) return { record: 'company', id: activity.companyId }\n // A lead is the narrowest: once converted, the contact it became leads.\n if (activity.leadId) return { record: 'lead', id: activity.leadId }\n return null\n}\n\nconst MINUTE_MS = 60_000\nconst HOUR_MS = 60 * MINUTE_MS\nconst DAY_MS = 24 * HOUR_MS\n\n/**\n * How long ago something happened, in the words a list row uses.\n *\n * Coarse on purpose. A row reads \"3 days ago\" and carries the full timestamp\n * in its tooltip; the sentence exists so that a reader scanning a log can\n * tell this week from last month without parsing dates. Past a week the\n * relative form stops helping — \"412 days ago\" is a date the reader has to\n * compute — so it becomes one. A time AHEAD of now is a date too: a call\n * logged for tomorrow is a scheduling mistake the page should show plainly\n * rather than dress as \"in 14 hours\". An unusable time reads as nothing.\n *\n * English rather than `Intl.RelativeTimeFormat` because the console is\n * English-only, and the thresholds are what the spec pins.\n */\nexport function activityTimeLabel(atMs: number, nowMs: number): string {\n if (!Number.isFinite(atMs) || !Number.isFinite(nowMs)) return ''\n const elapsed = nowMs - atMs\n if (elapsed < -MINUTE_MS || elapsed >= 7 * DAY_MS) {\n return new Date(atMs).toLocaleDateString()\n }\n if (elapsed < MINUTE_MS) return 'just now'\n if (elapsed < HOUR_MS) return `${Math.floor(elapsed / MINUTE_MS)} min ago`\n if (elapsed < DAY_MS) return `${Math.floor(elapsed / HOUR_MS)} h ago`\n if (elapsed < 2 * DAY_MS) return 'yesterday'\n return `${Math.floor(elapsed / DAY_MS)} days ago`\n}\n\nexport type ContactFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'select'\n | 'checkbox'\n | 'url'\n\n/**\n * `orgs/{orgId}/contactFields/{fieldId}` — a custom field a holder defined.\n *\n * The `key` is the map key under a facet's `custom`, so it is IMMUTABLE once\n * a value has been written under it; a rename is a new field and a retire.\n * `retiredAt` rather than a delete for the same reason: values written under\n * the key survive, and a retired field still has to be able to read them\n * back on an export.\n */\nexport interface ContactFieldDefinition extends CrmScoped {\n /** `^[a-z][a-z0-9_]{0,39}$` — see {@link normalizeContactFieldKey}. */\n key: string\n label: string\n type: ContactFieldType\n /** The choices, for `select`. */\n options?: string[]\n required?: boolean\n /** Position in the form and the export, ascending. */\n order: number\n retiredAt?: number | null\n /**\n * Which record the field describes (AGL-2661). ABSENT means `contact`,\n * because every definition written before companies and deals could carry\n * custom fields described a contact, and a backfill that stamped them\n * would touch every org for a fact the reader can infer. Read it through\n * {@link fieldDefinitionObject}, never directly, so that inference lives\n * in one place. Keys are unique PER OBJECT: a company field and a contact\n * field may both be called `region`.\n */\n object?: CrmFieldObject\n}\n\n/**\n * The records a custom field may describe, in the order the Fields\n * section tabs them (AGL-2661), with `lead` since AGL-3272.\n *\n * A lead is last because it is the record a person leaves: the three\n * before it are what a lead becomes, and the tabs read in that order.\n */\nexport const CRM_FIELD_OBJECTS = ['contact', 'company', 'deal', 'lead'] as const\n\nexport type CrmFieldObject = (typeof CRM_FIELD_OBJECTS)[number]\n\n/** How each object reads on the Fields section's tabs and in a refusal. */\nexport const CRM_FIELD_OBJECT_LABELS: Record<CrmFieldObject, string> = {\n contact: 'Contacts',\n company: 'Companies',\n deal: 'Deals',\n lead: 'Leads',\n}\n\nexport function isCrmFieldObject(value: unknown): value is CrmFieldObject {\n return (\n typeof value === 'string' &&\n (CRM_FIELD_OBJECTS as readonly string[]).includes(value)\n )\n}\n\n/**\n * Which record a definition describes — `contact` when the document says\n * nothing, or says something no reader understands.\n *\n * The one reader of {@link ContactFieldDefinition.object}. A stored value\n * outside the list is read as `contact` rather than refused, for the\n * reason an absent one is: the definition predates the field, or was\n * written by something that never learned it, and either way the values\n * under its key sit on contacts.\n */\nexport function fieldDefinitionObject(\n definition: Pick<ContactFieldDefinition, 'object'> | null | undefined,\n): CrmFieldObject {\n return isCrmFieldObject(definition?.object) ? definition.object : 'contact'\n}\n\n/**\n * What the CRM › Fields table's query reads on a definition (AGL-3335):\n *\n * object which tab it belongs to, stored even for a contact's, because\n * a query cannot find a field's absence;\n * required a boolean on every definition, for the Required filter;\n * searchTokens every word prefix of its name and of its key, the key read\n * whole and split at `_`, `-` and `.`, so \"plan\" finds\n * `plan_interest` and \"interest\" does too.\n *\n * Its script-side twin is `crmFieldListFields` in\n * `tools/scripts/lib/org-record-list-fields.mjs`; both answer the\n * `fieldDefinitions` of `org-record-list-fields.fixtures.json`.\n */\nexport function crmFieldListFields(definition: Record<string, unknown> | null | undefined): {\n object: CrmFieldObject\n required: boolean\n searchTokens: string[]\n} {\n const label = typeof definition?.['label'] === 'string' ? definition['label'] : ''\n const key = typeof definition?.['key'] === 'string' ? definition['key'] : ''\n return {\n object: fieldDefinitionObject(definition as Pick<ContactFieldDefinition, 'object'>),\n required: definition?.['required'] === true,\n searchTokens: nameSearchTokens([label, key, key.replace(/[_.-]+/g, ' ')].join(' ')),\n }\n}\n\n/**\n * The definition type under the name new code uses (AGL-2661). The type,\n * the collection (`contactFields`) and the rules match keep the contact\n * name: renaming them would ripple through the rules, the REST resources\n * and the docs for no change in what is stored.\n */\nexport type CrmFieldDefinition = ContactFieldDefinition\n\n/** What a stored field key must look like — a letter, then up to 39 of `[a-z0-9_]`. */\nexport const CONTACT_FIELD_KEY_PATTERN = /^[a-z][a-z0-9_]{0,39}$/\n\n/**\n * A typed label, as the key it would be stored under, or `null` when nothing\n * usable survives.\n *\n * Lowercased and snake-cased rather than refused, because the key is derived\n * from the label a merchant typed (\"Annual revenue\" → `annual_revenue`) and\n * they should not have to learn the grammar. Anything outside `[a-z0-9_]` is\n * dropped after separators become underscores, runs collapse, and a leading\n * run of digits or underscores goes — a key has to start with a letter so it\n * can never collide with an array index or read as a number in a filter.\n */\nexport function normalizeContactFieldKey(input: unknown): string | null {\n const key = String(input ?? '')\n .trim()\n .toLowerCase()\n .replace(/[\\s-]+/g, '_')\n .replace(/[^a-z0-9_]/g, '')\n .replace(/_+/g, '_')\n .replace(/^[^a-z]+/, '')\n .replace(/_+$/, '')\n .slice(0, 40)\n return CONTACT_FIELD_KEY_PATTERN.test(key) ? key : null\n}\n\n/** The stage a deal is in, or `null` when the pipeline no longer has it. */\nexport function dealStageById(\n pipeline: Pick<CrmPipeline, 'stages'> | null | undefined,\n stageId: string,\n): CrmDealStage | null {\n return pipeline?.stages?.find((stage) => stage.id === stageId) ?? null\n}\n\n/**\n * What a deal is worth to a forecast, in cents.\n *\n * STATUS wins over the stage. A deal marked won is worth its full amount\n * whatever stage it happens to sit in, and a lost one is worth nothing —\n * the stage's probability is the odds of an OPEN deal, and applying it to a\n * closed one would forecast a sale that has already happened as a fraction\n * of itself. An open deal with no resolvable stage is worth nothing rather\n * than everything: the pipeline lost the stage, and a forecast that filled\n * the gap with 100% would be the most optimistic number available.\n */\nexport function weightedDealAmountCents(\n deal: Pick<CrmDeal, 'amountCents' | 'status'>,\n stage: Pick<CrmDealStage, 'probability'> | null | undefined,\n): number {\n const amount = Math.max(0, Math.round(Number(deal.amountCents ?? 0) || 0))\n if (deal.status === 'won') return amount\n if (deal.status === 'lost' || !stage) return 0\n const probability = Math.min(100, Math.max(0, Number(stage.probability) || 0))\n return Math.round((amount * probability) / 100)\n}\n\n/** Whether a deal's amount is derived — it has at least one line item. */\nexport function dealHasLineItems(\n deal: Pick<CrmDeal, 'lineItems'> | null | undefined,\n): boolean {\n return Array.isArray(deal?.lineItems) && deal.lineItems.length > 0\n}\n\n/**\n * What a set of line items adds up to, in cents — the amount a deal with\n * line items stores. Whole cents, never negative: a line's quantity and\n * unit amount have both been through `readDealLineItems`, but a stored\n * document from an older writer is trusted no further than that.\n */\nexport function lineItemsTotalCents(\n items: readonly Pick<CrmDealLineItem, 'quantity' | 'unitAmountCents'>[] | null | undefined,\n): number {\n let total = 0\n for (const item of items ?? []) {\n const quantity = Math.max(0, Math.round(Number(item.quantity) || 0))\n const unit = Math.max(0, Math.round(Number(item.unitAmountCents) || 0))\n total += quantity * unit\n }\n return total\n}\n\n/**\n * Line items as a client typed or sent them, validated into the stored\n * shape, or the one reason they were refused.\n *\n * The one reader for both writers — the products card on a deal's page\n * and `POST`/`PATCH /v1/deals` — so a line the console accepts is a line\n * the API accepts and the other way round. The rules: at most\n * {@link DEAL_LINE_ITEMS_MAX} lines; every line a name, a whole quantity\n * of one or more, a whole unit amount of zero or more; every line in the\n * DEAL's currency (`currency`), which a line may omit and may not\n * contradict. An empty list is valid and means \"no line items\" — the\n * amount goes back to being typed.\n */\nexport function readDealLineItems(\n input: unknown,\n currency: string,\n): { items: CrmDealLineItem[] } | { error: string } {\n if (input === undefined || input === null) return { items: [] }\n if (!Array.isArray(input)) return { error: 'Line items must be a list' }\n if (input.length > DEAL_LINE_ITEMS_MAX) {\n return { error: `A deal carries at most ${DEAL_LINE_ITEMS_MAX} line items` }\n }\n const dealCurrency = String(currency || 'usd').trim().toLowerCase()\n const items: CrmDealLineItem[] = []\n for (const [index, raw] of input.entries()) {\n const at = `Line ${index + 1}`\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {\n return { error: `${at} must be an object` }\n }\n const line = raw as Record<string, unknown>\n const name = String(line.name ?? '')\n .trim()\n .slice(0, DEAL_LINE_ITEM_NAME_MAX)\n if (!name) return { error: `${at} needs a name` }\n const quantity = Number(line.quantity)\n if (!Number.isInteger(quantity) || quantity < 1 || quantity > DEAL_LINE_ITEM_QUANTITY_MAX) {\n return {\n error: `${at}: the quantity must be a whole number from 1 to ${DEAL_LINE_ITEM_QUANTITY_MAX.toLocaleString()}`,\n }\n }\n const unitAmountCents = Number(line.unitAmountCents)\n if (!Number.isInteger(unitAmountCents) || unitAmountCents < 0) {\n return { error: `${at}: the unit amount must be a whole number of cents, 0 or more` }\n }\n const lineCurrency =\n line.currency === undefined || line.currency === null || line.currency === ''\n ? dealCurrency\n : String(line.currency).trim().toLowerCase()\n if (lineCurrency !== dealCurrency) {\n return { error: `${at} is in ${lineCurrency.toUpperCase()}; every line must be in the deal's currency, ${dealCurrency.toUpperCase()}` }\n }\n const productId =\n typeof line.productId === 'string' && line.productId.trim()\n ? line.productId.trim().slice(0, 200)\n : undefined\n items.push({\n ...(productId ? { productId } : {}),\n name,\n quantity,\n unitAmountCents,\n currency: dealCurrency,\n })\n }\n return { items }\n}\n\nexport type TaskDueState = 'overdue' | 'today' | 'upcoming' | 'none' | 'done'\n\n/**\n * Where a task stands against the clock.\n *\n * `today` is decided on the LOCAL calendar day and beats `overdue`: a task\n * due at nine this morning is today's work until midnight, not something the\n * list should paint red at 9:01. Overdue is yesterday or earlier. `done`\n * comes first because a completed task's due date is history whichever side\n * of now it fell on.\n */\nexport function taskDueState(\n task: Pick<CrmTask, 'status' | 'dueAtMs'>,\n nowMs: number,\n): TaskDueState {\n if (task.status === 'done') return 'done'\n const dueAtMs = task.dueAtMs\n if (typeof dueAtMs !== 'number' || !Number.isFinite(dueAtMs)) return 'none'\n const due = new Date(dueAtMs)\n const now = new Date(nowMs)\n const sameDay =\n due.getFullYear() === now.getFullYear() &&\n due.getMonth() === now.getMonth() &&\n due.getDate() === now.getDate()\n if (sameDay) return 'today'\n return dueAtMs < nowMs ? 'overdue' : 'upcoming'\n}\n\n/**\n * A hostname label: `[a-z0-9-]`, not starting or ending with a hyphen, at\n * most 63 characters — RFC 1123 as far as a company domain needs it.\n */\nconst DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/\n\n/**\n * A typed or pasted domain, reduced to the bare lowercase hostname a company\n * is keyed by, or `null` when what is left is not one.\n *\n * Strips what people paste along with a domain — the protocol, a `www.`, a\n * path, a query, a port — because the value is a KEY: two contacts at\n * `https://www.acme.com/about` and `acme.com` work for one company, and the\n * match is only findable if both reduce to the same string. The last label\n * has to be letters because a company's domain has a TLD; a bare IP address\n * or a single word is not one and answers `null` rather than being stored as\n * something a later match will never hit.\n */\nexport function normalizeCompanyDomain(input: unknown): string | null {\n let value = String(input ?? '')\n .trim()\n .toLowerCase()\n if (!value) return null\n value = value.replace(/^[a-z][a-z0-9+.-]*:\\/\\//, '')\n value = value.replace(/^www\\./, '')\n value = value.split(/[/?#]/, 1)[0]\n value = value.split(':', 1)[0]\n value = value.replace(/\\.+$/, '')\n if (!value || value.length > 253) return null\n const labels = value.split('.')\n if (labels.length < 2) return null\n if (!labels.every((label) => DOMAIN_LABEL.test(label))) return null\n if (!/^[a-z]{2,}$/.test(labels[labels.length - 1])) return null\n return value\n}\n\n/** The longest website URL a company stores. */\nconst COMPANY_WEBSITE_MAX = 500\n\n/**\n * The typed website, as a URL; `''` for a blank; `null` when it cannot be\n * one.\n *\n * People type `acme.com` where a URL is asked for, and refusing that is\n * pedantry — it becomes `https://acme.com`. What IS refused is anything the\n * URL parser cannot read or a scheme other than http(s): a `javascript:` link\n * on a record that renders as an anchor is not a website. One function for\n * the company drawer and the companies import, so a website typed and a\n * website imported are stored the same way.\n */\nexport function normalizeCompanyWebsite(input: unknown): string | null {\n const raw = String(input ?? '').trim()\n if (!raw) return ''\n const candidate = /^[a-z][a-z0-9+.-]*:\\/\\//i.test(raw) ? raw : `https://${raw}`\n try {\n const url = new URL(candidate)\n if (url.protocol !== 'http:' && url.protocol !== 'https:') return null\n return url.href.length > COMPANY_WEBSITE_MAX ? null : url.href\n } catch {\n return null\n }\n}\n\n/**\n * Mailbox domains that belong to people rather than to a business: the\n * providers anybody can sign up with, and the ones an internet provider\n * hands its subscribers.\n *\n * Auto-associating a contact with a company by email domain is the whole\n * reason {@link companyDomainForEmail} exists, and it is wrong for every\n * address at a public mailbox: `gmail.com` is not a company, and a rule\n * that treated it as one would file half of a consumer list under a single\n * phantom account. It is also the fact a cold email has to know — an\n * address here is a person's own, not their work address — which is why the\n * list is exported with {@link isPublicMailboxDomain}: a second list kept\n * for that question would drift from this one.\n *\n * Exact domains, not suffixes: a subscriber's regional host (`nc.rr.com`)\n * is not listed, and neither is a company that happens to end in `mail.com`.\n */\nexport const PUBLIC_MAILBOX_DOMAINS: ReadonlySet<string> = new Set([\n // Google, Yahoo, Microsoft, Apple, AOL.\n 'gmail.com',\n 'googlemail.com',\n 'yahoo.com',\n 'yahoo.co.uk',\n 'yahoo.ca',\n 'yahoo.com.au',\n 'yahoo.co.in',\n 'yahoo.fr',\n 'yahoo.de',\n 'ymail.com',\n 'rocketmail.com',\n 'hotmail.com',\n 'hotmail.co.uk',\n 'hotmail.fr',\n 'hotmail.de',\n 'hotmail.it',\n 'outlook.com',\n 'live.com',\n 'live.co.uk',\n 'live.ca',\n 'msn.com',\n 'icloud.com',\n 'me.com',\n 'mac.com',\n 'aol.com',\n 'aim.com',\n // Independent and privacy-first providers.\n 'proton.me',\n 'protonmail.com',\n 'protonmail.ch',\n 'pm.me',\n 'tutanota.com',\n 'tuta.io',\n 'hushmail.com',\n 'gmx.com',\n 'gmx.net',\n 'gmx.de',\n 'web.de',\n 'mail.com',\n 'yandex.com',\n 'yandex.ru',\n 'zoho.com',\n 'zohomail.com',\n 'fastmail.com',\n 'hey.com',\n // The largest consumer providers outside the US and Europe.\n 'qq.com',\n '163.com',\n '126.com',\n 'sina.com',\n 'naver.com',\n 'hanmail.net',\n 'rediffmail.com',\n // US internet providers' subscriber mailboxes.\n 'comcast.net',\n 'att.net',\n 'sbcglobal.net',\n 'bellsouth.net',\n 'pacbell.net',\n 'verizon.net',\n 'cox.net',\n 'charter.net',\n 'earthlink.net',\n 'optonline.net',\n 'frontier.com',\n 'windstream.net',\n 'centurylink.net',\n 'juno.com',\n 'netzero.net',\n // Subscriber mailboxes at the largest providers in Canada, the UK, France,\n // Germany, Italy and Australia.\n 'shaw.ca',\n 'sympatico.ca',\n 'btinternet.com',\n 'sky.com',\n 'virginmedia.com',\n 'orange.fr',\n 'free.fr',\n 'laposte.net',\n 't-online.de',\n 'libero.it',\n 'bigpond.com',\n])\n\n/**\n * Whether a domain — or the domain of an address — is one of the\n * {@link PUBLIC_MAILBOX_DOMAINS}. Read the way a company domain is, so case,\n * a trailing dot and a pasted `www.` do not hide one.\n */\nexport function isPublicMailboxDomain(domainOrEmail: unknown): boolean {\n const value = String(domainOrEmail ?? '').trim()\n const domain = normalizeCompanyDomain(value.slice(value.lastIndexOf('@') + 1))\n return domain !== null && PUBLIC_MAILBOX_DOMAINS.has(domain)\n}\n\n/**\n * The company domain an email address implies, or `null` when it implies\n * none — a malformed address, or one at a public mailbox provider.\n */\nexport function companyDomainForEmail(email: unknown): string | null {\n const value = String(email ?? '')\n .trim()\n .toLowerCase()\n const at = value.lastIndexOf('@')\n if (at < 1 || at === value.length - 1) return null\n if (/[\\s@]/.test(value.slice(0, at))) return null\n const domain = normalizeCompanyDomain(value.slice(at + 1))\n if (!domain || PUBLIC_MAILBOX_DOMAINS.has(domain)) return null\n return domain\n}\n\n/**\n * The name a company minted from a domain starts with: the first label with\n * a capital — `acme.com` → `Acme`, `initech.co.uk` → `Initech`.\n *\n * A starting point and not a claim about what the business is called: the\n * lead-convert dialog offers it for editing, and a company the capture door\n * creates on its own carries it until somebody renames the record. The\n * domain itself would be an honest name too, but a list of companies that\n * reads `acme.com`, `globex.example` looks like a list of websites, and the\n * domain is on the row beside the name anyway.\n */\nexport function companyNameForDomain(domain: string): string {\n const label = domain.split('.')[0] || domain\n return label.charAt(0).toUpperCase() + label.slice(1)\n}\n\n/*==========================================\n * THE CONTACT–COMPANY LINK (AGL-2597, AGL-2613).\n *\n * The association proper is `facets.{groupId}.companyId` — one company per\n * holder, inside that holder's facet like the notes and the tags, and for\n * the same reason: which account a person belongs to is one business's\n * knowledge of them. But a facet path is per group, and Firestore cannot\n * answer \"every contact whose facet, whichever group's, names company X\" —\n * a `where` needs one field path, and the group id is part of the path. So\n * the facet is MIRRORED into a top-level array an `array-contains` can\n * query, and the company carries a COUNT of the contacts whose mirror names\n * it, so a list of companies can say how many people are at each without\n * reading a person.\n *\n * Three fields, kept in step by ONE planner. Every writer — the picker on a\n * contact's page, the company page's link control, the bulk bar, the server\n * doors that link on capture — asks {@link planContactCompanyLink} what\n * changes and applies the answer with its own SDK's sentinels. None of them\n * decides for itself whether the old id leaves the mirror, which is the rule\n * a second copy would get wrong: another business filing the same person\n * under the same account is THEIR link, and this holder letting go must not\n * take it away.\n *=========================================*/\n\n/**\n * The top-level field on a CONTACT naming every company it is linked to.\n *\n * It carries the union of every holder's link, so removing an id from it is\n * only correct when no other holder's facet still names that id — the rule\n * the planner enforces, and the reason nothing writes this field without\n * going through it. The facet is the truth and this is its index: a reader\n * answering \"which company is this person at\" reads the facet; only a QUERY\n * reads this.\n */\nexport const CONTACT_COMPANY_IDS_FIELD = 'companyIds'\n\n/**\n * The field on a COMPANY counting the contacts whose mirror names it.\n *\n * Denormalized because the honest figure is an aggregate over every contact\n * in the org, which a list of two hundred companies cannot afford to take\n * per row. Moved by `increment` in the same batch as the link that changes\n * it, so the two cannot disagree by a failed second write; a company's own\n * page still takes the live aggregate, which is what corrects a count that\n * predates the counter.\n */\nexport const COMPANY_CONTACTS_COUNT_FIELD = 'contactsCount'\n\n/** What the planner needs to know about a contact's links, and nothing else. */\nexport interface ContactCompanyLinkState {\n /** This holder's link, or `null` when the facet names no company. */\n companyId: string | null\n /** The mirror as stored, in no order. */\n companyIds: string[]\n /**\n * Ids named by OTHER holders' facets — what keeps an id in the mirror\n * when this holder lets go of it.\n */\n heldElsewhere: string[]\n}\n\n/** The link state of a stored contact, read for one holder. */\nexport function readContactCompanyLink(\n contact: Record<string, unknown> | null | undefined,\n groupId: string,\n): ContactCompanyLinkState {\n const document = contact ?? {}\n const facets = document[CONTACT_FACETS_FIELD]\n const heldElsewhere = new Set<string>()\n if (facets && typeof facets === 'object' && !Array.isArray(facets)) {\n for (const [holder, facet] of Object.entries(\n facets as Record<string, unknown>,\n )) {\n if (holder === groupId) continue\n const named =\n facet && typeof facet === 'object' && !Array.isArray(facet)\n ? (facet as Record<string, unknown>)['companyId']\n : undefined\n if (typeof named === 'string' && named) heldElsewhere.add(named)\n }\n }\n const mirror = document[CONTACT_COMPANY_IDS_FIELD]\n return {\n companyId: readContactFacet(document, groupId).companyId ?? null,\n companyIds: Array.isArray(mirror)\n ? mirror.filter((id): id is string => typeof id === 'string' && !!id)\n : [],\n heldElsewhere: [...heldElsewhere],\n }\n}\n\n/**\n * Every holder whose facet names this company, in no order.\n *\n * What a company's DELETION clears. Unlike a link change, which is one\n * holder's, a deletion takes the record away from every holder at once: a\n * facet still naming it would be a link to nothing, on a surface its holder\n * has no reason to revisit. Nothing else about another holder's facet is\n * read.\n */\nexport function contactGroupsNamingCompany(\n contact: Record<string, unknown> | null | undefined,\n companyId: string,\n): string[] {\n const facets = (contact ?? {})[CONTACT_FACETS_FIELD]\n if (!companyId || !facets || typeof facets !== 'object' || Array.isArray(facets)) {\n return []\n }\n return Object.entries(facets as Record<string, unknown>)\n .filter(([, facet]) =>\n Boolean(\n facet &&\n typeof facet === 'object' &&\n !Array.isArray(facet) &&\n (facet as Record<string, unknown>)['companyId'] === companyId,\n ),\n )\n .map(([groupId]) => groupId)\n}\n\n/**\n * How the mirror changes. Three shapes because Firestore takes ONE transform\n * per field per write: an `arrayUnion` and an `arrayRemove` on the same\n * field cannot share an update, so a move rewrites the array whole.\n */\nexport type ContactCompanyMirrorChange =\n | { op: 'union'; companyId: string }\n | { op: 'remove'; companyId: string }\n | { op: 'set'; companyIds: string[] }\n\nexport interface ContactCompanyLinkPlan {\n /** The facet's new value: the id, or `null` meaning delete the field. */\n companyId: string | null\n /** What happens to the mirror, or `null` when it already carries the right ids. */\n mirror: ContactCompanyMirrorChange | null\n /**\n * Per company, how its contacts count moves. A company enters the list\n * only when the mirror actually gains or loses it, so a link some other\n * holder already made is not counted twice and a mirror that never carried\n * an id is not decremented for it.\n */\n counts: Array<{ companyId: string; delta: 1 | -1 }>\n}\n\n/**\n * What linking a contact to a company FOR ONE HOLDER changes — or unlinking\n * them, with `null`. `null` when the document already says what was asked.\n *\n * Three cases, and the mirror is handled differently in each because it is\n * shared across holders while the facet is not:\n *\n * - A first link `union`s the id in, which is safe against a concurrent\n * writer adding another holder's id.\n * - A MOVE from one company to another rewrites the mirror as a whole. The\n * old id is dropped only if no other holder's facet still names it.\n * - An unlink `remove`s the old id, on the same condition, and leaves the\n * mirror alone when another holder still needs it there.\n *\n * The counts follow the mirror, not the facet: the company's figure is \"how\n * many contacts name me in the mirror\", and that is the quantity the\n * company page's live aggregate measures.\n */\nexport function planContactCompanyLink(\n state: ContactCompanyLinkState,\n companyId: string | null,\n): ContactCompanyLinkPlan | null {\n const previous = state.companyId\n if (previous === companyId) return null\n const mirror = new Set(state.companyIds)\n const previousLeaves =\n previous !== null && !state.heldElsewhere.includes(previous)\n const counts: ContactCompanyLinkPlan['counts'] = []\n let change: ContactCompanyMirrorChange | null = null\n if (companyId && !previous) {\n change = { op: 'union', companyId }\n if (!mirror.has(companyId)) counts.push({ companyId, delta: 1 })\n } else if (companyId && previous) {\n const kept = state.companyIds.filter(\n (id) => id !== previous || !previousLeaves,\n )\n change = { op: 'set', companyIds: [...new Set([...kept, companyId])] }\n if (previousLeaves && mirror.has(previous)) {\n counts.push({ companyId: previous, delta: -1 })\n }\n if (!mirror.has(companyId)) counts.push({ companyId, delta: 1 })\n } else if (previous && previousLeaves) {\n change = { op: 'remove', companyId: previous }\n if (mirror.has(previous)) counts.push({ companyId: previous, delta: -1 })\n }\n return { companyId, mirror: change, counts }\n}\n\n/*==========================================\n * THE CRM's ORGANIZATION SETTINGS (AGL-2613).\n *\n * One map under `crm` on the org document, so the CRM → Settings section\n * can grow a key per setting without a rules change each time: the org\n * document's client branch is a deny-list, and `crm` is declared\n * client-writable in `ORG_CLIENT_WRITABLE_FIELDS` with its reason.\n *=========================================*/\n\n/** The org-document key the CRM's settings live under. */\nexport const ORG_CRM_SETTINGS_FIELD = 'crm'\n\n/** The dotted path an `update()` writes the auto-create switch by. */\nexport const CRM_AUTO_CREATE_COMPANIES_PATH = `${ORG_CRM_SETTINGS_FIELD}.autoCreateCompanies`\n\n/**\n * Whether a capture from a work email domain no visible company carries\n * should CREATE the company. Off unless the org document says `true`: a\n * company minted from every domain that ever submitted a form is a list\n * nobody asked for, so the default is the quiet one.\n *\n * Read off the raw document rather than a typed field, because the capture\n * door holds a `Partial<AglynOrganization>` and the console a\n * `Partial<AglynOrgBilling>`, and one reader has to answer both.\n */\nexport function orgAutoCreatesCompanies(\n orgDocument: Record<string, unknown> | null | undefined,\n): boolean {\n const settings = (orgDocument ?? {})[ORG_CRM_SETTINGS_FIELD]\n return Boolean(\n settings &&\n typeof settings === 'object' &&\n !Array.isArray(settings) &&\n (settings as Record<string, unknown>)['autoCreateCompanies'] === true,\n )\n}\n\n/*==========================================\n * WHO A NEW RECORD BELONGS TO (AGL-2618).\n *\n * Salesforce calls these assignment rules and HubSpot rotates to an owner;\n * either way they are the first thing a sales team configures, because a\n * record with no owner is one nobody follows up. The shape here is what the\n * CRM → Settings section writes onto the org document under `crm`, what the\n * capture door's assignment pass reads, and what the `assignContactOwner`\n * automation step's round-robin mode reads — one vocabulary, three readers.\n *\n * ## The order of decision\n *\n * The rules are tried in their stored order and the FIRST match assigns; a\n * rule that names a member the roster no longer has, or a round-robin rule\n * over an empty pool, is passed over rather than stopping the pass. When no\n * rule claims the capture, the capturing site's default owner does; when\n * the site has none, the record stays unassigned, which is what the product\n * did before any of this existed and is the honest answer to \"nobody has\n * said\".\n *\n * ## Only a record with no owner\n *\n * The capture pass runs when a record is CREATED and never overwrites an\n * owner: a door that named one (the console's drawer, an import column, a\n * conversion with a picked owner) has expressed a person's choice, and a\n * returning visitor's contact already belongs to somebody. The automation\n * step is the deliberate exception — an author who put \"assign an owner\"\n * on a stage change means to reassign.\n *\n * ## Why the pointer is a uid, and why the server moves it\n *\n * See `OrgCrmRoundRobin`. The pool is edited in the console and the pointer\n * is advanced by the Admin SDK inside the transaction that writes the owner,\n * so concurrent captures take distinct turns and an edit to the pool never\n * skips or repeats a member.\n *=========================================*/\n\n/** Where the ordered rules live on the org document. */\nexport const CRM_ASSIGNMENT_RULES_PATH = `${ORG_CRM_SETTINGS_FIELD}.assignmentRules`\n\n/** The round-robin pool's member list, in rotation order. */\nexport const CRM_ROUND_ROBIN_POOL_PATH = `${ORG_CRM_SETTINGS_FIELD}.roundRobin.memberUids`\n\n/** The member handed the most recent round-robin record. */\nexport const CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH = `${ORG_CRM_SETTINGS_FIELD}.roundRobin.lastAssignedUid`\n\n/**\n * The most rules an org may keep. A first-match list longer than this is\n * one nobody can reason about, and the section refuses the fifty-first\n * rather than letting the document grow unbounded.\n */\nexport const CRM_ASSIGNMENT_RULES_MAX = 50\n\n/** The most members a round-robin pool may hold. */\nexport const CRM_ROUND_ROBIN_POOL_MAX = 50\n\n/**\n * The map of per-site CRM settings under `crm` on the ORG document, keyed\n * by host id. A field on the org, not a subcollection of the host: the\n * host-subcollection guards read a quoted `'hosts'` beside a site id as a\n * client path under `hosts/{hostId}`, which this is not, so the key is\n * named here and spelled nowhere else.\n */\nconst CRM_HOST_SETTINGS_KEY = 'hosts'\n\n/**\n * The field-path SEGMENTS of a site's default owner on the org document.\n *\n * Segments rather than a dotted string, because the host id is a document\n * id and a document id may contain a dot; joined with dots it would be read\n * as two path elements and the write would land beside the setting rather\n * than in it. A caller builds a `FieldPath` from these on either SDK.\n */\nexport function crmHostDefaultOwnerSegments(hostId: string): string[] {\n if (!hostId) throw new Error('a site default owner must name a site')\n return [ORG_CRM_SETTINGS_FIELD, CRM_HOST_SETTINGS_KEY, hostId, 'defaultOwnerUid']\n}\n\n/** The conditions a rule may name — every one present must hold. */\nexport interface CrmAssignmentRuleWhen {\n /** The capture door: `form`, `booking`, `order`, `manual`… */\n source?: ContactSource\n /** The form the capture came through, by its document id. */\n formId?: string\n /** The captured address's domain, lowercased, without a leading `@`. */\n emailDomain?: string\n /** A tag the capture carries or the contact already wears. */\n tag?: string\n}\n\n/** How a matching rule assigns: one member, or the next in the pool. */\nexport type CrmAssignmentTarget = { memberUid: string } | { roundRobin: true }\n\n/**\n * One rule as the CRM reads it — `OrgCrmAssignmentRule` with the source\n * narrowed to the capture vocabulary.\n */\nexport interface CrmAssignmentRule extends OrgCrmAssignmentRule {\n when: CrmAssignmentRuleWhen\n assign: CrmAssignmentTarget\n}\n\n/** The pool as the CRM reads it: never absent, possibly empty. */\nexport interface CrmRoundRobinPool {\n memberUids: string[]\n lastAssignedUid: string | null\n}\n\n/**\n * The org's assignment settings, read tolerantly off the raw document.\n *\n * Every field is optional on the document and the section writes each on\n * its own, so a reader that trusted the shape would throw on the first org\n * that has set the pool but never a rule. Malformed entries are dropped\n * rather than refused wholesale: one hand-edited rule must not stop the\n * others from assigning.\n */\nexport interface CrmAssignmentSettings {\n rules: CrmAssignmentRule[]\n pool: CrmRoundRobinPool\n /** Site id → default owner uid, for the sites that set one. */\n hostDefaultOwners: Record<string, string>\n}\n\nfunction isContactSourceValue(value: unknown): value is ContactSource {\n return typeof value === 'string' && value in CONTACT_SOURCE_LABELS\n}\n\nfunction cleanText(value: unknown, max: number): string | undefined {\n const text = typeof value === 'string' ? value.trim().slice(0, max) : ''\n return text || undefined\n}\n\n/** A rule as the document holds it, or `null` for one that cannot assign. */\nexport function readCrmAssignmentRule(raw: unknown): CrmAssignmentRule | null {\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null\n const entry = raw as Record<string, unknown>\n const id = cleanText(entry['id'], 64)\n if (!id) return null\n const whenRaw =\n entry['when'] && typeof entry['when'] === 'object' && !Array.isArray(entry['when'])\n ? (entry['when'] as Record<string, unknown>)\n : {}\n const when: CrmAssignmentRuleWhen = {}\n if (isContactSourceValue(whenRaw['source'])) when.source = whenRaw['source']\n const formId = cleanText(whenRaw['formId'], 128)\n if (formId) when.formId = formId\n // A person types a domain the way they see one in an address — `@acme.com`\n // — so the `@` is what the field means, not part of the domain.\n const emailDomain = normalizeCompanyDomain(\n String(whenRaw['emailDomain'] ?? '')\n .trim()\n .replace(/^@/, ''),\n )\n if (emailDomain) when.emailDomain = emailDomain\n const tag = cleanText(whenRaw['tag'], 60)?.toLowerCase()\n if (tag) when.tag = tag\n const assignRaw =\n entry['assign'] &&\n typeof entry['assign'] === 'object' &&\n !Array.isArray(entry['assign'])\n ? (entry['assign'] as Record<string, unknown>)\n : null\n if (!assignRaw) return null\n if (assignRaw['roundRobin'] === true) {\n return { id, when, assign: { roundRobin: true } }\n }\n const memberUid = cleanText(assignRaw['memberUid'], 128)\n return memberUid ? { id, when, assign: { memberUid } } : null\n}\n\nexport function readCrmAssignmentSettings(\n orgDocument: Record<string, unknown> | null | undefined,\n): CrmAssignmentSettings {\n const settings = (orgDocument ?? {})[ORG_CRM_SETTINGS_FIELD]\n const crm =\n settings && typeof settings === 'object' && !Array.isArray(settings)\n ? (settings as Record<string, unknown>)\n : {}\n const rules = (Array.isArray(crm['assignmentRules']) ? crm['assignmentRules'] : [])\n .map(readCrmAssignmentRule)\n .filter((rule): rule is CrmAssignmentRule => rule !== null)\n .slice(0, CRM_ASSIGNMENT_RULES_MAX)\n const roundRobin =\n crm['roundRobin'] && typeof crm['roundRobin'] === 'object'\n ? (crm['roundRobin'] as Record<string, unknown>)\n : {}\n const memberUids = [\n ...new Set(\n (Array.isArray(roundRobin['memberUids']) ? roundRobin['memberUids'] : [])\n .map((uid) => cleanText(uid, 128))\n .filter((uid): uid is string => Boolean(uid)),\n ),\n ].slice(0, CRM_ROUND_ROBIN_POOL_MAX)\n const hostSettings = crm[CRM_HOST_SETTINGS_KEY]\n const hosts =\n hostSettings && typeof hostSettings === 'object' && !Array.isArray(hostSettings)\n ? (hostSettings as Record<string, unknown>)\n : {}\n const hostDefaultOwners: Record<string, string> = {}\n for (const [hostId, value] of Object.entries(hosts)) {\n const owner =\n value && typeof value === 'object'\n ? cleanText((value as Record<string, unknown>)['defaultOwnerUid'], 128)\n : undefined\n if (owner) hostDefaultOwners[hostId] = owner\n }\n return {\n rules,\n pool: {\n memberUids,\n lastAssignedUid: cleanText(roundRobin['lastAssignedUid'], 128) ?? null,\n },\n hostDefaultOwners,\n }\n}\n\n/** The member a site hands unclaimed captures to, or `null` for nobody. */\nexport function crmHostDefaultOwner(\n orgDocument: Record<string, unknown> | null | undefined,\n hostId: string,\n): string | null {\n return readCrmAssignmentSettings(orgDocument).hostDefaultOwners[hostId] ?? null\n}\n\n/** What a rule is matched against: the capture, as its door described it. */\nexport interface CrmAssignmentCapture {\n source: ContactSource\n email: string\n formId?: string | null\n /** The capture's own tags and whatever the contact already wears. */\n tags?: readonly string[]\n}\n\n/**\n * The domain a rule's `emailDomain` is compared with — the address's own,\n * lowercased. Not `companyDomainForEmail`, which answers `null` for a public\n * mailbox: a team may well route every `gmail.com` sign-up to one rep, and\n * a rule that could not name a consumer domain could not say so.\n */\nexport function assignmentEmailDomain(email: string): string | null {\n const at = email.lastIndexOf('@')\n if (at < 0) return null\n return normalizeCompanyDomain(email.slice(at + 1))\n}\n\n/**\n * Whether every condition the rule names holds for this capture. A rule\n * with no condition matches every capture — the catch-all.\n */\nexport function assignmentRuleMatches(\n when: CrmAssignmentRuleWhen,\n capture: CrmAssignmentCapture,\n): boolean {\n if (when.source && when.source !== capture.source) return false\n if (when.formId && when.formId !== (capture.formId ?? '')) return false\n if (when.emailDomain && when.emailDomain !== assignmentEmailDomain(capture.email)) {\n return false\n }\n if (when.tag) {\n const worn = (capture.tags ?? []).map((tag) => String(tag).trim().toLowerCase())\n if (!worn.includes(when.tag)) return false\n }\n return true\n}\n\n/**\n * The pool in the order the next record should try it: the member after\n * the last recipient first, wrapping round, and the last recipient\n * themselves at the end — so a pool of one still assigns, and a pointer\n * naming somebody no longer in the pool starts from the top.\n */\nexport function roundRobinOrder(\n memberUids: readonly string[],\n lastAssignedUid: string | null | undefined,\n): string[] {\n if (!memberUids.length) return []\n const at = lastAssignedUid ? memberUids.indexOf(lastAssignedUid) : -1\n if (at < 0) return [...memberUids]\n return [...memberUids.slice(at + 1), ...memberUids.slice(0, at + 1)]\n}\n\n/**\n * How a rule reads in the settings list — its conditions in words and\n * where it sends the record — so a reader can tell two rules apart without\n * opening either. `memberLabel` turns a uid into a name; the section passes\n * the roster's.\n */\nexport function describeAssignmentRule(\n rule: CrmAssignmentRule,\n memberLabel: (uid: string) => string,\n): { when: string; assign: string } {\n const parts: string[] = []\n if (rule.when.source) parts.push(`source is ${CONTACT_SOURCE_LABELS[rule.when.source]}`)\n if (rule.when.formId) parts.push(`form is ${rule.when.formId}`)\n if (rule.when.emailDomain) parts.push(`email domain is ${rule.when.emailDomain}`)\n if (rule.when.tag) parts.push(`tagged ${rule.when.tag}`)\n return {\n when: parts.length ? parts.join(' and ') : 'Every capture',\n assign:\n 'roundRobin' in rule.assign ? 'Round robin' : memberLabel(rule.assign.memberUid),\n }\n}\n\n/**\n * A fresh rule id, unique among the ones the org already holds. Time and\n * entropy rather than a counter, so two admins adding a rule in two tabs\n * do not mint the same id and have one reorder clobber the other's rule.\n */\nexport function newAssignmentRuleId(existing: readonly string[]): string {\n for (;;) {\n const id = `rule-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`\n if (!existing.includes(id)) return id\n }\n}\n\n/**\n * The `visibleTo` every CRM creator stamps — client or server, whichever\n * door the record comes in through.\n *\n * The contact create path's own expression (`upsertHostContact`), lifted\n * here so that a company, a deal or a task created from a site's console\n * lands in exactly the scope a contact captured on that site would: the\n * whole org when the org has chosen `defaultResourceScope: 'org'`, and\n * otherwise the sites that present as one sender — which, undeclared, is\n * this site alone. That is the agency's isolation, arrived at with nothing\n * configured, and the reason this is a function rather than a convention\n * eight creators are asked to remember.\n *\n * Widening past the group is an ACT — an org-wide member editing the scope\n * on the record — never a default. The rules refuse a scoped member creating\n * a record outside their own tokens, so a creator that bypassed this could\n * only make its record invisible to itself, never wider.\n */\nexport function crmScopeTokens(\n org: Record<string, unknown> | null | undefined,\n group: ConsentGroup,\n): ScopeToken[] {\n return (org ?? {})['defaultResourceScope'] === 'org'\n ? [ORG_SCOPE_TOKEN]\n : consentGroupScope(group)\n}\n\n/**\n * The `visibleTo` tokens a reader of this group may LIST, for the\n * `array-contains-any` every CRM listener filters by.\n *\n * The other half of {@link crmScopeTokens}: that is what a creator stamps,\n * this is what a reader asks for, and the two differ by exactly one token.\n * `'org'` leads because an org-wide record is visible to every site — an org\n * that widened its default deliberately still sees its own rows — and the\n * group's sites follow. The contacts list computes this inline; every other\n * CRM reader takes it from here so that none of them can drop the org token\n * and lose the org-wide rows, or forget the cap and have Firestore refuse\n * the query outright.\n *\n * Capped at {@link MAX_SCOPE_HOSTS}: a group wider than the operator's limit\n * is listed as far as the limit reaches, which is the contacts list's\n * behavior and the only one short of an error.\n */\nexport function crmReadTokens(group: ConsentGroup): ScopeToken[] {\n return [ORG_SCOPE_TOKEN, ...consentGroupScope(group)].slice(0, MAX_SCOPE_HOSTS)\n}\n\n/*==========================================\n * THE TEAM, AS A RECORD NAMES THEM (AGL-2614).\n *\n * An owner and an assignee are stored by uid — `orgs/{orgId}/members/{uid}`\n * is keyed by it, and a uid outlives every address change. But the people\n * who NAME an owner do not think in uids: an automation step is typed, a\n * CSV column is an address, and a picker lists names. So a reference\n * arrives as one of two things, and every reader that turns a reference\n * into a person has to accept both or the roster splits into the members\n * who can be named and the members who cannot.\n *\n * The member documents make the second case real. Two production paths\n * create a member document WITHOUT its `email` — a host-access re-grant,\n * and an add whose auth record carried none — and such a member was\n * unnameable everywhere a surface asked for an address: an automation\n * could not assign to them, and a roster mapper that dropped a row with no\n * address could not even list them. A name they still have (the roster's\n * `displayName`), and a uid they always have, so a label falls through to\n * the uid rather than to nothing, and a reference is resolved by uid first\n * and by address second.\n *\n * Roster-only, deliberately. The project's Auth records could resolve an\n * address the roster cannot, and would resolve people who are not on this\n * organization at all (AGL-1122); a reference that names nobody on the\n * roster names nobody.\n *=========================================*/\n\n/** One person on the team, as a picker lists them and a reference resolves to them. */\nexport interface CrmMemberOption {\n /** The account uid — what `ownerUid` and `assigneeUid` store. */\n uid: string\n /** The name a colleague recognizes: display name, else address, else the uid. */\n label: string\n /** The roster's address, when the member document carries one. */\n email?: string\n}\n\n/**\n * How a step or a column names somebody on the team: an address when the\n * text is one, otherwise a uid. `null` for a blank, which every caller\n * treats as \"nobody named\" rather than as a member called \"\".\n */\nexport type CrmMemberRef =\n | { kind: 'uid'; uid: string }\n | { kind: 'email'; email: string }\n\nexport function parseCrmMemberRef(value: unknown): CrmMemberRef | null {\n const text = String(value ?? '').trim()\n if (!text) return null\n if (text.includes('@')) {\n const email = normalizeContactEmail(text)\n return email ? { kind: 'email', email } : null\n }\n return { kind: 'uid', uid: text }\n}\n\n/**\n * A roster row as every CRM picker and column shows it.\n *\n * One mapping rather than four, because four is what the CRM had — and one\n * of them dropped a member whose document had neither a display name nor an\n * address, which is precisely the member the uid fallback exists for.\n */\nexport function crmMemberOption(\n member: Record<string, unknown>,\n): CrmMemberOption | null {\n const uid = String(member['$id'] ?? member['uid'] ?? '').trim()\n if (!uid) return null\n const displayName = String(member['displayName'] ?? '').trim()\n const email = String(member['email'] ?? '').trim()\n return {\n uid,\n label: displayName || email || uid,\n ...(email ? { email } : {}),\n }\n}\n\n/**\n * A roster row as a PICKER lists it: the name, and the address in\n * parentheses when the roster holds one that the name does not already\n * spell. Two teammates can share a display name — a workspace with two\n * \"Zach Gover\" rows cannot tell them apart by name alone, and the address is\n * the one thing the roster guarantees is unique. A column keeps `label`,\n * because a column is read at a glance and a picker is chosen from.\n */\nexport function crmMemberPickerLabel(\n member: Pick<CrmMemberOption, 'label' | 'email'>,\n): string {\n const label = String(member.label ?? '').trim()\n const email = String(member.email ?? '').trim()\n if (!email || email.toLowerCase() === label.toLowerCase()) return label || email\n return `${label} (${email})`\n}\n\n/**\n * The member a stored reference names: by uid first — the stored shape —\n * and by address second, so a record that carries an address where a uid\n * belongs (an older import, a hand edit) still names the person the roster\n * has under it. Undefined for nobody, which the caller renders honestly as\n * \"former member\" or as the reference itself rather than as unassigned.\n */\nexport function findOrgMember<T extends { uid: string; email?: string | null }>(\n members: readonly T[],\n ref: string | null | undefined,\n): T | undefined {\n const parsed = parseCrmMemberRef(ref)\n if (!parsed) return undefined\n if (parsed.kind === 'uid') {\n return members.find((member) => member.uid === parsed.uid)\n }\n return members.find(\n (member) => normalizeContactEmail(member.email) === parsed.email,\n )\n}\n\n/*==========================================\n * LEADS (AGL-2608).\n *\n * A lead is NOT one of the six collections above. It lives at\n * `hosts/{hostId}/leads/{personKey}`, written by `addHostLead` when a\n * visitor signs up, books or submits a form, and it is host-scoped by PATH:\n * no `visibleTo`, no facet map, private to the site that captured it. What\n * the CRM adds is the working state a sales team keeps on such a capture —\n * a status, an owner, notes — and the record of its conversion into the\n * contact, company and deal that live in the org collections.\n *\n * These fields are typed here rather than beside `addHostLead` because the\n * writer of the capture and the reader of the working state are different\n * programs: the capture door stamps none of them, and a lead that predates\n * this block carries none of them, which is why every field is optional and\n * {@link crmLeadStatus} reads an absent status as `new`.\n *=========================================*/\n\n/**\n * Where a lead stands, in the order a person works one.\n *\n * `nurturing` is the stage automated email holds a lead in: a sequence step\n * or a campaign email has reached it and no person has engaged yet. It is\n * open — the lead still needs somebody — but it is not untouched, so the\n * digest's unworked list leaves it out. Automation writes it only over\n * `new` (a person may also set it by hand), and a reply moves it on to\n * `working`.\n *\n * `qualified` is the CONVERTED state — a lead becomes a contact by being\n * qualified, and the conversion stamps `convertedContactId` beside it — and\n * `unqualified` is the closed-without-conversion state with its reason. A\n * fixed list rather than free text for the reason the lifecycle stages are:\n * the section filters on it and a report counts by it.\n */\nexport const CRM_LEAD_STATUSES = [\n 'new',\n 'nurturing',\n 'working',\n 'qualified',\n 'unqualified',\n] as const\n\nexport type CrmLeadStatus = (typeof CRM_LEAD_STATUSES)[number]\n\n/** How a lead status reads on screen — typed so a status cannot ship unlabeled. */\nexport const CRM_LEAD_STATUS_LABELS: Record<CrmLeadStatus, string> = {\n new: 'New',\n nurturing: 'Nurturing',\n working: 'Working',\n qualified: 'Qualified',\n unqualified: 'Unqualified',\n}\n\n/**\n * The statuses a lead still needs somebody's attention in — what the Leads\n * section shows by default, so the list opens on the work rather than on\n * the history.\n */\nexport const CRM_LEAD_OPEN_STATUSES: readonly CrmLeadStatus[] = [\n 'new',\n 'nurturing',\n 'working',\n]\n\n/**\n * The statuses that mean nobody needs to work the lead any more — the\n * operand of the `in` clause an open-lead figure subtracts with. Subtracts,\n * because an untouched lead carries no status field at all and Firestore\n * cannot select on a field's absence; the closed statuses are always\n * written, so they can be counted, and what remains is open. See\n * `openLeadsFromCounts`.\n */\nexport const CRM_LEAD_CLOSED_STATUSES: readonly CrmLeadStatus[] =\n CRM_LEAD_STATUSES.filter((status) => !CRM_LEAD_OPEN_STATUSES.includes(status))\n\n/**\n * The leads still needing somebody, from two server counts: every lead,\n * less the ones closed one way or the other.\n *\n * Subtraction rather than a count of the open statuses because a lead\n * nobody has touched carries NO status field — see `crmLeadStatus` — and\n * Firestore cannot select a document by a field's absence. The closed\n * statuses are always written, so they can be counted; what remains is\n * open. Clamped at zero for the moment between the two counts in which a\n * lead was closed.\n */\nexport function openLeadsFromCounts(total: number, closed: number): number {\n return Math.max(0, Math.round(Number(total) || 0) - Math.round(Number(closed) || 0))\n}\n\nexport function isCrmLeadStatus(value: unknown): value is CrmLeadStatus {\n return (\n typeof value === 'string' &&\n (CRM_LEAD_STATUSES as readonly string[]).includes(value)\n )\n}\n\n/**\n * The working state the CRM writes onto a lead document, beside what the\n * capture door wrote (`email`, `name`, `sources`, `submissionCount`,\n * `firstSeenAtMs`, `lastSeenAtMs`, the consent map).\n *\n * The four `converted*`/`dealId`/`companyId` fields are stamped ONLY by the\n * `crm/lead-convert` server route, in one write after the contact exists, so\n * a lead that carries `convertedContactId` names a contact that was really\n * created and a lead without it was never converted, whatever its status\n * says. `unqualifiedReason` travels with `status: 'unqualified'` and is the\n * one free-text field a report will want to read back.\n */\nexport interface CrmLeadFields extends CrmLeadProfile {\n status?: CrmLeadStatus\n /** The team member working the lead. */\n ownerUid?: string\n notes?: string\n unqualifiedReason?: string\n /** `orgs/{orgId}/contacts/{contactId}` — the person this lead became. */\n convertedContactId?: string\n convertedAtMs?: number\n /** The deal the conversion opened, when the converter asked for one. */\n dealId?: string\n /** The company the conversion created or linked, when it named one. */\n companyId?: string\n /**\n * The last verdict on the lead's address (AGL-3245) — bounced, blocked,\n * unsubscribed, do-not-contact — written by the platform's senders only,\n * never by a form or the lead's editor. Absent means nothing is known.\n */\n emailState?: EmailState\n /**\n * The campaigns the lead is filed under (AGL-3254): container ids from\n * `orgs/{orgId}/emailCampaigns`, at the top of the document the way a\n * form carries them (`container-membership.ts`), never names. Written by\n * the New lead drawer, the import, the bulk bar and a sequence's enroll;\n * handed to the contact's facet when the lead converts.\n */\n campaignIds?: string[]\n /**\n * The org's custom LEAD fields, keyed by each definition's `key`\n * (AGL-3272) — the same map a contact facet, a company and a deal keep,\n * judged against the definitions whose `object` is `lead`.\n *\n * It does NOT travel to the contact on convert. Keys are unique per\n * object, so a lead's `budget` and a contact's `budget` are two\n * definitions that may hold two types, and copying one map into the\n * other would write a value the receiving definition never agreed to.\n * Salesforce answers this with an explicit lead field mapping; until\n * there is one here the values stay on the lead, which a converted lead\n * is still read back for.\n */\n custom?: Record<string, CrmCustomValue>\n}\n\n/*==========================================\n * THE LEAD'S OWN PROFILE (AGL-3231).\n *\n * A Salesforce lead is a record of its own: it carries the person AND\n * their company as text — company, title, phone, website, address, the\n * source that produced it — and none of it is a contact until the lead\n * converts, when the convert step hands every field to the contact and the\n * account it creates. A lead that held only an address and a name could\n * not be worked without a contact beside it, which is how one person came\n * to sit in both lists; these fields are what let the lead stand alone.\n *\n * ## Text, not links\n *\n * `company` is the company's NAME, never a company id. A lead has not been\n * qualified, so the account it names may not deserve a record yet — a\n * thousand imported leads must not mint a thousand companies — and the\n * conversion is where the text becomes a link: it finds the company by\n * that name or by the address's domain, or creates it. The `companyId` on\n * `CrmLeadFields` is the conversion's stamp, written once, and never what\n * a lead carries while it is open.\n *\n * ## The same shapes the contact keeps\n *\n * The phone is E.164 through `normalizePhone`, the address is\n * `AglynPostalAddress` through `normalizeAddress`, tags are lower-cased and\n * capped as the contact's are, and the website is normalized as a\n * company's is — so the conversion copies values rather than translating\n * them, and a lead typed in the drawer and a contact typed in its drawer\n * refuse the same phone number in the same sentence.\n *\n * ## `leadSource` beside `sources`\n *\n * `sources` is the capture door's record of which SURFACES met the person\n * — `signup`, `booking`, `form:{id}`, `import` — and no file may rewrite\n * it. `leadSource` is what Salesforce calls Lead Source: one of the org's\n * own picklist values (\"Trade show\", \"Referral\" — see LEAD SOURCE IS A\n * PICKLIST below), reported on and filtered by, and never derived. Two\n * fields because they answer two questions, and a file that could write\n * the first would be rewriting the site's own history.\n *=========================================*/\n\n/** The longest text one profile field holds — the contact's own cap. */\nexport const CRM_LEAD_TEXT_MAX = 120\n/** The most tags one lead keeps — the contact's own cap. */\nexport const CRM_LEAD_TAGS_MAX = 20\n/** The longest note a lead keeps — the lead page's own cap. */\nexport const CRM_LEAD_NOTES_MAX = 4000\n\nexport interface CrmLeadProfile {\n /** The company's name, as text — see the block header. */\n company?: string\n jobTitle?: string\n /** E.164 — `normalizePhone` before writing. */\n phone?: string\n /** An http(s) URL — `normalizeCompanyWebsite` before writing. */\n website?: string\n address?: AglynPostalAddress | null\n /** Lower-cased, deduplicated, at most {@link CRM_LEAD_TAGS_MAX}. */\n tags?: string[]\n /**\n * Where the lead came from — Salesforce's Lead Source: the LABEL of one\n * of the org's lead source values (see {@link CrmPicklist}).\n */\n leadSource?: string\n}\n\n/** The profile's keys, in the order a card lists them. */\nexport const CRM_LEAD_PROFILE_KEYS = [\n 'company',\n 'jobTitle',\n 'phone',\n 'website',\n 'address',\n 'tags',\n 'leadSource',\n] as const satisfies readonly (keyof CrmLeadProfile)[]\n\nexport type CrmLeadProfileKey = (typeof CRM_LEAD_PROFILE_KEYS)[number]\n\n/**\n * A profile as a writer receives it: a key present is a value to store, a\n * key present and `null` is a field to clear, a key absent is left alone.\n * The PATCH semantics every CRM write has.\n */\nexport type CrmLeadProfilePatch = {\n [K in CrmLeadProfileKey]?: NonNullable<CrmLeadProfile[K]> | null\n}\n\n/** Why one field of a profile could not be read, under the field. */\nexport type CrmLeadProfileErrors = Partial<Record<CrmLeadProfileKey, string>>\n\n/** The sentence an unreadable phone number is refused with, under the field. */\nexport const CRM_LEAD_PHONE_REFUSAL =\n 'That phone number could not be read. Enter it with its country code, like ' +\n '+1 512 555 0107.'\n\n/** The sentence an unreadable website is refused with, under the field. */\nexport const CRM_LEAD_WEBSITE_REFUSAL = 'Enter a web address, like acme.com'\n\n/**\n * Tags as a lead stores them: lower-cased, trimmed, deduplicated, capped —\n * from an array or a comma-separated string, so a file cell and a form\n * field are one input.\n */\nexport function normalizeCrmLeadTags(value: unknown): string[] {\n const items = Array.isArray(value)\n ? value\n : typeof value === 'string'\n ? value.split(',')\n : []\n return [\n ...new Set(\n items\n .map((tag) =>\n String(tag ?? '')\n .trim()\n .slice(0, 40)\n .toLowerCase(),\n )\n .filter(Boolean),\n ),\n ].slice(0, CRM_LEAD_TAGS_MAX)\n}\n\n/**\n * Read a profile off an untrusted body, one field at a time.\n *\n * Every key the body names is answered: as a normalized value, as `null`\n * when the body cleared it (an empty string, `null`, an empty list, a blank\n * address), or as an error under the field when the value cannot be\n * stored as typed. Keys the body does not name are not in the answer, so\n * a caller can write exactly what was asked. A `website` is normalized the\n * way a company's is; a phone the way a contact's is; both refuse rather\n * than store a value the record would then render as a link to nowhere.\n */\nexport function normalizeCrmLeadProfile(\n input: Record<string, unknown> | null | undefined,\n): { patch: CrmLeadProfilePatch; errors: CrmLeadProfileErrors } {\n const body = input ?? {}\n const patch: CrmLeadProfilePatch = {}\n const errors: CrmLeadProfileErrors = {}\n const text = (key: 'company' | 'jobTitle' | 'leadSource') => {\n if (!(key in body)) return\n const value = String(body[key] ?? '')\n .trim()\n .replace(/\\s+/g, ' ')\n .slice(0, CRM_LEAD_TEXT_MAX)\n patch[key] = value || null\n }\n text('company')\n text('jobTitle')\n text('leadSource')\n if ('phone' in body) {\n const raw = String(body['phone'] ?? '').trim()\n if (!raw) patch.phone = null\n else {\n const phone = normalizePhone(raw)\n if (phone) patch.phone = phone\n else errors.phone = CRM_LEAD_PHONE_REFUSAL\n }\n }\n if ('website' in body) {\n const raw = String(body['website'] ?? '').trim()\n if (!raw) patch.website = null\n else {\n const website = normalizeCompanyWebsite(raw)\n if (website) patch.website = website\n else errors.website = CRM_LEAD_WEBSITE_REFUSAL\n }\n }\n if ('address' in body) {\n const raw = body['address']\n patch.address =\n raw && typeof raw === 'object' && !Array.isArray(raw)\n ? normalizeAddress(raw as AglynPostalAddress)\n : null\n }\n if ('tags' in body) {\n const tags = normalizeCrmLeadTags(body['tags'])\n patch.tags = tags.length ? tags : null\n }\n return { patch, errors }\n}\n\n/*==========================================\n * LEAD SOURCE IS A PICKLIST (AGL-3298).\n *\n * Salesforce's Lead Source is a restricted picklist: an admin keeps the\n * list of values, a rep picks one, and an import or an API write naming\n * anything else is refused. Here the list is one org document,\n * `orgs/{orgId}/crmPicklists/leadSource`, holding the values in order as\n * `{ id, label, active }` and an optional default for new records.\n *\n * ## Records store the LABEL\n *\n * A lead carries `leadSource: \"Trade show\"`, not an id. Every reader that\n * existed before the picklist — the CSV export, the REST resource, the\n * record facts, a saved view's filter, a report grouped by the field —\n * reads the text and keeps working, and a file exported from here\n * re-imports as is. What an id would have bought is a free rename; here a\n * rename (and a delete) REPLACES the old label on every record in the same\n * operation, the way Salesforce's own Replace does, so a report grouped\n * by the field follows the rename rather than splitting in two. The value's\n * `id` is what the list itself is keyed by — the default, a reorder, the\n * manage page's rows — and it never changes.\n *\n * ## Inactive, not gone\n *\n * A deactivated value leaves every picker but stays on the records that\n * hold it, shown with an \"(inactive)\" hint, and a write that keeps a\n * record's current value is never refused for it. A value is removed only\n * by Delete, which names the value its records move to.\n *\n * ## An org that never edited the list\n *\n * Has no document, and reads {@link CRM_LEAD_SOURCE_STARTER_LABELS} — a\n * small Salesforce-like set. The first edit on the manage page writes that\n * set down as the org's own, so a new org starts with a usable list and no\n * org-creation step has to seed it.\n *=========================================*/\n\n/** The picklists an org keeps, by document id — one per standard field. */\nexport const CRM_PICKLIST_IDS = ['leadSource'] as const\n\nexport type CrmPicklistId = (typeof CRM_PICKLIST_IDS)[number]\n\n/** The lead source value set's document id. */\nexport const CRM_LEAD_SOURCE_PICKLIST: CrmPicklistId = 'leadSource'\n\n/** The most values one picklist holds — a menu, not a table. */\nexport const CRM_PICKLIST_VALUES_MAX = 200\n\n/** One value of a picklist. `id` never changes; `label` is what records store. */\nexport interface CrmPicklistValue {\n id: string\n label: string\n active: boolean\n}\n\n/** A picklist as stored and as every reader takes it. */\nexport interface CrmPicklist {\n /** In the order every picker lists them. */\n values: CrmPicklistValue[]\n /** The value a new record starts with, by id — `null` for none. */\n defaultValueId: string | null\n}\n\n/**\n * The values an org that has never edited its list reads — see the block\n * header. Salesforce's own standard set, trimmed to what a small team\n * recognizes.\n */\nexport const CRM_LEAD_SOURCE_STARTER_LABELS: readonly string[] = [\n 'Web',\n 'Phone inquiry',\n 'Referral',\n 'Partner',\n 'Purchased list',\n 'Trade show',\n 'Other',\n]\n\n/** A label as a picklist stores it: trimmed, single-spaced, capped. */\nexport function normalizeCrmPicklistLabel(value: unknown): string {\n return String(value ?? '')\n .trim()\n .replace(/\\s+/g, ' ')\n .slice(0, CRM_LEAD_TEXT_MAX)\n}\n\n/** The key two labels are compared by: case and spacing do not make a new value. */\nfunction picklistKey(label: string): string {\n return normalizeCrmPicklistLabel(label).toLowerCase()\n}\n\n/**\n * A new value's id: the label as a slug, suffixed until it is not one of\n * `taken`. Derived rather than random so a seeded list reads the same in\n * every org, and never reused, because a default names a value by it.\n */\nexport function crmPicklistValueId(label: string, taken: Iterable<string>): string {\n const used = new Set(taken)\n const base =\n normalizeCrmPicklistLabel(label)\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 40) || 'value'\n let id = base\n for (let n = 2; used.has(id); n += 1) id = `${base}-${n}`\n return id\n}\n\n/** A picklist of active values from labels, in order, with no default. */\nexport function crmPicklistFromLabels(labels: readonly string[]): CrmPicklist {\n const values: CrmPicklistValue[] = []\n const seen = new Set<string>()\n for (const raw of labels) {\n const label = normalizeCrmPicklistLabel(raw)\n if (!label || seen.has(picklistKey(label))) continue\n seen.add(picklistKey(label))\n values.push({ id: crmPicklistValueId(label, values.map((value) => value.id)), label, active: true })\n }\n return { values: values.slice(0, CRM_PICKLIST_VALUES_MAX), defaultValueId: null }\n}\n\n/**\n * A stored document as a picklist, or `null` for one that holds none.\n *\n * Tolerant, because the document is client-written: a value with no label\n * is dropped, a second value with the same label (in any case) or the same\n * id is dropped, `active` is true unless it is `false`, and a default that\n * names no value is no default.\n */\nexport function normalizeCrmPicklist(raw: unknown): CrmPicklist | null {\n if (!raw || typeof raw !== 'object') return null\n const stored = raw as Record<string, unknown>\n if (!Array.isArray(stored['values'])) return null\n const values: CrmPicklistValue[] = []\n const labels = new Set<string>()\n const ids = new Set<string>()\n for (const entry of stored['values']) {\n if (!entry || typeof entry !== 'object') continue\n const value = entry as Record<string, unknown>\n const label = normalizeCrmPicklistLabel(value['label'])\n if (!label || labels.has(picklistKey(label))) continue\n let id = String(value['id'] ?? '').trim().slice(0, 64)\n if (!id || id.includes('/') || ids.has(id)) id = crmPicklistValueId(label, ids)\n labels.add(picklistKey(label))\n ids.add(id)\n values.push({ id, label, active: value['active'] !== false })\n if (values.length >= CRM_PICKLIST_VALUES_MAX) break\n }\n const defaultId = String(stored['defaultValueId'] ?? '')\n return {\n values,\n defaultValueId: ids.has(defaultId) ? defaultId : null,\n }\n}\n\n/** The org's lead source list as every reader should take it — see the block header. */\nexport function effectiveCrmLeadSourcePicklist(raw: unknown): CrmPicklist {\n return normalizeCrmPicklist(raw) ?? crmPicklistFromLabels(CRM_LEAD_SOURCE_STARTER_LABELS)\n}\n\n/** The value a label names, in any case or spacing — `null` for none. */\nexport function crmPicklistValueByLabel(\n picklist: CrmPicklist,\n label: unknown,\n): CrmPicklistValue | null {\n const key = picklistKey(String(label ?? ''))\n if (!key) return null\n return picklist.values.find((value) => picklistKey(value.label) === key) ?? null\n}\n\n/** The values a picker offers. */\nexport function crmPicklistActiveValues(picklist: CrmPicklist): CrmPicklistValue[] {\n return picklist.values.filter((value) => value.active)\n}\n\n/** The label a new record starts with — only while the default value is active. */\nexport function crmPicklistDefaultLabel(picklist: CrmPicklist): string | null {\n const value = picklist.values.find((entry) => entry.id === picklist.defaultValueId)\n return value?.active ? value.label : null\n}\n\n/**\n * The sentence a lead source outside the list is refused with, naming what\n * the list allows. A long list is cut at twenty names so the sentence stays\n * one a person can read.\n */\nexport function crmLeadSourceRefusal(picklist: CrmPicklist): string {\n const active = crmPicklistActiveValues(picklist).map((value) => value.label)\n if (!active.length) {\n return 'This organization has no active lead sources. Add one under CRM › Fields › Leads.'\n }\n const shown = active.slice(0, 20).join(', ')\n const more = active.length > 20 ? `, and ${active.length - 20} more` : ''\n return `Lead source must be one of: ${shown}${more}.`\n}\n\n/**\n * Whether a write may store `value` as a record's lead source, and the\n * label it stores.\n *\n * - Blank or `null` clears the field — lead source is never required.\n * - An ACTIVE value, in any case or spacing, stores that value's label.\n * - The record's `current` value is kept even when it is inactive or no\n * longer listed: re-saving a record must never be refused for a value\n * somebody else deactivated.\n * - Anything else is refused with {@link crmLeadSourceRefusal}.\n */\nexport function judgeCrmLeadSource(\n picklist: CrmPicklist,\n value: unknown,\n current?: unknown,\n): { ok: true; value: string | null } | { ok: false; error: string } {\n const label = normalizeCrmPicklistLabel(value)\n if (!label) return { ok: true, value: null }\n const held = normalizeCrmPicklistLabel(current)\n if (held && picklistKey(held) === picklistKey(label)) return { ok: true, value: held }\n const match = crmPicklistValueByLabel(picklist, label)\n if (match?.active) return { ok: true, value: match.label }\n return { ok: false, error: crmLeadSourceRefusal(picklist) }\n}\n\n/** One option of a lead source select. */\nexport interface CrmPicklistOption {\n label: string\n /** Listed but deactivated — shown with an \"(inactive)\" hint. */\n inactive: boolean\n /** Not in the list at all — a value written before the list existed. */\n unlisted: boolean\n}\n\n/**\n * What a select offers for a record holding `current`: every active value\n * in order, then the record's own value when the list would not otherwise\n * show it, so the select keeps what the record holds until it is changed.\n */\nexport function crmPicklistOptions(\n picklist: CrmPicklist,\n current?: unknown,\n): CrmPicklistOption[] {\n const options: CrmPicklistOption[] = crmPicklistActiveValues(picklist).map((value) => ({\n label: value.label,\n inactive: false,\n unlisted: false,\n }))\n const held = normalizeCrmPicklistLabel(current)\n if (held && !options.some((option) => picklistKey(option.label) === picklistKey(held))) {\n const listed = crmPicklistValueByLabel(picklist, held)\n options.push({ label: held, inactive: Boolean(listed), unlisted: !listed })\n }\n return options\n}\n\n/**\n * Where a label sorts: its value's position in the list, unlisted values\n * after every listed one, and a record with none last. The order the admin\n * chose is the order a sort by the field reads in, as Salesforce's does.\n */\nexport function crmPicklistRank(picklist: CrmPicklist, label: unknown): number {\n const text = normalizeCrmPicklistLabel(label)\n if (!text) return Number.MAX_SAFE_INTEGER\n const at = picklist.values.findIndex((value) => picklistKey(value.label) === picklistKey(text))\n return at < 0 ? CRM_PICKLIST_VALUES_MAX : at\n}\n\n/** The name a lead is listed under: the name it carries, else its address. */\nexport function crmLeadDisplayName(\n lead: Record<string, unknown> | null | undefined,\n): string {\n const name = String(lead?.['name'] ?? '').trim()\n return name || String(lead?.['email'] ?? '').trim()\n}\n\n/**\n * A lead's status as the list and the filter should read it.\n *\n * `new` for a lead that carries no status at all — every lead captured\n * before the CRM existed, and every lead the capture door writes today,\n * because `addHostLead` stamps none. Reading the absence as `new` is what\n * lets the section open on the leads a site already holds rather than on an\n * empty list until each one has been touched once. A stored value the union\n * does not name also reads as `new`: it is a document some other writer\n * produced, and refusing to list it would hide a person.\n */\nexport function crmLeadStatus(\n lead: Pick<CrmLeadFields, 'status'> | null | undefined,\n): CrmLeadStatus {\n const status = lead?.status\n return isCrmLeadStatus(status) ? status : 'new'\n}\n\n/** Whether a lead still needs working — see {@link CRM_LEAD_OPEN_STATUSES}. */\nexport function isCrmLeadOpen(\n lead: Pick<CrmLeadFields, 'status'> | null | undefined,\n): boolean {\n return CRM_LEAD_OPEN_STATUSES.includes(crmLeadStatus(lead))\n}\n\n/*==========================================\n * SAVED VIEWS (AGL-2617) — a list the way one person works it.\n *\n * \"My open leads in Texas\" is a list a rep opens every morning, and it is\n * three things at once: the filters that pick the people, the columns they\n * want to see about them, and the order they want them in. A `ContactSegment`\n * kept the first of those for two dimensions — tags and capture sources —\n * and nothing kept the rest, so every reload put the reader back at the\n * whole list. A view keeps all three, for every list in the CRM, under a\n * name, and is addressable: `?view=<id>` on the section's own URL opens it.\n *\n * ## The filters are the LIST'S grammar, not a second one\n *\n * A clause here is `{ field, op, value }` — structurally the\n * `ListFilterRequest` the shared list-filter grammar already speaks, the\n * request a grid's filter panel produces and the one the query translator\n * and the in-memory matcher both read. A view stores a list of them and\n * nothing more, so a view can only ask what the list can already answer,\n * and a list that gains a filterable field gains it for its views at no\n * cost. The type is restated here rather than imported because this module\n * is pure data and the grammar's home is a UI library; the shape is the\n * contract, and `normalizeCrmViewFilters` is what holds a stored document\n * to it.\n *\n * ## Mine, and shared\n *\n * A view is one person's working arrangement until they say otherwise, so\n * it lists only for its owner; `shared` puts it in front of everyone who\n * can read the section. Both are stamped `visibleTo` like every CRM row,\n * because a view is a description of the people it selects and answers to\n * the same authority they do — a private view is hidden by the LISTING, not\n * by the rules, and the rules let the creator or an org-wide member change\n * or remove it.\n *\n * ## The default is the reader's, not the view's\n *\n * Which view a section opens on is a preference of one person for one\n * organization, so it lives on the reader's own profile document beside\n * their notification mutes rather than on the view — a shared view somebody\n * else made their default must not become everybody's.\n *=========================================*/\n\n/** The lists a view can be saved for — every CRM section that has one. */\nexport const CRM_VIEW_SECTIONS = [\n 'contacts',\n 'companies',\n 'deals',\n 'tasks',\n 'leads',\n] as const\n\nexport type CrmViewSection = (typeof CRM_VIEW_SECTIONS)[number]\n\nexport function isCrmViewSection(value: unknown): value is CrmViewSection {\n return (\n typeof value === 'string' &&\n (CRM_VIEW_SECTIONS as readonly string[]).includes(value)\n )\n}\n\n/**\n * One filter a view carries: a field, an operator and a value, exactly as a\n * list's own filter panel would send them to its query.\n *\n * `label` is the human reading of an OPAQUE value — a team member's name\n * beside their uid, a company's name beside its id — kept on the clause so\n * the chip that shows it can say \"Owner is Dana\" without a read. Display\n * only: nothing matches on it, and a clause without one shows its value.\n */\nexport interface CrmViewFilterClause {\n field: string\n op: string\n value: string\n label?: string\n}\n\n/** The column a view orders by, and which way. */\nexport interface CrmViewSort {\n field: string\n direction: 'asc' | 'desc'\n}\n\n/**\n * What a view holds about a list — the part a section reads and writes,\n * without the name and the ownership around it.\n *\n * `columns` is the VISIBLE set, by column field, or empty for the list's\n * own default; `sort` is one column or none, because a table sorts by one.\n */\nexport interface CrmViewState {\n filters: CrmViewFilterClause[]\n columns: string[]\n sort: CrmViewSort | null\n}\n\n/** `orgs/{orgId}/crmViews/{viewId}`. */\nexport interface CrmSavedView extends CrmScoped, CrmViewState {\n section: CrmViewSection\n name: string\n /** Whose working arrangement this is — the creator, and the one member the rules let change it. */\n ownerUid: string\n createdByUid: string\n /** Listed for everybody who can read the section, rather than the owner alone. */\n shared: boolean\n}\n\nexport const CRM_VIEW_NAME_MAX = 60\n/**\n * The most clauses one view carries. A bound on the predicate, not the\n * audience: the list matches most of them in memory over a bounded window,\n * and a stored array has no natural end.\n */\nexport const CRM_VIEW_MAX_FILTERS = 20\nexport const CRM_VIEW_MAX_COLUMNS = 60\n/** Filter operators that carry no value, so an empty `value` is not a dropped clause. */\nconst VALUELESS_VIEW_OPS: ReadonlySet<string> = new Set(['isEmpty', 'isNotEmpty'])\n\nexport const EMPTY_CRM_VIEW_STATE: CrmViewState = Object.freeze({\n filters: [],\n columns: [],\n sort: null,\n}) as CrmViewState\n\nconst asViewText = (value: unknown, max: number): string =>\n typeof value === 'string' ? value.trim().slice(0, max) : ''\n\n/**\n * A stored view's filters, held to the clause shape.\n *\n * Strict about what it keeps: a clause with no field or no operator is not\n * a filter, and a valued operator with no value would read as a filter\n * that matches nothing — so both are dropped rather than kept, for the same\n * reason the dynamic-list rule drops an empty branch: a view must not\n * quietly select a different population than the one it reads as.\n */\nexport function normalizeCrmViewFilters(value: unknown): CrmViewFilterClause[] {\n if (!Array.isArray(value)) return []\n const clauses: CrmViewFilterClause[] = []\n for (const raw of value) {\n const entry = (raw ?? {}) as Record<string, unknown>\n const field = asViewText(entry['field'], 80)\n const op = asViewText(entry['op'], 40)\n const text = asViewText(entry['value'], 500)\n if (!field || !op) continue\n if (!text && !VALUELESS_VIEW_OPS.has(op)) continue\n const label = asViewText(entry['label'], 120)\n clauses.push({ field, op, value: text, ...(label ? { label } : {}) })\n if (clauses.length >= CRM_VIEW_MAX_FILTERS) break\n }\n return clauses\n}\n\nexport function normalizeCrmViewColumns(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n const seen = new Set<string>()\n for (const raw of value) {\n const column = asViewText(raw, 80)\n if (column) seen.add(column)\n if (seen.size >= CRM_VIEW_MAX_COLUMNS) break\n }\n return [...seen]\n}\n\nexport function normalizeCrmViewSort(value: unknown): CrmViewSort | null {\n const entry = (value ?? null) as Record<string, unknown> | null\n const field = entry ? asViewText(entry['field'], 80) : ''\n if (!field) return null\n return { field, direction: entry?.['direction'] === 'desc' ? 'desc' : 'asc' }\n}\n\n/** A stored document's three list-facing fields, held to their shapes. */\nexport function normalizeCrmViewState(value: unknown): CrmViewState {\n const entry = (value ?? {}) as Record<string, unknown>\n return {\n filters: normalizeCrmViewFilters(entry['filters']),\n columns: normalizeCrmViewColumns(entry['columns']),\n sort: normalizeCrmViewSort(entry['sort']),\n }\n}\n\n/**\n * Whether two states describe the same list — what \"unsaved changes\" means.\n *\n * Clause order matters (a list applies them in order, and the first one the\n * query can serve is the one it serves); the label on a clause does not,\n * because nothing matches on it.\n */\nexport function crmViewStateEquals(a: CrmViewState, b: CrmViewState): boolean {\n if (a.filters.length !== b.filters.length) return false\n for (let index = 0; index < a.filters.length; index += 1) {\n const left = a.filters[index]\n const right = b.filters[index]\n if (\n left.field !== right.field ||\n left.op !== right.op ||\n left.value !== right.value\n ) {\n return false\n }\n }\n if (a.columns.length !== b.columns.length) return false\n if (a.columns.some((column, index) => column !== b.columns[index])) {\n return false\n }\n if (!a.sort || !b.sort) return a.sort === b.sort\n return a.sort.field === b.sort.field && a.sort.direction === b.sort.direction\n}\n\n/**\n * Whether a view belongs in this reader's menu: their own, or one somebody\n * shared. The rules admit the read either way — see the module note — so\n * this is what keeps one person's private arrangement out of another's\n * list.\n */\nexport function crmViewIsListed(\n view: Pick<CrmSavedView, 'shared' | 'ownerUid'>,\n uid: string | null | undefined,\n): boolean {\n return view.shared === true || (Boolean(uid) && view.ownerUid === uid)\n}\n\n/**\n * The Contacts list's filterable fields, by the names a view clause carries.\n *\n * A contract between three readers that cannot import one another: the\n * list's own filter grammar (which declares these as its columns), a saved\n * view (which stores them), and the dynamic-list translator below in\n * `dynamic-list-rule.ts` (which turns them into audience dimensions). Named\n * once so a rename on the list cannot silently stop an audience matching.\n * `custom` is a PREFIX: a custom field's column is the prefix and its key.\n */\nexport const CRM_CONTACT_VIEW_FIELDS = {\n tags: 'tags',\n source: 'source',\n owner: 'ownerUid',\n stage: 'lifecycleStage',\n company: 'companyId',\n createdAt: 'createdAt',\n updatedAt: 'updatedAt',\n orders: 'ordersCount',\n ltv: 'ltvCents',\n custom: 'custom_',\n} as const\n\n/** The column a custom contact field filters and shows as. */\nexport const crmContactCustomColumn = (key: string): string =>\n `${CRM_CONTACT_VIEW_FIELDS.custom}${key}`\n\n/** The field key a custom column stands for, or `null` for any other column. */\nexport function crmContactCustomKey(column: string): string | null {\n const prefix = CRM_CONTACT_VIEW_FIELDS.custom\n return column.startsWith(prefix) && column.length > prefix.length\n ? column.slice(prefix.length)\n : null\n}\n\n/**\n * A stored phone number as the `href` of a click-to-call link, or `null`\n * when the value is not one (AGL-2661).\n *\n * The console stores E.164 (`normalizePhone` runs before every write), and\n * a `tel:` URL wants exactly that: digits with a leading `+`, nothing else.\n * Spaces, dots, dashes and parentheses are dropped rather than refused\n * because a value written before normalization existed may carry them;\n * anything that is not a run of digits after that — a word, an extension\n * typed as `x123` — is refused, because a dialer handed it would ring\n * nothing and the link would be a lie.\n */\nexport function crmTelHref(phone: unknown): string | null {\n const text = String(phone ?? '').trim()\n if (!text) return null\n const compact = text.replace(/[\\s().-]/g, '')\n return /^\\+?\\d{3,20}$/.test(compact) ? `tel:${compact}` : null\n}\n\nconst CRM_VIEW_TAGS_FIELD = CRM_CONTACT_VIEW_FIELDS.tags\nconst CRM_VIEW_SOURCE_FIELD = CRM_CONTACT_VIEW_FIELDS.source\n\n/**\n * A saved segment, as the filters of a contacts view.\n *\n * A segment is \"any of these tags AND any of these sources\", so each\n * becomes one `isAnyOf` clause — OR within, AND across, which is the\n * reading `contactMatchesSegment` gives it. A single tag is emitted as\n * `contains` instead: that operator is the one the contacts query can serve\n * from the index, so a one-tag segment opened as a view reaches the whole\n * collection rather than the loaded window. The values are the segment's\n * own, joined the way the grammar's `isAnyOf` splits them.\n */\nexport function crmViewFiltersFromSegment(\n segment: Pick<ContactSegment, 'tags' | 'sources'>,\n): CrmViewFilterClause[] {\n const tags = (segment.tags ?? []).map((tag) => tag.trim()).filter(Boolean)\n const sources = (segment.sources ?? []).filter(\n (source) => source in CONTACT_SOURCE_LABELS,\n )\n return [\n ...(tags.length === 1\n ? [{ field: CRM_VIEW_TAGS_FIELD, op: 'contains', value: tags[0] }]\n : tags.length\n ? [{ field: CRM_VIEW_TAGS_FIELD, op: 'isAnyOf', value: tags.join(',') }]\n : []),\n ...(sources.length === 1\n ? [{ field: CRM_VIEW_SOURCE_FIELD, op: 'equals', value: sources[0] }]\n : sources.length\n ? [\n {\n field: CRM_VIEW_SOURCE_FIELD,\n op: 'isAnyOf',\n value: sources.join(','),\n },\n ]\n : []),\n ]\n}\n\n/**\n * The other direction: the segment a view's filters describe, or `null`\n * when the view carries no tag or source clause a segment could hold.\n *\n * Only the two dimensions a segment has are read, and only through the\n * operators that mean \"has one of these\"; a `name startsWith` or an owner\n * clause is not a segment's to keep. What comes back is what \"Save as\n * segment\" writes, so a campaign audience built from it selects exactly the\n * tags and sources the reader could see on the chips.\n */\nexport function crmViewSegmentFilters(\n filters: readonly CrmViewFilterClause[],\n): Pick<ContactSegment, 'tags' | 'sources'> | null {\n const tags = new Set<string>()\n const sources = new Set<ContactSource>()\n const split = (value: string) =>\n value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n for (const clause of filters) {\n if (clause.field === CRM_VIEW_TAGS_FIELD) {\n if (clause.op === 'contains') tags.add(clause.value.trim().toLowerCase())\n else if (clause.op === 'isAnyOf') {\n for (const tag of split(clause.value)) tags.add(tag.toLowerCase())\n }\n } else if (clause.field === CRM_VIEW_SOURCE_FIELD) {\n const values = clause.op === 'equals' ? [clause.value.trim()] : clause.op === 'isAnyOf' ? split(clause.value) : []\n for (const source of values) {\n if (source in CONTACT_SOURCE_LABELS) sources.add(source as ContactSource)\n }\n }\n }\n tags.delete('')\n if (!tags.size && !sources.size) return null\n return {\n ...(tags.size ? { tags: [...tags] } : {}),\n ...(sources.size ? { sources: [...sources] } : {}),\n }\n}\n\n/**\n * `users/{uid}.crmDefaultViews` — `{ [orgId]: { [section]: viewId } }`.\n *\n * On the reader's own profile document, beside `notificationPrefs`, because\n * it is the same kind of fact: how one person wants their console to\n * behave, owned and written by them alone. Keyed by org first because one\n * account sits on several organizations and a view id is only meaningful\n * inside the one that holds it.\n */\nexport const CRM_DEFAULT_VIEWS_FIELD = 'crmDefaultViews'\n\n/** The view a section opens on for this reader in this org, or none. */\nexport function crmDefaultViewId(\n profile: Record<string, unknown> | null | undefined,\n orgId: string | null | undefined,\n section: CrmViewSection,\n): string | null {\n if (!profile || !orgId) return null\n const byOrg = profile[CRM_DEFAULT_VIEWS_FIELD]\n if (!byOrg || typeof byOrg !== 'object' || Array.isArray(byOrg)) return null\n const bySection = (byOrg as Record<string, unknown>)[orgId]\n if (!bySection || typeof bySection !== 'object' || Array.isArray(bySection)) {\n return null\n }\n const viewId = (bySection as Record<string, unknown>)[section]\n return typeof viewId === 'string' && viewId.trim() ? viewId : null\n}\n\n/**\n * The merge patch that sets — or, with `null`, clears — one default.\n *\n * Shaped for a merged write so the other organizations' and sections'\n * defaults on the same document survive it; `null` rather than a delete\n * sentinel because this module carries no Firestore, and the reader above\n * treats a null as no default.\n */\nexport function crmDefaultViewPatch(\n orgId: string,\n section: CrmViewSection,\n viewId: string | null,\n): Record<string, unknown> {\n return { [CRM_DEFAULT_VIEWS_FIELD]: { [orgId]: { [section]: viewId } } }\n}\n\n/*==========================================\n * FILES ON A RECORD (AGL-2662)\n *=========================================*/\n\n/** The field a record's attached files are stored under, on all three. */\nexport const CRM_MEDIA_IDS_FIELD = 'mediaIds'\n\n/**\n * The most files one record may carry.\n *\n * A platform ceiling in the family of {@link CRM_ACTIVITIES_PER_RECORD_CEILING}\n * rather than a plan dimension: attachments are bounded by human effort, and\n * this is the bound. It is also what the Firestore rules can actually check\n * — a rule can assert a list and its size, and cannot walk one — so the\n * number is enforced rather than merely documented.\n */\nexport const CRM_MEDIA_IDS_MAX = 20\n\n/**\n * A stored or submitted attachment list, cleaned.\n *\n * Ids rather than URLs, deduplicated, trimmed, non-empty and capped. Written\n * through by every path that sets the field — the console card, the REST\n * write — so a record cannot hold a list one of them would refuse.\n *\n * An id is a media DOCUMENT id, resolved against the organization's library\n * (`orgs/{orgId}/media/{mediaId}`) at read time. Storing the id and not the\n * URL is what lets a file move between folders, and what keeps a private\n * asset behind the signed CDN door rather than pinned to a raw storage URL\n * that names its current location.\n */\nexport function normalizeCrmMediaIds(value: unknown): string[] {\n if (!Array.isArray(value)) return []\n return [\n ...new Set(\n value\n .map((id) => String(id ?? '').trim())\n .filter((id) => id.length > 0 && id.length <= 200),\n ),\n ].slice(0, CRM_MEDIA_IDS_MAX)\n}\n\n/*==========================================\n * WHAT A CRM LIST QUERIES BY (AGL-3321).\n *\n * Every filter and every search word on the Leads, Contacts, Companies,\n * Deals and Tasks lists is a predicate on the list's Firestore query, and a\n * query can only ask about a field a WRITER stored in the shape the query\n * asks for. Three kinds of field exist for that and for nothing else:\n *\n * `searchTokens` word prefixes of what the list's search box reads\n * (a lead's name, address, company, title and tags;\n * a deal's title), for `array-contains` on one typed\n * word — see `nameSearchTokens`.\n * `scopedSearchTokens` the same tokens behind each of the record's\n * `visibleTo` tokens (`host:abc~acme`). A list under\n * a site already spends its one array clause on\n * `visibleTo array-contains-any`, so its search asks\n * `array-contains-any` over the site's tokens joined\n * to the word — one clause answering both.\n * keys a normalized scalar where the stored value cannot\n * be asked as it is: a lead's status (absent on a\n * capture), its lead source (compared the way the\n * picklist compares labels), a verdict on the\n * address (`none` when there is none) and a contact's\n * per-holder facet values (`facetKeys`).\n *\n * All of them are DERIVED: a pure function of the document, computed here\n * and nowhere else, and written by every path that writes one of its inputs\n * — see `CRM_LIST_FIELD_INPUTS` — so a record a list cannot find is a writer\n * that forgot, never a second formula that disagrees. A server writer that\n * holds the whole document spreads `crmListFields`; one that patched a field\n * restamps the record from what was stored (`restampCrmListFields` in the\n * admin library); the one-time backfill (`backfill-crm-list-fields.mjs`)\n * restates them for a plain Node script and is held to the same worked\n * examples (`tools/scripts/lib/org-record-list-fields.fixtures.json`), which this\n * library's spec asserts too.\n *=========================================*/\n\n/** The search box's word-prefix tokens, on every CRM record a list searches. */\nexport const CRM_SEARCH_TOKENS_FIELD = 'searchTokens'\n/** The same tokens behind each `visibleTo` token — see the block header. */\nexport const CRM_SCOPED_SEARCH_TOKENS_FIELD = 'scopedSearchTokens'\n/** What joins a scope token to a search token — the platform's one join (`SCOPED_SEARCH_JOIN`). */\nexport const CRM_SCOPED_SEARCH_JOIN = SCOPED_SEARCH_JOIN\n/**\n * The most search tokens one record keeps. A name, an address, a company\n * and a title are a few dozen prefixes; the cap is what keeps a record with\n * twenty long tags from spending hundreds of index entries on them.\n */\nexport const CRM_SEARCH_TOKENS_MAX = 200\n\n/** The collections whose lists query the fields below. */\nexport type CrmListCollection = 'leads' | 'contacts' | 'companies' | 'deals' | 'crmTasks'\n\nconst SEARCH_KEY = nameSearchKey\n\n/**\n * The word-prefix tokens of several values, merged: every prefix (up to\n * twelve characters) of every whitespace-separated word, lower-cased —\n * exactly `nameSearchTokens` per value — plus, for an email address, the\n * words it is made of, so `acme` finds `dana@acme.com` and `dana` does too.\n */\nexport function crmSearchTokens(values: readonly unknown[]): string[] {\n const tokens = new Set<string>()\n const addWord = (word: string) => {\n const capped = word.slice(0, NAME_TOKEN_MAX_PREFIX)\n for (let end = 1; end <= capped.length; end += 1) {\n if (tokens.size >= CRM_SEARCH_TOKENS_MAX) return\n tokens.add(capped.slice(0, end))\n }\n }\n const addText = (text: string) => {\n const key = SEARCH_KEY(text)\n if (!key) return\n for (const word of key.split(' ')) {\n if (!word) continue\n addWord(word)\n // An address, a domain or a hyphenated name is also its parts.\n if (/[@.+_-]/.test(word)) {\n for (const part of word.split(/[@.+_-]+/)) if (part) addWord(part)\n }\n }\n }\n for (const value of values) {\n if (Array.isArray(value)) {\n for (const entry of value) if (typeof entry === 'string') addText(entry)\n } else if (typeof value === 'string') {\n addText(value)\n }\n }\n return [...tokens]\n}\n\n/**\n * The two search fields for a record whose search reads `values`: the\n * tokens, and the same tokens behind each of its scope tokens through the\n * platform's one scoped writer (`scopedSearchTokens`).\n */\nexport function crmSearchFields(\n visibleTo: unknown,\n values: readonly unknown[],\n): { searchTokens: string[]; scopedSearchTokens: string[] } {\n const searchTokens = crmSearchTokens(values)\n return { searchTokens, scopedSearchTokens: scopedSearchTokens(visibleTo, searchTokens) }\n}\n\n/**\n * The address verdict as a list asks it (AGL-3245): the state's status, or\n * `none` when nothing is known — stored rather than left absent, because a\n * query cannot find a field's absence and \"Nothing known\" is a filter.\n */\nexport const CRM_EMAIL_STATUS_FIELD = 'emailStatus'\nexport const CRM_EMAIL_STATUS_NONE = 'none'\n\nexport function crmEmailStatusKey(record: Record<string, unknown> | null | undefined): string {\n const raw = record?.['emailState']\n const status =\n raw && typeof raw === 'object' ? (raw as Record<string, unknown>)['status'] : undefined\n // A status outside the vocabulary reads as nothing known, as `readEmailState` reads it.\n return isEmailStateStatus(status) ? status : CRM_EMAIL_STATUS_NONE\n}\n\n/** A lead's lead source as the list compares it: the picklist's key, `null` for none. */\nexport const CRM_LEAD_SOURCE_KEY_FIELD = 'leadSourceKey'\n\nexport function crmLeadSourceKey(value: unknown): string | null {\n const key = SEARCH_KEY(normalizeCrmPicklistLabel(value))\n return key || null\n}\n\n/** What a lead's search box reads (AGL-3246). */\nexport const CRM_LEAD_SEARCH_SOURCES = ['name', 'email', 'company', 'jobTitle', 'tags'] as const\n\n/**\n * A lead's campaigns behind each of its `visibleTo` tokens\n * (`host:abc~{campaignId}`), for the Leads list's Campaign filter under a\n * site: there the scope clause is the query's one array clause, and a\n * campaign asked of these keys answers both at once — the way the search\n * folds into the scope through `scopedSearchTokens`.\n */\nexport const CRM_LEAD_SCOPED_CAMPAIGNS_FIELD = 'scopedCampaignIds'\n\nfunction leadCampaignIds(lead: Record<string, unknown>): string[] {\n const raw = lead['campaignIds']\n return Array.isArray(raw)\n ? raw.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)\n : []\n}\n\n/**\n * Every field the Leads list queries by, from the lead as stored (or as it\n * will be once written): the search tokens, its campaigns behind its scope,\n * the status — `new` for a lead nobody has touched, written so `status in\n * [new, working]` finds it — the lead source key and the address verdict.\n */\nexport function crmLeadListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n scopedCampaignIds: string[]\n status: CrmLeadStatus\n leadSourceKey: string | null\n emailStatus: string\n} {\n const lead = record as Record<string, unknown>\n return {\n ...crmSearchFields(\n lead['visibleTo'],\n CRM_LEAD_SEARCH_SOURCES.map((field) => lead[field]),\n ),\n scopedCampaignIds: scopedSearchTokens(lead['visibleTo'], leadCampaignIds(lead)),\n status: crmLeadStatus(lead as Pick<CrmLeadFields, 'status'>),\n leadSourceKey: crmLeadSourceKey(lead['leadSource']),\n emailStatus: crmEmailStatusKey(lead),\n }\n}\n\n/** What a company's search box reads: its name and its domain. */\nexport const CRM_COMPANY_SEARCH_SOURCES = ['name', 'domain'] as const\n\nexport function crmCompanyListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n} {\n const company = record as Record<string, unknown>\n return crmSearchFields(\n company['visibleTo'],\n CRM_COMPANY_SEARCH_SOURCES.map((field) => company[field]),\n )\n}\n\n/**\n * What a deal's search box reads: its title (AGL-3315) — as word prefixes,\n * and as `titleLower`, the whole title's key, which a reader whose access\n * is some sites searches by its start.\n */\nexport function crmDealListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n titleLower: string\n} {\n const deal = record as Record<string, unknown>\n return {\n ...crmSearchFields(deal['visibleTo'], [deal['title']]),\n titleLower: SEARCH_KEY(typeof deal['title'] === 'string' ? deal['title'] : ''),\n }\n}\n\n/** What a task's search box reads: its title. */\nexport function crmTaskListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n} {\n const task = record as Record<string, unknown>\n return crmSearchFields(task['visibleTo'], [task['title']])\n}\n\n/*------------------------------------------\n * A CONTACT'S FACET KEYS.\n *\n * An owner, a stage, a source, a company, a tag and a custom value live on\n * a holder's facet — `facets.{groupId}` — so a `where` on one would be a path\n * per holder and an index per holder. `facetKeys` flattens them into one\n * array a query can ask `array-contains` of:\n *\n * `{groupId}:{field}={value}` this holder's value\n * `{groupId}:{field}` this holder has one (for \"is set\")\n * `*:{field}={value}`, `*:{field}` any holder's, for the organization level\n *\n * `field` is `owner`, `stage`, `source`, `company`, `tag` or `custom.{key}`,\n * plus two presence-only fields: `orders` for a holder the person has bought\n * from, and `ltv` for one they are worth something to — the per-holder\n * figures a range cannot reach, asked only whether there are any.\n * A text value is keyed lower-cased and single-spaced; a number or a flag\n * as its string. Under a site the viewing group's keys are asked; at the\n * organization level, where no group is viewing, the `*` keys are.\n *-----------------------------------------*/\n\nexport const CRM_CONTACT_FACET_KEYS_FIELD = 'facetKeys'\n/** The group a key names at the organization level: any holder. */\nexport const CRM_FACET_KEY_ANY_GROUP = '*'\n/** The most facet keys one contact keeps. */\nexport const CRM_FACET_KEYS_MAX = 600\n\n/** The facet fields a contact list filters by, as their keys name them. */\nexport type CrmFacetKeyField =\n | 'owner'\n | 'stage'\n | 'source'\n | 'company'\n | 'tag'\n | 'orders'\n | 'ltv'\n | `custom.${string}`\n\n/** A facet value as its key spells it, or `null` for a value a key cannot hold. */\nexport function crmFacetKeyValue(value: unknown): string | null {\n if (typeof value === 'boolean') return value ? 'true' : 'false'\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null\n if (typeof value !== 'string') return null\n const key = SEARCH_KEY(value).slice(0, 120)\n return key || null\n}\n\n/** One facet key: `{group}:{field}` for presence, `{group}:{field}={value}` for a value. */\nexport function crmFacetKey(group: string, field: CrmFacetKeyField, value?: string): string {\n return value === undefined ? `${group}:${field}` : `${group}:${field}=${value}`\n}\n\n/** The keys one holder's facet contributes, under `group`. */\nfunction facetKeysOf(group: string, facet: Record<string, unknown>, into: Set<string>): void {\n const add = (field: CrmFacetKeyField, raw: unknown) => {\n const value = crmFacetKeyValue(raw)\n if (value === null) return\n into.add(crmFacetKey(group, field))\n into.add(crmFacetKey(group, field, value))\n }\n add('owner', facet['ownerUid'])\n if (isContactLifecycleStage(facet['lifecycleStage'])) add('stage', facet['lifecycleStage'])\n add('company', facet['companyId'])\n const sources = facet['sources']\n if (sources && typeof sources === 'object' && !Array.isArray(sources)) {\n for (const [source, on] of Object.entries(sources as Record<string, unknown>)) {\n if (on) add('source', source)\n }\n }\n if (Array.isArray(facet['tags'])) {\n for (const tag of facet['tags']) add('tag', tag)\n }\n // Presence only: a holder's commercial figures are asked \"any?\", never ranged.\n if (typeof facet['ordersCount'] === 'number' && facet['ordersCount'] > 0) {\n into.add(crmFacetKey(group, 'orders'))\n }\n if (typeof facet['ltvCents'] === 'number' && facet['ltvCents'] > 0) {\n into.add(crmFacetKey(group, 'ltv'))\n }\n const custom = facet['custom']\n if (custom && typeof custom === 'object' && !Array.isArray(custom)) {\n for (const [key, value] of Object.entries(custom as Record<string, unknown>)) {\n if (/^[A-Za-z0-9_-]{1,64}$/.test(key)) add(`custom.${key}`, value)\n }\n }\n}\n\n/** Every facet key a contact carries — see the block above. */\nexport function crmContactFacetKeys(record: object): string[] {\n const facets = (record as Record<string, unknown>)[CONTACT_FACETS_FIELD]\n if (!facets || typeof facets !== 'object' || Array.isArray(facets)) return []\n const groups = new Set<string>()\n const any = new Set<string>()\n for (const [groupId, facet] of Object.entries(facets as Record<string, unknown>)) {\n if (!groupId || !facet || typeof facet !== 'object' || Array.isArray(facet)) continue\n facetKeysOf(groupId, facet as Record<string, unknown>, groups)\n facetKeysOf(CRM_FACET_KEY_ANY_GROUP, facet as Record<string, unknown>, any)\n }\n return [...any, ...groups].slice(0, CRM_FACET_KEYS_MAX)\n}\n\n/**\n * What a contact's search box reads: the shared identity — the canonical\n * name and every address — and the two profile values a person is looked\n * up by, through their top-level search echoes (`HostContact.phone`,\n * `HostContact.companyName`): the company's words, and the phone's digits.\n */\nexport const CRM_CONTACT_SEARCH_SOURCES = [\n 'name',\n 'email',\n CONTACT_ALTERNATE_EMAILS_FIELD,\n 'companyName',\n] as const\n\n/**\n * A phone number as the words a search box finds it by: its digits whole,\n * without a leading country code of one to three digits, and its last seven\n * and last four — so `5551234`, `555123` and `4567` all find +1 555 123 4567.\n * Typed as digits: a search reads one word, and `(555)` is not one of these.\n */\nexport function crmPhoneSearchWords(phone: unknown): string[] {\n const digits = typeof phone === 'string' ? phone.replace(/\\D/g, '') : ''\n if (digits.length < 4) return []\n const words = new Set<string>([digits])\n for (let strip = 1; strip <= 3; strip += 1) {\n if (digits.length - strip >= 7) words.add(digits.slice(strip))\n }\n if (digits.length > 7) words.add(digits.slice(-7))\n words.add(digits.slice(-4))\n return [...words]\n}\n\nexport function crmContactListFields(record: object): {\n searchTokens: string[]\n scopedSearchTokens: string[]\n facetKeys: string[]\n emailStatus: string\n} {\n const contact = record as Record<string, unknown>\n return {\n ...crmSearchFields(contact['visibleTo'], [\n ...CRM_CONTACT_SEARCH_SOURCES.map((field) => contact[field]),\n crmPhoneSearchWords(contact['phone']),\n ]),\n facetKeys: crmContactFacetKeys(contact),\n emailStatus: crmEmailStatusKey(contact),\n }\n}\n\n/**\n * The stored fields each collection's list fields are computed from. A\n * write that sets one of them must restamp the record — or spread\n * {@link crmListFields} over the document it wrote.\n */\nexport const CRM_LIST_FIELD_INPUTS: Readonly<Record<CrmListCollection, readonly string[]>> = {\n leads: ['visibleTo', ...CRM_LEAD_SEARCH_SOURCES, 'status', 'leadSource', 'emailState', 'campaignIds'],\n contacts: ['visibleTo', ...CRM_CONTACT_SEARCH_SOURCES, 'phone', CONTACT_FACETS_FIELD, 'emailState'],\n companies: ['visibleTo', ...CRM_COMPANY_SEARCH_SOURCES],\n deals: ['visibleTo', 'title'],\n crmTasks: ['visibleTo', 'title'],\n}\n\n/** Every list field of one record, from the record as stored. */\nexport function crmListFields(\n collection: CrmListCollection,\n record: object,\n): Record<string, unknown> {\n switch (collection) {\n case 'leads':\n return crmLeadListFields(record)\n case 'contacts':\n return crmContactListFields(record)\n case 'companies':\n return crmCompanyListFields(record)\n case 'deals':\n return crmDealListFields(record)\n case 'crmTasks':\n return crmTaskListFields(record)\n }\n}\n\nconst sameValue = (a: unknown, b: unknown): boolean => {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((entry, at) => entry === b[at])\n }\n return a === b\n}\n\n/**\n * The list fields a stored record carries WRONGLY — the patch that brings\n * it level, `{}` when it already is. What a restamp and a backfill write.\n */\nexport function crmListFieldsPatch(\n collection: CrmListCollection,\n record: object,\n): Record<string, unknown> {\n const fields = crmListFields(collection, record)\n const stored = record as Record<string, unknown>\n const patch: Record<string, unknown> = {}\n for (const [field, value] of Object.entries(fields)) {\n if (!sameValue(stored[field], value)) patch[field] = value\n }\n return patch\n}\n\n/**\n * The field each collection's list ORDERS or asks `null` of, which a record\n * must therefore carry even when it has no value: a contact, a company and a\n * deal carry `nextTaskAtMs` (\"No next activity\" asks `== null`), and a task\n * carries `dueAtMs` (every task view orders by it, and Firestore leaves a\n * document without the ordered field out of the answer). An absent field is\n * neither `null` nor anything else a query can ask for.\n */\nexport const CRM_LIST_NULLABLE_FIELD: Readonly<Record<CrmListCollection, string | null>> = {\n leads: null,\n contacts: 'nextTaskAtMs',\n companies: 'nextTaskAtMs',\n deals: 'nextTaskAtMs',\n crmTasks: 'dueAtMs',\n}\n\n/**\n * The list fields a NEW record is written with: {@link crmListFields}, and\n * the collection's {@link CRM_LIST_NULLABLE_FIELD} as `null` when the record\n * does not set it — nothing is scheduled against a record that did not\n * exist, and a task created undated is still one its views must reach.\n */\nexport function crmNewRecordListFields(\n collection: CrmListCollection,\n record: object,\n): Record<string, unknown> {\n const fields = crmListFields(collection, record)\n const nullable = CRM_LIST_NULLABLE_FIELD[collection]\n const stored = (record as Record<string, unknown>)[nullable ?? '']\n if (!nullable || (nullable in record && stored !== undefined)) return fields\n return { ...fields, [nullable]: null }\n}\n\n/** Whether a patch touches a field the collection's list fields are computed from. */\nexport function crmListFieldsTouched(\n collection: CrmListCollection,\n patch: Record<string, unknown>,\n): boolean {\n const inputs = CRM_LIST_FIELD_INPUTS[collection]\n return Object.keys(patch).some((key) =>\n inputs.some((input) => key === input || key.startsWith(`${input}.`)),\n )\n}\n"],"names":["normalizeAddress","normalizePhone","consentGroupScope","CONTACT_LIFECYCLE_STAGES","isContactLifecycleStage","CONTACT_ALTERNATE_EMAILS_FIELD","CONTACT_FACETS_FIELD","CONTACT_SOURCE_LABELS","normalizeContactEmail","readContactFacet","isEmailStateStatus","NAME_TOKEN_MAX_PREFIX","nameSearchKey","nameSearchTokens","SCOPED_SEARCH_JOIN","scopedSearchTokens","MAX_SCOPE_HOSTS","ORG_SCOPE_TOKEN","CRM_COLLECTIONS","companies","pipelines","deals","tasks","activities","contactFields","views","emailTemplates","picklists","CRM_RECORD_COLLECTIONS","CRM_RECORDS_BAND_FULL_MESSAGE","CRM_ACTIVITIES_PER_RECORD_CEILING","CRM_ACTIVITY_LOG_FULL_MESSAGE","toLocaleString","crmActivityCeilingLink","link","contactId","field","id","String","companyId","dealId","leadId","crmActivityLogHasRoom","existing","count","Number","isFinite","CONTACT_LIFECYCLE_STAGE_LABELS","subscriber","lead","opportunity","customer","evangelist","other","advanceContactLifecycleStage","current","floor","held","undefined","order","indexOf","contactLifecycleStageAfterPurchase","isPipelineArchived","pipeline","archivedAt","DEFAULT_DEAL_STAGES","name","probability","kind","DEAL_LINE_ITEMS_MAX","DEAL_LINE_ITEM_NAME_MAX","DEAL_LINE_ITEM_QUANTITY_MAX","CRM_TASK_KIND_LABELS","call","email","meeting","todo","CRM_ACTIVITY_KIND_LABELS","note","activityKindHasOutcome","CRM_EMAIL_DELIVERY_STATES","CRM_EMAIL_DELIVERY_STATE_LABELS","sent","delivered","opened","clicked","bounced","complained","isCrmEmailDeliveryState","value","includes","isCrmEmailDeliveryFailure","state","CRM_EMAIL_DELIVERY_RANK","nextCrmEmailDeliveryState","incoming","CRM_EMAIL_SUBJECT_MAX","CRM_EMAIL_BODY_MAX","CRM_EMAIL_CONTEXT","CRM_EMAIL_ACTIVITY_TAG","CRM_EMAIL_ORG_TAG","PROVIDER_TAG_VALUE","crmEmailDeliveryTags","input","orgId","activityId","hostId","test","buildCrmEmailActivity","subject","slice","body","to","direction","deliveryState","deliveryAtMs","atMs","byUid","byName","sourceActionId","visibleTo","campaignEmailSummary","counted","word","parts","deliveredAtMs","push","openedAtMs","openCount","clickedAtMs","clickCount","bouncedAtMs","complainedAtMs","sortableMs","NEGATIVE_INFINITY","mergeContactTimeline","interactions","campaignEmails","entries","map","interaction","index","key","type","refId","activity","$id","messageId","sentAtMs","sort","a","b","crmActivityRecordLink","record","MINUTE_MS","HOUR_MS","DAY_MS","activityTimeLabel","nowMs","elapsed","Date","toLocaleDateString","Math","CRM_FIELD_OBJECTS","CRM_FIELD_OBJECT_LABELS","contact","company","deal","isCrmFieldObject","fieldDefinitionObject","definition","object","crmFieldListFields","label","required","searchTokens","replace","join","CONTACT_FIELD_KEY_PATTERN","normalizeContactFieldKey","trim","toLowerCase","dealStageById","stageId","stages","find","stage","weightedDealAmountCents","amount","max","round","amountCents","status","min","dealHasLineItems","Array","isArray","lineItems","length","lineItemsTotalCents","items","total","item","quantity","unit","unitAmountCents","readDealLineItems","currency","error","dealCurrency","raw","line","at","isInteger","lineCurrency","toUpperCase","productId","taskDueState","task","dueAtMs","due","now","sameDay","getFullYear","getMonth","getDate","DOMAIN_LABEL","normalizeCompanyDomain","split","labels","every","COMPANY_WEBSITE_MAX","normalizeCompanyWebsite","candidate","url","URL","protocol","href","PUBLIC_MAILBOX_DOMAINS","Set","isPublicMailboxDomain","domainOrEmail","domain","lastIndexOf","has","companyDomainForEmail","companyNameForDomain","charAt","CONTACT_COMPANY_IDS_FIELD","COMPANY_CONTACTS_COUNT_FIELD","readContactCompanyLink","groupId","document","facets","heldElsewhere","holder","facet","Object","named","add","mirror","companyIds","filter","contactGroupsNamingCompany","Boolean","planContactCompanyLink","previous","previousLeaves","counts","change","op","delta","kept","ORG_CRM_SETTINGS_FIELD","CRM_AUTO_CREATE_COMPANIES_PATH","orgAutoCreatesCompanies","orgDocument","settings","CRM_ASSIGNMENT_RULES_PATH","CRM_ROUND_ROBIN_POOL_PATH","CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH","CRM_ASSIGNMENT_RULES_MAX","CRM_ROUND_ROBIN_POOL_MAX","CRM_HOST_SETTINGS_KEY","crmHostDefaultOwnerSegments","Error","isContactSourceValue","cleanText","text","readCrmAssignmentRule","whenRaw","entry","when","source","formId","emailDomain","tag","assignRaw","assign","roundRobin","memberUid","readCrmAssignmentSettings","crm","rules","rule","memberUids","uid","hostSettings","hosts","hostDefaultOwners","owner","pool","lastAssignedUid","crmHostDefaultOwner","assignmentEmailDomain","assignmentRuleMatches","capture","worn","tags","roundRobinOrder","describeAssignmentRule","memberLabel","newAssignmentRuleId","toString","random","crmScopeTokens","org","group","crmReadTokens","parseCrmMemberRef","crmMemberOption","member","displayName","crmMemberPickerLabel","findOrgMember","members","ref","parsed","CRM_LEAD_STATUSES","CRM_LEAD_STATUS_LABELS","new","nurturing","working","qualified","unqualified","CRM_LEAD_OPEN_STATUSES","CRM_LEAD_CLOSED_STATUSES","openLeadsFromCounts","closed","isCrmLeadStatus","CRM_LEAD_TEXT_MAX","CRM_LEAD_TAGS_MAX","CRM_LEAD_NOTES_MAX","CRM_LEAD_PROFILE_KEYS","CRM_LEAD_PHONE_REFUSAL","CRM_LEAD_WEBSITE_REFUSAL","normalizeCrmLeadTags","normalizeCrmLeadProfile","patch","errors","phone","website","address","CRM_PICKLIST_IDS","CRM_LEAD_SOURCE_PICKLIST","CRM_PICKLIST_VALUES_MAX","CRM_LEAD_SOURCE_STARTER_LABELS","normalizeCrmPicklistLabel","picklistKey","crmPicklistValueId","taken","used","base","normalize","n","crmPicklistFromLabels","values","seen","active","defaultValueId","normalizeCrmPicklist","stored","ids","defaultId","effectiveCrmLeadSourcePicklist","crmPicklistValueByLabel","picklist","crmPicklistActiveValues","crmPicklistDefaultLabel","crmLeadSourceRefusal","shown","more","judgeCrmLeadSource","ok","match","crmPicklistOptions","options","inactive","unlisted","some","option","listed","crmPicklistRank","MAX_SAFE_INTEGER","findIndex","crmLeadDisplayName","crmLeadStatus","isCrmLeadOpen","CRM_VIEW_SECTIONS","isCrmViewSection","CRM_VIEW_NAME_MAX","CRM_VIEW_MAX_FILTERS","CRM_VIEW_MAX_COLUMNS","VALUELESS_VIEW_OPS","EMPTY_CRM_VIEW_STATE","freeze","filters","columns","asViewText","normalizeCrmViewFilters","clauses","normalizeCrmViewColumns","column","size","normalizeCrmViewSort","normalizeCrmViewState","crmViewStateEquals","left","right","crmViewIsListed","view","shared","ownerUid","CRM_CONTACT_VIEW_FIELDS","createdAt","updatedAt","orders","ltv","custom","crmContactCustomColumn","crmContactCustomKey","prefix","startsWith","crmTelHref","compact","CRM_VIEW_TAGS_FIELD","CRM_VIEW_SOURCE_FIELD","crmViewFiltersFromSegment","segment","sources","crmViewSegmentFilters","clause","delete","CRM_DEFAULT_VIEWS_FIELD","crmDefaultViewId","profile","section","byOrg","bySection","viewId","crmDefaultViewPatch","CRM_MEDIA_IDS_FIELD","CRM_MEDIA_IDS_MAX","normalizeCrmMediaIds","CRM_SEARCH_TOKENS_FIELD","CRM_SCOPED_SEARCH_TOKENS_FIELD","CRM_SCOPED_SEARCH_JOIN","CRM_SEARCH_TOKENS_MAX","SEARCH_KEY","crmSearchTokens","tokens","addWord","capped","end","addText","part","crmSearchFields","CRM_EMAIL_STATUS_FIELD","CRM_EMAIL_STATUS_NONE","crmEmailStatusKey","CRM_LEAD_SOURCE_KEY_FIELD","crmLeadSourceKey","CRM_LEAD_SEARCH_SOURCES","CRM_LEAD_SCOPED_CAMPAIGNS_FIELD","leadCampaignIds","crmLeadListFields","scopedCampaignIds","leadSourceKey","emailStatus","CRM_COMPANY_SEARCH_SOURCES","crmCompanyListFields","crmDealListFields","titleLower","crmTaskListFields","CRM_CONTACT_FACET_KEYS_FIELD","CRM_FACET_KEY_ANY_GROUP","CRM_FACET_KEYS_MAX","crmFacetKeyValue","crmFacetKey","facetKeysOf","into","on","crmContactFacetKeys","groups","any","CRM_CONTACT_SEARCH_SOURCES","crmPhoneSearchWords","digits","words","strip","crmContactListFields","facetKeys","CRM_LIST_FIELD_INPUTS","leads","contacts","crmTasks","crmListFields","collection","sameValue","crmListFieldsPatch","fields","CRM_LIST_NULLABLE_FIELD","crmNewRecordListFields","nullable","crmListFieldsTouched","inputs","keys"],"mappings":"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"}