@aglyn/plugins-crm 1.0.0-beta.285 → 1.0.0-beta.288

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 (30) hide show
  1. package/package.json +14 -14
  2. package/src/lib/components/contact-associations-card.js +5 -7
  3. package/src/lib/components/contact-associations-card.js.map +1 -1
  4. package/src/lib/components/contact-list-columns.js +5 -7
  5. package/src/lib/components/contact-list-columns.js.map +1 -1
  6. package/src/lib/components/contact-timeline-card.js +4 -3
  7. package/src/lib/components/contact-timeline-card.js.map +1 -1
  8. package/src/lib/components/erase-person-action.d.ts +6 -4
  9. package/src/lib/components/erase-person-action.js +10 -8
  10. package/src/lib/components/erase-person-action.js.map +1 -1
  11. package/src/lib/components/reports/contacts-mix-card.js +4 -6
  12. package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
  13. package/src/lib/components/reports/source-conversion-card.js +2 -4
  14. package/src/lib/components/reports/source-conversion-card.js.map +1 -1
  15. package/src/lib/components/use-can-erase-people.d.ts +32 -0
  16. package/src/lib/components/use-can-erase-people.js +56 -0
  17. package/src/lib/components/use-can-erase-people.js.map +1 -0
  18. package/src/lib/model/record-facts-disclosed.js +4 -6
  19. package/src/lib/model/record-facts-disclosed.js.map +1 -1
  20. package/src/lib/model/record-facts.js +3 -5
  21. package/src/lib/model/record-facts.js.map +1 -1
  22. package/src/lib/server/capture-contact.d.ts +5 -5
  23. package/src/lib/server/capture-contact.js +7 -6
  24. package/src/lib/server/capture-contact.js.map +1 -1
  25. package/src/lib/server/erase-person.d.ts +10 -7
  26. package/src/lib/server/erase-person.js +18 -13
  27. package/src/lib/server/erase-person.js.map +1 -1
  28. package/src/lib/server/org-caller.d.ts +16 -4
  29. package/src/lib/server/org-caller.js +20 -4
  30. package/src/lib/server/org-caller.js.map +1 -1
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import { CONTACT_ALTERNATE_EMAILS_FIELD, CONTACT_LIFECYCLE_STAGE_LABELS, CONTACT_SOURCE_LABELS, CRM_ACTIVITY_KIND_LABELS, CRM_EMAIL_DELIVERY_STATE_LABELS, CRM_TASK_KIND_LABELS, contactDisplayName, crmLeadStatusLabel, CRM_FORECAST_CATEGORY_LABELS, crmActivityDirection, customImportTarget, DEAL_NEXT_STEP_MAX, dealContactRolesOf, dealForecastCategory, dealProbability, dealStageById, dealStageForecastCategory, fieldDefinitionObject, interactionsForGroup, isContactLifecycleStage, isCrmEmailDeliveryState, readContactFacet, readMarketingBasis } from "@aglyn/aglyn/server";
18
+ import { labelContactSource } from "@aglyn/aglyn/plugin-manager/plugin-contact-capture";
18
19
  import { COMPANY_TRANSFER_FIELDS, CONTACT_TRANSFER_FIELDS, DEAL_TRANSFER_FIELDS, LEAD_TRANSFER_FIELDS } from "../transfer/fields.js";
