@aglyn/plugins-crm 1.0.0-beta.221 → 1.0.0-beta.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +13 -13
- package/src/lib/components/activity-list.js +12 -5
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.d.ts +1 -4
- package/src/lib/components/companies-bulk-bar.js +7 -20
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/companies-section.d.ts +3 -3
- package/src/lib/components/companies-section.js +149 -46
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +281 -20
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-properties-card.js +99 -8
- package/src/lib/components/company-properties-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +3 -1
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +33 -10
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +0 -69
- package/src/lib/components/contact-properties-card.js +350 -60
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/contact-reports-to-field.d.ts +46 -0
- package/src/lib/components/contact-reports-to-field.js +67 -0
- package/src/lib/components/contact-reports-to-field.js.map +1 -0
- package/src/lib/components/contacts-bulk-bar.d.ts +1 -8
- package/src/lib/components/contacts-bulk-bar.js +7 -19
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +46 -33
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-call-actions.d.ts +10 -0
- package/src/lib/components/crm-call-actions.js +26 -9
- package/src/lib/components/crm-call-actions.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.js +1 -1
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/crm-record-picker.d.ts +15 -0
- package/src/lib/components/crm-record-picker.js +16 -10
- package/src/lib/components/crm-record-picker.js.map +1 -1
- package/src/lib/components/crm-transfer-buttons.d.ts +54 -0
- package/src/lib/components/crm-transfer-buttons.js +167 -0
- package/src/lib/components/crm-transfer-buttons.js.map +1 -0
- package/src/lib/components/deal-contact-roles-card.d.ts +46 -0
- package/src/lib/components/deal-contact-roles-card.js +368 -0
- package/src/lib/components/deal-contact-roles-card.js.map +1 -0
- package/src/lib/components/deal-detail-page.d.ts +3 -2
- package/src/lib/components/deal-detail-page.js +25 -6
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.d.ts +10 -0
- package/src/lib/components/deal-edit-drawer.js +219 -10
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deal-properties-card.d.ts +7 -0
- package/src/lib/components/deal-properties-card.js +34 -3
- package/src/lib/components/deal-properties-card.js.map +1 -1
- package/src/lib/components/deal-stage-card.d.ts +4 -0
- package/src/lib/components/deal-stage-card.js +10 -2
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.d.ts +0 -3
- package/src/lib/components/deals-bulk-bar.js +7 -20
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +3 -3
- package/src/lib/components/deals-section.js +123 -63
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.js +31 -17
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-convert-dialog.d.ts +4 -0
- package/src/lib/components/lead-convert-dialog.js +23 -1
- package/src/lib/components/lead-convert-dialog.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +8 -2
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-profile-fields.d.ts +71 -0
- package/src/lib/components/lead-profile-fields.js +386 -0
- package/src/lib/components/lead-profile-fields.js.map +1 -0
- package/src/lib/components/lead-properties-card.d.ts +2 -1
- package/src/lib/components/lead-properties-card.js +85 -176
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-source-select.d.ts +4 -23
- package/src/lib/components/lead-source-select.js +9 -73
- package/src/lib/components/lead-source-select.js.map +1 -1
- package/src/lib/components/lead-status-chip.d.ts +9 -3
- package/src/lib/components/lead-status-chip.js +8 -4
- package/src/lib/components/lead-status-chip.js.map +1 -1
- package/src/lib/components/lead-status-options.d.ts +20 -0
- package/src/lib/components/lead-status-options.js +51 -0
- package/src/lib/components/lead-status-options.js.map +1 -0
- package/src/lib/components/lead-unqualify-dialog.d.ts +5 -0
- package/src/lib/components/lead-unqualify-dialog.js +50 -18
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.d.ts +0 -3
- package/src/lib/components/leads-bulk-bar.js +29 -29
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +120 -68
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/linked-deals-card.d.ts +4 -0
- package/src/lib/components/linked-deals-card.js +47 -3
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/log-activity-dialog.js +43 -3
- package/src/lib/components/log-activity-dialog.js.map +1 -1
- package/src/lib/components/new-contact-drawer.d.ts +8 -0
- package/src/lib/components/new-contact-drawer.js +108 -6
- package/src/lib/components/new-contact-drawer.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +9 -2
- package/src/lib/components/new-lead-drawer.js +88 -121
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/components/picklist-select.d.ts +36 -0
- package/src/lib/components/picklist-select.js +114 -0
- package/src/lib/components/picklist-select.js.map +1 -0
- package/src/lib/components/picklist-values-card.d.ts +34 -0
- package/src/lib/components/{lead-source-values-card.js → picklist-values-card.js} +241 -58
- package/src/lib/components/picklist-values-card.js.map +1 -0
- package/src/lib/components/pipeline-stages-dialog.d.ts +7 -0
- package/src/lib/components/pipeline-stages-dialog.js +26 -4
- package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
- package/src/lib/components/pipelines-dialog.js +15 -6
- package/src/lib/components/pipelines-dialog.js.map +1 -1
- package/src/lib/components/recent-activity-feed.js +20 -5
- package/src/lib/components/recent-activity-feed.js.map +1 -1
- package/src/lib/components/recipes-card.js +4 -2
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-tasks-card.js +15 -8
- package/src/lib/components/record-tasks-card.js.map +1 -1
- package/src/lib/components/reports/forecast-card.d.ts +6 -0
- package/src/lib/components/reports/forecast-card.js +101 -3
- package/src/lib/components/reports/forecast-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.d.ts +12 -0
- package/src/lib/components/reports/lead-funnel-card.js +9 -5
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/lead-source-card.d.ts +23 -0
- package/src/lib/components/reports/lead-source-card.js +318 -0
- package/src/lib/components/reports/lead-source-card.js.map +1 -0
- package/src/lib/components/reports/pipeline-card.js +2 -1
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-export.js +1 -1
- package/src/lib/components/reports/report-export.js.map +1 -1
- package/src/lib/components/reports-section.js +8 -0
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/sharing-rule-drawer.d.ts +5 -0
- package/src/lib/components/sharing-rule-drawer.js +108 -19
- package/src/lib/components/sharing-rule-drawer.js.map +1 -1
- package/src/lib/components/sharing-rules-card.js +11 -3
- package/src/lib/components/sharing-rules-card.js.map +1 -1
- package/src/lib/components/task-cells.d.ts +9 -1
- package/src/lib/components/task-cells.js +9 -4
- package/src/lib/components/task-cells.js.map +1 -1
- package/src/lib/components/task-edit-drawer.js +111 -43
- package/src/lib/components/task-edit-drawer.js.map +1 -1
- package/src/lib/components/tasks-bulk-bar.d.ts +0 -3
- package/src/lib/components/tasks-bulk-bar.js +7 -20
- package/src/lib/components/tasks-bulk-bar.js.map +1 -1
- package/src/lib/components/tasks-calendar.d.ts +3 -0
- package/src/lib/components/tasks-calendar.js +8 -1
- package/src/lib/components/tasks-calendar.js.map +1 -1
- package/src/lib/components/tasks-section.d.ts +3 -3
- package/src/lib/components/tasks-section.js +58 -43
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +2 -9
- package/src/lib/constants/api-routes.js +1 -7
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +6 -0
- package/src/lib/constants/bundle-common.js +11 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +14 -0
- package/src/lib/constants/company-filters.js +51 -1
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +2 -0
- package/src/lib/constants/contact-filters.js +18 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +22 -1
- package/src/lib/constants/deal-filters.js +67 -4
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +3 -0
- package/src/lib/declarations.console-server.js +35 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +35 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/declarations.server.d.ts +11 -0
- package/src/lib/declarations.server.js +97 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-company-picklists.d.ts +15 -0
- package/src/lib/hooks/use-company-picklists.js +55 -0
- package/src/lib/hooks/use-company-picklists.js.map +1 -0
- package/src/lib/hooks/use-crm-picklist.d.ts +25 -0
- package/src/lib/hooks/use-crm-picklist.js +53 -0
- package/src/lib/hooks/use-crm-picklist.js.map +1 -0
- package/src/lib/hooks/use-crm-record-names.d.ts +16 -0
- package/src/lib/hooks/use-crm-record-names.js +45 -19
- package/src/lib/hooks/use-crm-record-names.js.map +1 -1
- package/src/lib/hooks/use-crm-task-picklists.d.ts +11 -0
- package/src/lib/hooks/use-crm-task-picklists.js +45 -0
- package/src/lib/hooks/use-crm-task-picklists.js.map +1 -0
- package/src/lib/hooks/use-deals.d.ts +14 -0
- package/src/lib/hooks/use-deals.js +29 -0
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/hooks/use-lead-picklists.d.ts +14 -0
- package/src/lib/hooks/use-lead-picklists.js +46 -0
- package/src/lib/hooks/use-lead-picklists.js.map +1 -0
- package/src/lib/hooks/use-lead-source-picklist.d.ts +5 -19
- package/src/lib/hooks/use-lead-source-picklist.js +12 -22
- package/src/lib/hooks/use-lead-source-picklist.js.map +1 -1
- package/src/lib/hooks/use-lead-status-picklist.d.ts +9 -0
- package/src/lib/hooks/use-lead-status-picklist.js +40 -0
- package/src/lib/hooks/use-lead-status-picklist.js.map +1 -0
- package/src/lib/model/companies.d.ts +32 -3
- package/src/lib/model/companies.js +133 -16
- package/src/lib/model/companies.js.map +1 -1
- package/src/lib/model/contact-merge.d.ts +27 -3
- package/src/lib/model/contact-merge.js +81 -7
- package/src/lib/model/contact-merge.js.map +1 -1
- package/src/lib/model/contact-record.d.ts +17 -0
- package/src/lib/model/contact-record.js +17 -2
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-update.d.ts +31 -2
- package/src/lib/model/contact-update.js +14 -0
- package/src/lib/model/contact-update.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +5 -0
- package/src/lib/model/crm-list-query.js +8 -0
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/crm-next-activity.d.ts +107 -0
- package/src/lib/model/crm-next-activity.js +140 -0
- package/src/lib/model/crm-next-activity.js.map +1 -0
- package/src/lib/model/crm-recipes.d.ts +40 -0
- package/src/lib/model/crm-recipes.js +259 -0
- package/src/lib/model/crm-recipes.js.map +1 -0
- package/src/lib/model/crm-record-counts.d.ts +36 -0
- package/src/lib/model/crm-record-counts.js +54 -0
- package/src/lib/model/crm-record-counts.js.map +1 -0
- package/src/lib/model/crm-record-lists.d.ts +23 -0
- package/src/lib/model/crm-record-lists.js +161 -0
- package/src/lib/model/crm-record-lists.js.map +1 -0
- package/src/lib/model/crm-reports.d.ts +73 -3
- package/src/lib/model/crm-reports.js +120 -1
- package/src/lib/model/crm-reports.js.map +1 -1
- package/src/lib/model/crm-sharing.d.ts +21 -3
- package/src/lib/model/crm-sharing.js +27 -6
- package/src/lib/model/crm-sharing.js.map +1 -1
- package/src/lib/model/crm-task-reminders.d.ts +152 -0
- package/src/lib/model/crm-task-reminders.js +161 -0
- package/src/lib/model/crm-task-reminders.js.map +1 -0
- package/src/lib/model/deal-board-model.d.ts +8 -2
- package/src/lib/model/deal-board-model.js +15 -2
- package/src/lib/model/deal-board-model.js.map +1 -1
- package/src/lib/model/deal-contact-roles.d.ts +45 -0
- package/src/lib/model/deal-contact-roles.js +73 -0
- package/src/lib/model/deal-contact-roles.js.map +1 -0
- package/src/lib/model/deal-form-model.d.ts +56 -0
- package/src/lib/model/deal-form-model.js +79 -7
- package/src/lib/model/deal-form-model.js.map +1 -1
- package/src/lib/model/download-text-file.d.ts +23 -0
- package/src/lib/model/download-text-file.js +34 -0
- package/src/lib/model/download-text-file.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +36 -2
- package/src/lib/model/lead-filters.js +123 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-source-direction.d.ts +38 -0
- package/src/lib/model/lead-source-direction.js +52 -0
- package/src/lib/model/lead-source-direction.js.map +1 -0
- package/src/lib/model/next-activity.d.ts +1 -1
- package/src/lib/model/next-activity.js.map +1 -1
- package/src/lib/model/picklist-values.d.ts +119 -0
- package/src/lib/model/picklist-values.js +256 -0
- package/src/lib/model/picklist-values.js.map +1 -0
- package/src/lib/model/record-facts-disclosed.d.ts +81 -0
- package/src/lib/model/record-facts-disclosed.js +281 -0
- package/src/lib/model/record-facts-disclosed.js.map +1 -0
- package/src/lib/model/record-facts.d.ts +215 -34
- package/src/lib/model/record-facts.js +307 -128
- package/src/lib/model/record-facts.js.map +1 -1
- package/src/lib/model/task-picklists.d.ts +52 -0
- package/src/lib/model/task-picklists.js +83 -0
- package/src/lib/model/task-picklists.js.map +1 -0
- package/src/lib/model/task-routes.d.ts +13 -0
- package/src/lib/model/task-routes.js +23 -4
- package/src/lib/model/task-routes.js.map +1 -1
- package/src/lib/plan-entitlements.js +6 -4
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +17 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts.js +220 -14
- package/src/lib/server/api-v1/contacts.js.map +1 -1
- package/src/lib/server/api-v1/crm-activities.js +14 -1
- package/src/lib/server/api-v1/crm-activities.js.map +1 -1
- package/src/lib/server/api-v1/crm-companies.js +123 -14
- package/src/lib/server/api-v1/crm-companies.js.map +1 -1
- package/src/lib/server/api-v1/crm-deals.js +291 -26
- package/src/lib/server/api-v1/crm-deals.js.map +1 -1
- package/src/lib/server/api-v1/crm-leads.js +144 -38
- package/src/lib/server/api-v1/crm-leads.js.map +1 -1
- package/src/lib/server/api-v1/crm-pipelines.d.ts +6 -0
- package/src/lib/server/api-v1/crm-pipelines.js +8 -2
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
- package/src/lib/server/api-v1/crm-tasks.js +78 -34
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
- package/src/lib/server/api-v1/openapi.d.ts +0 -11
- package/src/lib/server/api-v1/openapi.js +171 -16
- package/src/lib/server/api-v1/openapi.js.map +1 -1
- package/src/lib/server/assign-contact-owner.d.ts +188 -0
- package/src/lib/server/assign-contact-owner.js +446 -0
- package/src/lib/server/assign-contact-owner.js.map +1 -0
- package/src/lib/server/associate-company-by-domain.d.ts +70 -0
- package/src/lib/server/associate-company-by-domain.js +153 -0
- package/src/lib/server/associate-company-by-domain.js.map +1 -0
- package/src/lib/server/automation-steps.d.ts +140 -0
- package/src/lib/server/automation-steps.js +669 -0
- package/src/lib/server/automation-steps.js.map +1 -0
- package/src/lib/server/capture-contact.d.ts +17 -0
- package/src/lib/server/capture-contact.js +95 -26
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/capture-host-contact.d.ts +104 -0
- package/src/lib/server/capture-host-contact.js +151 -0
- package/src/lib/server/capture-host-contact.js.map +1 -0
- package/src/lib/server/company-children.d.ts +25 -0
- package/src/lib/server/company-children.js +42 -0
- package/src/lib/server/company-children.js.map +1 -0
- package/src/lib/server/company-delete.d.ts +18 -0
- package/src/lib/server/company-delete.js +44 -21
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/contact-email-history.js +21 -19
- package/src/lib/server/contact-email-history.js.map +1 -1
- package/src/lib/server/contact-remove.js +27 -2
- package/src/lib/server/contact-remove.js.map +1 -1
- package/src/lib/server/contact-reports-to.d.ts +61 -0
- package/src/lib/server/contact-reports-to.js +114 -0
- package/src/lib/server/contact-reports-to.js.map +1 -0
- package/src/lib/server/contact-update.d.ts +42 -1
- package/src/lib/server/contact-update.js +204 -23
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/convert-host-lead.d.ts +46 -1
- package/src/lib/server/convert-host-lead.js +173 -15
- package/src/lib/server/convert-host-lead.js.map +1 -1
- package/src/lib/server/convert-open-lead.js +12 -5
- package/src/lib/server/convert-open-lead.js.map +1 -1
- package/src/lib/server/crm-email-activity.d.ts +118 -0
- package/src/lib/server/crm-email-activity.js +148 -0
- package/src/lib/server/crm-email-activity.js.map +1 -0
- package/src/lib/server/crm-inbound-email.js +1 -1
- package/src/lib/server/crm-inbound-email.js.map +1 -1
- package/src/lib/server/crm-next-activity.d.ts +74 -0
- package/src/lib/server/crm-next-activity.js +194 -0
- package/src/lib/server/crm-next-activity.js.map +1 -0
- package/src/lib/server/crm-record-indexes.d.ts +45 -0
- package/src/lib/server/crm-record-indexes.js +78 -0
- package/src/lib/server/crm-record-indexes.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +9 -1
- package/src/lib/server/crm-sharing.js +23 -3
- package/src/lib/server/crm-sharing.js.map +1 -1
- package/src/lib/server/deal-contact-roles.d.ts +27 -0
- package/src/lib/server/deal-contact-roles.js +89 -0
- package/src/lib/server/deal-contact-roles.js.map +1 -0
- package/src/lib/server/email-send.js +2 -1
- package/src/lib/server/email-send.js.map +1 -1
- package/src/lib/server/lead-convert.d.ts +5 -0
- package/src/lib/server/lead-convert.js +19 -3
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +17 -2
- package/src/lib/server/lead-create.js +46 -14
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-nurturing.d.ts +6 -0
- package/src/lib/server/lead-nurturing.js +57 -20
- package/src/lib/server/lead-nurturing.js.map +1 -1
- package/src/lib/server/lead-picklists.d.ts +35 -0
- package/src/lib/server/lead-picklists.js +52 -0
- package/src/lib/server/lead-picklists.js.map +1 -0
- package/src/lib/server/lead-source-picklist.d.ts +1 -13
- package/src/lib/server/lead-source-picklist.js +6 -33
- package/src/lib/server/lead-source-picklist.js.map +1 -1
- package/src/lib/server/merge-contacts.js +23 -5
- package/src/lib/server/merge-contacts.js.map +1 -1
- package/src/lib/server/next-activity-routes.js +2 -1
- package/src/lib/server/next-activity-routes.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +53 -0
- package/src/lib/server/person-eraser.js +149 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server/person-records.d.ts +69 -0
- package/src/lib/server/person-records.js +317 -0
- package/src/lib/server/person-records.js.map +1 -0
- package/src/lib/server/person-refund.d.ts +97 -0
- package/src/lib/server/person-refund.js +221 -0
- package/src/lib/server/person-refund.js.map +1 -0
- package/src/lib/server/picklist-values.d.ts +21 -0
- package/src/lib/server/picklist-values.js +308 -0
- package/src/lib/server/picklist-values.js.map +1 -0
- package/src/lib/server/read-picklist.d.ts +44 -0
- package/src/lib/server/read-picklist.js +65 -0
- package/src/lib/server/read-picklist.js.map +1 -0
- package/src/lib/server/recipe-routes.js +22 -8
- package/src/lib/server/recipe-routes.js.map +1 -1
- package/src/lib/server/record-email-state.js +7 -1
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-facts.d.ts +3 -0
- package/src/lib/server/record-facts.js +233 -31
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/record-origin.d.ts +27 -0
- package/src/lib/server/record-origin.js +104 -0
- package/src/lib/server/record-origin.js.map +1 -0
- package/src/lib/server/record-timeline.js +40 -3
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/suite-gate.js +4 -3
- package/src/lib/server/suite-gate.js.map +1 -1
- package/src/lib/server/task-reminders-route.js +2 -1
- package/src/lib/server/task-reminders-route.js.map +1 -1
- package/src/lib/server/task-routes.js +33 -17
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server-deal-stage.d.ts +5 -1
- package/src/lib/server-deal-stage.js +10 -5
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +8 -3
- package/src/lib/server.js +47 -43
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +55 -0
- package/src/lib/testing/memory-firestore.js +335 -0
- package/src/lib/testing/memory-firestore.js.map +1 -0
- package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
- package/src/lib/testing/stand-in-email-send-index.js +49 -0
- package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
- package/src/lib/transfer/activities.d.ts +30 -0
- package/src/lib/transfer/activities.js +132 -0
- package/src/lib/transfer/activities.js.map +1 -0
- package/src/lib/transfer/aliases.d.ts +22 -0
- package/src/lib/transfer/aliases.js +1342 -0
- package/src/lib/transfer/aliases.js.map +1 -0
- package/src/lib/transfer/common.d.ts +222 -0
- package/src/lib/transfer/common.js +832 -0
- package/src/lib/transfer/common.js.map +1 -0
- package/src/lib/transfer/companies.d.ts +43 -0
- package/src/lib/transfer/companies.js +549 -0
- package/src/lib/transfer/companies.js.map +1 -0
- package/src/lib/transfer/contacts.d.ts +31 -0
- package/src/lib/transfer/contacts.js +727 -0
- package/src/lib/transfer/contacts.js.map +1 -0
- package/src/lib/transfer/deals.d.ts +66 -0
- package/src/lib/transfer/deals.js +698 -0
- package/src/lib/transfer/deals.js.map +1 -0
- package/src/lib/transfer/email-templates-package.d.ts +40 -0
- package/src/lib/transfer/email-templates-package.js +76 -0
- package/src/lib/transfer/email-templates-package.js.map +1 -0
- package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
- package/src/lib/transfer/email-templates-package.server.js +176 -0
- package/src/lib/transfer/email-templates-package.server.js.map +1 -0
- package/src/lib/transfer/fields.d.ts +102 -0
- package/src/lib/transfer/fields.js +1755 -0
- package/src/lib/transfer/fields.js.map +1 -0
- package/src/lib/transfer/leads.d.ts +30 -0
- package/src/lib/transfer/leads.js +582 -0
- package/src/lib/transfer/leads.js.map +1 -0
- package/src/lib/transfer/pipelines.d.ts +21 -0
- package/src/lib/transfer/pipelines.js +172 -0
- package/src/lib/transfer/pipelines.js.map +1 -0
- package/src/lib/transfer/register.d.ts +47 -0
- package/src/lib/transfer/register.js +277 -0
- package/src/lib/transfer/register.js.map +1 -0
- package/src/lib/transfer/tasks.d.ts +30 -0
- package/src/lib/transfer/tasks.js +418 -0
- package/src/lib/transfer/tasks.js.map +1 -0
- package/src/lib/transfer/ui.d.ts +20 -0
- package/src/lib/transfer/ui.js +42 -0
- package/src/lib/transfer/ui.js.map +1 -0
- package/src/lib/components/company-import-drawer.d.ts +0 -42
- package/src/lib/components/company-import-drawer.js +0 -99
- package/src/lib/components/company-import-drawer.js.map +0 -1
- package/src/lib/components/contact-import-drawer.d.ts +0 -46
- package/src/lib/components/contact-import-drawer.js +0 -149
- package/src/lib/components/contact-import-drawer.js.map +0 -1
- package/src/lib/components/crm-export-all-button.d.ts +0 -39
- package/src/lib/components/crm-export-all-button.js +0 -98
- package/src/lib/components/crm-export-all-button.js.map +0 -1
- package/src/lib/components/csv-import-drawer.d.ts +0 -139
- package/src/lib/components/csv-import-drawer.js +0 -757
- package/src/lib/components/csv-import-drawer.js.map +0 -1
- package/src/lib/components/deal-import-drawer.d.ts +0 -24
- package/src/lib/components/deal-import-drawer.js +0 -83
- package/src/lib/components/deal-import-drawer.js.map +0 -1
- package/src/lib/components/lead-import-drawer.d.ts +0 -27
- package/src/lib/components/lead-import-drawer.js +0 -118
- package/src/lib/components/lead-import-drawer.js.map +0 -1
- package/src/lib/components/lead-source-values-card.d.ts +0 -27
- package/src/lib/components/lead-source-values-card.js.map +0 -1
- package/src/lib/components/task-import-drawer.d.ts +0 -24
- package/src/lib/components/task-import-drawer.js +0 -82
- package/src/lib/components/task-import-drawer.js.map +0 -1
- package/src/lib/model/companies-csv.d.ts +0 -36
- package/src/lib/model/companies-csv.js +0 -38
- package/src/lib/model/companies-csv.js.map +0 -1
- package/src/lib/model/contacts-csv.d.ts +0 -54
- package/src/lib/model/contacts-csv.js +0 -67
- package/src/lib/model/contacts-csv.js.map +0 -1
- package/src/lib/model/crm-company-import.d.ts +0 -154
- package/src/lib/model/crm-company-import.js +0 -331
- package/src/lib/model/crm-company-import.js.map +0 -1
- package/src/lib/model/crm-csv.d.ts +0 -239
- package/src/lib/model/crm-csv.js +0 -467
- package/src/lib/model/crm-csv.js.map +0 -1
- package/src/lib/model/crm-deal-import.d.ts +0 -112
- package/src/lib/model/crm-deal-import.js +0 -263
- package/src/lib/model/crm-deal-import.js.map +0 -1
- package/src/lib/model/crm-import.d.ts +0 -250
- package/src/lib/model/crm-import.js +0 -467
- package/src/lib/model/crm-import.js.map +0 -1
- package/src/lib/model/crm-lead-import.d.ts +0 -138
- package/src/lib/model/crm-lead-import.js +0 -446
- package/src/lib/model/crm-lead-import.js.map +0 -1
- package/src/lib/model/crm-task-import.d.ts +0 -94
- package/src/lib/model/crm-task-import.js +0 -275
- package/src/lib/model/crm-task-import.js.map +0 -1
- package/src/lib/model/deals-csv.d.ts +0 -32
- package/src/lib/model/deals-csv.js +0 -36
- package/src/lib/model/deals-csv.js.map +0 -1
- package/src/lib/model/import-column-shapes.d.ts +0 -36
- package/src/lib/model/import-column-shapes.js +0 -76
- package/src/lib/model/import-column-shapes.js.map +0 -1
- package/src/lib/model/lead-source-values.d.ts +0 -82
- package/src/lib/model/lead-source-values.js +0 -149
- package/src/lib/model/lead-source-values.js.map +0 -1
- package/src/lib/model/leads-csv.d.ts +0 -40
- package/src/lib/model/leads-csv.js +0 -40
- package/src/lib/model/leads-csv.js.map +0 -1
- package/src/lib/model/tasks-csv.d.ts +0 -28
- package/src/lib/model/tasks-csv.js +0 -28
- package/src/lib/model/tasks-csv.js.map +0 -1
- package/src/lib/server/companies-import.d.ts +0 -57
- package/src/lib/server/companies-import.js +0 -242
- package/src/lib/server/companies-import.js.map +0 -1
- package/src/lib/server/contacts-import.d.ts +0 -109
- package/src/lib/server/contacts-import.js +0 -393
- package/src/lib/server/contacts-import.js.map +0 -1
- package/src/lib/server/deals-import.d.ts +0 -76
- package/src/lib/server/deals-import.js +0 -248
- package/src/lib/server/deals-import.js.map +0 -1
- package/src/lib/server/export-route.d.ts +0 -60
- package/src/lib/server/export-route.js +0 -447
- package/src/lib/server/export-route.js.map +0 -1
- package/src/lib/server/import-context.d.ts +0 -87
- package/src/lib/server/import-context.js +0 -182
- package/src/lib/server/import-context.js.map +0 -1
- package/src/lib/server/lead-source-values.d.ts +0 -50
- package/src/lib/server/lead-source-values.js +0 -191
- package/src/lib/server/lead-source-values.js.map +0 -1
- package/src/lib/server/leads-import.d.ts +0 -84
- package/src/lib/server/leads-import.js +0 -367
- package/src/lib/server/leads-import.js.map +0 -1
- package/src/lib/server/tasks-import.d.ts +0 -54
- package/src/lib/server/tasks-import.js +0 -160
- package/src/lib/server/tasks-import.js.map +0 -1
|
@@ -73,8 +73,27 @@
|
|
|
73
73
|
* nobody chose an owner for — and no rule assigned — stays unowned, which
|
|
74
74
|
* the console renders honestly as unassigned. Either way the caller has
|
|
75
75
|
* already been authorized by its door; nothing here asks again.
|
|
76
|
+
*
|
|
77
|
+
* ## The deal carries the lead's lead source (AGL-3516)
|
|
78
|
+
*
|
|
79
|
+
* As Salesforce's conversion does, the deal opened here takes the lead's
|
|
80
|
+
* `leadSource` — a label of the same picklist, already judged when the lead
|
|
81
|
+
* was written — and its Type as the door judged it, or the org's default
|
|
82
|
+
* Type when the converter named none. Its forecast category is its stage's.
|
|
83
|
+
*
|
|
84
|
+
* ## Every standard field lands where Salesforce's conversion puts it (AGL-3513)
|
|
85
|
+
*
|
|
86
|
+
* The contact's facet takes the salutation, the name's parts, the mobile,
|
|
87
|
+
* the fax and Do not call. The company takes the account fields — industry,
|
|
88
|
+
* rating, revenue and its currency, head count, website, phone, fax, the
|
|
89
|
+
* address as its billing address, and the lead source as its Account
|
|
90
|
+
* Source — whole when the conversion creates it, and only into its BLANK
|
|
91
|
+
* fields when it links one the org already holds: a lead is one person's
|
|
92
|
+
* account of the business, and the account record may know better. The
|
|
93
|
+
* deal's Primary Campaign Source is the lead's most recent campaign — see
|
|
94
|
+
* {@link leadLatestCampaignId}.
|
|
76
95
|
*/
|
|
77
|
-
import { type CrmDealStage, type CrmPipeline } from '@aglyn/aglyn/server';
|
|
96
|
+
import { type CrmDealStage, type CrmPipeline, type CrmPicklist, type CrmPicklistId } from '@aglyn/aglyn/server';
|
|
78
97
|
/** Who is converting, as far as the writes need to know. */
|
|
79
98
|
export interface LeadConvertActor {
|
|
80
99
|
/**
|
|
@@ -128,6 +147,11 @@ export interface ConvertHostLeadInput {
|
|
|
128
147
|
currency: string;
|
|
129
148
|
/** A stage of the default pipeline; its first open stage when absent. */
|
|
130
149
|
stageId?: string;
|
|
150
|
+
/**
|
|
151
|
+
* The deal's Type, as the door judged it against the org's
|
|
152
|
+
* `opportunityType` list (AGL-3516); absent takes the list's default.
|
|
153
|
+
*/
|
|
154
|
+
type?: string;
|
|
131
155
|
} | null;
|
|
132
156
|
}
|
|
133
157
|
/**
|
|
@@ -182,6 +206,27 @@ export type ConvertHostLeadResult = {
|
|
|
182
206
|
* in the lead.
|
|
183
207
|
*/
|
|
184
208
|
export declare function stageForNewDeal(pipeline: Pick<CrmPipeline, 'stages'>, requestedStageId: string | undefined): CrmDealStage | null;
|
|
209
|
+
/**
|
|
210
|
+
* The lead's MOST RECENT campaign, which becomes the deal's Primary
|
|
211
|
+
* Campaign Source as Salesforce's conversion makes it. A lead keeps its
|
|
212
|
+
* campaigns as one `campaignIds` array that every writer extends with
|
|
213
|
+
* `arrayUnion`, which appends — so the last id is the campaign the lead was
|
|
214
|
+
* filed under last. `undefined` for a lead filed under none.
|
|
215
|
+
*/
|
|
216
|
+
export declare function leadLatestCampaignId(lead: Readonly<Record<string, unknown>>): string | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* The account fields a lead hands its company (AGL-3513), normalized and
|
|
219
|
+
* judged as every company door judges them: the lead's industry and rating
|
|
220
|
+
* against the org's lists, its lead source as the Account Source, and its
|
|
221
|
+
* revenue, currency, head count, fax, website, phone and address (as the
|
|
222
|
+
* billing address). With `current` — a company the org already holds —
|
|
223
|
+
* only the fields it leaves blank are answered, and the picklists it does
|
|
224
|
+
* not name keep no default; without it, the company is being created and
|
|
225
|
+
* each picklist the lead leaves empty starts from its list's default. A
|
|
226
|
+
* value a list no longer holds is left behind rather than refusing the
|
|
227
|
+
* conversion.
|
|
228
|
+
*/
|
|
229
|
+
export declare function leadCompanyFields(lead: Readonly<Record<string, unknown>>, lists: Readonly<Partial<Record<CrmPicklistId, CrmPicklist>>>, current: Readonly<Record<string, unknown>> | null): Record<string, unknown>;
|
|
185
230
|
/**
|
|
186
231
|
* Convert one lead. Throws only on infrastructure failure; every refusal
|
|
187
232
|
* the data can produce is a `ConvertHostLeadResult`.
|
|
@@ -73,12 +73,33 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
73
73
|
* nobody chose an owner for — and no rule assigned — stays unowned, which
|
|
74
74
|
* the console renders honestly as unassigned. Either way the caller has
|
|
75
75
|
* already been authorized by its door; nothing here asks again.
|
|
76
|
-
|
|
76
|
+
*
|
|
77
|
+
* ## The deal carries the lead's lead source (AGL-3516)
|
|
78
|
+
*
|
|
79
|
+
* As Salesforce's conversion does, the deal opened here takes the lead's
|
|
80
|
+
* `leadSource` — a label of the same picklist, already judged when the lead
|
|
81
|
+
* was written — and its Type as the door judged it, or the org's default
|
|
82
|
+
* Type when the converter named none. Its forecast category is its stage's.
|
|
83
|
+
*
|
|
84
|
+
* ## Every standard field lands where Salesforce's conversion puts it (AGL-3513)
|
|
85
|
+
*
|
|
86
|
+
* The contact's facet takes the salutation, the name's parts, the mobile,
|
|
87
|
+
* the fax and Do not call. The company takes the account fields — industry,
|
|
88
|
+
* rating, revenue and its currency, head count, website, phone, fax, the
|
|
89
|
+
* address as its billing address, and the lead source as its Account
|
|
90
|
+
* Source — whole when the conversion creates it, and only into its BLANK
|
|
91
|
+
* fields when it links one the org already holds: a lead is one person's
|
|
92
|
+
* account of the business, and the account record may know better. The
|
|
93
|
+
* deal's Primary Campaign Source is the lead's most recent campaign — see
|
|
94
|
+
* {@link leadLatestCampaignId}.
|
|
95
|
+
*/ import { consentGroupDisclosureKey, consentGroupScope, CRM_COLLECTIONS, CRM_LEAD_STATUS_PICKLIST, crmLeadStatusLabelFor, CRM_COMPANY_PICKLIST_FIELDS, crmPicklistDefaultLabel, judgeCrmCompanyPicklists, readCrmCompanyAccountFields, crmScopeTokens, dealStageForecastCategory, DEFAULT_DEAL_STAGES, grantedUnderConsentGroup, nameSearchFields, contactFacetPath, normalizeContactEmail, ORG_SCOPE_TOKEN, readContactFacet, crmNewRecordListFields, readMarketingBasis, soloConsentGroup } from "@aglyn/aglyn/server";
|
|
77
96
|
import { consentGroupForSite, crmRecordsQuotaForOrg, findContactByEmail, logHostActivity, orgDataCollectionForHost, restampCrmListFieldsAt, writeContactCompanyLink } from "@aglyn/tenant-data-admin";
|
|
78
97
|
import { FieldPath, FieldValue } from "firebase-admin/firestore";
|
|
79
|
-
import { assignOwnerForCapture, notifyRecordAssigned } from "
|
|
80
|
-
import { captureHostContact } from "
|
|
98
|
+
import { assignOwnerForCapture, notifyRecordAssigned } from "./assign-contact-owner.js";
|
|
99
|
+
import { captureHostContact } from "./capture-host-contact.js";
|
|
100
|
+
import { readCrmPicklist } from "./read-picklist.js";
|
|
81
101
|
import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
102
|
+
import { readContainerIds } from "@aglyn/aglyn/app-utils/container-membership";
|
|
82
103
|
/**
|
|
83
104
|
* The stage a new deal opens in.
|
|
84
105
|
*
|
|
@@ -98,6 +119,96 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
98
119
|
const requested = requestedStageId ? stages.find((stage)=>stage.id === requestedStageId) : undefined;
|
|
99
120
|
return (_ref = requested != null ? requested : stages.find((stage)=>stage.kind === 'open')) != null ? _ref : stages[0];
|
|
100
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The lead's MOST RECENT campaign, which becomes the deal's Primary
|
|
124
|
+
* Campaign Source as Salesforce's conversion makes it. A lead keeps its
|
|
125
|
+
* campaigns as one `campaignIds` array that every writer extends with
|
|
126
|
+
* `arrayUnion`, which appends — so the last id is the campaign the lead was
|
|
127
|
+
* filed under last. `undefined` for a lead filed under none.
|
|
128
|
+
*/ export function leadLatestCampaignId(lead) {
|
|
129
|
+
const ids = readContainerIds(lead, 'campaign');
|
|
130
|
+
return ids.length ? ids[ids.length - 1] : undefined;
|
|
131
|
+
}
|
|
132
|
+
/** Whether a stored field holds nothing a fill would overwrite. */ const blankField = (value)=>value === undefined || value === null || typeof value === 'string' && !value.trim() || typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length;
|
|
133
|
+
/**
|
|
134
|
+
* The account fields a lead hands its company (AGL-3513), normalized and
|
|
135
|
+
* judged as every company door judges them: the lead's industry and rating
|
|
136
|
+
* against the org's lists, its lead source as the Account Source, and its
|
|
137
|
+
* revenue, currency, head count, fax, website, phone and address (as the
|
|
138
|
+
* billing address). With `current` — a company the org already holds —
|
|
139
|
+
* only the fields it leaves blank are answered, and the picklists it does
|
|
140
|
+
* not name keep no default; without it, the company is being created and
|
|
141
|
+
* each picklist the lead leaves empty starts from its list's default. A
|
|
142
|
+
* value a list no longer holds is left behind rather than refusing the
|
|
143
|
+
* conversion.
|
|
144
|
+
*/ export function leadCompanyFields(lead, lists, current) {
|
|
145
|
+
const fill = (field)=>!current || blankField(current[field]);
|
|
146
|
+
const offered = (value)=>blankField(value) ? undefined : value;
|
|
147
|
+
const fields = {};
|
|
148
|
+
const requested = {};
|
|
149
|
+
const picklistSource = {
|
|
150
|
+
industry: lead['industry'],
|
|
151
|
+
rating: lead['rating'],
|
|
152
|
+
accountSource: lead['leadSource']
|
|
153
|
+
};
|
|
154
|
+
for (const { field } of CRM_COMPANY_PICKLIST_FIELDS){
|
|
155
|
+
const value = offered(picklistSource[field]);
|
|
156
|
+
if (value !== undefined && fill(field)) requested[field] = value;
|
|
157
|
+
}
|
|
158
|
+
const picklists = judgeCrmCompanyPicklists(lists, requested, {
|
|
159
|
+
current: current != null ? current : {},
|
|
160
|
+
created: !current
|
|
161
|
+
});
|
|
162
|
+
for (const [field, value] of Object.entries(picklists.values)){
|
|
163
|
+
if (value && !picklists.errors[field]) fields[field] = value;
|
|
164
|
+
}
|
|
165
|
+
const account = {};
|
|
166
|
+
for (const field of [
|
|
167
|
+
'annualRevenueCents',
|
|
168
|
+
'numberOfEmployees',
|
|
169
|
+
'fax'
|
|
170
|
+
]){
|
|
171
|
+
const value = offered(lead[field]);
|
|
172
|
+
if (value !== undefined && fill(field)) account[field] = value;
|
|
173
|
+
}
|
|
174
|
+
// The revenue's currency travels with the revenue it describes.
|
|
175
|
+
if (account['annualRevenueCents'] !== undefined && offered(lead['currency']) !== undefined) {
|
|
176
|
+
account['currency'] = lead['currency'];
|
|
177
|
+
}
|
|
178
|
+
const read = readCrmCompanyAccountFields(account);
|
|
179
|
+
for (const [field, value] of Object.entries(read.values)){
|
|
180
|
+
if (value !== null && value !== undefined && !read.errors[field]) fields[field] = value;
|
|
181
|
+
}
|
|
182
|
+
// Already in the company's shapes: the lead normalized them as a company does.
|
|
183
|
+
for (const [field, from] of [
|
|
184
|
+
[
|
|
185
|
+
'website',
|
|
186
|
+
'website'
|
|
187
|
+
],
|
|
188
|
+
[
|
|
189
|
+
'phone',
|
|
190
|
+
'phone'
|
|
191
|
+
],
|
|
192
|
+
[
|
|
193
|
+
'address',
|
|
194
|
+
'address'
|
|
195
|
+
]
|
|
196
|
+
]){
|
|
197
|
+
const value = offered(lead[from]);
|
|
198
|
+
if (value !== undefined && fill(field)) fields[field] = value;
|
|
199
|
+
}
|
|
200
|
+
return fields;
|
|
201
|
+
}
|
|
202
|
+
/** The org's lists behind every company picklist field, for {@link leadCompanyFields}. */ async function readCompanyPicklists(firestore, orgId) {
|
|
203
|
+
const ids = [
|
|
204
|
+
...new Set(CRM_COMPANY_PICKLIST_FIELDS.map((entry)=>entry.picklistId))
|
|
205
|
+
];
|
|
206
|
+
const lists = await Promise.all(ids.map((id)=>readCrmPicklist(firestore, orgId, id)));
|
|
207
|
+
return Object.fromEntries(ids.map((id, at)=>[
|
|
208
|
+
id,
|
|
209
|
+
lists[at]
|
|
210
|
+
]));
|
|
211
|
+
}
|
|
101
212
|
/** A stage's kind as the deal's denormalized status. */ function dealStatusForStage(stage) {
|
|
102
213
|
return stage.kind === 'won' ? 'won' : stage.kind === 'lost' ? 'lost' : 'open';
|
|
103
214
|
}
|
|
@@ -224,6 +335,17 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
224
335
|
address: lead.address
|
|
225
336
|
} : {}, typeof lead.company === 'string' && lead.company ? {
|
|
226
337
|
companyName: lead.company
|
|
338
|
+
} : {}, Object.fromEntries([
|
|
339
|
+
'salutation',
|
|
340
|
+
'firstName',
|
|
341
|
+
'lastName',
|
|
342
|
+
'mobilePhone',
|
|
343
|
+
'fax'
|
|
344
|
+
].filter((key)=>typeof lead[key] === 'string' && lead[key]).map((key)=>[
|
|
345
|
+
key,
|
|
346
|
+
lead[key]
|
|
347
|
+
])), lead.doNotCall === true ? {
|
|
348
|
+
doNotCall: true
|
|
227
349
|
} : {})
|
|
228
350
|
}));
|
|
229
351
|
/*
|
|
@@ -312,6 +434,7 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
312
434
|
/*==========================================
|
|
313
435
|
* 2. THE COMPANY — linked, found by domain, or created.
|
|
314
436
|
*=========================================*/ let companyId;
|
|
437
|
+
/** The company the conversion found rather than made — its blanks are filled below. */ let existingCompany;
|
|
315
438
|
if (requestedCompanyId) {
|
|
316
439
|
const companySnapshot = await orgRef.collection(CRM_COLLECTIONS.companies).doc(requestedCompanyId).get();
|
|
317
440
|
if (!companySnapshot.exists) return {
|
|
@@ -319,6 +442,7 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
319
442
|
reason: 'unknown-company'
|
|
320
443
|
};
|
|
321
444
|
companyId = requestedCompanyId;
|
|
445
|
+
existingCompany = companySnapshot;
|
|
322
446
|
} else if (createCompany) {
|
|
323
447
|
/*
|
|
324
448
|
* FIND BEFORE CREATE. The NAME first (AGL-3233): a lead names its
|
|
@@ -328,25 +452,23 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
328
452
|
* document for `acme.com` would split the account the key exists to
|
|
329
453
|
* join. Only a company the caller can see counts as found — see
|
|
330
454
|
* `readableTokens`.
|
|
331
|
-
*/ const visibleAmong = (snapshots)=>{
|
|
332
|
-
var _snapshots_find;
|
|
333
|
-
return (_snapshots_find = snapshots.find((snapshot)=>{
|
|
455
|
+
*/ const visibleAmong = (snapshots)=>snapshots.find((snapshot)=>{
|
|
334
456
|
const tokens = snapshot.get('visibleTo');
|
|
335
457
|
return Array.isArray(tokens) && tokens.some((token)=>readableTokens.has(String(token)));
|
|
336
|
-
})
|
|
337
|
-
};
|
|
458
|
+
});
|
|
338
459
|
const byName = await orgRef.collection(CRM_COLLECTIONS.companies).where('nameLower', '==', createCompany.name.toLowerCase()).limit(5).get();
|
|
339
|
-
|
|
340
|
-
if (!
|
|
460
|
+
existingCompany = visibleAmong(byName.docs);
|
|
461
|
+
if (!existingCompany && createCompany.domain) {
|
|
341
462
|
const byDomain = await orgRef.collection(CRM_COLLECTIONS.companies).where('domain', '==', createCompany.domain).limit(5).get();
|
|
342
|
-
|
|
463
|
+
existingCompany = visibleAmong(byDomain.docs);
|
|
343
464
|
}
|
|
465
|
+
companyId = existingCompany == null ? void 0 : existingCompany.id;
|
|
344
466
|
if (!companyId) {
|
|
345
467
|
if (await bandFull()) return {
|
|
346
468
|
ok: false,
|
|
347
469
|
reason: 'band-full'
|
|
348
470
|
};
|
|
349
|
-
const company = _extends({}, nameSearchFields(createCompany.name), createCompany.domain ? {
|
|
471
|
+
const company = _extends({}, leadCompanyFields(lead, await readCompanyPicklists(firestore, orgId), null), nameSearchFields(createCompany.name), createCompany.domain ? {
|
|
350
472
|
domain: createCompany.domain
|
|
351
473
|
} : {}, ownerUid ? {
|
|
352
474
|
ownerUid
|
|
@@ -361,6 +483,20 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
361
483
|
companyId = created.id;
|
|
362
484
|
}
|
|
363
485
|
}
|
|
486
|
+
if (companyId && existingCompany) {
|
|
487
|
+
var _existingCompany_data;
|
|
488
|
+
/*
|
|
489
|
+
* A company the org already held takes the lead's account fields only
|
|
490
|
+
* where it has none (AGL-3513) — and is restamped, because its
|
|
491
|
+
* industry, rating and account source are what its list filters by.
|
|
492
|
+
*/ const filled = leadCompanyFields(lead, await readCompanyPicklists(firestore, orgId), (_existingCompany_data = existingCompany.data()) != null ? _existingCompany_data : {});
|
|
493
|
+
if (Object.keys(filled).length) {
|
|
494
|
+
await existingCompany.ref.update(_extends({}, filled, {
|
|
495
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
496
|
+
}));
|
|
497
|
+
await restampCrmListFieldsAt(existingCompany.ref, 'companies');
|
|
498
|
+
}
|
|
499
|
+
}
|
|
364
500
|
if (companyId) {
|
|
365
501
|
/*
|
|
366
502
|
* The one link writer (AGL-2613): the holder's facet field the contact
|
|
@@ -381,6 +517,7 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
381
517
|
* 3. THE DEAL — in the default pipeline, seeded if the org has none.
|
|
382
518
|
*=========================================*/ let dealId;
|
|
383
519
|
if (deal) {
|
|
520
|
+
var _ref, _deal_type;
|
|
384
521
|
const pipelinesRef = orgRef.collection(CRM_COLLECTIONS.pipelines);
|
|
385
522
|
let pipelineId;
|
|
386
523
|
let pipeline;
|
|
@@ -429,6 +566,10 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
429
566
|
ok: false,
|
|
430
567
|
reason: 'band-full'
|
|
431
568
|
};
|
|
569
|
+
const type = (_ref = (_deal_type = deal.type) != null ? _deal_type : crmPicklistDefaultLabel(await readCrmPicklist(firestore, orgId, 'opportunityType'))) != null ? _ref : undefined;
|
|
570
|
+
const leadSource = typeof lead.leadSource === 'string' ? lead.leadSource.trim() : '';
|
|
571
|
+
// The Primary Campaign Source: the lead's most recent campaign (AGL-3513).
|
|
572
|
+
const campaignId = leadLatestCampaignId(lead);
|
|
432
573
|
const dealRecord = _extends({
|
|
433
574
|
title: deal.title,
|
|
434
575
|
titleLower: deal.title.toLowerCase(),
|
|
@@ -439,11 +580,26 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
439
580
|
amountCents: deal.amountCents
|
|
440
581
|
} : {}, {
|
|
441
582
|
currency: deal.currency,
|
|
442
|
-
stageChangedAtMs: now
|
|
443
|
-
|
|
583
|
+
stageChangedAtMs: now,
|
|
584
|
+
forecastCategory: dealStageForecastCategory(stage)
|
|
585
|
+
}, type ? {
|
|
586
|
+
type
|
|
587
|
+
} : {}, leadSource ? {
|
|
588
|
+
leadSource
|
|
589
|
+
} : {}, campaignId ? {
|
|
590
|
+
campaignId
|
|
591
|
+
} : {}, ownerUid ? {
|
|
444
592
|
ownerUid
|
|
445
593
|
} : {}, {
|
|
446
|
-
contactId
|
|
594
|
+
contactId,
|
|
595
|
+
// The converted person is the deal's Primary contact role, with no
|
|
596
|
+
// role yet, as Salesforce's conversion makes them (AGL-3521).
|
|
597
|
+
contactRoles: [
|
|
598
|
+
{
|
|
599
|
+
contactId,
|
|
600
|
+
primary: true
|
|
601
|
+
}
|
|
602
|
+
]
|
|
447
603
|
}, companyId ? {
|
|
448
604
|
companyId
|
|
449
605
|
} : {}, {
|
|
@@ -460,6 +616,8 @@ import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
|
460
616
|
* 4. THE LEAD — stamped last, once everything it names exists.
|
|
461
617
|
*=========================================*/ const stamp = _extends({
|
|
462
618
|
status: 'qualified',
|
|
619
|
+
// The org's label for Qualified, beside the meaning (AGL-3512).
|
|
620
|
+
statusLabel: crmLeadStatusLabelFor(await readCrmPicklist(firestore, orgId, CRM_LEAD_STATUS_PICKLIST), 'qualified'),
|
|
463
621
|
convertedContactId: contactId,
|
|
464
622
|
convertedAtMs: now
|
|
465
623
|
}, ownerUid ? {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/convert-host-lead.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A lead becomes a contact, and optionally a company and a deal (AGL-2608,\n * AGL-2627).\n *\n * ## Why one function under two doors\n *\n * The console's convert dialog posts to the CRM plugin's `crm/lead-convert`\n * route, and an integration posts to `POST /v1/leads/{id}/convert`. The two\n * doors authenticate differently — an ID token against the site's role, an\n * API key against its scopes — and answer in different envelopes, but what\n * they DO has to be one thing: a conversion that opened a second deal for\n * one person because two callers reached the same lead through two\n * implementations is the bug this file exists to make impossible. The doors\n * own their transport; this owns the writes.\n *\n * It lives here, in the tenancy runtime, rather than beside the plugin\n * route it came from, because the console's REST layer may not import a\n * feature plugin (the app→addons boundary), while both the plugin and the\n * console may import the runtime — which already holds the capture door\n * and the owner assignment this conversion goes through.\n *\n * ## Why a server function and not four client writes\n *\n * The lead is a host document the browser may edit, and a company or a deal\n * is a client-creatable org document. What the browser cannot do is CREATE A\n * CONTACT: the contacts collection is written only by `captureHostContact`\n * (the runtime's wrapper over `upsertHostContact`, so `contactCreated`\n * fires), because that function is the dedupe — one human, one row, found by\n * normalized address across every site in the org — and the audience-band\n * gate. A client that wrote a contact document of its own would mint a\n * second row for a person the org already holds, and would do it past the\n * band.\n *\n * Once the contact has to come from the server, the rest follows it: the\n * conversion is one act with four writes, and a caller that made three of\n * them and lost the connection before the fourth leaves a lead marked\n * qualified with no contact, or a deal pointing at a contact that was never\n * stamped on the lead. Here the order is fixed — contact, then company, then\n * deal, then the lead — and the lead is stamped LAST, so a lead that carries\n * `convertedContactId` names a contact that exists.\n *\n * ## Idempotent on the lead\n *\n * A lead already carrying `convertedContactId` answers with the ids it has\n * and creates nothing more. A double-click on the dialog's button, a retry\n * after a slow response, or a Zapier zap replaying a step must not open a\n * second deal for one person.\n *\n * ## The actor\n *\n * A signed-in member converting from the console owns what nobody else\n * claimed: their uid is the owner of last resort, written directly rather\n * than through the deliberate reassignment, because they may be staff\n * converting on a workspace's behalf and a roster check would refuse the one\n * person who is actually here. An API key is not a person and is on no\n * roster, so for an `api` actor that last step is skipped and a contact\n * nobody chose an owner for — and no rule assigned — stays unowned, which\n * the console renders honestly as unassigned. Either way the caller has\n * already been authorized by its door; nothing here asks again.\n */\n\nimport {\n consentGroupDisclosureKey,\n consentGroupScope,\n CRM_COLLECTIONS,\n type CrmDealStage,\n type CrmDealStatus,\n type CrmLeadFields,\n type CrmPipeline,\n crmScopeTokens,\n DEFAULT_DEAL_STAGES,\n grantedUnderConsentGroup,\n nameSearchFields,\n contactFacetPath,\n normalizeContactEmail,\n ORG_SCOPE_TOKEN,\n readContactFacet,\n crmNewRecordListFields,\n readMarketingBasis,\n soloConsentGroup,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n crmRecordsQuotaForOrg,\n findContactByEmail,\n logHostActivity,\n orgDataCollectionForHost,\n restampCrmListFieldsAt,\n writeContactCompanyLink,\n} from '@aglyn/tenant-data-admin'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport { assignOwnerForCapture, notifyRecordAssigned } from '@aglyn/tenant-runtime/assign-contact-owner'\nimport { captureHostContact } from '@aglyn/tenant-runtime/capture-host-contact'\nimport { handOffLeadRecords } from '@aglyn/tenant-runtime/hand-off-lead'\n\n/** Who is converting, as far as the writes need to know. */\nexport interface LeadConvertActor {\n /**\n * What `createdByUid` records on every record the conversion opens, and\n * who the audit line names. A member's uid, or the literal `'api'` for a\n * key — the same attribution the REST creates stamp.\n */\n uid: string\n email?: string | null\n /**\n * `member`: a signed-in person, who becomes the owner when nobody else\n * does. `api`: a key, which cannot own a record — see the header.\n */\n kind: 'member' | 'api'\n /**\n * For an `api` actor, the key's name — what the audit line attributes the\n * conversion to, since a key has no address to be named by.\n */\n apiKeyName?: string | null\n}\n\n/** What a conversion is asked to do. Validated by the door before it gets here. */\nexport interface ConvertHostLeadInput {\n firestore: FirebaseFirestore.Firestore\n hostId: string\n orgId: string\n /** The org document, as the door resolved it. */\n org: Record<string, unknown>\n /** `hosts/{hostId}/leads/{leadId}` — the document id, which is the person key. */\n leadId: string\n actor: LeadConvertActor\n /**\n * Who owns the resulting contact (and deal). Defaults to the lead's own\n * owner; failing that the org's assignment rules and the site's default\n * owner decide (AGL-2618), and failing those a member actor — somebody\n * converted this person, and a record with no owner is one nobody follows\n * up.\n */\n ownerUid?: string\n /** Link an existing `orgs/{orgId}/companies/{companyId}`. */\n companyId?: string\n /** Or create one. Ignored when `companyId` is given. Already normalized. */\n createCompany?: { name: string; domain: string | null } | null\n /** Open a deal in the org's default pipeline. Already normalized. */\n deal?: {\n title: string\n amountCents: number | null\n /** Lowercase ISO 4217. */\n currency: string\n /** A stage of the default pipeline; its first open stage when absent. */\n stageId?: string\n } | null\n}\n\n/**\n * Why a conversion did not happen. Each is a state of the data, not a fault\n * of the request — the door has already refused a malformed body — and each\n * leaves the lead unconverted so a retry after the remedy finds it where it\n * was.\n */\nexport type ConvertHostLeadRefusal =\n /** No document at `hosts/{hostId}/leads/{leadId}`. */\n | 'unknown-lead'\n /** The lead's address cannot become a contact. */\n | 'no-email'\n /**\n * The capture door produced no contact — it swallows its own failures and\n * drops a creation past a Free org's audience band. Nothing was written.\n */\n | 'contact-not-created'\n /**\n * The site holds an erasure for the lead's address (AGL-2623): the person\n * asked to be forgotten, and the capture door refuses to rebuild a record\n * for them — by a conversion as by a form. Nothing was written, and the\n * lead stays as it was; the erasure removes it when it runs.\n */\n | 'erased'\n /** The CRM records band is full (AGL-2611); the contact stands, nothing more was opened. */\n | 'band-full'\n /** `companyId` names no company in this organization. The contact stands. */\n | 'unknown-company'\n /** The default pipeline has no stages to open a deal in. The contact stands. */\n | 'no-stages'\n\nexport type ConvertHostLeadResult =\n | {\n ok: true\n contactId: string\n companyId?: string\n dealId?: string\n /** The lead was converted before this call; nothing was created now. */\n alreadyConverted: boolean\n }\n | { ok: false; reason: ConvertHostLeadRefusal }\n\n/** The stamp the conversion leaves on the lead — see `CrmLeadFields`. */\ntype LeadConversionStamp = Required<\n Pick<CrmLeadFields, 'status' | 'convertedContactId' | 'convertedAtMs'>\n> &\n Pick<CrmLeadFields, 'dealId' | 'companyId' | 'ownerUid'>\n\n/**\n * The stage a new deal opens in.\n *\n * The caller's choice when the pipeline has it, otherwise the first OPEN\n * stage by order — a deal created by converting a lead is by definition not\n * yet won or lost, so defaulting to a closed stage would record an outcome\n * nobody reached. A pipeline with no open stage at all falls back to its\n * first stage rather than refusing: the merchant edited their pipeline into\n * that shape, and a conversion that failed because of it would read as a bug\n * in the lead.\n */\nexport function stageForNewDeal(\n pipeline: Pick<CrmPipeline, 'stages'>,\n requestedStageId: string | undefined,\n): CrmDealStage | null {\n const stages = [...(pipeline.stages ?? [])].sort((a, b) => a.order - b.order)\n if (!stages.length) return null\n const requested = requestedStageId\n ? stages.find((stage) => stage.id === requestedStageId)\n : undefined\n return requested ?? stages.find((stage) => stage.kind === 'open') ?? stages[0]\n}\n\n/** A stage's kind as the deal's denormalized status. */\nfunction dealStatusForStage(stage: CrmDealStage): CrmDealStatus {\n return stage.kind === 'won' ? 'won' : stage.kind === 'lost' ? 'lost' : 'open'\n}\n\n/**\n * Convert one lead. Throws only on infrastructure failure; every refusal\n * the data can produce is a `ConvertHostLeadResult`.\n */\nexport async function convertHostLead(\n input: ConvertHostLeadInput,\n): Promise<ConvertHostLeadResult> {\n const { firestore, hostId, orgId, org, leadId, actor, createCompany, deal } = input\n const requestedCompanyId = String(input.companyId ?? '').trim()\n // The lead's one home, on the org (AGL-3275). `orgId` is already resolved\n // by the caller, so this needs no second lookup to find it.\n const leadRef = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('leads')\n .doc(leadId)\n const leadSnapshot = await leadRef.get()\n if (!leadSnapshot.exists) return { ok: false, reason: 'unknown-lead' }\n const lead = (leadSnapshot.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n if (lead.convertedContactId) {\n return {\n ok: true,\n contactId: lead.convertedContactId,\n ...(lead.companyId ? { companyId: lead.companyId } : {}),\n ...(lead.dealId ? { dealId: lead.dealId } : {}),\n alreadyConverted: true,\n }\n }\n const email = normalizeContactEmail(lead['email'])\n if (!email) return { ok: false, reason: 'no-email' }\n\n const group = await consentGroupForSite(hostId, org)\n const visibleTo = crmScopeTokens(org, group)\n /*\n * What the CALLER may read, for the company-by-domain reuse below: their\n * group's own tokens plus `org`, exactly the set the console's listeners\n * filter on. A company outside it is one they could not open, so linking\n * a contact to it would point at a record the page then 404s on.\n */\n const readableTokens = new Set<string>([\n ORG_SCOPE_TOKEN,\n ...consentGroupScope(group),\n ])\n /*\n * THE OWNER A PERSON CHOSE, when one did: the converter's pick, else\n * whoever was already working the lead. Handed to the capture as the\n * facet's owner, which is what tells the capture's own assignment pass\n * to stand down — a person's choice outranks a rule. When nobody chose,\n * the facet names no owner and the pass runs the org's rules and the\n * site's default for a contact it creates (AGL-2618).\n */\n const pickedOwner = String(input.ownerUid ?? '').trim()\n const chosenOwner =\n pickedOwner || (typeof lead.ownerUid === 'string' ? lead.ownerUid : '')\n const now = Date.now()\n const leadName = typeof lead['name'] === 'string' ? lead['name'] : undefined\n\n /*==========================================\n * 1. THE CONTACT — through the one door that dedupes and meters.\n *\n * `source: 'manual'` because a person converted this lead by hand, and\n * the source filter should say so; the interaction names the lead so the\n * contact's timeline can be walked back to what was captured. The facet\n * carries the stage and the owner, which is what makes this a sales\n * record rather than another form capture.\n *\n * THE LEAD'S OWN PROFILE TRAVELS WITH IT (AGL-3233), the way Salesforce\n * hands a lead's fields to the contact: phone, title, lead source, address, the\n * company as text (until step 2 links a record, whose own name then\n * replaces it), tags, and the marketing basis the lead recorded — the\n * person ticked the box on the lead, and a conversion must not be the\n * moment they lose it. Only what the lead holds is handed over; a value\n * the contact already carries is the contact's, because the capture door\n * writes a profile field only where the facet has none.\n *=========================================*/\n const leadTags = Array.isArray(lead.tags) ? lead.tags.map(String) : []\n const leadConsent = readMarketingBasis(lead, soloConsentGroup(hostId))\n /*\n * WHICH SITES THE CONTACT'S GRANT COVERS (AGL-3320): the lead's, never\n * more. The capture door records a fresh grant, so it is handed the group's\n * disclosure key only when the lead ALREADY holds a grant given under the\n * group as it stands, at every site it names — a lead captured by one site\n * before the group existed, or before a site joined, converts to a contact\n * that site alone may mail, exactly as the lead was.\n */\n const leadDisclosure = grantedUnderConsentGroup(lead, group)\n ? consentGroupDisclosureKey(group)\n : null\n const captured = await captureHostContact({\n hostId,\n email,\n ...(leadName ? { name: leadName } : {}),\n source: 'manual',\n interaction: { summary: 'Converted from a lead', refId: leadId },\n // Who converted it, for the runs its contactCreated sets off (AGL-3376).\n actor:\n actor.kind === 'member'\n ? { kind: 'member', uid: actor.uid, email: actor.email ?? null }\n : { kind: 'apiKey', apiKeyName: actor.apiKeyName ?? null },\n ...(leadConsent.basis === 'granted' ? { marketingConsent: true } : {}),\n ...(leadConsent.basis === 'granted' && leadDisclosure\n ? { disclosedConsentGroup: leadDisclosure }\n : {}),\n ...(leadTags.length ? { tags: leadTags } : {}),\n facet: {\n lifecycleStage: 'sales-qualified',\n ...(chosenOwner ? { ownerUid: chosenOwner } : {}),\n ...(typeof lead.phone === 'string' && lead.phone ? { phone: lead.phone } : {}),\n ...(typeof lead.jobTitle === 'string' && lead.jobTitle ? { jobTitle: lead.jobTitle } : {}),\n // Salesforce's Lead Source travels to the contact (AGL-3298).\n ...(typeof lead.leadSource === 'string' && lead.leadSource\n ? { leadSource: lead.leadSource }\n : {}),\n ...(lead.address && typeof lead.address === 'object' ? { address: lead.address } : {}),\n ...(typeof lead.company === 'string' && lead.company\n ? { companyName: lead.company }\n : {}),\n },\n })\n /*\n * The one verdict of the door's that is named here. Its other refusals —\n * the band, an address it will not take, its own swallowed error — all\n * read the same from this side: no row to find below, answered as\n * `contact-not-created`. An erasure is different in kind: not a limit the\n * org can lift or a fault in the request but a decision the workspace made\n * about this person, and a caller told \"the band may be full\" would retry\n * against it forever.\n */\n if (captured && 'refused' in captured && captured.refused === 'erased') {\n return { ok: false, reason: 'erased' }\n }\n const contactsRef = await orgDataCollectionForHost(hostId, 'contacts')\n /*\n * The same resolution the capture door just made (AGL-2625): the\n * address index first, so a lead whose address is an ALTERNATE on a\n * merged contact converts into the survivor the capture landed on\n * rather than reading as a contact the band dropped.\n */\n const contactSnapshot = await findContactByEmail(contactsRef, email)\n if (!contactSnapshot) return { ok: false, reason: 'contact-not-created' }\n const contactRef = contactSnapshot.ref\n const contactId = contactSnapshot.id\n const contactData = (contactSnapshot.data() ?? {}) as Record<string, unknown>\n const orgRef = firestore.collection('orgs').doc(orgId)\n /*\n * THE RECORDS BAND (AGL-2611), asked before each record this conversion\n * CREATES. The contact went through the capture door, which refuses at\n * the band on its own; a company and a deal are records of the same\n * band, and on a Free org at its hundred the honest answer is the one\n * the drawers give — refuse, write nothing more, and leave the lead\n * unconverted so a retry after the upgrade finds it where it was. The\n * contact step 1 captured stays: it is one person's record, and a second\n * conversion merges onto it rather than making another. Measured fresh\n * each time because the company created in step 2 is itself a record.\n */\n const bandFull = async () => {\n const room = await crmRecordsQuotaForOrg(org as never, orgRef)\n return !room.allowed\n }\n\n /*\n * WHOSE THE CONTACT IS, now that it exists. The capture wrote the chosen\n * owner, or its pass assigned one to a contact it created; a contact\n * the org already held with no owner has had neither, so the same pass\n * is asked once more — it touches only a record with no owner and\n * answers `unchanged` for one that has — and a member actor is the last\n * resort. A colleague the converter picked is told; the lead's existing\n * owner and the caller are not, having chosen for themselves.\n */\n let ownerUid = readContactFacet(contactData, group.groupId).ownerUid ?? ''\n if (!ownerUid) {\n const assigned = await assignOwnerForCapture({\n hostId,\n contactId,\n email,\n source: 'manual',\n actorUid: actor.kind === 'member' ? actor.uid : null,\n })\n if (assigned.outcome !== 'none') ownerUid = assigned.ownerUid\n }\n if (!ownerUid && actor.kind === 'member') {\n ownerUid = actor.uid\n await contactRef.update({\n [contactFacetPath(group.groupId, 'ownerUid')]: ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n // The owner is what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(contactRef, 'contacts')\n } else if (ownerUid && pickedOwner && pickedOwner === ownerUid) {\n await notifyRecordAssigned({\n hostId,\n orgId,\n ownerUid,\n actorUid: actor.kind === 'member' ? actor.uid : null,\n record: { kind: 'contact', id: contactId },\n who: leadName || email,\n })\n }\n\n /*==========================================\n * 2. THE COMPANY — linked, found by domain, or created.\n *=========================================*/\n let companyId: string | undefined\n if (requestedCompanyId) {\n const companySnapshot = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .doc(requestedCompanyId)\n .get()\n if (!companySnapshot.exists) return { ok: false, reason: 'unknown-company' }\n companyId = requestedCompanyId\n } else if (createCompany) {\n /*\n * FIND BEFORE CREATE. The NAME first (AGL-3233): a lead names its\n * company as text, and a company the org already files under that\n * name is the account, whatever domain it carries. Then the domain,\n * the key two contacts at one business share — a second company\n * document for `acme.com` would split the account the key exists to\n * join. Only a company the caller can see counts as found — see\n * `readableTokens`.\n */\n const visibleAmong = (\n snapshots: readonly FirebaseFirestore.QueryDocumentSnapshot[],\n ): string | undefined =>\n snapshots.find((snapshot) => {\n const tokens = snapshot.get('visibleTo')\n return (\n Array.isArray(tokens) &&\n tokens.some((token) => readableTokens.has(String(token)))\n )\n })?.id\n const byName = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .where('nameLower', '==', createCompany.name.toLowerCase())\n .limit(5)\n .get()\n companyId = visibleAmong(byName.docs)\n if (!companyId && createCompany.domain) {\n const byDomain = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .where('domain', '==', createCompany.domain)\n .limit(5)\n .get()\n companyId = visibleAmong(byDomain.docs)\n }\n if (!companyId) {\n if (await bandFull()) return { ok: false, reason: 'band-full' }\n const company: Record<string, unknown> = {\n ...nameSearchFields(createCompany.name),\n ...(createCompany.domain ? { domain: createCompany.domain } : {}),\n ...(ownerUid ? { ownerUid } : {}),\n visibleTo,\n hostId,\n createdByUid: actor.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n const created = await orgRef.collection(CRM_COLLECTIONS.companies).add({\n ...company,\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', company),\n })\n companyId = created.id\n }\n }\n if (companyId) {\n /*\n * The one link writer (AGL-2613): the holder's facet field the contact\n * record reads, the queryable top-level mirror the company's contacts\n * card filters on, and the company's contacts count — planned from\n * the row as the capture left it, so a person the org already held\n * under another company is MOVED rather than counted twice.\n */\n await writeContactCompanyLink({\n firestore,\n contactRef,\n contact: contactData,\n companiesRef: orgRef.collection(CRM_COLLECTIONS.companies),\n groupId: group.groupId,\n companyId,\n })\n }\n\n /*==========================================\n * 3. THE DEAL — in the default pipeline, seeded if the org has none.\n *=========================================*/\n let dealId: string | undefined\n if (deal) {\n const pipelinesRef = orgRef.collection(CRM_COLLECTIONS.pipelines)\n let pipelineId: string\n let pipeline: Pick<CrmPipeline, 'stages'>\n const defaults = await pipelinesRef\n .where('isDefault', '==', true)\n .limit(1)\n .get()\n if (!defaults.empty) {\n pipelineId = defaults.docs[0].id\n pipeline = defaults.docs[0].data() as CrmPipeline\n } else {\n /*\n * The deals section's own read shape, so the two agree on which\n * pipeline \"the org's pipeline\" is when none is flagged default: the\n * first by document id of a bounded window. Seeding a SECOND pipeline\n * here while one exists unflagged would leave the merchant with two\n * boards and their deals split between them.\n */\n const any = await pipelinesRef\n .orderBy(FieldPath.documentId())\n .limit(20)\n .get()\n if (!any.empty) {\n pipelineId = any.docs[0].id\n pipeline = any.docs[0].data() as CrmPipeline\n } else {\n // Exactly what the Deals section seeds: one `Sales` pipeline\n // carrying a COPY of the default stages, flagged default.\n const seeded: Omit<CrmPipeline, 'createdAt' | 'updatedAt'> = {\n name: 'Sales',\n stages: [...DEFAULT_DEAL_STAGES],\n isDefault: true,\n visibleTo,\n hostId,\n }\n const created = await pipelinesRef.add({\n ...seeded,\n createdByUid: actor.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n pipelineId = created.id\n pipeline = seeded\n }\n }\n const stage = stageForNewDeal(pipeline, deal.stageId)\n if (!stage) return { ok: false, reason: 'no-stages' }\n if (await bandFull()) return { ok: false, reason: 'band-full' }\n const dealRecord: Record<string, unknown> = {\n title: deal.title,\n titleLower: deal.title.toLowerCase(),\n pipelineId,\n stageId: stage.id,\n status: dealStatusForStage(stage),\n ...(deal.amountCents !== null ? { amountCents: deal.amountCents } : {}),\n currency: deal.currency,\n stageChangedAtMs: now,\n ...(ownerUid ? { ownerUid } : {}),\n contactId,\n ...(companyId ? { companyId } : {}),\n visibleTo,\n hostId,\n createdByUid: actor.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n const created = await orgRef.collection(CRM_COLLECTIONS.deals).add({\n ...dealRecord,\n // What the Deals list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('deals', dealRecord),\n })\n dealId = created.id\n }\n\n /*==========================================\n * 4. THE LEAD — stamped last, once everything it names exists.\n *=========================================*/\n const stamp: LeadConversionStamp = {\n status: 'qualified',\n convertedContactId: contactId,\n convertedAtMs: now,\n ...(ownerUid ? { ownerUid } : {}),\n ...(companyId ? { companyId } : {}),\n ...(dealId ? { dealId } : {}),\n }\n await leadRef.update({ ...stamp, updatedAt: FieldValue.serverTimestamp() })\n\n /*\n * WHAT THE LEAD HANDS OVER (AGL-3233): its activities and tasks gain the\n * contact, and every plugin with a listener — Sequences, for an\n * enrollment naming the lead — re-points its own records. After the\n * stamp, so a listener reading the lead finds the conversion on it; it\n * never throws.\n */\n await handOffLeadRecords({\n firestore,\n orgId,\n hostId,\n leadId,\n contactId,\n email,\n by: actor.kind === 'member' ? 'member' : 'api',\n })\n\n /*\n * The audit line, from the function that did the work (AGL-2622): the\n * conversion is one act however many records it opened, so it is one\n * entry, on the lead, in the feed of the site that holds the lead. A\n * repeat call answered above with the ids it already had wrote nothing\n * and logs nothing.\n */\n await logHostActivity(\n hostId,\n {\n uid: actor.uid,\n email: actor.email ?? null,\n ...(actor.apiKeyName ? { apiKeyName: actor.apiKeyName } : {}),\n },\n 'Converted lead',\n { type: 'lead', id: leadId, name: String(lead['name'] ?? '') || email },\n )\n\n return {\n ok: true,\n contactId,\n ...(companyId ? { companyId } : {}),\n ...(dealId ? { dealId } : {}),\n alreadyConverted: false,\n }\n}\n"],"names":["consentGroupDisclosureKey","consentGroupScope","CRM_COLLECTIONS","crmScopeTokens","DEFAULT_DEAL_STAGES","grantedUnderConsentGroup","nameSearchFields","contactFacetPath","normalizeContactEmail","ORG_SCOPE_TOKEN","readContactFacet","crmNewRecordListFields","readMarketingBasis","soloConsentGroup","consentGroupForSite","crmRecordsQuotaForOrg","findContactByEmail","logHostActivity","orgDataCollectionForHost","restampCrmListFieldsAt","writeContactCompanyLink","FieldPath","FieldValue","assignOwnerForCapture","notifyRecordAssigned","captureHostContact","handOffLeadRecords","stageForNewDeal","pipeline","requestedStageId","requested","stages","sort","a","b","order","length","find","stage","id","undefined","kind","dealStatusForStage","convertHostLead","input","leadSnapshot","actor","contactSnapshot","lead","firestore","hostId","orgId","org","leadId","createCompany","deal","requestedCompanyId","String","companyId","trim","leadRef","collection","doc","get","exists","ok","reason","data","convertedContactId","contactId","dealId","alreadyConverted","email","group","visibleTo","readableTokens","Set","pickedOwner","ownerUid","chosenOwner","now","Date","leadName","leadTags","Array","isArray","tags","map","leadConsent","leadDisclosure","captured","name","source","interaction","summary","refId","uid","apiKeyName","basis","marketingConsent","disclosedConsentGroup","facet","lifecycleStage","phone","jobTitle","leadSource","address","company","companyName","refused","contactsRef","contactRef","ref","contactData","orgRef","bandFull","room","allowed","groupId","assigned","actorUid","outcome","update","updatedAt","serverTimestamp","record","who","companySnapshot","companies","visibleAmong","snapshots","snapshot","tokens","some","token","has","byName","where","toLowerCase","limit","docs","domain","byDomain","createdByUid","createdAt","created","add","contact","companiesRef","pipelinesRef","pipelines","pipelineId","defaults","empty","any","orderBy","documentId","seeded","isDefault","stageId","dealRecord","title","titleLower","status","amountCents","currency","stageChangedAtMs","deals","stamp","convertedAtMs","by","type"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/convert-host-lead.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * A lead becomes a contact, and optionally a company and a deal (AGL-2608,\n * AGL-2627).\n *\n * ## Why one function under two doors\n *\n * The console's convert dialog posts to the CRM plugin's `crm/lead-convert`\n * route, and an integration posts to `POST /v1/leads/{id}/convert`. The two\n * doors authenticate differently — an ID token against the site's role, an\n * API key against its scopes — and answer in different envelopes, but what\n * they DO has to be one thing: a conversion that opened a second deal for\n * one person because two callers reached the same lead through two\n * implementations is the bug this file exists to make impossible. The doors\n * own their transport; this owns the writes.\n *\n * It lives here, in the tenancy runtime, rather than beside the plugin\n * route it came from, because the console's REST layer may not import a\n * feature plugin (the app→addons boundary), while both the plugin and the\n * console may import the runtime — which already holds the capture door\n * and the owner assignment this conversion goes through.\n *\n * ## Why a server function and not four client writes\n *\n * The lead is a host document the browser may edit, and a company or a deal\n * is a client-creatable org document. What the browser cannot do is CREATE A\n * CONTACT: the contacts collection is written only by `captureHostContact`\n * (the runtime's wrapper over `upsertHostContact`, so `contactCreated`\n * fires), because that function is the dedupe — one human, one row, found by\n * normalized address across every site in the org — and the audience-band\n * gate. A client that wrote a contact document of its own would mint a\n * second row for a person the org already holds, and would do it past the\n * band.\n *\n * Once the contact has to come from the server, the rest follows it: the\n * conversion is one act with four writes, and a caller that made three of\n * them and lost the connection before the fourth leaves a lead marked\n * qualified with no contact, or a deal pointing at a contact that was never\n * stamped on the lead. Here the order is fixed — contact, then company, then\n * deal, then the lead — and the lead is stamped LAST, so a lead that carries\n * `convertedContactId` names a contact that exists.\n *\n * ## Idempotent on the lead\n *\n * A lead already carrying `convertedContactId` answers with the ids it has\n * and creates nothing more. A double-click on the dialog's button, a retry\n * after a slow response, or a Zapier zap replaying a step must not open a\n * second deal for one person.\n *\n * ## The actor\n *\n * A signed-in member converting from the console owns what nobody else\n * claimed: their uid is the owner of last resort, written directly rather\n * than through the deliberate reassignment, because they may be staff\n * converting on a workspace's behalf and a roster check would refuse the one\n * person who is actually here. An API key is not a person and is on no\n * roster, so for an `api` actor that last step is skipped and a contact\n * nobody chose an owner for — and no rule assigned — stays unowned, which\n * the console renders honestly as unassigned. Either way the caller has\n * already been authorized by its door; nothing here asks again.\n *\n * ## The deal carries the lead's lead source (AGL-3516)\n *\n * As Salesforce's conversion does, the deal opened here takes the lead's\n * `leadSource` — a label of the same picklist, already judged when the lead\n * was written — and its Type as the door judged it, or the org's default\n * Type when the converter named none. Its forecast category is its stage's.\n *\n * ## Every standard field lands where Salesforce's conversion puts it (AGL-3513)\n *\n * The contact's facet takes the salutation, the name's parts, the mobile,\n * the fax and Do not call. The company takes the account fields — industry,\n * rating, revenue and its currency, head count, website, phone, fax, the\n * address as its billing address, and the lead source as its Account\n * Source — whole when the conversion creates it, and only into its BLANK\n * fields when it links one the org already holds: a lead is one person's\n * account of the business, and the account record may know better. The\n * deal's Primary Campaign Source is the lead's most recent campaign — see\n * {@link leadLatestCampaignId}.\n */\n\nimport {\n consentGroupDisclosureKey,\n consentGroupScope,\n CRM_COLLECTIONS,\n CRM_LEAD_STATUS_PICKLIST,\n crmLeadStatusLabelFor,\n type CrmDealStage,\n type CrmDealStatus,\n type CrmLeadFields,\n type CrmPipeline,\n CRM_COMPANY_PICKLIST_FIELDS,\n type CrmPicklist,\n type CrmPicklistId,\n crmPicklistDefaultLabel,\n judgeCrmCompanyPicklists,\n readCrmCompanyAccountFields,\n crmScopeTokens,\n dealStageForecastCategory,\n DEFAULT_DEAL_STAGES,\n grantedUnderConsentGroup,\n nameSearchFields,\n contactFacetPath,\n normalizeContactEmail,\n ORG_SCOPE_TOKEN,\n readContactFacet,\n crmNewRecordListFields,\n readMarketingBasis,\n soloConsentGroup,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n crmRecordsQuotaForOrg,\n findContactByEmail,\n logHostActivity,\n orgDataCollectionForHost,\n restampCrmListFieldsAt,\n writeContactCompanyLink,\n} from '@aglyn/tenant-data-admin'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport { assignOwnerForCapture, notifyRecordAssigned } from './assign-contact-owner'\nimport { captureHostContact } from './capture-host-contact'\nimport { readCrmPicklist } from './read-picklist'\nimport { handOffLeadRecords } from '@aglyn/tenant-runtime/hand-off-lead'\nimport { readContainerIds } from '@aglyn/aglyn/app-utils/container-membership'\n\n/** Who is converting, as far as the writes need to know. */\nexport interface LeadConvertActor {\n /**\n * What `createdByUid` records on every record the conversion opens, and\n * who the audit line names. A member's uid, or the literal `'api'` for a\n * key — the same attribution the REST creates stamp.\n */\n uid: string\n email?: string | null\n /**\n * `member`: a signed-in person, who becomes the owner when nobody else\n * does. `api`: a key, which cannot own a record — see the header.\n */\n kind: 'member' | 'api'\n /**\n * For an `api` actor, the key's name — what the audit line attributes the\n * conversion to, since a key has no address to be named by.\n */\n apiKeyName?: string | null\n}\n\n/** What a conversion is asked to do. Validated by the door before it gets here. */\nexport interface ConvertHostLeadInput {\n firestore: FirebaseFirestore.Firestore\n hostId: string\n orgId: string\n /** The org document, as the door resolved it. */\n org: Record<string, unknown>\n /** `hosts/{hostId}/leads/{leadId}` — the document id, which is the person key. */\n leadId: string\n actor: LeadConvertActor\n /**\n * Who owns the resulting contact (and deal). Defaults to the lead's own\n * owner; failing that the org's assignment rules and the site's default\n * owner decide (AGL-2618), and failing those a member actor — somebody\n * converted this person, and a record with no owner is one nobody follows\n * up.\n */\n ownerUid?: string\n /** Link an existing `orgs/{orgId}/companies/{companyId}`. */\n companyId?: string\n /** Or create one. Ignored when `companyId` is given. Already normalized. */\n createCompany?: { name: string; domain: string | null } | null\n /** Open a deal in the org's default pipeline. Already normalized. */\n deal?: {\n title: string\n amountCents: number | null\n /** Lowercase ISO 4217. */\n currency: string\n /** A stage of the default pipeline; its first open stage when absent. */\n stageId?: string\n /**\n * The deal's Type, as the door judged it against the org's\n * `opportunityType` list (AGL-3516); absent takes the list's default.\n */\n type?: string\n } | null\n}\n\n/**\n * Why a conversion did not happen. Each is a state of the data, not a fault\n * of the request — the door has already refused a malformed body — and each\n * leaves the lead unconverted so a retry after the remedy finds it where it\n * was.\n */\nexport type ConvertHostLeadRefusal =\n /** No document at `hosts/{hostId}/leads/{leadId}`. */\n | 'unknown-lead'\n /** The lead's address cannot become a contact. */\n | 'no-email'\n /**\n * The capture door produced no contact — it swallows its own failures and\n * drops a creation past a Free org's audience band. Nothing was written.\n */\n | 'contact-not-created'\n /**\n * The site holds an erasure for the lead's address (AGL-2623): the person\n * asked to be forgotten, and the capture door refuses to rebuild a record\n * for them — by a conversion as by a form. Nothing was written, and the\n * lead stays as it was; the erasure removes it when it runs.\n */\n | 'erased'\n /** The CRM records band is full (AGL-2611); the contact stands, nothing more was opened. */\n | 'band-full'\n /** `companyId` names no company in this organization. The contact stands. */\n | 'unknown-company'\n /** The default pipeline has no stages to open a deal in. The contact stands. */\n | 'no-stages'\n\nexport type ConvertHostLeadResult =\n | {\n ok: true\n contactId: string\n companyId?: string\n dealId?: string\n /** The lead was converted before this call; nothing was created now. */\n alreadyConverted: boolean\n }\n | { ok: false; reason: ConvertHostLeadRefusal }\n\n/** The stamp the conversion leaves on the lead — see `CrmLeadFields`. */\ntype LeadConversionStamp = Required<\n Pick<CrmLeadFields, 'status' | 'statusLabel' | 'convertedContactId' | 'convertedAtMs'>\n> &\n Pick<CrmLeadFields, 'dealId' | 'companyId' | 'ownerUid'>\n\n/**\n * The stage a new deal opens in.\n *\n * The caller's choice when the pipeline has it, otherwise the first OPEN\n * stage by order — a deal created by converting a lead is by definition not\n * yet won or lost, so defaulting to a closed stage would record an outcome\n * nobody reached. A pipeline with no open stage at all falls back to its\n * first stage rather than refusing: the merchant edited their pipeline into\n * that shape, and a conversion that failed because of it would read as a bug\n * in the lead.\n */\nexport function stageForNewDeal(\n pipeline: Pick<CrmPipeline, 'stages'>,\n requestedStageId: string | undefined,\n): CrmDealStage | null {\n const stages = [...(pipeline.stages ?? [])].sort((a, b) => a.order - b.order)\n if (!stages.length) return null\n const requested = requestedStageId\n ? stages.find((stage) => stage.id === requestedStageId)\n : undefined\n return requested ?? stages.find((stage) => stage.kind === 'open') ?? stages[0]\n}\n\n/**\n * The lead's MOST RECENT campaign, which becomes the deal's Primary\n * Campaign Source as Salesforce's conversion makes it. A lead keeps its\n * campaigns as one `campaignIds` array that every writer extends with\n * `arrayUnion`, which appends — so the last id is the campaign the lead was\n * filed under last. `undefined` for a lead filed under none.\n */\nexport function leadLatestCampaignId(lead: Readonly<Record<string, unknown>>): string | undefined {\n const ids = readContainerIds(lead as Record<string, unknown>, 'campaign')\n return ids.length ? ids[ids.length - 1] : undefined\n}\n\n/** Whether a stored field holds nothing a fill would overwrite. */\nconst blankField = (value: unknown): boolean =>\n value === undefined ||\n value === null ||\n (typeof value === 'string' && !value.trim()) ||\n (typeof value === 'object' && !Array.isArray(value) && !Object.keys(value as object).length)\n\n/**\n * The account fields a lead hands its company (AGL-3513), normalized and\n * judged as every company door judges them: the lead's industry and rating\n * against the org's lists, its lead source as the Account Source, and its\n * revenue, currency, head count, fax, website, phone and address (as the\n * billing address). With `current` — a company the org already holds —\n * only the fields it leaves blank are answered, and the picklists it does\n * not name keep no default; without it, the company is being created and\n * each picklist the lead leaves empty starts from its list's default. A\n * value a list no longer holds is left behind rather than refusing the\n * conversion.\n */\nexport function leadCompanyFields(\n lead: Readonly<Record<string, unknown>>,\n lists: Readonly<Partial<Record<CrmPicklistId, CrmPicklist>>>,\n current: Readonly<Record<string, unknown>> | null,\n): Record<string, unknown> {\n const fill = (field: string) => !current || blankField(current[field])\n const offered = (value: unknown) => (blankField(value) ? undefined : value)\n const fields: Record<string, unknown> = {}\n const requested: Record<string, unknown> = {}\n const picklistSource: Record<string, unknown> = {\n industry: lead['industry'],\n rating: lead['rating'],\n accountSource: lead['leadSource'],\n }\n for (const { field } of CRM_COMPANY_PICKLIST_FIELDS) {\n const value = offered(picklistSource[field])\n if (value !== undefined && fill(field)) requested[field] = value\n }\n const picklists = judgeCrmCompanyPicklists(lists, requested, {\n current: current ?? {},\n created: !current,\n })\n for (const [field, value] of Object.entries(picklists.values)) {\n if (value && !picklists.errors[field]) fields[field] = value\n }\n const account: Record<string, unknown> = {}\n for (const field of ['annualRevenueCents', 'numberOfEmployees', 'fax'] as const) {\n const value = offered(lead[field])\n if (value !== undefined && fill(field)) account[field] = value\n }\n // The revenue's currency travels with the revenue it describes.\n if (account['annualRevenueCents'] !== undefined && offered(lead['currency']) !== undefined) {\n account['currency'] = lead['currency']\n }\n const read = readCrmCompanyAccountFields(account)\n for (const [field, value] of Object.entries(read.values)) {\n if (value !== null && value !== undefined && !read.errors[field]) fields[field] = value\n }\n // Already in the company's shapes: the lead normalized them as a company does.\n for (const [field, from] of [\n ['website', 'website'],\n ['phone', 'phone'],\n ['address', 'address'],\n ] as const) {\n const value = offered(lead[from])\n if (value !== undefined && fill(field)) fields[field] = value\n }\n return fields\n}\n\n/** The org's lists behind every company picklist field, for {@link leadCompanyFields}. */\nasync function readCompanyPicklists(\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n): Promise<Partial<Record<CrmPicklistId, CrmPicklist>>> {\n const ids = [...new Set(CRM_COMPANY_PICKLIST_FIELDS.map((entry) => entry.picklistId))]\n const lists = await Promise.all(ids.map((id) => readCrmPicklist(firestore, orgId, id)))\n return Object.fromEntries(ids.map((id, at) => [id, lists[at]]))\n}\n\n/** A stage's kind as the deal's denormalized status. */\nfunction dealStatusForStage(stage: CrmDealStage): CrmDealStatus {\n return stage.kind === 'won' ? 'won' : stage.kind === 'lost' ? 'lost' : 'open'\n}\n\n/**\n * Convert one lead. Throws only on infrastructure failure; every refusal\n * the data can produce is a `ConvertHostLeadResult`.\n */\nexport async function convertHostLead(\n input: ConvertHostLeadInput,\n): Promise<ConvertHostLeadResult> {\n const { firestore, hostId, orgId, org, leadId, actor, createCompany, deal } = input\n const requestedCompanyId = String(input.companyId ?? '').trim()\n // The lead's one home, on the org (AGL-3275). `orgId` is already resolved\n // by the caller, so this needs no second lookup to find it.\n const leadRef = firestore\n .collection('orgs')\n .doc(orgId)\n .collection('leads')\n .doc(leadId)\n const leadSnapshot = await leadRef.get()\n if (!leadSnapshot.exists) return { ok: false, reason: 'unknown-lead' }\n const lead = (leadSnapshot.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n if (lead.convertedContactId) {\n return {\n ok: true,\n contactId: lead.convertedContactId,\n ...(lead.companyId ? { companyId: lead.companyId } : {}),\n ...(lead.dealId ? { dealId: lead.dealId } : {}),\n alreadyConverted: true,\n }\n }\n const email = normalizeContactEmail(lead['email'])\n if (!email) return { ok: false, reason: 'no-email' }\n\n const group = await consentGroupForSite(hostId, org)\n const visibleTo = crmScopeTokens(org, group)\n /*\n * What the CALLER may read, for the company-by-domain reuse below: their\n * group's own tokens plus `org`, exactly the set the console's listeners\n * filter on. A company outside it is one they could not open, so linking\n * a contact to it would point at a record the page then 404s on.\n */\n const readableTokens = new Set<string>([\n ORG_SCOPE_TOKEN,\n ...consentGroupScope(group),\n ])\n /*\n * THE OWNER A PERSON CHOSE, when one did: the converter's pick, else\n * whoever was already working the lead. Handed to the capture as the\n * facet's owner, which is what tells the capture's own assignment pass\n * to stand down — a person's choice outranks a rule. When nobody chose,\n * the facet names no owner and the pass runs the org's rules and the\n * site's default for a contact it creates (AGL-2618).\n */\n const pickedOwner = String(input.ownerUid ?? '').trim()\n const chosenOwner =\n pickedOwner || (typeof lead.ownerUid === 'string' ? lead.ownerUid : '')\n const now = Date.now()\n const leadName = typeof lead['name'] === 'string' ? lead['name'] : undefined\n\n /*==========================================\n * 1. THE CONTACT — through the one door that dedupes and meters.\n *\n * `source: 'manual'` because a person converted this lead by hand, and\n * the source filter should say so; the interaction names the lead so the\n * contact's timeline can be walked back to what was captured. The facet\n * carries the stage and the owner, which is what makes this a sales\n * record rather than another form capture.\n *\n * THE LEAD'S OWN PROFILE TRAVELS WITH IT (AGL-3233), the way Salesforce\n * hands a lead's fields to the contact: phone, title, lead source, address, the\n * company as text (until step 2 links a record, whose own name then\n * replaces it), tags, and the marketing basis the lead recorded — the\n * person ticked the box on the lead, and a conversion must not be the\n * moment they lose it. Only what the lead holds is handed over; a value\n * the contact already carries is the contact's, because the capture door\n * writes a profile field only where the facet has none.\n *=========================================*/\n const leadTags = Array.isArray(lead.tags) ? lead.tags.map(String) : []\n const leadConsent = readMarketingBasis(lead, soloConsentGroup(hostId))\n /*\n * WHICH SITES THE CONTACT'S GRANT COVERS (AGL-3320): the lead's, never\n * more. The capture door records a fresh grant, so it is handed the group's\n * disclosure key only when the lead ALREADY holds a grant given under the\n * group as it stands, at every site it names — a lead captured by one site\n * before the group existed, or before a site joined, converts to a contact\n * that site alone may mail, exactly as the lead was.\n */\n const leadDisclosure = grantedUnderConsentGroup(lead, group)\n ? consentGroupDisclosureKey(group)\n : null\n const captured = await captureHostContact({\n hostId,\n email,\n ...(leadName ? { name: leadName } : {}),\n source: 'manual',\n interaction: { summary: 'Converted from a lead', refId: leadId },\n // Who converted it, for the runs its contactCreated sets off (AGL-3376).\n actor:\n actor.kind === 'member'\n ? { kind: 'member', uid: actor.uid, email: actor.email ?? null }\n : { kind: 'apiKey', apiKeyName: actor.apiKeyName ?? null },\n ...(leadConsent.basis === 'granted' ? { marketingConsent: true } : {}),\n ...(leadConsent.basis === 'granted' && leadDisclosure\n ? { disclosedConsentGroup: leadDisclosure }\n : {}),\n ...(leadTags.length ? { tags: leadTags } : {}),\n facet: {\n lifecycleStage: 'sales-qualified',\n ...(chosenOwner ? { ownerUid: chosenOwner } : {}),\n ...(typeof lead.phone === 'string' && lead.phone ? { phone: lead.phone } : {}),\n ...(typeof lead.jobTitle === 'string' && lead.jobTitle ? { jobTitle: lead.jobTitle } : {}),\n // Salesforce's Lead Source travels to the contact (AGL-3298).\n ...(typeof lead.leadSource === 'string' && lead.leadSource\n ? { leadSource: lead.leadSource }\n : {}),\n ...(lead.address && typeof lead.address === 'object' ? { address: lead.address } : {}),\n ...(typeof lead.company === 'string' && lead.company\n ? { companyName: lead.company }\n : {}),\n // Salesforce's standard lead fields (AGL-3513), as the contact keeps them.\n ...Object.fromEntries(\n (['salutation', 'firstName', 'lastName', 'mobilePhone', 'fax'] as const)\n .filter((key) => typeof lead[key] === 'string' && lead[key])\n .map((key) => [key, lead[key]]),\n ),\n // Only ever `true`: a lead that never said is not a person who asked to be called.\n ...(lead.doNotCall === true ? { doNotCall: true } : {}),\n },\n })\n /*\n * The one verdict of the door's that is named here. Its other refusals —\n * the band, an address it will not take, its own swallowed error — all\n * read the same from this side: no row to find below, answered as\n * `contact-not-created`. An erasure is different in kind: not a limit the\n * org can lift or a fault in the request but a decision the workspace made\n * about this person, and a caller told \"the band may be full\" would retry\n * against it forever.\n */\n if (captured && 'refused' in captured && captured.refused === 'erased') {\n return { ok: false, reason: 'erased' }\n }\n const contactsRef = await orgDataCollectionForHost(hostId, 'contacts')\n /*\n * The same resolution the capture door just made (AGL-2625): the\n * address index first, so a lead whose address is an ALTERNATE on a\n * merged contact converts into the survivor the capture landed on\n * rather than reading as a contact the band dropped.\n */\n const contactSnapshot = await findContactByEmail(contactsRef, email)\n if (!contactSnapshot) return { ok: false, reason: 'contact-not-created' }\n const contactRef = contactSnapshot.ref\n const contactId = contactSnapshot.id\n const contactData = (contactSnapshot.data() ?? {}) as Record<string, unknown>\n const orgRef = firestore.collection('orgs').doc(orgId)\n /*\n * THE RECORDS BAND (AGL-2611), asked before each record this conversion\n * CREATES. The contact went through the capture door, which refuses at\n * the band on its own; a company and a deal are records of the same\n * band, and on a Free org at its hundred the honest answer is the one\n * the drawers give — refuse, write nothing more, and leave the lead\n * unconverted so a retry after the upgrade finds it where it was. The\n * contact step 1 captured stays: it is one person's record, and a second\n * conversion merges onto it rather than making another. Measured fresh\n * each time because the company created in step 2 is itself a record.\n */\n const bandFull = async () => {\n const room = await crmRecordsQuotaForOrg(org as never, orgRef)\n return !room.allowed\n }\n\n /*\n * WHOSE THE CONTACT IS, now that it exists. The capture wrote the chosen\n * owner, or its pass assigned one to a contact it created; a contact\n * the org already held with no owner has had neither, so the same pass\n * is asked once more — it touches only a record with no owner and\n * answers `unchanged` for one that has — and a member actor is the last\n * resort. A colleague the converter picked is told; the lead's existing\n * owner and the caller are not, having chosen for themselves.\n */\n let ownerUid = readContactFacet(contactData, group.groupId).ownerUid ?? ''\n if (!ownerUid) {\n const assigned = await assignOwnerForCapture({\n hostId,\n contactId,\n email,\n source: 'manual',\n actorUid: actor.kind === 'member' ? actor.uid : null,\n })\n if (assigned.outcome !== 'none') ownerUid = assigned.ownerUid\n }\n if (!ownerUid && actor.kind === 'member') {\n ownerUid = actor.uid\n await contactRef.update({\n [contactFacetPath(group.groupId, 'ownerUid')]: ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n // The owner is what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(contactRef, 'contacts')\n } else if (ownerUid && pickedOwner && pickedOwner === ownerUid) {\n await notifyRecordAssigned({\n hostId,\n orgId,\n ownerUid,\n actorUid: actor.kind === 'member' ? actor.uid : null,\n record: { kind: 'contact', id: contactId },\n who: leadName || email,\n })\n }\n\n /*==========================================\n * 2. THE COMPANY — linked, found by domain, or created.\n *=========================================*/\n let companyId: string | undefined\n /** The company the conversion found rather than made — its blanks are filled below. */\n let existingCompany: FirebaseFirestore.DocumentSnapshot | undefined\n if (requestedCompanyId) {\n const companySnapshot = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .doc(requestedCompanyId)\n .get()\n if (!companySnapshot.exists) return { ok: false, reason: 'unknown-company' }\n companyId = requestedCompanyId\n existingCompany = companySnapshot\n } else if (createCompany) {\n /*\n * FIND BEFORE CREATE. The NAME first (AGL-3233): a lead names its\n * company as text, and a company the org already files under that\n * name is the account, whatever domain it carries. Then the domain,\n * the key two contacts at one business share — a second company\n * document for `acme.com` would split the account the key exists to\n * join. Only a company the caller can see counts as found — see\n * `readableTokens`.\n */\n const visibleAmong = (\n snapshots: readonly FirebaseFirestore.QueryDocumentSnapshot[],\n ): FirebaseFirestore.QueryDocumentSnapshot | undefined =>\n snapshots.find((snapshot) => {\n const tokens = snapshot.get('visibleTo')\n return (\n Array.isArray(tokens) &&\n tokens.some((token) => readableTokens.has(String(token)))\n )\n })\n const byName = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .where('nameLower', '==', createCompany.name.toLowerCase())\n .limit(5)\n .get()\n existingCompany = visibleAmong(byName.docs)\n if (!existingCompany && createCompany.domain) {\n const byDomain = await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .where('domain', '==', createCompany.domain)\n .limit(5)\n .get()\n existingCompany = visibleAmong(byDomain.docs)\n }\n companyId = existingCompany?.id\n if (!companyId) {\n if (await bandFull()) return { ok: false, reason: 'band-full' }\n const company: Record<string, unknown> = {\n // The lead's account fields, whole (AGL-3513).\n ...leadCompanyFields(lead, await readCompanyPicklists(firestore, orgId), null),\n ...nameSearchFields(createCompany.name),\n ...(createCompany.domain ? { domain: createCompany.domain } : {}),\n ...(ownerUid ? { ownerUid } : {}),\n visibleTo,\n hostId,\n createdByUid: actor.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n const created = await orgRef.collection(CRM_COLLECTIONS.companies).add({\n ...company,\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', company),\n })\n companyId = created.id\n }\n }\n if (companyId && existingCompany) {\n /*\n * A company the org already held takes the lead's account fields only\n * where it has none (AGL-3513) — and is restamped, because its\n * industry, rating and account source are what its list filters by.\n */\n const filled = leadCompanyFields(\n lead,\n await readCompanyPicklists(firestore, orgId),\n (existingCompany.data() ?? {}) as Record<string, unknown>,\n )\n if (Object.keys(filled).length) {\n await existingCompany.ref.update({ ...filled, updatedAt: FieldValue.serverTimestamp() })\n await restampCrmListFieldsAt(existingCompany.ref, 'companies')\n }\n }\n if (companyId) {\n /*\n * The one link writer (AGL-2613): the holder's facet field the contact\n * record reads, the queryable top-level mirror the company's contacts\n * card filters on, and the company's contacts count — planned from\n * the row as the capture left it, so a person the org already held\n * under another company is MOVED rather than counted twice.\n */\n await writeContactCompanyLink({\n firestore,\n contactRef,\n contact: contactData,\n companiesRef: orgRef.collection(CRM_COLLECTIONS.companies),\n groupId: group.groupId,\n companyId,\n })\n }\n\n /*==========================================\n * 3. THE DEAL — in the default pipeline, seeded if the org has none.\n *=========================================*/\n let dealId: string | undefined\n if (deal) {\n const pipelinesRef = orgRef.collection(CRM_COLLECTIONS.pipelines)\n let pipelineId: string\n let pipeline: Pick<CrmPipeline, 'stages'>\n const defaults = await pipelinesRef\n .where('isDefault', '==', true)\n .limit(1)\n .get()\n if (!defaults.empty) {\n pipelineId = defaults.docs[0].id\n pipeline = defaults.docs[0].data() as CrmPipeline\n } else {\n /*\n * The deals section's own read shape, so the two agree on which\n * pipeline \"the org's pipeline\" is when none is flagged default: the\n * first by document id of a bounded window. Seeding a SECOND pipeline\n * here while one exists unflagged would leave the merchant with two\n * boards and their deals split between them.\n */\n const any = await pipelinesRef\n .orderBy(FieldPath.documentId())\n .limit(20)\n .get()\n if (!any.empty) {\n pipelineId = any.docs[0].id\n pipeline = any.docs[0].data() as CrmPipeline\n } else {\n // Exactly what the Deals section seeds: one `Sales` pipeline\n // carrying a COPY of the default stages, flagged default.\n const seeded: Omit<CrmPipeline, 'createdAt' | 'updatedAt'> = {\n name: 'Sales',\n stages: [...DEFAULT_DEAL_STAGES],\n isDefault: true,\n visibleTo,\n hostId,\n }\n const created = await pipelinesRef.add({\n ...seeded,\n createdByUid: actor.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n pipelineId = created.id\n pipeline = seeded\n }\n }\n const stage = stageForNewDeal(pipeline, deal.stageId)\n if (!stage) return { ok: false, reason: 'no-stages' }\n if (await bandFull()) return { ok: false, reason: 'band-full' }\n const type =\n deal.type ??\n crmPicklistDefaultLabel(await readCrmPicklist(firestore, orgId, 'opportunityType')) ??\n undefined\n const leadSource = typeof lead.leadSource === 'string' ? lead.leadSource.trim() : ''\n // The Primary Campaign Source: the lead's most recent campaign (AGL-3513).\n const campaignId = leadLatestCampaignId(lead)\n const dealRecord: Record<string, unknown> = {\n title: deal.title,\n titleLower: deal.title.toLowerCase(),\n pipelineId,\n stageId: stage.id,\n status: dealStatusForStage(stage),\n ...(deal.amountCents !== null ? { amountCents: deal.amountCents } : {}),\n currency: deal.currency,\n stageChangedAtMs: now,\n forecastCategory: dealStageForecastCategory(stage),\n ...(type ? { type } : {}),\n ...(leadSource ? { leadSource } : {}),\n ...(campaignId ? { campaignId } : {}),\n ...(ownerUid ? { ownerUid } : {}),\n contactId,\n // The converted person is the deal's Primary contact role, with no\n // role yet, as Salesforce's conversion makes them (AGL-3521).\n contactRoles: [{ contactId, primary: true }],\n ...(companyId ? { companyId } : {}),\n visibleTo,\n hostId,\n createdByUid: actor.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n const created = await orgRef.collection(CRM_COLLECTIONS.deals).add({\n ...dealRecord,\n // What the Deals list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('deals', dealRecord),\n })\n dealId = created.id\n }\n\n /*==========================================\n * 4. THE LEAD — stamped last, once everything it names exists.\n *=========================================*/\n const stamp: LeadConversionStamp = {\n status: 'qualified',\n // The org's label for Qualified, beside the meaning (AGL-3512).\n statusLabel: crmLeadStatusLabelFor(\n await readCrmPicklist(firestore, orgId, CRM_LEAD_STATUS_PICKLIST),\n 'qualified',\n ),\n convertedContactId: contactId,\n convertedAtMs: now,\n ...(ownerUid ? { ownerUid } : {}),\n ...(companyId ? { companyId } : {}),\n ...(dealId ? { dealId } : {}),\n }\n await leadRef.update({ ...stamp, updatedAt: FieldValue.serverTimestamp() })\n\n /*\n * WHAT THE LEAD HANDS OVER (AGL-3233): its activities and tasks gain the\n * contact, and every plugin with a listener — Sequences, for an\n * enrollment naming the lead — re-points its own records. After the\n * stamp, so a listener reading the lead finds the conversion on it; it\n * never throws.\n */\n await handOffLeadRecords({\n firestore,\n orgId,\n hostId,\n leadId,\n contactId,\n email,\n by: actor.kind === 'member' ? 'member' : 'api',\n })\n\n /*\n * The audit line, from the function that did the work (AGL-2622): the\n * conversion is one act however many records it opened, so it is one\n * entry, on the lead, in the feed of the site that holds the lead. A\n * repeat call answered above with the ids it already had wrote nothing\n * and logs nothing.\n */\n await logHostActivity(\n hostId,\n {\n uid: actor.uid,\n email: actor.email ?? null,\n ...(actor.apiKeyName ? { apiKeyName: actor.apiKeyName } : {}),\n },\n 'Converted lead',\n { type: 'lead', id: leadId, name: String(lead['name'] ?? '') || email },\n )\n\n return {\n ok: true,\n contactId,\n ...(companyId ? { companyId } : {}),\n ...(dealId ? { dealId } : {}),\n alreadyConverted: false,\n }\n}\n"],"names":["consentGroupDisclosureKey","consentGroupScope","CRM_COLLECTIONS","CRM_LEAD_STATUS_PICKLIST","crmLeadStatusLabelFor","CRM_COMPANY_PICKLIST_FIELDS","crmPicklistDefaultLabel","judgeCrmCompanyPicklists","readCrmCompanyAccountFields","crmScopeTokens","dealStageForecastCategory","DEFAULT_DEAL_STAGES","grantedUnderConsentGroup","nameSearchFields","contactFacetPath","normalizeContactEmail","ORG_SCOPE_TOKEN","readContactFacet","crmNewRecordListFields","readMarketingBasis","soloConsentGroup","consentGroupForSite","crmRecordsQuotaForOrg","findContactByEmail","logHostActivity","orgDataCollectionForHost","restampCrmListFieldsAt","writeContactCompanyLink","FieldPath","FieldValue","assignOwnerForCapture","notifyRecordAssigned","captureHostContact","readCrmPicklist","handOffLeadRecords","readContainerIds","stageForNewDeal","pipeline","requestedStageId","requested","stages","sort","a","b","order","length","find","stage","id","undefined","kind","leadLatestCampaignId","lead","ids","blankField","value","trim","Array","isArray","Object","keys","leadCompanyFields","lists","current","fill","field","offered","fields","picklistSource","industry","rating","accountSource","picklists","created","entries","values","errors","account","read","from","readCompanyPicklists","firestore","orgId","Set","map","entry","picklistId","Promise","all","fromEntries","at","dealStatusForStage","convertHostLead","input","leadSnapshot","actor","contactSnapshot","hostId","org","leadId","createCompany","deal","requestedCompanyId","String","companyId","leadRef","collection","doc","get","exists","ok","reason","data","convertedContactId","contactId","dealId","alreadyConverted","email","group","visibleTo","readableTokens","pickedOwner","ownerUid","chosenOwner","now","Date","leadName","leadTags","tags","leadConsent","leadDisclosure","captured","name","source","interaction","summary","refId","uid","apiKeyName","basis","marketingConsent","disclosedConsentGroup","facet","lifecycleStage","phone","jobTitle","leadSource","address","company","companyName","filter","key","doNotCall","refused","contactsRef","contactRef","ref","contactData","orgRef","bandFull","room","allowed","groupId","assigned","actorUid","outcome","update","updatedAt","serverTimestamp","record","who","existingCompany","companySnapshot","companies","visibleAmong","snapshots","snapshot","tokens","some","token","has","byName","where","toLowerCase","limit","docs","domain","byDomain","createdByUid","createdAt","add","filled","contact","companiesRef","pipelinesRef","pipelines","pipelineId","defaults","empty","any","orderBy","documentId","seeded","isDefault","stageId","type","campaignId","dealRecord","title","titleLower","status","amountCents","currency","stageChangedAtMs","forecastCategory","contactRoles","primary","deals","stamp","statusLabel","convertedAtMs","by"],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
1
2
|
/**
|
|
2
3
|
* @license
|
|
3
4
|
* Copyright 2026 Aglyn LLC
|
|
@@ -39,10 +40,11 @@
|
|
|
39
40
|
*
|
|
40
41
|
* Never throws: the capture that made the contact has already happened,
|
|
41
42
|
* and a lead that could not be stamped is one somebody converts by hand.
|
|
42
|
-
*/ import { isCrmLeadOpen, normalizeContactEmail, personKey } from "@aglyn/aglyn/server";
|
|
43
|
+
*/ import { CRM_LEAD_STATUS_PICKLIST, crmLeadStatusLabelFor, isCrmLeadOpen, normalizeContactEmail, personKey } from "@aglyn/aglyn/server";
|
|
43
44
|
import { firebaseAdmin, getOrgForHost, leadForWrite } from "@aglyn/tenant-data-admin";
|
|
44
45
|
import { handOffLeadRecords } from "@aglyn/tenant-runtime/hand-off-lead";
|
|
45
46
|
import { FieldValue } from "firebase-admin/firestore";
|
|
47
|
+
import { readCrmPicklist } from "./read-picklist.js";
|
|
46
48
|
/**
|
|
47
49
|
* Stamp the site's open lead for `email` as converted onto `contactId`.
|
|
48
50
|
* `true` when a lead was stamped; `false` when the site holds none, holds
|
|
@@ -63,16 +65,21 @@ import { FieldValue } from "firebase-admin/firestore";
|
|
|
63
65
|
if (!snapshot.exists) return false;
|
|
64
66
|
const lead = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
|
|
65
67
|
if (lead.convertedContactId || !isCrmLeadOpen(lead)) return false;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
// The org's label for Qualified, beside the meaning (AGL-3512).
|
|
69
|
+
const resolved = await getOrgForHost(input.hostId);
|
|
70
|
+
const statuses = resolved ? await readCrmPicklist(firestore, resolved.orgId, CRM_LEAD_STATUS_PICKLIST) : null;
|
|
71
|
+
await leadRef.set(_extends({
|
|
72
|
+
status: 'qualified'
|
|
73
|
+
}, statuses ? {
|
|
74
|
+
statusLabel: crmLeadStatusLabelFor(statuses, 'qualified')
|
|
75
|
+
} : {}, {
|
|
68
76
|
convertedContactId: input.contactId,
|
|
69
77
|
convertedAtMs: Date.now(),
|
|
70
78
|
convertedBy: input.by,
|
|
71
79
|
updatedAt: FieldValue.serverTimestamp()
|
|
72
|
-
}, {
|
|
80
|
+
}), {
|
|
73
81
|
merge: true
|
|
74
82
|
});
|
|
75
|
-
const resolved = await getOrgForHost(input.hostId);
|
|
76
83
|
if (resolved) {
|
|
77
84
|
await handOffLeadRecords({
|
|
78
85
|
firestore,
|