19
20
  /**
20
21
  * WHAT THE CRM TELLS ANOTHER PLUGIN ABOUT ONE OF ITS RECORDS (AGL-2917).
@@ -216,7 +217,7 @@ const memberName = (names, uid)=>{
216
217
  /** A capture the platform recorded on a contact, as a timeline entry. */ export function crmInteractionFact(interaction) {
217
218
  const on = crmFactDay(interaction.atMs);
218
219
  if (!on) return null;
219
- const kind = interaction.type && CONTACT_SOURCE_LABELS[interaction.type] ? CONTACT_SOURCE_LABELS[interaction.type] : 'Capture';
220
+ const kind = labelContactSource(interaction.type, 'Capture');
220
221
  const text = crmFactText(interaction.summary, CRM_FACTS_TEXT_MAX);
221
222
  return text ? {
222
223
  on,
@@ -298,10 +299,7 @@ export function contactFacts(input) {
298
299
  var _facet_sources, _facet_phone;
299
300
  const { row, group, names } = input;
300
301
  const facet = readContactFacet(row, group.groupId);
301
- const sources = Object.entries((_facet_sources = facet.sources) != null ? _facet_sources : {}).filter(([, on])=>on === true).map(([source])=>{
302
- var _CONTACT_SOURCE_LABELS_source;
303
- return (_CONTACT_SOURCE_LABELS_source = CONTACT_SOURCE_LABELS[source]) != null ? _CONTACT_SOURCE_LABELS_source : '';
304
- }).filter(Boolean);
302
+ const sources = Object.entries((_facet_sources = facet.sources) != null ? _facet_sources : {}).filter(([, on])=>on === true).map(([source])=>labelContactSource(source, '')).filter(Boolean);
305
303
  const interactions = interactionsForGroup(facet.interactions, group.hostIds);
306
304
  const alternates = Array.isArray(row[CONTACT_ALTERNATE_EMAILS_FIELD]) ? row[CONTACT_ALTERNATE_EMAILS_FIELD] : [];
307
305
  const emails = [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/record-facts.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 */\nimport {\n CONTACT_ALTERNATE_EMAILS_FIELD,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_SOURCE_LABELS,\n CRM_ACTIVITY_KIND_LABELS,\n CRM_EMAIL_DELIVERY_STATE_LABELS,\n CRM_TASK_KIND_LABELS,\n contactDisplayName,\n crmLeadStatusLabel,\n CRM_FORECAST_CATEGORY_LABELS,\n crmActivityDirection,\n customImportTarget,\n DEAL_NEXT_STEP_MAX,\n dealContactRolesOf,\n dealForecastCategory,\n dealProbability,\n dealStageById,\n dealStageForecastCategory,\n fieldDefinitionObject,\n interactionsForGroup,\n isContactLifecycleStage,\n isCrmEmailDeliveryState,\n readContactFacet,\n readMarketingBasis,\n type AglynPostalAddress,\n type ConsentGroup,\n type ContactFieldDefinition,\n type ContactInteraction,\n type CrmActivity,\n type CrmCompany,\n type CrmDeal,\n type CrmFieldObject,\n type CrmPicklist,\n type CrmPipeline,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport type { TransferField, TransferFieldType } from '@aglyn/aglyn/data-transfer'\nimport {\n COMPANY_TRANSFER_FIELDS,\n CONTACT_TRANSFER_FIELDS,\n DEAL_TRANSFER_FIELDS,\n LEAD_TRANSFER_FIELDS,\n} from '../transfer/fields'\n\n/**\n * WHAT THE CRM TELLS ANOTHER PLUGIN ABOUT ONE OF ITS RECORDS (AGL-2917).\n *\n * The facts a reader on the core's record-facts seam reports: one contact,\n * company, deal or lead as a person on the team sees it, and the fields a\n * spreadsheet import may fill. The server half reads the documents and\n * applies who may see them (`server/record-facts.ts`); this half shapes\n * them, so the list below is the whole of what a caller — an assistant\n * summarizing a record, an automation deciding on one — can pass on.\n *\n * ## The whole record (AGL-3520)\n *\n * These builders answer only for an organization with\n * `release_crm_assist_whole_record` on; with it off, the readers answer\n * `record-facts-disclosed.ts`, the fields the published pages promise.\n * A record is reported as the CRM shows it: every standard field —\n * Salesforce's included — with its email addresses, phone numbers, postal\n * addresses, birthdate, assistant and reports-to; the labels of its\n * picklists as the record holds them; its custom field values under their\n * labels; its marketing consent; its notes, newest timeline entries and\n * open tasks and deals. Text a person wrote is reported as written.\n *\n * ## What still never leaves\n *\n * No authentication token, no account identifier and no internal record\n * id. A related record — the company a person works for, the contact a deal\n * is with, the person someone reports to, a parent company, a campaign — is\n * named by its name; a team member by their display name. The one id a\n * caller is handed is a pipeline stage's, which a stage proposal must name.\n * An import catalog carries field keys, labels and types, never a row of\n * the file.\n *\n * ## Bounded\n *\n * Every list and every long text is cut — notes to\n * {@link CRM_FACTS_NOTES_MAX}, a timeline entry to {@link CRM_FACTS_TEXT_MAX},\n * at most {@link CRM_FACTS_CUSTOM_FIELDS_MAX} custom fields — so a record\n * holding everything still fits the request a caller builds from it.\n *\n * ## Stable bytes\n *\n * Every builder writes its keys in one order, dates as UTC calendar days and\n * money as a currency code and a fixed-point amount, so the same records read\n * twice report the same facts — a caller may key a cache on them, and one\n * that does sees a change exactly when something it was told has changed.\n */\n\n/** The records a reader is registered for, by the seam's resource name. */\nexport const CRM_RECORD_FACTS_RESOURCES = {\n contact: 'crm.contact',\n company: 'crm.company',\n deal: 'crm.deal',\n lead: 'crm.lead',\n} as const\n\nexport type CrmRecordFactsKind = keyof typeof CRM_RECORD_FACTS_RESOURCES\n\n/** The import catalogs, read by collection name as the record id. */\nexport const CRM_IMPORT_FACTS_RESOURCE = 'crm.import'\n\nexport const CRM_IMPORT_FACTS_COLLECTIONS = ['contacts', 'companies', 'deals', 'leads'] as const\n\nexport type CrmImportFactsCollection = (typeof CRM_IMPORT_FACTS_COLLECTIONS)[number]\n\nexport function isCrmImportFactsCollection(value: unknown): value is CrmImportFactsCollection {\n return typeof value === 'string' && (CRM_IMPORT_FACTS_COLLECTIONS as readonly string[]).includes(value)\n}\n\n/** Newest timeline entries reported for a record. */\nexport const CRM_FACTS_TIMELINE_MAX = 12\n/** Open tasks reported for a record. */\nexport const CRM_FACTS_TASKS_MAX = 8\n/** Deals reported for a contact or a company. */\nexport const CRM_FACTS_DEALS_MAX = 5\n/** Tags reported for a record. */\nexport const CRM_FACTS_TAGS_MAX = 10\n/** How much of a logged activity's text, a capture's summary, a task's notes or a custom value is reported. */\nexport const CRM_FACTS_TEXT_MAX = 280\n/** How much of a record's notes is reported. */\nexport const CRM_FACTS_NOTES_MAX = 600\n/** How long a name, a title or a subject may run. */\nexport const CRM_FACTS_LABEL_MAX = 120\n/** Custom fields an import catalog, or a record, reports. */\nexport const CRM_FACTS_CUSTOM_FIELDS_MAX = 40\n/** Products a deal's facts list. */\nexport const CRM_FACTS_PRODUCTS_MAX = 20\n/** Email addresses and campaigns a record's facts list. */\nexport const CRM_FACTS_LIST_MAX = 10\n\n/** One thing that happened, as a record's timeline reports it. */\nexport interface CrmTimelineFact {\n /** The UTC day it happened, `YYYY-MM-DD`. */\n on: string\n /** What happened: a logged kind (Call, Email) or the capture it came through (Form, Booking). */\n kind: string\n /** `outbound` or `inbound` for an email; also `internal` for a call (AGL-3517); absent otherwise. */\n direction?: string\n /** Who an email came from and went to, as the activity recorded them (AGL-3520). */\n from?: string\n to?: string\n subject?: string\n text?: string\n outcome?: string\n /** How far a sent email got: Sent, Delivered, Opened, Clicked, Bounced, Complained. */\n delivery?: string\n}\n\n/** One task still to do. */\nexport interface CrmTaskFact {\n title: string\n /** The org's Type label when the task holds one, else its kind's name (AGL-3517). */\n kind: string\n /** The meaning — `high`, `normal` or `low` — whatever the org calls it. */\n priority: string\n /** The org's Status label (\"In Progress\"), when the task holds one (AGL-3517). */\n status?: string\n /** The UTC day it is due, or `null` for a task with no due date. */\n due: string | null\n overdue: boolean\n /** Who it is assigned to, by name (AGL-3520). */\n assignee?: string\n /** What the task says beyond its title (AGL-3520). */\n notes?: string\n}\n\n/** One deal, as a contact's or a company's facts report it. */\nexport interface CrmDealFact {\n title: string\n stage: string\n status: string\n amount: string | null\n expectedClose: string | null\n}\n\n/** One custom field's value, under the label the org gave the field (AGL-3520). */\nexport interface CrmCustomFact {\n label: string\n value: string\n}\n\n/** One product on a deal (AGL-3520). */\nexport interface CrmProductFact {\n name: string\n quantity: number\n unitAmount: string | null\n}\n\nexport interface CrmContactFacts {\n record: 'contact'\n name: string\n salutation: string\n firstName: string\n lastName: string\n /** Every address the person is reached at: the canonical one first (AGL-3520). */\n emails: string[]\n phone: string\n mobilePhone: string\n homePhone: string\n otherPhone: string\n fax: string\n jobTitle: string\n department: string\n /** A `YYYY-MM-DD` day, or `''`. */\n birthdate: string\n assistant: string\n assistantPhone: string\n /** The name of the contact the person reports to (AGL-3515). */\n reportsTo: string\n mailingAddress: string\n otherAddress: string\n /** They asked not to be phoned — a fact an assistant drafting a next step must respect. */\n doNotCall: boolean\n company: string\n lifecycleStage: string\n leadSource: string\n owner: string\n /** Whether the person agreed to marketing email from this site, and since when. */\n marketingConsent: string\n tags: string[]\n /** The captures that met the person: Form, Booking, Customer. */\n sources: string[]\n orders: number\n lastPurchase: string | null\n since: string | null\n lastEmailEngagement: string | null\n custom: CrmCustomFact[]\n notes: string\n timeline: CrmTimelineFact[]\n openTasks: CrmTaskFact[]\n deals: CrmDealFact[]\n}\n\nexport interface CrmCompanyFacts {\n record: 'company'\n name: string\n domain: string\n website: string\n phone: string\n fax: string\n industry: string\n /** Salesforce's Account fields (AGL-3514). */\n type: string\n rating: string\n ownership: string\n accountSource: string\n employees: number | null\n annualRevenue: string | null\n accountNumber: string\n site: string\n tickerSymbol: string\n sicCode: string\n billingAddress: string\n shippingAddress: string\n /** The name of the company this one sits under. */\n parentCompany: string\n owner: string\n tags: string[]\n people: number\n since: string | null\n custom: CrmCustomFact[]\n notes: string\n timeline: CrmTimelineFact[]\n openTasks: CrmTaskFact[]\n deals: CrmDealFact[]\n}\n\n/** One stage of a deal's pipeline, in order. */\nexport interface CrmStageFact {\n id: string\n name: string\n kind: 'open' | 'won' | 'lost'\n /** Chance of closing from the stage, 0–100 (AGL-3516). */\n probability: number\n /** The forecast category a deal takes in the stage, as its label. */\n forecastCategory: string\n}\n\n/** One contact on a deal, as the facts name them (AGL-3521). */\nexport interface CrmContactRoleFact {\n /** The person's name, or `''` where it could not be read. */\n name: string\n /** The role's label, or `''` for none. */\n role: string\n primary: boolean\n}\n\n/** The contacts a deal's facts name — the committee, not the whole list. */\nexport const CRM_FACTS_CONTACT_ROLES_MAX = 10\n\nexport interface CrmDealFacts {\n record: 'deal'\n title: string\n pipeline: string\n stages: CrmStageFact[]\n stageId: string\n stage: string\n status: string\n amount: string | null\n expectedClose: string | null\n inStageSince: string | null\n lostReason: string\n /*\n * Salesforce's Opportunity fields (AGL-3516).\n */\n type: string\n leadSource: string\n nextStep: string\n /** The deal's own odds when it overrides the stage's, else the stage's; `null` for neither. */\n probability: number | null\n /** The forecast category's label. */\n forecastCategory: string\n /** The campaign the deal is credited to, by name (AGL-3520). */\n campaign: string\n /** The name of the person the deal is with, as the deal copied it. */\n contact: string\n /**\n * Every contact on the deal and the part each plays (AGL-3521), the\n * Primary first — named as the reader resolved them, at most\n * {@link CRM_FACTS_CONTACT_ROLES_MAX}.\n */\n contactRoles: CrmContactRoleFact[]\n company: string\n owner: string\n products: CrmProductFact[]\n since: string | null\n custom: CrmCustomFact[]\n notes: string\n timeline: CrmTimelineFact[]\n openTasks: CrmTaskFact[]\n}\n\nexport interface CrmLeadFacts {\n record: 'lead'\n name: string\n /*\n * The lead's own profile and Salesforce's standard lead fields\n * (AGL-3231, AGL-3513), with how to reach the person (AGL-3520).\n */\n salutation: string\n firstName: string\n lastName: string\n email: string\n phone: string\n mobilePhone: string\n fax: string\n website: string\n address: string\n company: string\n jobTitle: string\n leadSource: string\n industry: string\n rating: string\n employees: number | null\n annualRevenue: string | null\n /** They asked not to be phoned — a fact an assistant drafting a next step must respect. */\n doNotCall: boolean\n status: string\n owner: string\n /** The campaigns the lead is filed under, by name. */\n campaigns: string[]\n marketingConsent: string\n tags: string[]\n /** Where the lead was captured: Form, Booking, Sign-up, Import. */\n sources: string[]\n captures: number\n firstSeen: string | null\n lastSeen: string | null\n assigned: boolean\n converted: boolean\n unqualifiedReason: string\n custom: CrmCustomFact[]\n notes: string\n timeline: CrmTimelineFact[]\n openTasks: CrmTaskFact[]\n}\n\nexport type CrmRecordFacts = CrmContactFacts | CrmCompanyFacts | CrmDealFacts | CrmLeadFacts\n\n/** What a column of a file may hold, as an import field expects it. */\nexport type CrmImportFieldType = 'email' | 'phone' | 'number' | 'date' | 'yes-no' | 'url' | 'text'\n\n/** One field an import may fill. */\nexport interface CrmImportFieldFact {\n /** The mapping target: a standard field's key, or `custom:<key>`. */\n key: string\n label: string\n type: CrmImportFieldType\n required: boolean\n}\n\nexport interface CrmImportFacts {\n record: 'import'\n collection: CrmImportFactsCollection\n fields: CrmImportFieldFact[]\n}\n\n/**\n * What the server half resolves for a builder, because a fact names a\n * related record or a person and the builder reads no document: a team\n * member's display name, the record's custom field definitions, and the\n * names of the records it points at.\n */\nexport interface CrmFactsNames {\n /** A team member's display name for a uid; `''` when the roster has none. */\n member?: (uid: string) => string\n /** The org's custom field definitions, every object's; each builder takes its own. */\n customFields?: ReadonlyArray<Partial<ContactFieldDefinition>>\n}\n\nconst DAY_MS = 86_400_000\n\n/** A value as trimmed, single-spaced text of at most `max` characters. */\nexport function crmFactText(value: unknown, max: number): string {\n if (typeof value !== 'string') return ''\n const text = value.replace(/\\s+/g, ' ').trim()\n return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text\n}\n\n/** Epoch millis, a `Date` or a stored timestamp, as epoch millis; `null` for anything else. */\nexport function crmFactMillis(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? value : null\n if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.getTime() : null\n const toMillis = (value as { toMillis?: () => number } | null | undefined)?.toMillis\n if (typeof toMillis === 'function') {\n const millis = toMillis.call(value)\n return Number.isFinite(millis) ? millis : null\n }\n return null\n}\n\n/** A UTC calendar day, `YYYY-MM-DD`, or `null`. */\nexport function crmFactDay(value: unknown): string | null {\n const millis = crmFactMillis(value)\n return millis === null ? null : new Date(millis).toISOString().slice(0, 10)\n}\n\n/** An amount as a currency code and a fixed-point figure: `USD 1250.00`. */\nexport function crmFactMoney(cents: unknown, currency: unknown): string | null {\n if (typeof cents !== 'number' || !Number.isFinite(cents)) return null\n const code = (typeof currency === 'string' && /^[a-z]{3}$/i.test(currency) ? currency : 'usd').toUpperCase()\n return `${code} ${(cents / 100).toFixed(2)}`\n}\n\n/** A postal address on one line, its parts in the order an envelope writes them; `''` for none. */\nexport function crmFactAddress(address: Partial<AglynPostalAddress> | null | undefined): string {\n if (!address || typeof address !== 'object') return ''\n const part = (value: unknown) => crmFactText(value, CRM_FACTS_LABEL_MAX)\n const cityLine = [part(address.city), [part(address.state), part(address.postalCode)].filter(Boolean).join(' ')]\n .filter(Boolean)\n .join(', ')\n return [part(address.line1), part(address.line2), cityLine, part(address.country)].filter(Boolean).join(', ')\n}\n\n/** A marketing basis as a sentence's worth: opted in (and since when), declined, or nothing recorded. */\nexport function crmFactConsent(record: Record<string, unknown>, group: ConsentGroup | null | undefined): string {\n if (!group) return ''\n const consent = readMarketingBasis(record, group)\n if (consent.basis === 'granted') {\n const since = crmFactDay(consent.basisAtMs)\n return since ? `opted in on ${since}` : 'opted in'\n }\n return consent.basis === 'declined' ? 'declined' : 'none recorded'\n}\n\n/** A custom value as text: a day for a date, yes or no for a checkbox. */\nfunction customValueText(value: unknown, type: unknown): string {\n if (value === null || value === undefined || value === '') return ''\n if (typeof value === 'boolean') return value ? 'yes' : 'no'\n if (type === 'date') return crmFactDay(typeof value === 'string' ? Date.parse(value) : value) ?? crmFactText(String(value), CRM_FACTS_TEXT_MAX)\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : ''\n return crmFactText(String(value), CRM_FACTS_TEXT_MAX)\n}\n\n/**\n * A record's custom values under their fields' labels, in the fields'\n * order — the active fields of `object` first, then a value whose field was\n * retired or never defined, under its key.\n */\nexport function crmCustomFacts(\n custom: unknown,\n object: CrmFieldObject,\n definitions: ReadonlyArray<Partial<ContactFieldDefinition>> = [],\n): CrmCustomFact[] {\n if (!custom || typeof custom !== 'object' || Array.isArray(custom)) return []\n const values = custom as Record<string, unknown>\n const own = definitions\n .filter((field) => field.key && fieldDefinitionObject(field as ContactFieldDefinition) === object)\n .sort(\n (a, b) =>\n Number(Boolean(a.retiredAt)) - Number(Boolean(b.retiredAt)) ||\n Number(a.order ?? 0) - Number(b.order ?? 0) ||\n String(a.key).localeCompare(String(b.key)),\n )\n const facts: CrmCustomFact[] = []\n const seen = new Set<string>()\n for (const field of own) {\n const key = String(field.key)\n seen.add(key)\n const value = customValueText(values[key], field.type)\n if (value) facts.push({ label: crmFactText(field.label, CRM_FACTS_LABEL_MAX) || key, value })\n }\n for (const key of Object.keys(values).sort()) {\n if (seen.has(key)) continue\n const value = customValueText(values[key], undefined)\n if (value) facts.push({ label: key, value })\n }\n return facts.slice(0, CRM_FACTS_CUSTOM_FIELDS_MAX)\n}\n\nfunction tagsOf(value: unknown): string[] {\n return Array.isArray(value)\n ? value\n .map((tag) => crmFactText(tag, 40))\n .filter(Boolean)\n .slice(0, CRM_FACTS_TAGS_MAX)\n : []\n}\n\nconst memberName = (names: CrmFactsNames | undefined, uid: unknown): string =>\n typeof uid === 'string' && uid ? crmFactText(names?.member?.(uid) ?? '', CRM_FACTS_LABEL_MAX) : ''\n\n/** A logged activity as a timeline entry. */\nexport function crmActivityFact(activity: Partial<CrmActivity>): CrmTimelineFact | null {\n const on = crmFactDay(activity.atMs)\n if (!on) return null\n const kind = activity.kind && CRM_ACTIVITY_KIND_LABELS[activity.kind] ? activity.kind : 'other'\n const fact: CrmTimelineFact = { on, kind: CRM_ACTIVITY_KIND_LABELS[kind] }\n const direction = crmActivityDirection(kind, activity.direction)\n if (direction) fact.direction = direction\n const from = crmFactText(activity.from, CRM_FACTS_LABEL_MAX)\n if (from) fact.from = from\n const to = crmFactText(activity.to, CRM_FACTS_LABEL_MAX)\n if (to) fact.to = to\n const subject = crmFactText(activity.subject ?? activity.threadSubject, CRM_FACTS_LABEL_MAX)\n if (subject) fact.subject = subject\n const text = crmFactText(activity.body, CRM_FACTS_TEXT_MAX)\n if (text) fact.text = text\n const outcome = crmFactText(activity.outcome, CRM_FACTS_LABEL_MAX)\n if (outcome) fact.outcome = outcome\n if (kind === 'email' && isCrmEmailDeliveryState(activity.deliveryState)) {\n fact.delivery = CRM_EMAIL_DELIVERY_STATE_LABELS[activity.deliveryState]\n }\n return fact\n}\n\n/** A capture the platform recorded on a contact, as a timeline entry. */\nexport function crmInteractionFact(interaction: Partial<ContactInteraction>): CrmTimelineFact | null {\n const on = crmFactDay(interaction.atMs)\n if (!on) return null\n const kind =\n interaction.type && CONTACT_SOURCE_LABELS[interaction.type] ? CONTACT_SOURCE_LABELS[interaction.type] : 'Capture'\n const text = crmFactText(interaction.summary, CRM_FACTS_TEXT_MAX)\n return text ? { on, kind, text } : { on, kind }\n}\n\n/**\n * Timeline entries newest first, at most {@link CRM_FACTS_TIMELINE_MAX}. A\n * tie keeps the order the entries arrived in, so the same reads report the\n * same list.\n */\nexport function crmTimelineFacts(\n entries: ReadonlyArray<{ atMs: unknown; fact: CrmTimelineFact | null }>,\n): CrmTimelineFact[] {\n return entries\n .map((entry, index) => ({ at: crmFactMillis(entry.atMs) ?? 0, index, fact: entry.fact }))\n .filter((entry): entry is { at: number; index: number; fact: CrmTimelineFact } => entry.fact !== null)\n .sort((a, b) => b.at - a.at || a.index - b.index)\n .slice(0, CRM_FACTS_TIMELINE_MAX)\n .map((entry) => entry.fact)\n}\n\n/** The open tasks among `tasks`, soonest due first, undated last. */\nexport function crmOpenTaskFacts(\n tasks: ReadonlyArray<Partial<CrmTask>>,\n nowMs: number,\n names?: CrmFactsNames,\n): CrmTaskFact[] {\n return tasks\n .filter((task) => task.status !== 'done' && crmFactText(task.title, CRM_FACTS_LABEL_MAX))\n .map((task, index) => ({ task, index, due: crmFactMillis(task.dueAtMs) }))\n .sort((a, b) => (a.due ?? Number.POSITIVE_INFINITY) - (b.due ?? Number.POSITIVE_INFINITY) || a.index - b.index)\n .slice(0, CRM_FACTS_TASKS_MAX)\n .map(({ task, due }) => {\n const assignee = memberName(names, task.assigneeUid)\n const notes = crmFactText(task.notes, CRM_FACTS_TEXT_MAX)\n return {\n title: crmFactText(task.title, CRM_FACTS_LABEL_MAX),\n kind:\n crmFactText(task.typeLabel, CRM_FACTS_LABEL_MAX) ||\n (task.kind && CRM_TASK_KIND_LABELS[task.kind] ? CRM_TASK_KIND_LABELS[task.kind] : 'To-do'),\n priority: task.priority === 'high' || task.priority === 'low' ? task.priority : 'normal',\n ...(crmFactText(task.statusLabel, CRM_FACTS_LABEL_MAX)\n ? { status: crmFactText(task.statusLabel, CRM_FACTS_LABEL_MAX) }\n : {}),\n due: due === null ? null : crmFactDay(due),\n // Overdue by the UTC day: a task due today is not overdue until tomorrow.\n overdue: due !== null && Math.floor(due / DAY_MS) < Math.floor(nowMs / DAY_MS),\n ...(assignee ? { assignee } : {}),\n ...(notes ? { notes } : {}),\n }\n })\n}\n\n/** One deal, with its stage named by the pipeline it is in. */\nexport function crmDealFact(deal: Partial<CrmDeal>, pipeline: CrmPipeline | null | undefined): CrmDealFact {\n const stage = dealStageById(pipeline ?? undefined, String(deal.stageId ?? ''))\n return {\n title: crmFactText(deal.title, CRM_FACTS_LABEL_MAX),\n stage: crmFactText(stage?.name, CRM_FACTS_LABEL_MAX),\n status: deal.status === 'won' || deal.status === 'lost' ? deal.status : 'open',\n amount: crmFactMoney(deal.amountCents, deal.currency),\n expectedClose: crmFactDay(deal.expectedCloseAtMs),\n }\n}\n\n/** Deals, open ones first, then most recently updated. */\nexport function crmDealFacts(\n deals: ReadonlyArray<Partial<CrmDeal>>,\n pipelines: ReadonlyMap<string, CrmPipeline>,\n): CrmDealFact[] {\n return deals\n .map((deal, index) => ({ deal, index, updated: crmFactMillis(deal.updatedAt) ?? 0 }))\n .sort(\n (a, b) =>\n Number(a.deal.status !== 'open') - Number(b.deal.status !== 'open') ||\n b.updated - a.updated ||\n a.index - b.index,\n )\n .slice(0, CRM_FACTS_DEALS_MAX)\n .map(({ deal }) => crmDealFact(deal, pipelines.get(String(deal.pipelineId ?? ''))))\n}\n\nconst label = (value: unknown) => crmFactText(value, CRM_FACTS_LABEL_MAX)\n\nexport interface ContactFactsInput {\n row: Record<string, unknown>\n /** The holder whose facet the person is read through. */\n group: ConsentGroup\n activities: ReadonlyArray<Partial<CrmActivity>>\n tasks: ReadonlyArray<Partial<CrmTask>>\n deals: ReadonlyArray<Partial<CrmDeal>>\n pipelines: ReadonlyMap<string, CrmPipeline>\n nowMs: number\n names?: CrmFactsNames\n /** The name of the contact the holder's reports-to names, when the reader could see it. */\n reportsToName?: string\n}\n\nexport function contactFacts(input: ContactFactsInput): CrmContactFacts {\n const { row, group, names } = input\n const facet = readContactFacet(row, group.groupId)\n const sources = Object.entries(facet.sources ?? {})\n .filter(([, on]) => on === true)\n .map(([source]) => CONTACT_SOURCE_LABELS[source as keyof typeof CONTACT_SOURCE_LABELS] ?? '')\n .filter(Boolean)\n const interactions = interactionsForGroup(facet.interactions, group.hostIds)\n const alternates = Array.isArray(row[CONTACT_ALTERNATE_EMAILS_FIELD])\n ? (row[CONTACT_ALTERNATE_EMAILS_FIELD] as unknown[])\n : []\n const emails = [...new Set([row['email'], ...alternates].map(label).filter(Boolean))].slice(0, CRM_FACTS_LIST_MAX)\n return {\n record: 'contact',\n name: label(contactDisplayName(row, group.groupId)),\n salutation: label(facet.salutation),\n firstName: label(facet.firstName),\n lastName: label(facet.lastName),\n emails,\n phone: label(facet.phone ?? row['phone']),\n mobilePhone: label(facet.mobilePhone),\n homePhone: label(facet.homePhone),\n otherPhone: label(facet.otherPhone),\n fax: label(facet.fax),\n jobTitle: label(facet.jobTitle),\n department: label(facet.department),\n birthdate: label(facet.birthdate),\n assistant: label(facet.assistantName),\n assistantPhone: label(facet.assistantPhone),\n reportsTo: label(input.reportsToName),\n mailingAddress: crmFactAddress(facet.address),\n otherAddress: crmFactAddress(facet.otherAddress),\n doNotCall: facet.doNotCall === true,\n company: label(facet.companyName),\n lifecycleStage: isContactLifecycleStage(facet.lifecycleStage)\n ? CONTACT_LIFECYCLE_STAGE_LABELS[facet.lifecycleStage]\n : '',\n leadSource: label(facet.leadSource),\n owner: memberName(names, facet.ownerUid),\n marketingConsent: crmFactConsent(row, group),\n tags: tagsOf(facet.tags),\n sources: [...new Set(sources)].sort(),\n orders: typeof facet.ordersCount === 'number' && facet.ordersCount > 0 ? Math.floor(facet.ordersCount) : 0,\n lastPurchase: crmFactDay(facet.lastPurchaseAtMs),\n since: crmFactDay(row['createdAt']),\n lastEmailEngagement: crmFactDay(facet.lastEmailEngagementAtMs),\n custom: crmCustomFacts(facet.custom, 'contact', names?.customFields),\n notes: crmFactText(facet.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts([\n ...input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmActivityFact(activity) })),\n ...interactions.map((interaction) => ({ atMs: interaction.atMs, fact: crmInteractionFact(interaction) })),\n ]),\n openTasks: crmOpenTaskFacts(input.tasks, input.nowMs, names),\n deals: crmDealFacts(input.deals, input.pipelines),\n }\n}\n\nexport interface CompanyFactsInput {\n company: Partial<CrmCompany>\n activities: ReadonlyArray<Partial<CrmActivity>>\n tasks: ReadonlyArray<Partial<CrmTask>>\n deals: ReadonlyArray<Partial<CrmDeal>>\n pipelines: ReadonlyMap<string, CrmPipeline>\n nowMs: number\n names?: CrmFactsNames\n /** The parent company's name, when the reader could see it. */\n parentCompanyName?: string\n}\n\nexport function companyFacts(input: CompanyFactsInput): CrmCompanyFacts {\n const { company, names } = input\n return {\n record: 'company',\n name: label(company.name),\n domain: label(company.domain),\n website: label(company.website),\n phone: label(company.phone),\n fax: label(company.fax),\n industry: label(company.industry),\n type: label(company.type),\n rating: label(company.rating),\n ownership: label(company.ownership),\n accountSource: label(company.accountSource),\n employees:\n typeof company.numberOfEmployees === 'number' && company.numberOfEmployees >= 0\n ? Math.floor(company.numberOfEmployees)\n : null,\n annualRevenue: crmFactMoney(company.annualRevenueCents, company.currency),\n accountNumber: label(company.accountNumber),\n site: label(company.site),\n tickerSymbol: label(company.tickerSymbol),\n sicCode: label(company.sicCode),\n billingAddress: crmFactAddress(company.address),\n shippingAddress: crmFactAddress(company.shippingAddress),\n parentCompany: label(input.parentCompanyName),\n owner: memberName(names, company.ownerUid),\n tags: tagsOf(company.tags),\n people:\n typeof company.contactsCount === 'number' && company.contactsCount > 0\n ? Math.floor(company.contactsCount)\n : 0,\n since: crmFactDay(company.createdAt),\n custom: crmCustomFacts(company.custom, 'company', names?.customFields),\n notes: crmFactText(company.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmActivityFact(activity) })),\n ),\n openTasks: crmOpenTaskFacts(input.tasks, input.nowMs, names),\n deals: crmDealFacts(input.deals, input.pipelines),\n }\n}\n\nexport interface DealFactsInput {\n deal: Partial<CrmDeal> & { contactName?: unknown; companyName?: unknown }\n /** The names of the deal's contacts, by id, as the reader may see them (AGL-3521). */\n contactNames?: ReadonlyMap<string, string>\n pipeline: CrmPipeline | null\n activities: ReadonlyArray<Partial<CrmActivity>>\n tasks: ReadonlyArray<Partial<CrmTask>>\n nowMs: number\n names?: CrmFactsNames\n /** The name of the campaign the deal is credited to, when the reader could see it. */\n campaignName?: string\n}\n\nexport function dealFacts(input: DealFactsInput): CrmDealFacts {\n const { deal, pipeline, names } = input\n const summary = crmDealFact(deal, pipeline)\n const stages = [...(pipeline?.stages ?? [])]\n .sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0))\n .map((stage) => ({\n id: String(stage.id),\n name: crmFactText(stage.name, CRM_FACTS_LABEL_MAX),\n kind: stage.kind === 'won' || stage.kind === 'lost' ? stage.kind : ('open' as const),\n probability: Math.min(100, Math.max(0, Number(stage.probability) || 0)),\n forecastCategory: CRM_FORECAST_CATEGORY_LABELS[dealStageForecastCategory(stage)],\n }))\n const stage = dealStageById(pipeline ?? undefined, String(deal.stageId ?? ''))\n const products = (Array.isArray(deal.lineItems) ? deal.lineItems : [])\n .slice(0, CRM_FACTS_PRODUCTS_MAX)\n .map((item) => ({\n name: label(item?.name),\n quantity: Number(item?.quantity) || 0,\n unitAmount: crmFactMoney(item?.unitAmountCents, item?.currency ?? deal.currency),\n }))\n return {\n record: 'deal',\n title: summary.title,\n pipeline: crmFactText(pipeline?.name, CRM_FACTS_LABEL_MAX),\n stages,\n stageId: String(deal.stageId ?? ''),\n stage: summary.stage,\n status: summary.status,\n amount: summary.amount,\n expectedClose: summary.expectedClose,\n inStageSince: crmFactDay(deal.stageChangedAtMs),\n lostReason: label(deal.lostReason),\n type: label(deal.type),\n leadSource: label(deal.leadSource),\n nextStep: crmFactText(deal.nextStep, DEAL_NEXT_STEP_MAX),\n probability: dealProbability(deal, stage),\n forecastCategory: CRM_FORECAST_CATEGORY_LABELS[dealForecastCategory(deal, stage)],\n campaign: label(input.campaignName),\n contact: label(deal.contactName),\n contactRoles: dealContactRoleFacts(deal, input.contactNames),\n company: label(deal.companyName),\n owner: memberName(names, deal.ownerUid),\n products,\n since: crmFactDay(deal.createdAt),\n custom: crmCustomFacts(deal.custom, 'deal', names?.customFields),\n notes: crmFactText(deal.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmActivityFact(activity) })),\n ),\n openTasks: crmOpenTaskFacts(input.tasks, input.nowMs, names),\n }\n}\n\n/**\n * A deal's contact roles as facts (AGL-3521): the Primary first, each by the\n * name the reader resolved — a contact the reader may not see, or could\n * not read, is left out rather than named by id.\n */\nexport function dealContactRoleFacts(\n deal: { contactId?: unknown; contactRoles?: unknown },\n names: ReadonlyMap<string, string> = new Map(),\n): CrmContactRoleFact[] {\n return dealContactRolesOf(deal)\n .sort((a, b) => Number(b.primary) - Number(a.primary))\n .filter((row) => names.has(row.contactId))\n .slice(0, CRM_FACTS_CONTACT_ROLES_MAX)\n .map((row) => ({\n name: label(names.get(row.contactId)),\n role: crmFactText(row.role, CRM_FACTS_LABEL_MAX),\n primary: row.primary,\n }))\n}\n\n/** What a lead's capture surfaces are called, the lead history card's words. */\nexport function leadSourceFact(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form' || source.startsWith('form:')) return CONTACT_SOURCE_LABELS.form\n return 'Other'\n}\n\nexport interface LeadFactsInput {\n lead: Record<string, unknown>\n activities: ReadonlyArray<Partial<CrmActivity>>\n /** The tasks filed against the lead (AGL-3520); none when the reader read none. */\n tasks?: ReadonlyArray<Partial<CrmTask>>\n nowMs?: number\n /** The org's Lead status list, whose label the status fact reads (AGL-3512); the standard ones without it. */\n leadStatuses?: CrmPicklist\n /** The site's consent group, which the lead's marketing consent is read under. */\n group?: ConsentGroup | null\n names?: CrmFactsNames\n /** The names of the campaigns the lead is filed under, as the reader resolved them. */\n campaignNames?: readonly string[]\n}\n\nexport function leadFacts(input: LeadFactsInput): CrmLeadFacts {\n const { lead, leadStatuses, names } = input\n const rawSources = Array.isArray(lead['sources'])\n ? lead['sources']\n : typeof lead['source'] === 'string'\n ? [lead['source']]\n : []\n const captures = Number(lead['submissionCount'])\n return {\n record: 'lead',\n name: label(lead['name']),\n salutation: label(lead['salutation']),\n firstName: label(lead['firstName']),\n lastName: label(lead['lastName']),\n email: label(lead['email']),\n phone: label(lead['phone']),\n mobilePhone: label(lead['mobilePhone']),\n fax: label(lead['fax']),\n website: label(lead['website']),\n address: crmFactAddress(lead['address'] as Partial<AglynPostalAddress> | null),\n company: label(lead['company']),\n jobTitle: label(lead['jobTitle']),\n leadSource: label(lead['leadSource']),\n industry: label(lead['industry']),\n rating: label(lead['rating']),\n employees:\n typeof lead['numberOfEmployees'] === 'number' && lead['numberOfEmployees'] >= 0\n ? Math.floor(lead['numberOfEmployees'])\n : null,\n annualRevenue: crmFactMoney(lead['annualRevenueCents'], lead['currency']),\n doNotCall: lead['doNotCall'] === true,\n status: crmLeadStatusLabel(lead as { status?: never; statusLabel?: string }, leadStatuses),\n owner: memberName(names, lead['ownerUid']),\n campaigns: (input.campaignNames ?? []).map(label).filter(Boolean).slice(0, CRM_FACTS_LIST_MAX),\n marketingConsent: crmFactConsent(lead, input.group),\n tags: tagsOf(lead['tags']),\n sources: [...new Set(rawSources.map((source) => leadSourceFact(String(source))))].sort(),\n captures: Number.isFinite(captures) && captures > 0 ? Math.floor(captures) : 0,\n firstSeen: crmFactDay(lead['firstSeenAtMs']),\n lastSeen: crmFactDay(lead['lastSeenAtMs']),\n assigned: typeof lead['ownerUid'] === 'string' && lead['ownerUid'] !== '',\n converted: typeof lead['convertedContactId'] === 'string' && lead['convertedContactId'] !== '',\n unqualifiedReason: label(lead['unqualifiedReason']),\n custom: crmCustomFacts(lead['custom'], 'lead', names?.customFields),\n notes: crmFactText(lead['notes'], CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmActivityFact(activity) })),\n ),\n openTasks: crmOpenTaskFacts(input.tasks ?? [], input.nowMs ?? Date.now(), names),\n }\n}\n\n/** A column's kind, as the matcher is told it, for each transfer field type. */\nconst TRANSFER_TYPES: Partial<Record<TransferFieldType, CrmImportFieldType>> = {\n email: 'email',\n phone: 'phone',\n url: 'url',\n number: 'number',\n integer: 'number',\n currency: 'number',\n percent: 'number',\n date: 'date',\n datetime: 'date',\n boolean: 'yes-no',\n}\n\n/** The fields a transfer catalog lets a file write, as the import facts list them. */\nfunction transferImportFields(fields: readonly TransferField[]): {\n keys: readonly string[]\n labels: Record<string, string>\n required: string\n types: Record<string, CrmImportFieldType>\n} {\n const writable = fields.filter((field) => !field.readOnly && !field.derived && !field.system)\n return {\n keys: writable.map((field) => field.id),\n labels: Object.fromEntries(writable.map((field) => [field.id, field.label])),\n required: writable.find((field) => field.required)?.id ?? '',\n types: Object.fromEntries(\n writable.flatMap((field) => (TRANSFER_TYPES[field.type] ? [[field.id, TRANSFER_TYPES[field.type] as CrmImportFieldType]] : [])),\n ),\n }\n}\n\n/** The standard fields of each import, with the type a column must hold for each. */\nconst STANDARD_IMPORT_FIELDS: Record<\n CrmImportFactsCollection,\n { keys: readonly string[]; labels: Record<string, string>; required: string; types: Record<string, CrmImportFieldType> }\n> = {\n // The transfer catalogs' own ids (AGL-3527): what the import wizard maps to.\n contacts: transferImportFields(CONTACT_TRANSFER_FIELDS),\n companies: transferImportFields(COMPANY_TRANSFER_FIELDS),\n deals: transferImportFields(DEAL_TRANSFER_FIELDS),\n leads: transferImportFields(LEAD_TRANSFER_FIELDS),\n}\n\n/** The object a collection's custom fields describe. */\nconst CUSTOM_FIELD_OBJECT: Partial<Record<CrmImportFactsCollection, CrmFieldObject>> = {\n contacts: 'contact',\n companies: 'company',\n deals: 'deal',\n leads: 'lead',\n}\n\nconst CUSTOM_FIELD_TYPES: Record<string, CrmImportFieldType> = {\n number: 'number',\n date: 'date',\n checkbox: 'yes-no',\n url: 'url',\n}\n\n/**\n * The fields an import of `collection` may fill: the standard ones in the\n * mapping menu's order, then the org's active custom fields for the record\n * the collection holds, in their own order.\n */\nexport function importFacts(\n collection: CrmImportFactsCollection,\n customFields: ReadonlyArray<Partial<ContactFieldDefinition>>,\n): CrmImportFacts {\n const standard = STANDARD_IMPORT_FIELDS[collection]\n const object = CUSTOM_FIELD_OBJECT[collection]\n const custom = object\n ? customFields\n .filter((field) => field.key && !field.retiredAt && fieldDefinitionObject(field as ContactFieldDefinition) === object)\n .sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0) || String(a.key).localeCompare(String(b.key)))\n .slice(0, CRM_FACTS_CUSTOM_FIELDS_MAX)\n .map((field) => ({\n key: customImportTarget(String(field.key)),\n label: crmFactText(field.label, CRM_FACTS_LABEL_MAX) || String(field.key),\n type: CUSTOM_FIELD_TYPES[String(field.type)] ?? ('text' as const),\n required: false,\n }))\n : []\n return {\n record: 'import',\n collection,\n fields: [\n ...standard.keys.map((key) => ({\n key,\n label: standard.labels[key] ?? key,\n type: standard.types[key] ?? ('text' as const),\n required: key === standard.required,\n })),\n ...custom,\n ],\n }\n}\n"],"names":["CONTACT_ALTERNATE_EMAILS_FIELD","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_SOURCE_LABELS","CRM_ACTIVITY_KIND_LABELS","CRM_EMAIL_DELIVERY_STATE_LABELS","CRM_TASK_KIND_LABELS","contactDisplayName","crmLeadStatusLabel","CRM_FORECAST_CATEGORY_LABELS","crmActivityDirection","customImportTarget","DEAL_NEXT_STEP_MAX","dealContactRolesOf","dealForecastCategory","dealProbability","dealStageById","dealStageForecastCategory","fieldDefinitionObject","interactionsForGroup","isContactLifecycleStage","isCrmEmailDeliveryState","readContactFacet","readMarketingBasis","COMPANY_TRANSFER_FIELDS","CONTACT_TRANSFER_FIELDS","DEAL_TRANSFER_FIELDS","LEAD_TRANSFER_FIELDS","CRM_RECORD_FACTS_RESOURCES","contact","company","deal","lead","CRM_IMPORT_FACTS_RESOURCE","CRM_IMPORT_FACTS_COLLECTIONS","isCrmImportFactsCollection","value","includes","CRM_FACTS_TIMELINE_MAX","CRM_FACTS_TASKS_MAX","CRM_FACTS_DEALS_MAX","CRM_FACTS_TAGS_MAX","CRM_FACTS_TEXT_MAX","CRM_FACTS_NOTES_MAX","CRM_FACTS_LABEL_MAX","CRM_FACTS_CUSTOM_FIELDS_MAX","CRM_FACTS_PRODUCTS_MAX","CRM_FACTS_LIST_MAX","CRM_FACTS_CONTACT_ROLES_MAX","DAY_MS","crmFactText","max","text","replace","trim","length","slice","trimEnd","crmFactMillis","Number","isFinite","Date","getTime","toMillis","millis","call","crmFactDay","toISOString","crmFactMoney","cents","currency","code","test","toUpperCase","toFixed","crmFactAddress","address","part","cityLine","city","state","postalCode","filter","Boolean","join","line1","line2","country","crmFactConsent","record","group","consent","basis","since","basisAtMs","customValueText","type","undefined","parse","String","crmCustomFacts","custom","object","definitions","Array","isArray","values","own","field","key","sort","a","b","retiredAt","order","localeCompare","facts","seen","Set","add","push","label","Object","keys","has","tagsOf","map","tag","memberName","names","uid","member","crmActivityFact","activity","on","atMs","kind","fact","direction","from","to","subject","threadSubject","body","outcome","deliveryState","delivery","crmInteractionFact","interaction","summary","crmTimelineFacts","entries","entry","index","at","crmOpenTaskFacts","tasks","nowMs","task","status","title","due","dueAtMs","POSITIVE_INFINITY","assignee","assigneeUid","notes","typeLabel","priority","statusLabel","overdue","Math","floor","crmDealFact","pipeline","stage","stageId","name","amount","amountCents","expectedClose","expectedCloseAtMs","crmDealFacts","deals","pipelines","updated","updatedAt","get","pipelineId","contactFacts","input","facet","row","groupId","sources","source","interactions","hostIds","alternates","emails","salutation","firstName","lastName","phone","mobilePhone","homePhone","otherPhone","fax","jobTitle","department","birthdate","assistant","assistantName","assistantPhone","reportsTo","reportsToName","mailingAddress","otherAddress","doNotCall","companyName","lifecycleStage","leadSource","owner","ownerUid","marketingConsent","tags","orders","ordersCount","lastPurchase","lastPurchaseAtMs","lastEmailEngagement","lastEmailEngagementAtMs","customFields","timeline","activities","openTasks","companyFacts","domain","website","industry","rating","ownership","accountSource","employees","numberOfEmployees","annualRevenue","annualRevenueCents","accountNumber","site","tickerSymbol","sicCode","billingAddress","shippingAddress","parentCompany","parentCompanyName","people","contactsCount","createdAt","dealFacts","stages","id","probability","min","forecastCategory","products","lineItems","item","quantity","unitAmount","unitAmountCents","inStageSince","stageChangedAtMs","lostReason","nextStep","campaign","campaignName","contactName","contactRoles","dealContactRoleFacts","contactNames","Map","primary","contactId","role","leadSourceFact","import","startsWith","form","leadFacts","leadStatuses","rawSources","captures","email","campaigns","campaignNames","firstSeen","lastSeen","assigned","converted","unqualifiedReason","now","TRANSFER_TYPES","url","number","integer","percent","date","datetime","boolean","transferImportFields","fields","writable","readOnly","derived","system","labels","fromEntries","required","find","types","flatMap","STANDARD_IMPORT_FIELDS","contacts","companies","leads","CUSTOM_FIELD_OBJECT","CUSTOM_FIELD_TYPES","checkbox","importFacts","collection","standard"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/model/record-facts.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 */\nimport {\n CONTACT_ALTERNATE_EMAILS_FIELD,\n CONTACT_LIFECYCLE_STAGE_LABELS,\n CONTACT_SOURCE_LABELS,\n CRM_ACTIVITY_KIND_LABELS,\n CRM_EMAIL_DELIVERY_STATE_LABELS,\n CRM_TASK_KIND_LABELS,\n contactDisplayName,\n crmLeadStatusLabel,\n CRM_FORECAST_CATEGORY_LABELS,\n crmActivityDirection,\n customImportTarget,\n DEAL_NEXT_STEP_MAX,\n dealContactRolesOf,\n dealForecastCategory,\n dealProbability,\n dealStageById,\n dealStageForecastCategory,\n fieldDefinitionObject,\n interactionsForGroup,\n isContactLifecycleStage,\n isCrmEmailDeliveryState,\n readContactFacet,\n readMarketingBasis,\n type AglynPostalAddress,\n type ConsentGroup,\n type ContactFieldDefinition,\n type ContactInteraction,\n type CrmActivity,\n type CrmCompany,\n type CrmDeal,\n type CrmFieldObject,\n type CrmPicklist,\n type CrmPipeline,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport { labelContactSource } from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport type { TransferField, TransferFieldType } from '@aglyn/aglyn/data-transfer'\nimport {\n COMPANY_TRANSFER_FIELDS,\n CONTACT_TRANSFER_FIELDS,\n DEAL_TRANSFER_FIELDS,\n LEAD_TRANSFER_FIELDS,\n} from '../transfer/fields'\n\n/**\n * WHAT THE CRM TELLS ANOTHER PLUGIN ABOUT ONE OF ITS RECORDS (AGL-2917).\n *\n * The facts a reader on the core's record-facts seam reports: one contact,\n * company, deal or lead as a person on the team sees it, and the fields a\n * spreadsheet import may fill. The server half reads the documents and\n * applies who may see them (`server/record-facts.ts`); this half shapes\n * them, so the list below is the whole of what a caller — an assistant\n * summarizing a record, an automation deciding on one — can pass on.\n *\n * ## The whole record (AGL-3520)\n *\n * These builders answer only for an organization with\n * `release_crm_assist_whole_record` on; with it off, the readers answer\n * `record-facts-disclosed.ts`, the fields the published pages promise.\n * A record is reported as the CRM shows it: every standard field —\n * Salesforce's included — with its email addresses, phone numbers, postal\n * addresses, birthdate, assistant and reports-to; the labels of its\n * picklists as the record holds them; its custom field values under their\n * labels; its marketing consent; its notes, newest timeline entries and\n * open tasks and deals. Text a person wrote is reported as written.\n *\n * ## What still never leaves\n *\n * No authentication token, no account identifier and no internal record\n * id. A related record — the company a person works for, the contact a deal\n * is with, the person someone reports to, a parent company, a campaign — is\n * named by its name; a team member by their display name. The one id a\n * caller is handed is a pipeline stage's, which a stage proposal must name.\n * An import catalog carries field keys, labels and types, never a row of\n * the file.\n *\n * ## Bounded\n *\n * Every list and every long text is cut — notes to\n * {@link CRM_FACTS_NOTES_MAX}, a timeline entry to {@link CRM_FACTS_TEXT_MAX},\n * at most {@link CRM_FACTS_CUSTOM_FIELDS_MAX} custom fields — so a record\n * holding everything still fits the request a caller builds from it.\n *\n * ## Stable bytes\n *\n * Every builder writes its keys in one order, dates as UTC calendar days and\n * money as a currency code and a fixed-point amount, so the same records read\n * twice report the same facts — a caller may key a cache on them, and one\n * that does sees a change exactly when something it was told has changed.\n */\n\n/** The records a reader is registered for, by the seam's resource name. */\nexport const CRM_RECORD_FACTS_RESOURCES = {\n contact: 'crm.contact',\n company: 'crm.company',\n deal: 'crm.deal',\n lead: 'crm.lead',\n} as const\n\nexport type CrmRecordFactsKind = keyof typeof CRM_RECORD_FACTS_RESOURCES\n\n/** The import catalogs, read by collection name as the record id. */\nexport const CRM_IMPORT_FACTS_RESOURCE = 'crm.import'\n\nexport const CRM_IMPORT_FACTS_COLLECTIONS = ['contacts', 'companies', 'deals', 'leads'] as const\n\nexport type CrmImportFactsCollection = (typeof CRM_IMPORT_FACTS_COLLECTIONS)[number]\n\nexport function isCrmImportFactsCollection(value: unknown): value is CrmImportFactsCollection {\n return typeof value === 'string' && (CRM_IMPORT_FACTS_COLLECTIONS as readonly string[]).includes(value)\n}\n\n/** Newest timeline entries reported for a record. */\nexport const CRM_FACTS_TIMELINE_MAX = 12\n/** Open tasks reported for a record. */\nexport const CRM_FACTS_TASKS_MAX = 8\n/** Deals reported for a contact or a company. */\nexport const CRM_FACTS_DEALS_MAX = 5\n/** Tags reported for a record. */\nexport const CRM_FACTS_TAGS_MAX = 10\n/** How much of a logged activity's text, a capture's summary, a task's notes or a custom value is reported. */\nexport const CRM_FACTS_TEXT_MAX = 280\n/** How much of a record's notes is reported. */\nexport const CRM_FACTS_NOTES_MAX = 600\n/** How long a name, a title or a subject may run. */\nexport const CRM_FACTS_LABEL_MAX = 120\n/** Custom fields an import catalog, or a record, reports. */\nexport const CRM_FACTS_CUSTOM_FIELDS_MAX = 40\n/** Products a deal's facts list. */\nexport const CRM_FACTS_PRODUCTS_MAX = 20\n/** Email addresses and campaigns a record's facts list. */\nexport const CRM_FACTS_LIST_MAX = 10\n\n/** One thing that happened, as a record's timeline reports it. */\nexport interface CrmTimelineFact {\n /** The UTC day it happened, `YYYY-MM-DD`. */\n on: string\n /** What happened: a logged kind (Call, Email) or the capture it came through (Form, Booking). */\n kind: string\n /** `outbound` or `inbound` for an email; also `internal` for a call (AGL-3517); absent otherwise. */\n direction?: string\n /** Who an email came from and went to, as the activity recorded them (AGL-3520). */\n from?: string\n to?: string\n subject?: string\n text?: string\n outcome?: string\n /** How far a sent email got: Sent, Delivered, Opened, Clicked, Bounced, Complained. */\n delivery?: string\n}\n\n/** One task still to do. */\nexport interface CrmTaskFact {\n title: string\n /** The org's Type label when the task holds one, else its kind's name (AGL-3517). */\n kind: string\n /** The meaning — `high`, `normal` or `low` — whatever the org calls it. */\n priority: string\n /** The org's Status label (\"In Progress\"), when the task holds one (AGL-3517). */\n status?: string\n /** The UTC day it is due, or `null` for a task with no due date. */\n due: string | null\n overdue: boolean\n /** Who it is assigned to, by name (AGL-3520). */\n assignee?: string\n /** What the task says beyond its title (AGL-3520). */\n notes?: string\n}\n\n/** One deal, as a contact's or a company's facts report it. */\nexport interface CrmDealFact {\n title: string\n stage: string\n status: string\n amount: string | null\n expectedClose: string | null\n}\n\n/** One custom field's value, under the label the org gave the field (AGL-3520). */\nexport interface CrmCustomFact {\n label: string\n value: string\n}\n\n/** One product on a deal (AGL-3520). */\nexport interface CrmProductFact {\n name: string\n quantity: number\n unitAmount: string | null\n}\n\nexport interface CrmContactFacts {\n record: 'contact'\n name: string\n salutation: string\n firstName: string\n lastName: string\n /** Every address the person is reached at: the canonical one first (AGL-3520). */\n emails: string[]\n phone: string\n mobilePhone: string\n homePhone: string\n otherPhone: string\n fax: string\n jobTitle: string\n department: string\n /** A `YYYY-MM-DD` day, or `''`. */\n birthdate: string\n assistant: string\n assistantPhone: string\n /** The name of the contact the person reports to (AGL-3515). */\n reportsTo: string\n mailingAddress: string\n otherAddress: string\n /** They asked not to be phoned — a fact an assistant drafting a next step must respect. */\n doNotCall: boolean\n company: string\n lifecycleStage: string\n leadSource: string\n owner: string\n /** Whether the person agreed to marketing email from this site, and since when. */\n marketingConsent: string\n tags: string[]\n /** The captures that met the person: Form, Booking, Customer. */\n sources: string[]\n orders: number\n lastPurchase: string | null\n since: string | null\n lastEmailEngagement: string | null\n custom: CrmCustomFact[]\n notes: string\n timeline: CrmTimelineFact[]\n openTasks: CrmTaskFact[]\n deals: CrmDealFact[]\n}\n\nexport interface CrmCompanyFacts {\n record: 'company'\n name: string\n domain: string\n website: string\n phone: string\n fax: string\n industry: string\n /** Salesforce's Account fields (AGL-3514). */\n type: string\n rating: string\n ownership: string\n accountSource: string\n employees: number | null\n annualRevenue: string | null\n accountNumber: string\n site: string\n tickerSymbol: string\n sicCode: string\n billingAddress: string\n shippingAddress: string\n /** The name of the company this one sits under. */\n parentCompany: string\n owner: string\n tags: string[]\n people: number\n since: string | null\n custom: CrmCustomFact[]\n notes: string\n timeline: CrmTimelineFact[]\n openTasks: CrmTaskFact[]\n deals: CrmDealFact[]\n}\n\n/** One stage of a deal's pipeline, in order. */\nexport interface CrmStageFact {\n id: string\n name: string\n kind: 'open' | 'won' | 'lost'\n /** Chance of closing from the stage, 0–100 (AGL-3516). */\n probability: number\n /** The forecast category a deal takes in the stage, as its label. */\n forecastCategory: string\n}\n\n/** One contact on a deal, as the facts name them (AGL-3521). */\nexport interface CrmContactRoleFact {\n /** The person's name, or `''` where it could not be read. */\n name: string\n /** The role's label, or `''` for none. */\n role: string\n primary: boolean\n}\n\n/** The contacts a deal's facts name — the committee, not the whole list. */\nexport const CRM_FACTS_CONTACT_ROLES_MAX = 10\n\nexport interface CrmDealFacts {\n record: 'deal'\n title: string\n pipeline: string\n stages: CrmStageFact[]\n stageId: string\n stage: string\n status: string\n amount: string | null\n expectedClose: string | null\n inStageSince: string | null\n lostReason: string\n /*\n * Salesforce's Opportunity fields (AGL-3516).\n */\n type: string\n leadSource: string\n nextStep: string\n /** The deal's own odds when it overrides the stage's, else the stage's; `null` for neither. */\n probability: number | null\n /** The forecast category's label. */\n forecastCategory: string\n /** The campaign the deal is credited to, by name (AGL-3520). */\n campaign: string\n /** The name of the person the deal is with, as the deal copied it. */\n contact: string\n /**\n * Every contact on the deal and the part each plays (AGL-3521), the\n * Primary first — named as the reader resolved them, at most\n * {@link CRM_FACTS_CONTACT_ROLES_MAX}.\n */\n contactRoles: CrmContactRoleFact[]\n company: string\n owner: string\n products: CrmProductFact[]\n since: string | null\n custom: CrmCustomFact[]\n notes: string\n timeline: CrmTimelineFact[]\n openTasks: CrmTaskFact[]\n}\n\nexport interface CrmLeadFacts {\n record: 'lead'\n name: string\n /*\n * The lead's own profile and Salesforce's standard lead fields\n * (AGL-3231, AGL-3513), with how to reach the person (AGL-3520).\n */\n salutation: string\n firstName: string\n lastName: string\n email: string\n phone: string\n mobilePhone: string\n fax: string\n website: string\n address: string\n company: string\n jobTitle: string\n leadSource: string\n industry: string\n rating: string\n employees: number | null\n annualRevenue: string | null\n /** They asked not to be phoned — a fact an assistant drafting a next step must respect. */\n doNotCall: boolean\n status: string\n owner: string\n /** The campaigns the lead is filed under, by name. */\n campaigns: string[]\n marketingConsent: string\n tags: string[]\n /** Where the lead was captured: Form, Booking, Sign-up, Import. */\n sources: string[]\n captures: number\n firstSeen: string | null\n lastSeen: string | null\n assigned: boolean\n converted: boolean\n unqualifiedReason: string\n custom: CrmCustomFact[]\n notes: string\n timeline: CrmTimelineFact[]\n openTasks: CrmTaskFact[]\n}\n\nexport type CrmRecordFacts = CrmContactFacts | CrmCompanyFacts | CrmDealFacts | CrmLeadFacts\n\n/** What a column of a file may hold, as an import field expects it. */\nexport type CrmImportFieldType = 'email' | 'phone' | 'number' | 'date' | 'yes-no' | 'url' | 'text'\n\n/** One field an import may fill. */\nexport interface CrmImportFieldFact {\n /** The mapping target: a standard field's key, or `custom:<key>`. */\n key: string\n label: string\n type: CrmImportFieldType\n required: boolean\n}\n\nexport interface CrmImportFacts {\n record: 'import'\n collection: CrmImportFactsCollection\n fields: CrmImportFieldFact[]\n}\n\n/**\n * What the server half resolves for a builder, because a fact names a\n * related record or a person and the builder reads no document: a team\n * member's display name, the record's custom field definitions, and the\n * names of the records it points at.\n */\nexport interface CrmFactsNames {\n /** A team member's display name for a uid; `''` when the roster has none. */\n member?: (uid: string) => string\n /** The org's custom field definitions, every object's; each builder takes its own. */\n customFields?: ReadonlyArray<Partial<ContactFieldDefinition>>\n}\n\nconst DAY_MS = 86_400_000\n\n/** A value as trimmed, single-spaced text of at most `max` characters. */\nexport function crmFactText(value: unknown, max: number): string {\n if (typeof value !== 'string') return ''\n const text = value.replace(/\\s+/g, ' ').trim()\n return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text\n}\n\n/** Epoch millis, a `Date` or a stored timestamp, as epoch millis; `null` for anything else. */\nexport function crmFactMillis(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) && value > 0 ? value : null\n if (value instanceof Date) return Number.isFinite(value.getTime()) ? value.getTime() : null\n const toMillis = (value as { toMillis?: () => number } | null | undefined)?.toMillis\n if (typeof toMillis === 'function') {\n const millis = toMillis.call(value)\n return Number.isFinite(millis) ? millis : null\n }\n return null\n}\n\n/** A UTC calendar day, `YYYY-MM-DD`, or `null`. */\nexport function crmFactDay(value: unknown): string | null {\n const millis = crmFactMillis(value)\n return millis === null ? null : new Date(millis).toISOString().slice(0, 10)\n}\n\n/** An amount as a currency code and a fixed-point figure: `USD 1250.00`. */\nexport function crmFactMoney(cents: unknown, currency: unknown): string | null {\n if (typeof cents !== 'number' || !Number.isFinite(cents)) return null\n const code = (typeof currency === 'string' && /^[a-z]{3}$/i.test(currency) ? currency : 'usd').toUpperCase()\n return `${code} ${(cents / 100).toFixed(2)}`\n}\n\n/** A postal address on one line, its parts in the order an envelope writes them; `''` for none. */\nexport function crmFactAddress(address: Partial<AglynPostalAddress> | null | undefined): string {\n if (!address || typeof address !== 'object') return ''\n const part = (value: unknown) => crmFactText(value, CRM_FACTS_LABEL_MAX)\n const cityLine = [part(address.city), [part(address.state), part(address.postalCode)].filter(Boolean).join(' ')]\n .filter(Boolean)\n .join(', ')\n return [part(address.line1), part(address.line2), cityLine, part(address.country)].filter(Boolean).join(', ')\n}\n\n/** A marketing basis as a sentence's worth: opted in (and since when), declined, or nothing recorded. */\nexport function crmFactConsent(record: Record<string, unknown>, group: ConsentGroup | null | undefined): string {\n if (!group) return ''\n const consent = readMarketingBasis(record, group)\n if (consent.basis === 'granted') {\n const since = crmFactDay(consent.basisAtMs)\n return since ? `opted in on ${since}` : 'opted in'\n }\n return consent.basis === 'declined' ? 'declined' : 'none recorded'\n}\n\n/** A custom value as text: a day for a date, yes or no for a checkbox. */\nfunction customValueText(value: unknown, type: unknown): string {\n if (value === null || value === undefined || value === '') return ''\n if (typeof value === 'boolean') return value ? 'yes' : 'no'\n if (type === 'date') return crmFactDay(typeof value === 'string' ? Date.parse(value) : value) ?? crmFactText(String(value), CRM_FACTS_TEXT_MAX)\n if (typeof value === 'number') return Number.isFinite(value) ? String(value) : ''\n return crmFactText(String(value), CRM_FACTS_TEXT_MAX)\n}\n\n/**\n * A record's custom values under their fields' labels, in the fields'\n * order — the active fields of `object` first, then a value whose field was\n * retired or never defined, under its key.\n */\nexport function crmCustomFacts(\n custom: unknown,\n object: CrmFieldObject,\n definitions: ReadonlyArray<Partial<ContactFieldDefinition>> = [],\n): CrmCustomFact[] {\n if (!custom || typeof custom !== 'object' || Array.isArray(custom)) return []\n const values = custom as Record<string, unknown>\n const own = definitions\n .filter((field) => field.key && fieldDefinitionObject(field as ContactFieldDefinition) === object)\n .sort(\n (a, b) =>\n Number(Boolean(a.retiredAt)) - Number(Boolean(b.retiredAt)) ||\n Number(a.order ?? 0) - Number(b.order ?? 0) ||\n String(a.key).localeCompare(String(b.key)),\n )\n const facts: CrmCustomFact[] = []\n const seen = new Set<string>()\n for (const field of own) {\n const key = String(field.key)\n seen.add(key)\n const value = customValueText(values[key], field.type)\n if (value) facts.push({ label: crmFactText(field.label, CRM_FACTS_LABEL_MAX) || key, value })\n }\n for (const key of Object.keys(values).sort()) {\n if (seen.has(key)) continue\n const value = customValueText(values[key], undefined)\n if (value) facts.push({ label: key, value })\n }\n return facts.slice(0, CRM_FACTS_CUSTOM_FIELDS_MAX)\n}\n\nfunction tagsOf(value: unknown): string[] {\n return Array.isArray(value)\n ? value\n .map((tag) => crmFactText(tag, 40))\n .filter(Boolean)\n .slice(0, CRM_FACTS_TAGS_MAX)\n : []\n}\n\nconst memberName = (names: CrmFactsNames | undefined, uid: unknown): string =>\n typeof uid === 'string' && uid ? crmFactText(names?.member?.(uid) ?? '', CRM_FACTS_LABEL_MAX) : ''\n\n/** A logged activity as a timeline entry. */\nexport function crmActivityFact(activity: Partial<CrmActivity>): CrmTimelineFact | null {\n const on = crmFactDay(activity.atMs)\n if (!on) return null\n const kind = activity.kind && CRM_ACTIVITY_KIND_LABELS[activity.kind] ? activity.kind : 'other'\n const fact: CrmTimelineFact = { on, kind: CRM_ACTIVITY_KIND_LABELS[kind] }\n const direction = crmActivityDirection(kind, activity.direction)\n if (direction) fact.direction = direction\n const from = crmFactText(activity.from, CRM_FACTS_LABEL_MAX)\n if (from) fact.from = from\n const to = crmFactText(activity.to, CRM_FACTS_LABEL_MAX)\n if (to) fact.to = to\n const subject = crmFactText(activity.subject ?? activity.threadSubject, CRM_FACTS_LABEL_MAX)\n if (subject) fact.subject = subject\n const text = crmFactText(activity.body, CRM_FACTS_TEXT_MAX)\n if (text) fact.text = text\n const outcome = crmFactText(activity.outcome, CRM_FACTS_LABEL_MAX)\n if (outcome) fact.outcome = outcome\n if (kind === 'email' && isCrmEmailDeliveryState(activity.deliveryState)) {\n fact.delivery = CRM_EMAIL_DELIVERY_STATE_LABELS[activity.deliveryState]\n }\n return fact\n}\n\n/** A capture the platform recorded on a contact, as a timeline entry. */\nexport function crmInteractionFact(interaction: Partial<ContactInteraction>): CrmTimelineFact | null {\n const on = crmFactDay(interaction.atMs)\n if (!on) return null\n const kind = labelContactSource(interaction.type, 'Capture')\n const text = crmFactText(interaction.summary, CRM_FACTS_TEXT_MAX)\n return text ? { on, kind, text } : { on, kind }\n}\n\n/**\n * Timeline entries newest first, at most {@link CRM_FACTS_TIMELINE_MAX}. A\n * tie keeps the order the entries arrived in, so the same reads report the\n * same list.\n */\nexport function crmTimelineFacts(\n entries: ReadonlyArray<{ atMs: unknown; fact: CrmTimelineFact | null }>,\n): CrmTimelineFact[] {\n return entries\n .map((entry, index) => ({ at: crmFactMillis(entry.atMs) ?? 0, index, fact: entry.fact }))\n .filter((entry): entry is { at: number; index: number; fact: CrmTimelineFact } => entry.fact !== null)\n .sort((a, b) => b.at - a.at || a.index - b.index)\n .slice(0, CRM_FACTS_TIMELINE_MAX)\n .map((entry) => entry.fact)\n}\n\n/** The open tasks among `tasks`, soonest due first, undated last. */\nexport function crmOpenTaskFacts(\n tasks: ReadonlyArray<Partial<CrmTask>>,\n nowMs: number,\n names?: CrmFactsNames,\n): CrmTaskFact[] {\n return tasks\n .filter((task) => task.status !== 'done' && crmFactText(task.title, CRM_FACTS_LABEL_MAX))\n .map((task, index) => ({ task, index, due: crmFactMillis(task.dueAtMs) }))\n .sort((a, b) => (a.due ?? Number.POSITIVE_INFINITY) - (b.due ?? Number.POSITIVE_INFINITY) || a.index - b.index)\n .slice(0, CRM_FACTS_TASKS_MAX)\n .map(({ task, due }) => {\n const assignee = memberName(names, task.assigneeUid)\n const notes = crmFactText(task.notes, CRM_FACTS_TEXT_MAX)\n return {\n title: crmFactText(task.title, CRM_FACTS_LABEL_MAX),\n kind:\n crmFactText(task.typeLabel, CRM_FACTS_LABEL_MAX) ||\n (task.kind && CRM_TASK_KIND_LABELS[task.kind] ? CRM_TASK_KIND_LABELS[task.kind] : 'To-do'),\n priority: task.priority === 'high' || task.priority === 'low' ? task.priority : 'normal',\n ...(crmFactText(task.statusLabel, CRM_FACTS_LABEL_MAX)\n ? { status: crmFactText(task.statusLabel, CRM_FACTS_LABEL_MAX) }\n : {}),\n due: due === null ? null : crmFactDay(due),\n // Overdue by the UTC day: a task due today is not overdue until tomorrow.\n overdue: due !== null && Math.floor(due / DAY_MS) < Math.floor(nowMs / DAY_MS),\n ...(assignee ? { assignee } : {}),\n ...(notes ? { notes } : {}),\n }\n })\n}\n\n/** One deal, with its stage named by the pipeline it is in. */\nexport function crmDealFact(deal: Partial<CrmDeal>, pipeline: CrmPipeline | null | undefined): CrmDealFact {\n const stage = dealStageById(pipeline ?? undefined, String(deal.stageId ?? ''))\n return {\n title: crmFactText(deal.title, CRM_FACTS_LABEL_MAX),\n stage: crmFactText(stage?.name, CRM_FACTS_LABEL_MAX),\n status: deal.status === 'won' || deal.status === 'lost' ? deal.status : 'open',\n amount: crmFactMoney(deal.amountCents, deal.currency),\n expectedClose: crmFactDay(deal.expectedCloseAtMs),\n }\n}\n\n/** Deals, open ones first, then most recently updated. */\nexport function crmDealFacts(\n deals: ReadonlyArray<Partial<CrmDeal>>,\n pipelines: ReadonlyMap<string, CrmPipeline>,\n): CrmDealFact[] {\n return deals\n .map((deal, index) => ({ deal, index, updated: crmFactMillis(deal.updatedAt) ?? 0 }))\n .sort(\n (a, b) =>\n Number(a.deal.status !== 'open') - Number(b.deal.status !== 'open') ||\n b.updated - a.updated ||\n a.index - b.index,\n )\n .slice(0, CRM_FACTS_DEALS_MAX)\n .map(({ deal }) => crmDealFact(deal, pipelines.get(String(deal.pipelineId ?? ''))))\n}\n\nconst label = (value: unknown) => crmFactText(value, CRM_FACTS_LABEL_MAX)\n\nexport interface ContactFactsInput {\n row: Record<string, unknown>\n /** The holder whose facet the person is read through. */\n group: ConsentGroup\n activities: ReadonlyArray<Partial<CrmActivity>>\n tasks: ReadonlyArray<Partial<CrmTask>>\n deals: ReadonlyArray<Partial<CrmDeal>>\n pipelines: ReadonlyMap<string, CrmPipeline>\n nowMs: number\n names?: CrmFactsNames\n /** The name of the contact the holder's reports-to names, when the reader could see it. */\n reportsToName?: string\n}\n\nexport function contactFacts(input: ContactFactsInput): CrmContactFacts {\n const { row, group, names } = input\n const facet = readContactFacet(row, group.groupId)\n const sources = Object.entries(facet.sources ?? {})\n .filter(([, on]) => on === true)\n .map(([source]) => labelContactSource(source, ''))\n .filter(Boolean)\n const interactions = interactionsForGroup(facet.interactions, group.hostIds)\n const alternates = Array.isArray(row[CONTACT_ALTERNATE_EMAILS_FIELD])\n ? (row[CONTACT_ALTERNATE_EMAILS_FIELD] as unknown[])\n : []\n const emails = [...new Set([row['email'], ...alternates].map(label).filter(Boolean))].slice(0, CRM_FACTS_LIST_MAX)\n return {\n record: 'contact',\n name: label(contactDisplayName(row, group.groupId)),\n salutation: label(facet.salutation),\n firstName: label(facet.firstName),\n lastName: label(facet.lastName),\n emails,\n phone: label(facet.phone ?? row['phone']),\n mobilePhone: label(facet.mobilePhone),\n homePhone: label(facet.homePhone),\n otherPhone: label(facet.otherPhone),\n fax: label(facet.fax),\n jobTitle: label(facet.jobTitle),\n department: label(facet.department),\n birthdate: label(facet.birthdate),\n assistant: label(facet.assistantName),\n assistantPhone: label(facet.assistantPhone),\n reportsTo: label(input.reportsToName),\n mailingAddress: crmFactAddress(facet.address),\n otherAddress: crmFactAddress(facet.otherAddress),\n doNotCall: facet.doNotCall === true,\n company: label(facet.companyName),\n lifecycleStage: isContactLifecycleStage(facet.lifecycleStage)\n ? CONTACT_LIFECYCLE_STAGE_LABELS[facet.lifecycleStage]\n : '',\n leadSource: label(facet.leadSource),\n owner: memberName(names, facet.ownerUid),\n marketingConsent: crmFactConsent(row, group),\n tags: tagsOf(facet.tags),\n sources: [...new Set(sources)].sort(),\n orders: typeof facet.ordersCount === 'number' && facet.ordersCount > 0 ? Math.floor(facet.ordersCount) : 0,\n lastPurchase: crmFactDay(facet.lastPurchaseAtMs),\n since: crmFactDay(row['createdAt']),\n lastEmailEngagement: crmFactDay(facet.lastEmailEngagementAtMs),\n custom: crmCustomFacts(facet.custom, 'contact', names?.customFields),\n notes: crmFactText(facet.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts([\n ...input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmActivityFact(activity) })),\n ...interactions.map((interaction) => ({ atMs: interaction.atMs, fact: crmInteractionFact(interaction) })),\n ]),\n openTasks: crmOpenTaskFacts(input.tasks, input.nowMs, names),\n deals: crmDealFacts(input.deals, input.pipelines),\n }\n}\n\nexport interface CompanyFactsInput {\n company: Partial<CrmCompany>\n activities: ReadonlyArray<Partial<CrmActivity>>\n tasks: ReadonlyArray<Partial<CrmTask>>\n deals: ReadonlyArray<Partial<CrmDeal>>\n pipelines: ReadonlyMap<string, CrmPipeline>\n nowMs: number\n names?: CrmFactsNames\n /** The parent company's name, when the reader could see it. */\n parentCompanyName?: string\n}\n\nexport function companyFacts(input: CompanyFactsInput): CrmCompanyFacts {\n const { company, names } = input\n return {\n record: 'company',\n name: label(company.name),\n domain: label(company.domain),\n website: label(company.website),\n phone: label(company.phone),\n fax: label(company.fax),\n industry: label(company.industry),\n type: label(company.type),\n rating: label(company.rating),\n ownership: label(company.ownership),\n accountSource: label(company.accountSource),\n employees:\n typeof company.numberOfEmployees === 'number' && company.numberOfEmployees >= 0\n ? Math.floor(company.numberOfEmployees)\n : null,\n annualRevenue: crmFactMoney(company.annualRevenueCents, company.currency),\n accountNumber: label(company.accountNumber),\n site: label(company.site),\n tickerSymbol: label(company.tickerSymbol),\n sicCode: label(company.sicCode),\n billingAddress: crmFactAddress(company.address),\n shippingAddress: crmFactAddress(company.shippingAddress),\n parentCompany: label(input.parentCompanyName),\n owner: memberName(names, company.ownerUid),\n tags: tagsOf(company.tags),\n people:\n typeof company.contactsCount === 'number' && company.contactsCount > 0\n ? Math.floor(company.contactsCount)\n : 0,\n since: crmFactDay(company.createdAt),\n custom: crmCustomFacts(company.custom, 'company', names?.customFields),\n notes: crmFactText(company.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmActivityFact(activity) })),\n ),\n openTasks: crmOpenTaskFacts(input.tasks, input.nowMs, names),\n deals: crmDealFacts(input.deals, input.pipelines),\n }\n}\n\nexport interface DealFactsInput {\n deal: Partial<CrmDeal> & { contactName?: unknown; companyName?: unknown }\n /** The names of the deal's contacts, by id, as the reader may see them (AGL-3521). */\n contactNames?: ReadonlyMap<string, string>\n pipeline: CrmPipeline | null\n activities: ReadonlyArray<Partial<CrmActivity>>\n tasks: ReadonlyArray<Partial<CrmTask>>\n nowMs: number\n names?: CrmFactsNames\n /** The name of the campaign the deal is credited to, when the reader could see it. */\n campaignName?: string\n}\n\nexport function dealFacts(input: DealFactsInput): CrmDealFacts {\n const { deal, pipeline, names } = input\n const summary = crmDealFact(deal, pipeline)\n const stages = [...(pipeline?.stages ?? [])]\n .sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0))\n .map((stage) => ({\n id: String(stage.id),\n name: crmFactText(stage.name, CRM_FACTS_LABEL_MAX),\n kind: stage.kind === 'won' || stage.kind === 'lost' ? stage.kind : ('open' as const),\n probability: Math.min(100, Math.max(0, Number(stage.probability) || 0)),\n forecastCategory: CRM_FORECAST_CATEGORY_LABELS[dealStageForecastCategory(stage)],\n }))\n const stage = dealStageById(pipeline ?? undefined, String(deal.stageId ?? ''))\n const products = (Array.isArray(deal.lineItems) ? deal.lineItems : [])\n .slice(0, CRM_FACTS_PRODUCTS_MAX)\n .map((item) => ({\n name: label(item?.name),\n quantity: Number(item?.quantity) || 0,\n unitAmount: crmFactMoney(item?.unitAmountCents, item?.currency ?? deal.currency),\n }))\n return {\n record: 'deal',\n title: summary.title,\n pipeline: crmFactText(pipeline?.name, CRM_FACTS_LABEL_MAX),\n stages,\n stageId: String(deal.stageId ?? ''),\n stage: summary.stage,\n status: summary.status,\n amount: summary.amount,\n expectedClose: summary.expectedClose,\n inStageSince: crmFactDay(deal.stageChangedAtMs),\n lostReason: label(deal.lostReason),\n type: label(deal.type),\n leadSource: label(deal.leadSource),\n nextStep: crmFactText(deal.nextStep, DEAL_NEXT_STEP_MAX),\n probability: dealProbability(deal, stage),\n forecastCategory: CRM_FORECAST_CATEGORY_LABELS[dealForecastCategory(deal, stage)],\n campaign: label(input.campaignName),\n contact: label(deal.contactName),\n contactRoles: dealContactRoleFacts(deal, input.contactNames),\n company: label(deal.companyName),\n owner: memberName(names, deal.ownerUid),\n products,\n since: crmFactDay(deal.createdAt),\n custom: crmCustomFacts(deal.custom, 'deal', names?.customFields),\n notes: crmFactText(deal.notes, CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmActivityFact(activity) })),\n ),\n openTasks: crmOpenTaskFacts(input.tasks, input.nowMs, names),\n }\n}\n\n/**\n * A deal's contact roles as facts (AGL-3521): the Primary first, each by the\n * name the reader resolved — a contact the reader may not see, or could\n * not read, is left out rather than named by id.\n */\nexport function dealContactRoleFacts(\n deal: { contactId?: unknown; contactRoles?: unknown },\n names: ReadonlyMap<string, string> = new Map(),\n): CrmContactRoleFact[] {\n return dealContactRolesOf(deal)\n .sort((a, b) => Number(b.primary) - Number(a.primary))\n .filter((row) => names.has(row.contactId))\n .slice(0, CRM_FACTS_CONTACT_ROLES_MAX)\n .map((row) => ({\n name: label(names.get(row.contactId)),\n role: crmFactText(row.role, CRM_FACTS_LABEL_MAX),\n primary: row.primary,\n }))\n}\n\n/** What a lead's capture surfaces are called, the lead history card's words. */\nexport function leadSourceFact(source: string): string {\n if (source === 'signup') return 'Sign-up'\n if (source === 'booking') return 'Booking'\n if (source === 'import') return CONTACT_SOURCE_LABELS.import\n if (source === 'form' || source.startsWith('form:')) return CONTACT_SOURCE_LABELS.form\n return 'Other'\n}\n\nexport interface LeadFactsInput {\n lead: Record<string, unknown>\n activities: ReadonlyArray<Partial<CrmActivity>>\n /** The tasks filed against the lead (AGL-3520); none when the reader read none. */\n tasks?: ReadonlyArray<Partial<CrmTask>>\n nowMs?: number\n /** The org's Lead status list, whose label the status fact reads (AGL-3512); the standard ones without it. */\n leadStatuses?: CrmPicklist\n /** The site's consent group, which the lead's marketing consent is read under. */\n group?: ConsentGroup | null\n names?: CrmFactsNames\n /** The names of the campaigns the lead is filed under, as the reader resolved them. */\n campaignNames?: readonly string[]\n}\n\nexport function leadFacts(input: LeadFactsInput): CrmLeadFacts {\n const { lead, leadStatuses, names } = input\n const rawSources = Array.isArray(lead['sources'])\n ? lead['sources']\n : typeof lead['source'] === 'string'\n ? [lead['source']]\n : []\n const captures = Number(lead['submissionCount'])\n return {\n record: 'lead',\n name: label(lead['name']),\n salutation: label(lead['salutation']),\n firstName: label(lead['firstName']),\n lastName: label(lead['lastName']),\n email: label(lead['email']),\n phone: label(lead['phone']),\n mobilePhone: label(lead['mobilePhone']),\n fax: label(lead['fax']),\n website: label(lead['website']),\n address: crmFactAddress(lead['address'] as Partial<AglynPostalAddress> | null),\n company: label(lead['company']),\n jobTitle: label(lead['jobTitle']),\n leadSource: label(lead['leadSource']),\n industry: label(lead['industry']),\n rating: label(lead['rating']),\n employees:\n typeof lead['numberOfEmployees'] === 'number' && lead['numberOfEmployees'] >= 0\n ? Math.floor(lead['numberOfEmployees'])\n : null,\n annualRevenue: crmFactMoney(lead['annualRevenueCents'], lead['currency']),\n doNotCall: lead['doNotCall'] === true,\n status: crmLeadStatusLabel(lead as { status?: never; statusLabel?: string }, leadStatuses),\n owner: memberName(names, lead['ownerUid']),\n campaigns: (input.campaignNames ?? []).map(label).filter(Boolean).slice(0, CRM_FACTS_LIST_MAX),\n marketingConsent: crmFactConsent(lead, input.group),\n tags: tagsOf(lead['tags']),\n sources: [...new Set(rawSources.map((source) => leadSourceFact(String(source))))].sort(),\n captures: Number.isFinite(captures) && captures > 0 ? Math.floor(captures) : 0,\n firstSeen: crmFactDay(lead['firstSeenAtMs']),\n lastSeen: crmFactDay(lead['lastSeenAtMs']),\n assigned: typeof lead['ownerUid'] === 'string' && lead['ownerUid'] !== '',\n converted: typeof lead['convertedContactId'] === 'string' && lead['convertedContactId'] !== '',\n unqualifiedReason: label(lead['unqualifiedReason']),\n custom: crmCustomFacts(lead['custom'], 'lead', names?.customFields),\n notes: crmFactText(lead['notes'], CRM_FACTS_NOTES_MAX),\n timeline: crmTimelineFacts(\n input.activities.map((activity) => ({ atMs: activity.atMs, fact: crmActivityFact(activity) })),\n ),\n openTasks: crmOpenTaskFacts(input.tasks ?? [], input.nowMs ?? Date.now(), names),\n }\n}\n\n/** A column's kind, as the matcher is told it, for each transfer field type. */\nconst TRANSFER_TYPES: Partial<Record<TransferFieldType, CrmImportFieldType>> = {\n email: 'email',\n phone: 'phone',\n url: 'url',\n number: 'number',\n integer: 'number',\n currency: 'number',\n percent: 'number',\n date: 'date',\n datetime: 'date',\n boolean: 'yes-no',\n}\n\n/** The fields a transfer catalog lets a file write, as the import facts list them. */\nfunction transferImportFields(fields: readonly TransferField[]): {\n keys: readonly string[]\n labels: Record<string, string>\n required: string\n types: Record<string, CrmImportFieldType>\n} {\n const writable = fields.filter((field) => !field.readOnly && !field.derived && !field.system)\n return {\n keys: writable.map((field) => field.id),\n labels: Object.fromEntries(writable.map((field) => [field.id, field.label])),\n required: writable.find((field) => field.required)?.id ?? '',\n types: Object.fromEntries(\n writable.flatMap((field) => (TRANSFER_TYPES[field.type] ? [[field.id, TRANSFER_TYPES[field.type] as CrmImportFieldType]] : [])),\n ),\n }\n}\n\n/** The standard fields of each import, with the type a column must hold for each. */\nconst STANDARD_IMPORT_FIELDS: Record<\n CrmImportFactsCollection,\n { keys: readonly string[]; labels: Record<string, string>; required: string; types: Record<string, CrmImportFieldType> }\n> = {\n // The transfer catalogs' own ids (AGL-3527): what the import wizard maps to.\n contacts: transferImportFields(CONTACT_TRANSFER_FIELDS),\n companies: transferImportFields(COMPANY_TRANSFER_FIELDS),\n deals: transferImportFields(DEAL_TRANSFER_FIELDS),\n leads: transferImportFields(LEAD_TRANSFER_FIELDS),\n}\n\n/** The object a collection's custom fields describe. */\nconst CUSTOM_FIELD_OBJECT: Partial<Record<CrmImportFactsCollection, CrmFieldObject>> = {\n contacts: 'contact',\n companies: 'company',\n deals: 'deal',\n leads: 'lead',\n}\n\nconst CUSTOM_FIELD_TYPES: Record<string, CrmImportFieldType> = {\n number: 'number',\n date: 'date',\n checkbox: 'yes-no',\n url: 'url',\n}\n\n/**\n * The fields an import of `collection` may fill: the standard ones in the\n * mapping menu's order, then the org's active custom fields for the record\n * the collection holds, in their own order.\n */\nexport function importFacts(\n collection: CrmImportFactsCollection,\n customFields: ReadonlyArray<Partial<ContactFieldDefinition>>,\n): CrmImportFacts {\n const standard = STANDARD_IMPORT_FIELDS[collection]\n const object = CUSTOM_FIELD_OBJECT[collection]\n const custom = object\n ? customFields\n .filter((field) => field.key && !field.retiredAt && fieldDefinitionObject(field as ContactFieldDefinition) === object)\n .sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0) || String(a.key).localeCompare(String(b.key)))\n .slice(0, CRM_FACTS_CUSTOM_FIELDS_MAX)\n .map((field) => ({\n key: customImportTarget(String(field.key)),\n label: crmFactText(field.label, CRM_FACTS_LABEL_MAX) || String(field.key),\n type: CUSTOM_FIELD_TYPES[String(field.type)] ?? ('text' as const),\n required: false,\n }))\n : []\n return {\n record: 'import',\n collection,\n fields: [\n ...standard.keys.map((key) => ({\n key,\n label: standard.labels[key] ?? key,\n type: standard.types[key] ?? ('text' as const),\n required: key === standard.required,\n })),\n ...custom,\n ],\n }\n}\n"],"names":["CONTACT_ALTERNATE_EMAILS_FIELD","CONTACT_LIFECYCLE_STAGE_LABELS","CONTACT_SOURCE_LABELS","CRM_ACTIVITY_KIND_LABELS","CRM_EMAIL_DELIVERY_STATE_LABELS","CRM_TASK_KIND_LABELS","contactDisplayName","crmLeadStatusLabel","CRM_FORECAST_CATEGORY_LABELS","crmActivityDirection","customImportTarget","DEAL_NEXT_STEP_MAX","dealContactRolesOf","dealForecastCategory","dealProbability","dealStageById","dealStageForecastCategory","fieldDefinitionObject","interactionsForGroup","isContactLifecycleStage","isCrmEmailDeliveryState","readContactFacet","readMarketingBasis","labelContactSource","COMPANY_TRANSFER_FIELDS","CONTACT_TRANSFER_FIELDS","DEAL_TRANSFER_FIELDS","LEAD_TRANSFER_FIELDS","CRM_RECORD_FACTS_RESOURCES","contact","company","deal","lead","CRM_IMPORT_FACTS_RESOURCE","CRM_IMPORT_FACTS_COLLECTIONS","isCrmImportFactsCollection","value","includes","CRM_FACTS_TIMELINE_MAX","CRM_FACTS_TASKS_MAX","CRM_FACTS_DEALS_MAX","CRM_FACTS_TAGS_MAX","CRM_FACTS_TEXT_MAX","CRM_FACTS_NOTES_MAX","CRM_FACTS_LABEL_MAX","CRM_FACTS_CUSTOM_FIELDS_MAX","CRM_FACTS_PRODUCTS_MAX","CRM_FACTS_LIST_MAX","CRM_FACTS_CONTACT_ROLES_MAX","DAY_MS","crmFactText","max","text","replace","trim","length","slice","trimEnd","crmFactMillis","Number","isFinite","Date","getTime","toMillis","millis","call","crmFactDay","toISOString","crmFactMoney","cents","currency","code","test","toUpperCase","toFixed","crmFactAddress","address","part","cityLine","city","state","postalCode","filter","Boolean","join","line1","line2","country","crmFactConsent","record","group","consent","basis","since","basisAtMs","customValueText","type","undefined","parse","String","crmCustomFacts","custom","object","definitions","Array","isArray","values","own","field","key","sort","a","b","retiredAt","order","localeCompare","facts","seen","Set","add","push","label","Object","keys","has","tagsOf","map","tag","memberName","names","uid","member","crmActivityFact","activity","on","atMs","kind","fact","direction","from","to","subject","threadSubject","body","outcome","deliveryState","delivery","crmInteractionFact","interaction","summary","crmTimelineFacts","entries","entry","index","at","crmOpenTaskFacts","tasks","nowMs","task","status","title","due","dueAtMs","POSITIVE_INFINITY","assignee","assigneeUid","notes","typeLabel","priority","statusLabel","overdue","Math","floor","crmDealFact","pipeline","stage","stageId","name","amount","amountCents","expectedClose","expectedCloseAtMs","crmDealFacts","deals","pipelines","updated","updatedAt","get","pipelineId","contactFacts","input","facet","row","groupId","sources","source","interactions","hostIds","alternates","emails","salutation","firstName","lastName","phone","mobilePhone","homePhone","otherPhone","fax","jobTitle","department","birthdate","assistant","assistantName","assistantPhone","reportsTo","reportsToName","mailingAddress","otherAddress","doNotCall","companyName","lifecycleStage","leadSource","owner","ownerUid","marketingConsent","tags","orders","ordersCount","lastPurchase","lastPurchaseAtMs","lastEmailEngagement","lastEmailEngagementAtMs","customFields","timeline","activities","openTasks","companyFacts","domain","website","industry","rating","ownership","accountSource","employees","numberOfEmployees","annualRevenue","annualRevenueCents","accountNumber","site","tickerSymbol","sicCode","billingAddress","shippingAddress","parentCompany","parentCompanyName","people","contactsCount","createdAt","dealFacts","stages","id","probability","min","forecastCategory","products","lineItems","item","quantity","unitAmount","unitAmountCents","inStageSince","stageChangedAtMs","lostReason","nextStep","campaign","campaignName","contactName","contactRoles","dealContactRoleFacts","contactNames","Map","primary","contactId","role","leadSourceFact","import","startsWith","form","leadFacts","leadStatuses","rawSources","captures","email","campaigns","campaignNames","firstSeen","lastSeen","assigned","converted","unqualifiedReason","now","TRANSFER_TYPES","url","number","integer","percent","date","datetime","boolean","transferImportFields","fields","writable","readOnly","derived","system","labels","fromEntries","required","find","types","flatMap","STANDARD_IMPORT_FIELDS","contacts","companies","leads","CUSTOM_FIELD_OBJECT","CUSTOM_FIELD_TYPES","checkbox","importFacts","collection","standard"],"mappings":"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"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import type { PluginContactCaptureRequest, PluginContactCaptured } from '@aglyn/aglyn/plugin-manager/plugin-contact-capture';
17
+ import { type PluginContactCaptureRequest, type PluginContactCaptured } from '@aglyn/aglyn/plugin-manager/plugin-contact-capture';
18
18
  import { type ContactSource } from '@aglyn/aglyn/app-utils/contacts';
19
19
  /**
20
20
  * The CRM answering the platform's contact-capture contract (AGL-3080).
@@ -94,9 +94,9 @@ export declare function leadCreatedPayload(lead: {
94
94
  * than rejected — refusing it would lose a third-party plugin's capture over
95
95
  * a label, and the capture is the part that matters.
96
96
  *
97
- * ⛔ What it costs, until the union is widened: the console's source filter
98
- * and `SOURCE_LABELS` key on these values, so an undeclared word renders raw
99
- * and matches no filter. Every first-party silo uses a word in the union, so
100
- * nothing does that today.
97
+ * A declared word READS by its plugin's label (`labelContactSource`, AGL-4042:
98
+ * the waivers plugin's `waiver`). ⛔ What it still costs, until the union is
99
+ * widened: the console's source filter keys on the union, so a plugin's word
100
+ * matches no filter option, and an UNDECLARED word also renders raw.
101
101
  */
102
102
  export declare function contactSourceOf(source: string): ContactSource;
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -13,7 +14,7 @@
13
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
15
  * See the License for the specific language governing permissions and
15
16
  * limitations under the License.
16
- */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ */ import { pluginContactSource } from "@aglyn/aglyn/plugin-manager/plugin-contact-capture";
17
18
  import { CONTACT_SOURCE_LABELS } from "@aglyn/aglyn/app-utils/contacts";
18
19
  import { formLeadSource } from "@aglyn/aglyn/app-utils/forms";
19
20
  import { normalizePhone } from "@aglyn/aglyn/foundation/definitions/contact.types";
@@ -401,13 +402,13 @@ import { crmRecordOriginWriter } from "./record-origin.js";
401
402
  * than rejected — refusing it would lose a third-party plugin's capture over
402
403
  * a label, and the capture is the part that matters.
403
404
  *
404
- * ⛔ What it costs, until the union is widened: the console's source filter
405
- * and `SOURCE_LABELS` key on these values, so an undeclared word renders raw
406
- * and matches no filter. Every first-party silo uses a word in the union, so
407
- * nothing does that today.
405
+ * A declared word READS by its plugin's label (`labelContactSource`, AGL-4042:
406
+ * the waivers plugin's `waiver`). ⛔ What it still costs, until the union is
407
+ * widened: the console's source filter keys on the union, so a plugin's word
408
+ * matches no filter option, and an UNDECLARED word also renders raw.
408
409
  */ export function contactSourceOf(source) {
409
410
  const word = String(source != null ? source : '').trim();
410
- if (!Object.hasOwn(CONTACT_SOURCE_LABELS, word)) {
411
+ if (!Object.hasOwn(CONTACT_SOURCE_LABELS, word) && !pluginContactSource(word)) {
411
412
  // Said once, where somebody debugging an unlabelled row will find it.
412
413
  // Not a refusal: the capture is worth more than the label.
413
414
  console.warn(`crm contact capture: source "${word}" has no label, so it will render ` + 'raw and match no filter in the console.');
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/capture-contact.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n PluginContactCaptureRequest,\n PluginContactCaptured,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport {\n CONTACT_SOURCE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { formLeadSource } from '@aglyn/aglyn/app-utils/forms'\nimport { normalizePhone } from '@aglyn/aglyn/foundation/definitions/contact.types'\nimport {\n type CrmLeadFields,\n isCrmLeadOpen,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { captureHostContact } from './capture-host-contact'\nimport { contactCaptureActor } from '@aglyn/tenant-runtime/capture-actor'\nimport { emitHostEvent } from '@aglyn/tenant-runtime/emit-host-event'\nimport {\n addHostLeadOutcome,\n findContactByEmail,\n firebaseAdmin,\n orgDataCollectionForHost,\n readLeadForHost,\n type UpsertHostContactOptions,\n} from '@aglyn/tenant-data-admin'\nimport {\n CONVERSION_TOUCH_DETAIL,\n type PluginConversionTouch,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { convertOpenLeadOntoContact } from './convert-open-lead'\nimport { crmRecordOriginWriter } from './record-origin'\n\n/**\n * The CRM answering the platform's contact-capture contract (AGL-3080).\n *\n * Four silos meet the same person — a form submission, a member signing up,\n * an order, a booking — and none of them is the record system. Each holds an\n * address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each imports `captureHostContact`\n * directly, which is the highest fan-in edge in the repo and makes the CRM\n * something a storefront cannot take a payment without.\n *\n * So this is the CRM's side of the seam: the silo reports what it saw, and\n * the plugin that keeps people decides everything a record system decides —\n * keying the address, whether this is somebody new, the audience band, the\n * erasure rows, the company, the owner, and what a new person sets off.\n * `captureHostContact` already does all of that; this translates the\n * contract's vocabulary into its options and its verdict back.\n *\n * ⚠️ IT NEVER THROWS. Every caller has already done the thing it is\n * recording — the submission was accepted, the order was paid — so a refusal\n * is RETURNED and costs the silo nothing. A throw here would lose an order\n * for a contact that could not be filed.\n */\n/**\n * WHICH RECORD A CAPTURE LANDS ON (AGL-3232) — the Salesforce rule, decided\n * here because the CRM is the plugin that models both records.\n *\n * One person is one record: a LEAD until somebody qualifies them, a CONTACT\n * after. Every door used to write both — a lead-routed form filed a lead\n * AND a contact at stage Lead — so one person sat in two lists and the Leads\n * queue was never the whole story. Now a door says what kind of surface it\n * is (`request.surface`) and this decides:\n *\n * - a LEAD surface files a lead, unless the workspace already holds the\n * address as a contact — a customer who books a demo is a customer's\n * interaction, not a new lead — in which case the capture lands on the\n * contact and no lead is filed;\n * - a RELATIONSHIP (a member account, a purchase) makes the contact, and\n * an open lead the site held for the address is stamped converted onto\n * it, so nobody keeps working a lead who already joined or bought;\n * - a TOUCH (an unrouted form, a newsletter opt-in) lands on the open lead\n * when the site holds one — its consent and its history stay on the one\n * record the rep is working — and on the contact otherwise.\n *\n * A lead is filed through `addHostLead`, the one writer of the leads silo,\n * so it is keyed on the person, counted against the ceiling and carries the\n * campaign touch the door resolved. A NEW lead announces itself with the\n * `lead` host event, which is what a \"new lead\" automation listens for; a\n * repeat capture on a lead the site already held announces nothing, the\n * way a repeat visit by a contact is an interaction and not a creation.\n */\nexport async function captureContactForCrm(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n const surface = request.surface ?? 'touch'\n // The one refusal every surface shares, answered before any read: an\n // address nothing can key is a person nothing can record.\n if (!normalizeContactEmail(request.identity.email)) return refusedEmail()\n try {\n if (surface === 'lead') {\n if (!(await heldAsContact(request))) return await stampedAfter(request, fileLead(request))\n } else if (surface === 'touch') {\n if (await openLeadFor(request)) return await stampedAfter(request, fileLead(request))\n }\n const verdict = await captureOnContact(request)\n if (verdict.ok) await stampOrigin(request)\n if (verdict.ok && verdict.record === 'contact' && surface === 'relationship') {\n await convertOpenLeadOntoContact({\n hostId: request.hostId,\n email: request.identity.email,\n contactId: verdict.contactId,\n // Money changing hands is a purchase whichever door took it — a paid\n // booking or a stay is one as much as an order is.\n by:\n request.interaction.source === 'order' || (request.purchaseCents ?? 0) > 0\n ? 'purchase'\n : 'signup',\n })\n }\n return verdict\n } catch (error) {\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/**\n * WHERE THE PERSON CAME FROM (AGL-3519): the door's word — a form, a\n * booking, a newsletter, a member, an order — as the built-in Lead source\n * it names, on a record the capture just started and that names none. A\n * person met before keeps whatever lead source they hold, or the lack of one.\n * Never fails the capture: the writer swallows its own errors.\n */\nasync function stampOrigin(request: PluginContactCaptureRequest): Promise<void> {\n await crmRecordOriginWriter.stamp({\n hostId: request.hostId,\n email: normalizeContactEmail(request.identity.email) ?? '',\n origin: request.interaction.source,\n firstTouchOnly: true,\n })\n}\n\n/** A filed lead's verdict, after its origin is stamped when the filing took. */\nasync function stampedAfter(\n request: PluginContactCaptureRequest,\n filed: Promise<PluginContactCaptured>,\n): Promise<PluginContactCaptured> {\n const verdict = await filed\n if (verdict.ok) await stampOrigin(request)\n return verdict\n}\n\n/** The refusal every door gets for an address nothing can key. */\nfunction refusedEmail(): PluginContactCaptured {\n return { ok: false, reason: 'invalid-email', error: refusalText('invalid-email') }\n}\n\n/**\n * Whether the workspace already holds the address as a contact — the\n * address index first, then the query, the same lookup every dedupe uses.\n * ORG-wide, not scoped to the capturing site: the contact door dedupes\n * across every site in the org, so a person any site holds as a contact\n * is a contact, whichever site met them this time.\n */\nasync function heldAsContact(request: PluginContactCaptureRequest): Promise<boolean> {\n const contacts = await orgDataCollectionForHost(request.hostId, 'contacts')\n return (await findContactByEmail(contacts, request.identity.email)) !== null\n}\n\n/** Whether the site holds an OPEN lead for the address — neither converted nor closed. */\nasync function openLeadFor(request: PluginContactCaptureRequest): Promise<boolean> {\n const key = personKey(request.identity.email) as string\n /*\n * Through the seam, which reads the org row and falls back to a lead the\n * backfill has not reached (AGL-3275). UNSCOPED on purpose: a sibling brand\n * in the same consent group has to find this person, or its door concludes\n * there is no lead and files the duplicate this migration exists to end.\n */\n const snapshot = await readLeadForHost(request.hostId, key)\n if (!snapshot) return false\n const lead = (snapshot.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n return !lead.convertedContactId && isCrmLeadOpen(lead)\n}\n\n/**\n * The capture as a LEAD. The source string is the lead silo's own\n * vocabulary — `form:{formId}` for a form, the door's word otherwise — so\n * a lead's provenance survives the form being renamed, as the submission's\n * does.\n */\nasync function fileLead(request: PluginContactCaptureRequest): Promise<PluginContactCaptured> {\n const email = normalizeContactEmail(request.identity.email) as string\n const key = personKey(email) as string\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(request.hostId)\n // `addHostLead` resolves and writes the org collection itself (AGL-3275);\n // this only needs to know whether the person is new, which decides the event.\n const created = !(await readLeadForHost(request.hostId, key))\n const { formId } = entryPointOf(request.detail)\n const source =\n request.interaction.source === 'form' && formId\n ? formLeadSource(formId)\n : request.interaction.source\n const outcome = await addHostLeadOutcome({\n hostRef,\n hostId: request.hostId,\n lead: {\n email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source,\n ...(request.marketingConsent ? { marketingConsent: true } : {}),\n ...disclosureOf(request),\n // Filed under the surface's campaigns, as the contact door files a\n // contact (AGL-3458): a lead-routed form in a campaign makes a lead\n // in that campaign.\n ...campaignsFiledUnder(request),\n ...phoneFillOf(request),\n },\n ...(conversionTouchOf(request.detail).conversionTouch\n ? { touch: conversionTouchOf(request.detail).conversionTouch }\n : {}),\n })\n if (!outcome.stored) {\n return {\n ok: false,\n reason: 'band',\n error: 'This site is at the number of leads it may hold, so the lead was not recorded.',\n }\n }\n if (created) {\n const cause = contactCaptureActor(\n request.interaction.source,\n email,\n request.actor,\n )\n await emitHostEvent(\n request.hostId,\n 'lead',\n leadCreatedPayload({\n leadId: key,\n email,\n name: request.identity.name,\n source,\n hostId: request.hostId,\n formId,\n campaignIds: campaignsFiledUnder(request).campaignIds,\n }),\n cause ? { actor: cause } : {},\n )\n }\n return { ok: true, record: 'lead', leadId: key, created, sourceAdded: outcome.sourceAdded }\n}\n\n/**\n * The `lead` payload (AGL-3458), as the scalars an event may carry — the\n * shape `contactCreatedPayload` gives a new contact, so one condition reads\n * the same on either event: `name` always present, empty when the door had\n * none; `formId` only when a form filed the lead, so `notEmpty` on it reads\n * as \"came in through a form\" and `equals` picks one form's people out; and\n * `campaignIds` comma-joined, only when the surface is filed under any.\n */\nexport function leadCreatedPayload(lead: {\n leadId: string\n email: string\n name?: string | null\n source: string\n hostId: string\n formId?: string | null\n campaignIds?: readonly string[]\n}): Record<string, string> {\n const formId = String(lead.formId ?? '').trim()\n return {\n leadId: lead.leadId,\n email: lead.email,\n name: String(lead.name ?? '').trim(),\n source: lead.source,\n hostId: lead.hostId,\n ...(formId ? { formId } : {}),\n ...(lead.campaignIds?.length ? { campaignIds: lead.campaignIds.join(',') } : {}),\n }\n}\n\n/** The capture as a CONTACT — what every capture was before the rule above. */\nasync function captureOnContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n try {\n const verdict = await captureHostContact({\n ...(request.actor ? { actor: request.actor } : {}),\n hostId: request.hostId,\n email: request.identity.email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source: contactSourceOf(request.interaction.source),\n interaction: {\n ...(request.interaction.atMs === undefined\n ? {}\n : { atMs: request.interaction.atMs }),\n ...(request.interaction.refId ? { refId: request.interaction.refId } : {}),\n ...(request.interaction.summary\n ? { summary: request.interaction.summary }\n : {}),\n ...entryPointOf(request.detail),\n },\n ...conversionTouchOf(request.detail),\n ...(request.marketingConsent === undefined\n ? {}\n : { marketingConsent: request.marketingConsent }),\n ...disclosureOf(request),\n ...(request.tags?.length ? { tags: request.tags } : {}),\n ...campaignsFiledUnder(request),\n ...(request.purchaseCents === undefined\n ? {}\n : { purchaseCents: request.purchaseCents }),\n ...(request.purchaseCurrency\n ? { purchaseCurrency: request.purchaseCurrency }\n : {}),\n ...(request.lifecycleFloor\n ? { initialLifecycleStage: request.lifecycleFloor as never }\n : {}),\n ...(request.profile ? { facet: request.profile as never } : {}),\n ...(phoneFillOf(request).phoneFill\n ? { facetFill: { phone: phoneFillOf(request).phoneFill } }\n : {}),\n })\n if ('refused' in verdict) {\n return { ok: false, reason: verdict.refused, error: refusalText(verdict.refused) }\n }\n return {\n ok: true,\n record: 'contact',\n contactId: verdict.contactId,\n created: verdict.created,\n }\n } catch (error) {\n /*\n * The contract's `error` is the last state, not a channel for a stack:\n * a silo may show or log it as it stands, so it says what happened and\n * names nothing internal. The detail goes to the log, where whoever is\n * debugging a missing contact will look.\n */\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/**\n * THE DISCLOSURE the capture surface rendered, handed to whichever record the\n * capture lands on (AGL-3320).\n *\n * Passed through unread: both writers resolve the site's group themselves and\n * pool the opt-in only when this is its current key, so a lead and a contact\n * written by one capture record the same sites. A string or nothing, since\n * the writers compare it exactly and anything else could only fail to match.\n */\nfunction disclosureOf(\n request: PluginContactCaptureRequest,\n): { disclosedConsentGroup?: string } {\n const key = request.disclosedConsentGroup\n return typeof key === 'string' && key ? { disclosedConsentGroup: key } : {}\n}\n\n/**\n * THE PHONE A CAPTURE LEARNED IN PASSING (AGL-3493), off `profileFill`.\n *\n * Normalized here so a lead and a contact written by one capture keep the\n * same E.164 string, and dropped when it will not normalize: a fill is an\n * enrichment, and a number in some other shape on the record is the\n * \"stored three ways\" failure `normalizePhone` exists to end. Both writers\n * put it only where the record holds no phone.\n */\nfunction phoneFillOf(request: PluginContactCaptureRequest): { phoneFill?: string } {\n const raw = request.profileFill?.['phone']\n const phone = typeof raw === 'string' ? normalizePhone(raw) : null\n return phone ? { phoneFill: phone } : {}\n}\n\n/**\n * THE ENTRY POINT, off the silo's own `detail` bag (AGL-3080).\n *\n * The contract carries a capture's silo-side facts opaquely, so the two the\n * CRM models are picked out here rather than typed into the platform. Both\n * ride the INTERACTION, which is where `upsertHostContact` already keeps\n * them: which form a person came in through routes the owner-assignment\n * rules and rides the `contactCreated` payload, and the page is what a\n * timeline row says about where they were.\n *\n * Absent, misspelled or the wrong type is the same answer as a door that\n * never had one — a capture without an entry point, which is every capture\n * that did not come through a form. It is never a reason to refuse: the\n * person is the part that matters.\n */\nfunction entryPointOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): { formId?: string; path?: string } {\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' && value.trim() ? value : undefined\n const formId = text(detail?.['formId'])\n const path = text(detail?.['path'])\n return {\n ...(formId ? { formId } : {}),\n ...(path ? { path } : {}),\n }\n}\n\n/**\n * The campaigns the capture surface is filed under, as the contact writer\n * records them. A capture names what its surface is filed under by container\n * kind; the CRM keeps the `campaign` kind on the person's site facet, and a\n * kind it does not keep is the capture's to name and the CRM's to ignore.\n */\nfunction campaignsFiledUnder(\n request: PluginContactCaptureRequest,\n): Pick<UpsertHostContactOptions, 'campaignIds'> {\n const ids = request.containers?.['campaign'] ?? []\n return ids.length ? { campaignIds: ids } : {}\n}\n\n/**\n * WHERE THE VISITOR CAME FROM, off the same bag.\n *\n * ⚠️ A different fact from `campaignIds` and the two must never be folded\n * together — `upsert-contact.ts` says so at the field itself. A touch is the\n * ad or the link the visitor arrived by, already resolved by the plugin that\n * credits outcomes (`plugin-conversion-credit`) at the silo's door;\n * `campaignIds` is which campaigns the merchant filed the capture SURFACE\n * under, which is true of everybody who fills that form in. Folding them\n * would credit a campaign for a visitor who typed the address.\n *\n * Passed through as the silo handed it, unread: the touch's shape belongs\n * to the creditor that resolved it, and the creditor checks it again when\n * the contact or the lead is credited. Only its presence is decided here,\n * because `null` and absent mean the same thing to the writer and a caller\n * should not have to know which one it sends.\n */\nfunction conversionTouchOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): Pick<UpsertHostContactOptions, 'conversionTouch'> {\n const touch = detail?.[CONVERSION_TOUCH_DETAIL]\n return touch && typeof touch === 'object'\n ? { conversionTouch: touch as PluginConversionTouch }\n : {}\n}\n\n/**\n * The silo's word for its door, as a source the CRM stores.\n *\n * ⚠️ `ContactSource` is a CLOSED union and the contract's `source` is an open\n * string, deliberately: a silo declares its door with\n * `registerPluginContactSource` rather than core enumerating every plugin's.\n * The two meet here, and a word outside the union is passed through rather\n * than rejected — refusing it would lose a third-party plugin's capture over\n * a label, and the capture is the part that matters.\n *\n * ⛔ What it costs, until the union is widened: the console's source filter\n * and `SOURCE_LABELS` key on these values, so an undeclared word renders raw\n * and matches no filter. Every first-party silo uses a word in the union, so\n * nothing does that today.\n */\nexport function contactSourceOf(source: string): ContactSource {\n const word = String(source ?? '').trim()\n if (!Object.hasOwn(CONTACT_SOURCE_LABELS, word)) {\n // Said once, where somebody debugging an unlabelled row will find it.\n // Not a refusal: the capture is worth more than the label.\n console.warn(\n `crm contact capture: source \"${word}\" has no label, so it will render ` +\n 'raw and match no filter in the console.',\n )\n }\n return word as ContactSource\n}\n\n/** What a refused caller is told — customer-safe, and never a key. */\nfunction refusalText(reason: 'invalid-email' | 'band' | 'erased' | 'error'): string {\n switch (reason) {\n case 'invalid-email':\n return 'That address could not be read, so no contact was recorded.'\n case 'band':\n return 'This workspace is at the number of contacts its plan holds.'\n case 'erased':\n return 'This person was erased from this workspace and was not recreated.'\n default:\n return 'The contact could not be recorded. Nothing else was affected.'\n }\n}\n"],"names":["CONTACT_SOURCE_LABELS","formLeadSource","normalizePhone","isCrmLeadOpen","normalizeContactEmail","personKey","captureHostContact","contactCaptureActor","emitHostEvent","addHostLeadOutcome","findContactByEmail","firebaseAdmin","orgDataCollectionForHost","readLeadForHost","CONVERSION_TOUCH_DETAIL","convertOpenLeadOntoContact","crmRecordOriginWriter","captureContactForCrm","request","surface","identity","email","refusedEmail","heldAsContact","stampedAfter","fileLead","openLeadFor","verdict","captureOnContact","ok","stampOrigin","record","hostId","contactId","by","interaction","source","purchaseCents","error","console","reason","stamp","origin","firstTouchOnly","filed","refusalText","contacts","snapshot","key","lead","data","convertedContactId","hostRef","app","firestore","collection","doc","created","formId","entryPointOf","detail","outcome","name","marketingConsent","disclosureOf","campaignsFiledUnder","phoneFillOf","conversionTouchOf","conversionTouch","touch","stored","cause","actor","leadCreatedPayload","leadId","campaignIds","sourceAdded","String","trim","length","join","contactSourceOf","atMs","undefined","refId","summary","tags","purchaseCurrency","lifecycleFloor","initialLifecycleStage","profile","facet","phoneFill","facetFill","phone","refused","disclosedConsentGroup","raw","profileFill","text","value","path","ids","containers","word","Object","hasOwn","warn"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/capture-contact.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n pluginContactSource,\n type PluginContactCaptureRequest,\n type PluginContactCaptured,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport {\n CONTACT_SOURCE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { formLeadSource } from '@aglyn/aglyn/app-utils/forms'\nimport { normalizePhone } from '@aglyn/aglyn/foundation/definitions/contact.types'\nimport {\n type CrmLeadFields,\n isCrmLeadOpen,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { captureHostContact } from './capture-host-contact'\nimport { contactCaptureActor } from '@aglyn/tenant-runtime/capture-actor'\nimport { emitHostEvent } from '@aglyn/tenant-runtime/emit-host-event'\nimport {\n addHostLeadOutcome,\n findContactByEmail,\n firebaseAdmin,\n orgDataCollectionForHost,\n readLeadForHost,\n type UpsertHostContactOptions,\n} from '@aglyn/tenant-data-admin'\nimport {\n CONVERSION_TOUCH_DETAIL,\n type PluginConversionTouch,\n} from '@aglyn/aglyn/plugin-manager/plugin-conversion-credit'\nimport { convertOpenLeadOntoContact } from './convert-open-lead'\nimport { crmRecordOriginWriter } from './record-origin'\n\n/**\n * The CRM answering the platform's contact-capture contract (AGL-3080).\n *\n * Four silos meet the same person — a form submission, a member signing up,\n * an order, a booking — and none of them is the record system. Each holds an\n * address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each imports `captureHostContact`\n * directly, which is the highest fan-in edge in the repo and makes the CRM\n * something a storefront cannot take a payment without.\n *\n * So this is the CRM's side of the seam: the silo reports what it saw, and\n * the plugin that keeps people decides everything a record system decides —\n * keying the address, whether this is somebody new, the audience band, the\n * erasure rows, the company, the owner, and what a new person sets off.\n * `captureHostContact` already does all of that; this translates the\n * contract's vocabulary into its options and its verdict back.\n *\n * ⚠️ IT NEVER THROWS. Every caller has already done the thing it is\n * recording — the submission was accepted, the order was paid — so a refusal\n * is RETURNED and costs the silo nothing. A throw here would lose an order\n * for a contact that could not be filed.\n */\n/**\n * WHICH RECORD A CAPTURE LANDS ON (AGL-3232) — the Salesforce rule, decided\n * here because the CRM is the plugin that models both records.\n *\n * One person is one record: a LEAD until somebody qualifies them, a CONTACT\n * after. Every door used to write both — a lead-routed form filed a lead\n * AND a contact at stage Lead — so one person sat in two lists and the Leads\n * queue was never the whole story. Now a door says what kind of surface it\n * is (`request.surface`) and this decides:\n *\n * - a LEAD surface files a lead, unless the workspace already holds the\n * address as a contact — a customer who books a demo is a customer's\n * interaction, not a new lead — in which case the capture lands on the\n * contact and no lead is filed;\n * - a RELATIONSHIP (a member account, a purchase) makes the contact, and\n * an open lead the site held for the address is stamped converted onto\n * it, so nobody keeps working a lead who already joined or bought;\n * - a TOUCH (an unrouted form, a newsletter opt-in) lands on the open lead\n * when the site holds one — its consent and its history stay on the one\n * record the rep is working — and on the contact otherwise.\n *\n * A lead is filed through `addHostLead`, the one writer of the leads silo,\n * so it is keyed on the person, counted against the ceiling and carries the\n * campaign touch the door resolved. A NEW lead announces itself with the\n * `lead` host event, which is what a \"new lead\" automation listens for; a\n * repeat capture on a lead the site already held announces nothing, the\n * way a repeat visit by a contact is an interaction and not a creation.\n */\nexport async function captureContactForCrm(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n const surface = request.surface ?? 'touch'\n // The one refusal every surface shares, answered before any read: an\n // address nothing can key is a person nothing can record.\n if (!normalizeContactEmail(request.identity.email)) return refusedEmail()\n try {\n if (surface === 'lead') {\n if (!(await heldAsContact(request))) return await stampedAfter(request, fileLead(request))\n } else if (surface === 'touch') {\n if (await openLeadFor(request)) return await stampedAfter(request, fileLead(request))\n }\n const verdict = await captureOnContact(request)\n if (verdict.ok) await stampOrigin(request)\n if (verdict.ok && verdict.record === 'contact' && surface === 'relationship') {\n await convertOpenLeadOntoContact({\n hostId: request.hostId,\n email: request.identity.email,\n contactId: verdict.contactId,\n // Money changing hands is a purchase whichever door took it — a paid\n // booking or a stay is one as much as an order is.\n by:\n request.interaction.source === 'order' || (request.purchaseCents ?? 0) > 0\n ? 'purchase'\n : 'signup',\n })\n }\n return verdict\n } catch (error) {\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/**\n * WHERE THE PERSON CAME FROM (AGL-3519): the door's word — a form, a\n * booking, a newsletter, a member, an order — as the built-in Lead source\n * it names, on a record the capture just started and that names none. A\n * person met before keeps whatever lead source they hold, or the lack of one.\n * Never fails the capture: the writer swallows its own errors.\n */\nasync function stampOrigin(request: PluginContactCaptureRequest): Promise<void> {\n await crmRecordOriginWriter.stamp({\n hostId: request.hostId,\n email: normalizeContactEmail(request.identity.email) ?? '',\n origin: request.interaction.source,\n firstTouchOnly: true,\n })\n}\n\n/** A filed lead's verdict, after its origin is stamped when the filing took. */\nasync function stampedAfter(\n request: PluginContactCaptureRequest,\n filed: Promise<PluginContactCaptured>,\n): Promise<PluginContactCaptured> {\n const verdict = await filed\n if (verdict.ok) await stampOrigin(request)\n return verdict\n}\n\n/** The refusal every door gets for an address nothing can key. */\nfunction refusedEmail(): PluginContactCaptured {\n return { ok: false, reason: 'invalid-email', error: refusalText('invalid-email') }\n}\n\n/**\n * Whether the workspace already holds the address as a contact — the\n * address index first, then the query, the same lookup every dedupe uses.\n * ORG-wide, not scoped to the capturing site: the contact door dedupes\n * across every site in the org, so a person any site holds as a contact\n * is a contact, whichever site met them this time.\n */\nasync function heldAsContact(request: PluginContactCaptureRequest): Promise<boolean> {\n const contacts = await orgDataCollectionForHost(request.hostId, 'contacts')\n return (await findContactByEmail(contacts, request.identity.email)) !== null\n}\n\n/** Whether the site holds an OPEN lead for the address — neither converted nor closed. */\nasync function openLeadFor(request: PluginContactCaptureRequest): Promise<boolean> {\n const key = personKey(request.identity.email) as string\n /*\n * Through the seam, which reads the org row and falls back to a lead the\n * backfill has not reached (AGL-3275). UNSCOPED on purpose: a sibling brand\n * in the same consent group has to find this person, or its door concludes\n * there is no lead and files the duplicate this migration exists to end.\n */\n const snapshot = await readLeadForHost(request.hostId, key)\n if (!snapshot) return false\n const lead = (snapshot.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n return !lead.convertedContactId && isCrmLeadOpen(lead)\n}\n\n/**\n * The capture as a LEAD. The source string is the lead silo's own\n * vocabulary — `form:{formId}` for a form, the door's word otherwise — so\n * a lead's provenance survives the form being renamed, as the submission's\n * does.\n */\nasync function fileLead(request: PluginContactCaptureRequest): Promise<PluginContactCaptured> {\n const email = normalizeContactEmail(request.identity.email) as string\n const key = personKey(email) as string\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(request.hostId)\n // `addHostLead` resolves and writes the org collection itself (AGL-3275);\n // this only needs to know whether the person is new, which decides the event.\n const created = !(await readLeadForHost(request.hostId, key))\n const { formId } = entryPointOf(request.detail)\n const source =\n request.interaction.source === 'form' && formId\n ? formLeadSource(formId)\n : request.interaction.source\n const outcome = await addHostLeadOutcome({\n hostRef,\n hostId: request.hostId,\n lead: {\n email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source,\n ...(request.marketingConsent ? { marketingConsent: true } : {}),\n ...disclosureOf(request),\n // Filed under the surface's campaigns, as the contact door files a\n // contact (AGL-3458): a lead-routed form in a campaign makes a lead\n // in that campaign.\n ...campaignsFiledUnder(request),\n ...phoneFillOf(request),\n },\n ...(conversionTouchOf(request.detail).conversionTouch\n ? { touch: conversionTouchOf(request.detail).conversionTouch }\n : {}),\n })\n if (!outcome.stored) {\n return {\n ok: false,\n reason: 'band',\n error: 'This site is at the number of leads it may hold, so the lead was not recorded.',\n }\n }\n if (created) {\n const cause = contactCaptureActor(\n request.interaction.source,\n email,\n request.actor,\n )\n await emitHostEvent(\n request.hostId,\n 'lead',\n leadCreatedPayload({\n leadId: key,\n email,\n name: request.identity.name,\n source,\n hostId: request.hostId,\n formId,\n campaignIds: campaignsFiledUnder(request).campaignIds,\n }),\n cause ? { actor: cause } : {},\n )\n }\n return { ok: true, record: 'lead', leadId: key, created, sourceAdded: outcome.sourceAdded }\n}\n\n/**\n * The `lead` payload (AGL-3458), as the scalars an event may carry — the\n * shape `contactCreatedPayload` gives a new contact, so one condition reads\n * the same on either event: `name` always present, empty when the door had\n * none; `formId` only when a form filed the lead, so `notEmpty` on it reads\n * as \"came in through a form\" and `equals` picks one form's people out; and\n * `campaignIds` comma-joined, only when the surface is filed under any.\n */\nexport function leadCreatedPayload(lead: {\n leadId: string\n email: string\n name?: string | null\n source: string\n hostId: string\n formId?: string | null\n campaignIds?: readonly string[]\n}): Record<string, string> {\n const formId = String(lead.formId ?? '').trim()\n return {\n leadId: lead.leadId,\n email: lead.email,\n name: String(lead.name ?? '').trim(),\n source: lead.source,\n hostId: lead.hostId,\n ...(formId ? { formId } : {}),\n ...(lead.campaignIds?.length ? { campaignIds: lead.campaignIds.join(',') } : {}),\n }\n}\n\n/** The capture as a CONTACT — what every capture was before the rule above. */\nasync function captureOnContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n try {\n const verdict = await captureHostContact({\n ...(request.actor ? { actor: request.actor } : {}),\n hostId: request.hostId,\n email: request.identity.email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source: contactSourceOf(request.interaction.source),\n interaction: {\n ...(request.interaction.atMs === undefined\n ? {}\n : { atMs: request.interaction.atMs }),\n ...(request.interaction.refId ? { refId: request.interaction.refId } : {}),\n ...(request.interaction.summary\n ? { summary: request.interaction.summary }\n : {}),\n ...entryPointOf(request.detail),\n },\n ...conversionTouchOf(request.detail),\n ...(request.marketingConsent === undefined\n ? {}\n : { marketingConsent: request.marketingConsent }),\n ...disclosureOf(request),\n ...(request.tags?.length ? { tags: request.tags } : {}),\n ...campaignsFiledUnder(request),\n ...(request.purchaseCents === undefined\n ? {}\n : { purchaseCents: request.purchaseCents }),\n ...(request.purchaseCurrency\n ? { purchaseCurrency: request.purchaseCurrency }\n : {}),\n ...(request.lifecycleFloor\n ? { initialLifecycleStage: request.lifecycleFloor as never }\n : {}),\n ...(request.profile ? { facet: request.profile as never } : {}),\n ...(phoneFillOf(request).phoneFill\n ? { facetFill: { phone: phoneFillOf(request).phoneFill } }\n : {}),\n })\n if ('refused' in verdict) {\n return { ok: false, reason: verdict.refused, error: refusalText(verdict.refused) }\n }\n return {\n ok: true,\n record: 'contact',\n contactId: verdict.contactId,\n created: verdict.created,\n }\n } catch (error) {\n /*\n * The contract's `error` is the last state, not a channel for a stack:\n * a silo may show or log it as it stands, so it says what happened and\n * names nothing internal. The detail goes to the log, where whoever is\n * debugging a missing contact will look.\n */\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/**\n * THE DISCLOSURE the capture surface rendered, handed to whichever record the\n * capture lands on (AGL-3320).\n *\n * Passed through unread: both writers resolve the site's group themselves and\n * pool the opt-in only when this is its current key, so a lead and a contact\n * written by one capture record the same sites. A string or nothing, since\n * the writers compare it exactly and anything else could only fail to match.\n */\nfunction disclosureOf(\n request: PluginContactCaptureRequest,\n): { disclosedConsentGroup?: string } {\n const key = request.disclosedConsentGroup\n return typeof key === 'string' && key ? { disclosedConsentGroup: key } : {}\n}\n\n/**\n * THE PHONE A CAPTURE LEARNED IN PASSING (AGL-3493), off `profileFill`.\n *\n * Normalized here so a lead and a contact written by one capture keep the\n * same E.164 string, and dropped when it will not normalize: a fill is an\n * enrichment, and a number in some other shape on the record is the\n * \"stored three ways\" failure `normalizePhone` exists to end. Both writers\n * put it only where the record holds no phone.\n */\nfunction phoneFillOf(request: PluginContactCaptureRequest): { phoneFill?: string } {\n const raw = request.profileFill?.['phone']\n const phone = typeof raw === 'string' ? normalizePhone(raw) : null\n return phone ? { phoneFill: phone } : {}\n}\n\n/**\n * THE ENTRY POINT, off the silo's own `detail` bag (AGL-3080).\n *\n * The contract carries a capture's silo-side facts opaquely, so the two the\n * CRM models are picked out here rather than typed into the platform. Both\n * ride the INTERACTION, which is where `upsertHostContact` already keeps\n * them: which form a person came in through routes the owner-assignment\n * rules and rides the `contactCreated` payload, and the page is what a\n * timeline row says about where they were.\n *\n * Absent, misspelled or the wrong type is the same answer as a door that\n * never had one — a capture without an entry point, which is every capture\n * that did not come through a form. It is never a reason to refuse: the\n * person is the part that matters.\n */\nfunction entryPointOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): { formId?: string; path?: string } {\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' && value.trim() ? value : undefined\n const formId = text(detail?.['formId'])\n const path = text(detail?.['path'])\n return {\n ...(formId ? { formId } : {}),\n ...(path ? { path } : {}),\n }\n}\n\n/**\n * The campaigns the capture surface is filed under, as the contact writer\n * records them. A capture names what its surface is filed under by container\n * kind; the CRM keeps the `campaign` kind on the person's site facet, and a\n * kind it does not keep is the capture's to name and the CRM's to ignore.\n */\nfunction campaignsFiledUnder(\n request: PluginContactCaptureRequest,\n): Pick<UpsertHostContactOptions, 'campaignIds'> {\n const ids = request.containers?.['campaign'] ?? []\n return ids.length ? { campaignIds: ids } : {}\n}\n\n/**\n * WHERE THE VISITOR CAME FROM, off the same bag.\n *\n * ⚠️ A different fact from `campaignIds` and the two must never be folded\n * together — `upsert-contact.ts` says so at the field itself. A touch is the\n * ad or the link the visitor arrived by, already resolved by the plugin that\n * credits outcomes (`plugin-conversion-credit`) at the silo's door;\n * `campaignIds` is which campaigns the merchant filed the capture SURFACE\n * under, which is true of everybody who fills that form in. Folding them\n * would credit a campaign for a visitor who typed the address.\n *\n * Passed through as the silo handed it, unread: the touch's shape belongs\n * to the creditor that resolved it, and the creditor checks it again when\n * the contact or the lead is credited. Only its presence is decided here,\n * because `null` and absent mean the same thing to the writer and a caller\n * should not have to know which one it sends.\n */\nfunction conversionTouchOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): Pick<UpsertHostContactOptions, 'conversionTouch'> {\n const touch = detail?.[CONVERSION_TOUCH_DETAIL]\n return touch && typeof touch === 'object'\n ? { conversionTouch: touch as PluginConversionTouch }\n : {}\n}\n\n/**\n * The silo's word for its door, as a source the CRM stores.\n *\n * ⚠️ `ContactSource` is a CLOSED union and the contract's `source` is an open\n * string, deliberately: a silo declares its door with\n * `registerPluginContactSource` rather than core enumerating every plugin's.\n * The two meet here, and a word outside the union is passed through rather\n * than rejected — refusing it would lose a third-party plugin's capture over\n * a label, and the capture is the part that matters.\n *\n * A declared word READS by its plugin's label (`labelContactSource`, AGL-4042:\n * the waivers plugin's `waiver`). ⛔ What it still costs, until the union is\n * widened: the console's source filter keys on the union, so a plugin's word\n * matches no filter option, and an UNDECLARED word also renders raw.\n */\nexport function contactSourceOf(source: string): ContactSource {\n const word = String(source ?? '').trim()\n if (!Object.hasOwn(CONTACT_SOURCE_LABELS, word) && !pluginContactSource(word)) {\n // Said once, where somebody debugging an unlabelled row will find it.\n // Not a refusal: the capture is worth more than the label.\n console.warn(\n `crm contact capture: source \"${word}\" has no label, so it will render ` +\n 'raw and match no filter in the console.',\n )\n }\n return word as ContactSource\n}\n\n/** What a refused caller is told — customer-safe, and never a key. */\nfunction refusalText(reason: 'invalid-email' | 'band' | 'erased' | 'error'): string {\n switch (reason) {\n case 'invalid-email':\n return 'That address could not be read, so no contact was recorded.'\n case 'band':\n return 'This workspace is at the number of contacts its plan holds.'\n case 'erased':\n return 'This person was erased from this workspace and was not recreated.'\n default:\n return 'The contact could not be recorded. Nothing else was affected.'\n }\n}\n"],"names":["pluginContactSource","CONTACT_SOURCE_LABELS","formLeadSource","normalizePhone","isCrmLeadOpen","normalizeContactEmail","personKey","captureHostContact","contactCaptureActor","emitHostEvent","addHostLeadOutcome","findContactByEmail","firebaseAdmin","orgDataCollectionForHost","readLeadForHost","CONVERSION_TOUCH_DETAIL","convertOpenLeadOntoContact","crmRecordOriginWriter","captureContactForCrm","request","surface","identity","email","refusedEmail","heldAsContact","stampedAfter","fileLead","openLeadFor","verdict","captureOnContact","ok","stampOrigin","record","hostId","contactId","by","interaction","source","purchaseCents","error","console","reason","stamp","origin","firstTouchOnly","filed","refusalText","contacts","snapshot","key","lead","data","convertedContactId","hostRef","app","firestore","collection","doc","created","formId","entryPointOf","detail","outcome","name","marketingConsent","disclosureOf","campaignsFiledUnder","phoneFillOf","conversionTouchOf","conversionTouch","touch","stored","cause","actor","leadCreatedPayload","leadId","campaignIds","sourceAdded","String","trim","length","join","contactSourceOf","atMs","undefined","refId","summary","tags","purchaseCurrency","lifecycleFloor","initialLifecycleStage","profile","facet","phoneFill","facetFill","phone","refused","disclosedConsentGroup","raw","profileFill","text","value","path","ids","containers","word","Object","hasOwn","warn"],"mappings":"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"}