@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/assign-contact-owner.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n assignmentRuleMatches,\n consentGroupForHost,\n contactFacetPath,\n type ContactSource,\n CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH,\n type CrmAssignmentTarget,\n personKey,\n readContactFacet,\n readCrmAssignmentSettings,\n roundRobinOrder,\n} from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n getHostDocAdmin,\n getOrgForHost,\n hostDisplayName,\n notifyUsers,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/**\n * WHO A NEW RECORD BELONGS TO — the one place an owner is decided on the\n * server (AGL-2618).\n *\n * Two entry points over one transaction. `assignOwnerForCapture` is the\n * capture door's pass: it runs the org's assignment rules in order, falls\n * back to the capturing site's default owner, and touches only a record\n * that has NO owner — a door that named one (the console's drawer, an\n * import column, a conversion with a picked owner) has expressed a person's\n * choice. `reassignContactOwner` is the automation's: an author who put\n * \"assign an owner\" on a stage change means to reassign, and the step hands\n * this either a member or the rotation.\n *\n * ## One transaction, because the pointer is shared\n *\n * The round-robin pool's pointer lives on the org document, and two\n * captures landing together must take two different members. Firestore\n * transactions serialize on what they read, so the org document, the\n * contact and the roster documents are all read inside the transaction and\n * the pointer is advanced in the same commit that writes the owner: a\n * concurrent capture that read the same pointer retries and reads the moved\n * one. The alternative — read the pointer, decide, then write — is exactly\n * the read-then-write shape that lets two captures pick the same person.\n *\n * ## The roster is checked, once per member per attempt\n *\n * A rule may name a member who has since left, and a pool may hold one. A\n * rule naming nobody on the roster is passed over rather than stopping the\n * pass — the next rule may well assign — and a pool member who is gone is\n * skipped for the next in the order. `orgs/{orgId}/members/{uid}` is keyed\n * by uid, so the check is one document read per candidate, and only the\n * roster is consulted: a project-level Auth lookup would admit people who\n * are not on this organization at all (AGL-1122).\n *\n * ## The lead follows the contact\n *\n * The capture doors that file a lead (`addHostLead`: a form routed to leads,\n * a sign-up, a booking) key it by `personKey(email)`, so the lead's document\n * is addressable from the capture alone. When it exists it is given the same\n * owner in the same transaction; two doors write the lead AFTER the contact\n * they `void`ed, so for those sources the mirror is retried a few times over\n * the next seconds, detached, and the person's Leads list shows the owner\n * as soon as the lead is there.\n *\n * ## The owner is told, unless they chose themselves\n *\n * One console notification per assignment, for the record the recipient\n * will open: the lead when the site holds one for this person, the contact\n * otherwise. Not when the actor IS the new owner — a rep who converts a\n * lead into their own contact has not been handed anything.\n *\n * ## Never rejects\n *\n * The capture has already succeeded and a form submission must not fail\n * because the CRM could not decide who follows up; a failure is logged and\n * answered as `none`, and the record is exactly as assignable by hand as it\n * was before this existed. The automation executor turns a `none` into the\n * step's recorded error.\n */\n\nexport interface OwnerAssignmentInput {\n hostId: string\n /** `orgs/{orgId}/contacts/{contactId}` — the record the owner is written on. */\n contactId: string\n /** The contact's address, which keys the lead and names the person in the notification. */\n email: string\n /**\n * The signed-in person making the assignment, when there is one. Never\n * notified about their own choice; absent for a capture and an automation.\n */\n actorUid?: string | null\n}\n\nexport interface CaptureAssignmentInput extends OwnerAssignmentInput {\n source: ContactSource\n /** The form the capture came through, for a rule conditioned on it. */\n formId?: string | null\n /** The tags the capture carried; the contact's own are read from the facet. */\n tags?: readonly string[]\n}\n\nexport interface ReassignmentInput extends OwnerAssignmentInput {\n assign: CrmAssignmentTarget\n}\n\n/** Why an assignment assigned nobody — the automation reports these verbatim. */\nexport type OwnerAssignmentRefusal =\n | 'no-org'\n | 'no-contact'\n | 'no-lead'\n | 'no-rule'\n | 'empty-pool'\n | 'not-a-member'\n | 'failed'\n\n/** What decided an assignment: a rule, the site default, the step's member, or the rotation. */\nexport type OwnerAssignedBy = 'rule' | 'default' | 'member' | 'roundRobin'\n\nexport type OwnerAssignment =\n | {\n outcome: 'assigned'\n ownerUid: string\n by: OwnerAssignedBy\n /** The rule that decided it, when one did. */\n ruleId?: string\n /** The lead document existed and now carries the owner too. */\n leadMirrored: boolean\n /** A notification was fanned out to the new owner. */\n notified: boolean\n }\n /** The record already had this owner (a capture), or the same one was named again (a reassignment). */\n | { outcome: 'unchanged'; ownerUid: string }\n | { outcome: 'none'; reason: OwnerAssignmentRefusal }\n\n/** The words the run history and the log give each refusal. */\nexport const OWNER_ASSIGNMENT_REFUSALS: Record<OwnerAssignmentRefusal, string> = {\n 'no-org': 'this site has no organization',\n 'no-contact': 'the contact no longer exists',\n 'no-lead': 'the lead no longer exists',\n 'no-rule': 'no assignment rule matched and the site has no default owner',\n 'empty-pool': 'the round-robin pool has nobody on the roster in it',\n 'not-a-member': 'the owner named is not on the team',\n failed: 'the owner could not be assigned',\n}\n\n/**\n * The doors whose lead lands after the contact they `void`ed — the ones the\n * mirror is worth retrying for. An order, an import, a manual add and an\n * API call file no lead, and a retry for them would be reads for nothing.\n */\nconst LEAD_FILING_SOURCES: ReadonlySet<ContactSource> = new Set([\n 'form',\n 'member',\n 'booking',\n])\n\n/** How long the detached lead mirror waits between its looks, in order. */\nconst LEAD_MIRROR_RETRY_MS: readonly number[] = [1_000, 2_000, 3_000]\n\ntype Policy =\n | { kind: 'capture'; input: CaptureAssignmentInput }\n | { kind: 'reassign'; input: ReassignmentInput }\n\ninterface Decision {\n ownerUid: string\n by: OwnerAssignedBy\n ruleId?: string\n /** The pointer moves only when the rotation chose. */\n advancePointer: boolean\n}\n\n/**\n * The console path the owner's notification opens, in the `/{hostDocId}/rest`\n * shape every host notification uses — the console rewrites it to\n * `/{orgSlug}/hosts/{subdomain}/rest` when the link is followed, which is\n * why `hostId` and `orgId` travel on the document. The same spelling the\n * CRM's task notification uses; the hub's own route builder lives in the\n * plugin, which this layer cannot import.\n */\nfunction recordLink(hostId: string, record: AssignedRecord): string {\n const section = record.kind === 'contact' ? 'contacts' : 'leads'\n return `/${hostId}/crm/${section}/${encodeURIComponent(record.id)}`\n}\n\n/** The record an assignment notification opens. */\nexport type AssignedRecord =\n | { kind: 'contact'; id: string }\n | { kind: 'lead'; id: string }\n\n/**\n * Tells a member a record is now theirs — one console notification, typed\n * by the record it opens — unless the member handed it to themselves.\n * Shared by the pass above and by the routes where a person picks the\n * owner outright (a lead conversion naming a colleague), so every server\n * path that writes `ownerUid` says so in the same words. Never throws;\n * the fan-out swallows its own failures.\n */\nexport async function notifyRecordAssigned(input: {\n hostId: string\n orgId: string\n ownerUid: string\n actorUid?: string | null\n record: AssignedRecord\n /** How the person reads in the message: their name, else their address. */\n who: string\n}): Promise<boolean> {\n if (!input.ownerUid || input.ownerUid === (input.actorUid ?? '')) return false\n const lead = input.record.kind === 'lead'\n /*\n * THE BODY SAYS WHAT HAPPENED, AND ON WHICH SITE (AGL-3432).\n *\n * It was the bare name, so a member who owns records on several sites read\n * \"Ada\" in an email body and could not tell what Ada was or where. The\n * site's name is one request-cached read of a document most callers have\n * already paid for, taken only when somebody is actually notified; a read\n * that fails drops the \"on {site}\" clause rather than the notification.\n */\n let site = ''\n try {\n const host = await getHostDocAdmin(input.hostId)\n if (host) site = ` on ${hostDisplayName(host, input.hostId)}`\n } catch {\n // The clause goes, never the notification.\n }\n await notifyUsers([input.ownerUid], {\n type: lead ? 'content.leadAssigned' : 'content.contactAssigned',\n title: lead ? 'Lead assigned to you' : 'Contact assigned to you',\n body: `${input.who} was assigned to you as a ${lead ? 'lead' : 'contact'}${site}.`,\n link: recordLink(input.hostId, input.record),\n orgId: input.orgId,\n hostId: input.hostId,\n })\n return true\n}\n\n/**\n * The roster checks one assignment transaction makes: whether a uid is on the\n * team, and who a target names — a member as named, or the next member of\n * the round-robin pool who is still on the roster. One read per candidate\n * per attempt; a retried transaction builds a fresh one and reads afresh, as\n * it must.\n */\nfunction rosterResolver(\n tx: FirebaseFirestore.Transaction,\n membersRef: FirebaseFirestore.CollectionReference,\n settings: ReturnType<typeof readCrmAssignmentSettings>,\n): {\n isMember: (uid: string) => Promise<boolean>\n resolveTarget: (\n target: CrmAssignmentTarget,\n ) => Promise<{ uid: string; advancePointer: boolean } | null>\n} {\n const onRoster = new Map<string, Promise<boolean>>()\n const isMember = (uid: string): Promise<boolean> => {\n let known = onRoster.get(uid)\n if (!known) {\n known = tx.get(membersRef.doc(uid)).then((snapshot) => snapshot.exists)\n onRoster.set(uid, known)\n }\n return known\n }\n const fromPool = async (): Promise<string | null> => {\n for (const uid of roundRobinOrder(\n settings.pool.memberUids,\n settings.pool.lastAssignedUid,\n )) {\n if (await isMember(uid)) return uid\n }\n return null\n }\n const resolveTarget = async (\n target: CrmAssignmentTarget,\n ): Promise<{ uid: string; advancePointer: boolean } | null> => {\n if ('roundRobin' in target) {\n const uid = await fromPool()\n return uid ? { uid, advancePointer: true } : null\n }\n return (await isMember(target.memberUid))\n ? { uid: target.memberUid, advancePointer: false }\n : null\n }\n return { isMember, resolveTarget }\n}\n\nasync function assignOwner(policy: Policy): Promise<OwnerAssignment> {\n const { input } = policy\n try {\n const resolved = await getOrgForHost(input.hostId)\n if (!resolved) return { outcome: 'none', reason: 'no-org' }\n const { orgId } = resolved\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const contactRef = orgRef.collection('contacts').doc(input.contactId)\n const membersRef = orgRef.collection('members')\n const leadKey = personKey(input.email)\n // Beside the contact on the org, keyed by the same address (AGL-3275).\n const leadRef = leadKey ? orgRef.collection('leads').doc(leadKey) : null\n\n let contactName = ''\n const verdict = await firestore.runTransaction(\n async (tx): Promise<OwnerAssignment> => {\n /*\n * ALL READS BEFORE THE WRITE, which Firestore requires. The org\n * document is read here rather than trusted from `getOrgForHost`,\n * because the pointer it carries is the one thing a concurrent\n * capture may have moved, and the rules and the pool are read from\n * the same snapshot so the decision and the pointer agree.\n */\n const [orgSnapshot, contactSnapshot, leadSnapshot] = await Promise.all([\n tx.get(orgRef),\n tx.get(contactRef),\n leadRef ? tx.get(leadRef) : Promise.resolve(null),\n ])\n if (!contactSnapshot.exists) {\n return { outcome: 'none', reason: 'no-contact' }\n }\n const org = (orgSnapshot.data() ?? {}) as Record<string, unknown>\n const contact = (contactSnapshot.data() ?? {}) as Record<string, unknown>\n contactName = String(contact['name'] ?? '')\n const group = consentGroupForHost(org, input.hostId)\n const facet = readContactFacet(contact, group.groupId)\n const current = typeof facet.ownerUid === 'string' ? facet.ownerUid : ''\n if (policy.kind === 'capture' && current) {\n return { outcome: 'unchanged', ownerUid: current }\n }\n const settings = readCrmAssignmentSettings(org)\n const { isMember, resolveTarget } = rosterResolver(tx, membersRef, settings)\n\n let decision: Decision | null = null\n let refusal: OwnerAssignmentRefusal = 'no-rule'\n if (policy.kind === 'capture') {\n const capture = {\n source: policy.input.source,\n email: input.email,\n formId: policy.input.formId ?? null,\n tags: [...(policy.input.tags ?? []), ...(facet.tags ?? [])],\n }\n for (const rule of settings.rules) {\n if (!assignmentRuleMatches(rule.when, capture)) continue\n const hit = await resolveTarget(rule.assign)\n if (hit) {\n decision = { ...hit, ownerUid: hit.uid, by: 'rule', ruleId: rule.id }\n break\n }\n }\n const fallback = settings.hostDefaultOwners[input.hostId]\n if (!decision && fallback && (await isMember(fallback))) {\n decision = { ownerUid: fallback, by: 'default', advancePointer: false }\n }\n } else {\n const { assign } = policy.input\n const hit = await resolveTarget(assign)\n if (hit) {\n decision = {\n ...hit,\n ownerUid: hit.uid,\n by: 'roundRobin' in assign ? 'roundRobin' : 'member',\n }\n } else {\n refusal = 'roundRobin' in assign ? 'empty-pool' : 'not-a-member'\n }\n }\n if (!decision) return { outcome: 'none', reason: refusal }\n if (decision.ownerUid === current) {\n return { outcome: 'unchanged', ownerUid: current }\n }\n\n tx.update(contactRef, {\n [contactFacetPath(group.groupId, 'ownerUid')]: decision.ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n if (decision.advancePointer) {\n tx.update(orgRef, {\n [CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH]: decision.ownerUid,\n })\n }\n /*\n * A lead somebody already assigned by hand keeps its owner on a\n * capture — the contact was new, the lead may not have been — and\n * follows the contact on a deliberate reassignment.\n */\n const leadOwner = String(leadSnapshot?.get('ownerUid') ?? '')\n const leadMirrored = Boolean(\n leadSnapshot?.exists && (policy.kind === 'reassign' || !leadOwner),\n )\n if (leadMirrored && leadRef) {\n tx.update(leadRef, {\n ownerUid: decision.ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n return {\n outcome: 'assigned',\n ownerUid: decision.ownerUid,\n by: decision.by,\n ...(decision.ruleId ? { ruleId: decision.ruleId } : {}),\n leadMirrored,\n notified: false,\n }\n },\n )\n if (verdict.outcome !== 'assigned') return verdict\n // The owner is what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(contactRef, 'contacts')\n\n if (\n !verdict.leadMirrored &&\n leadRef &&\n policy.kind === 'capture' &&\n LEAD_FILING_SOURCES.has(policy.input.source)\n ) {\n void mirrorOwnerOntoLeadLater(leadRef, verdict.ownerUid)\n }\n\n const notified = await notifyRecordAssigned({\n hostId: input.hostId,\n orgId,\n ownerUid: verdict.ownerUid,\n actorUid: input.actorUid,\n record:\n verdict.leadMirrored && leadKey\n ? { kind: 'lead', id: leadKey }\n : { kind: 'contact', id: input.contactId },\n who: contactName || input.email,\n })\n return { ...verdict, notified }\n } catch (error) {\n console.error(\n 'assignContactOwner failed',\n input.hostId,\n input.contactId,\n error,\n )\n return { outcome: 'none', reason: 'failed' }\n }\n}\n\n/**\n * The detached second look for a lead that lands after the contact.\n *\n * Bounded, and only ever ADDS an owner: a lead that arrived with one (a\n * person assigned it in the meantime) is left alone, and a lead that never\n * arrives costs three reads over six seconds and nothing else. Detached\n * because the capture must not hold `contactCreated` back for it — an\n * automation welcoming the contact has nothing to do with the lead's owner\n * column.\n */\nasync function mirrorOwnerOntoLeadLater(\n leadRef: FirebaseFirestore.DocumentReference,\n ownerUid: string,\n delaysMs: readonly number[] = LEAD_MIRROR_RETRY_MS,\n): Promise<boolean> {\n for (const delay of delaysMs) {\n await new Promise((resolve) => setTimeout(resolve, delay))\n try {\n const lead = await leadRef.get()\n if (!lead.exists) continue\n if (String(lead.get('ownerUid') ?? '')) return false\n await leadRef.update({\n ownerUid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n } catch (error) {\n console.error('assignContactOwner lead mirror failed', leadRef.path, error)\n return false\n }\n }\n return false\n}\n\n/**\n * The capture door's pass: the org's rules in order, then the capturing\n * site's default owner, on a record that has no owner yet. See the module\n * comment for the whole of it.\n */\nexport function assignOwnerForCapture(\n input: CaptureAssignmentInput,\n): Promise<OwnerAssignment> {\n return assignOwner({ kind: 'capture', input })\n}\n\n/**\n * The deliberate assignment — the `assignContactOwner` step, and a person\n * handing a record to somebody: a member by uid (verified on the roster) or\n * the next in the round-robin pool, overwriting whoever held it. Naming the\n * owner the record already has changes nothing and tells nobody.\n */\nexport function reassignContactOwner(\n input: ReassignmentInput,\n): Promise<OwnerAssignment> {\n return assignOwner({ kind: 'reassign', input })\n}\n\n/** The deliberate assignment of a LEAD the site holds and no contact has absorbed yet. */\nexport interface LeadReassignmentInput {\n hostId: string\n /** `orgs/{orgId}/leads/{leadId}` — the person's key. */\n leadId: string\n assign: CrmAssignmentTarget\n /** As {@link OwnerAssignmentInput.actorUid}: never told about their own choice. */\n actorUid?: string | null\n}\n\n/**\n * {@link reassignContactOwner} for a person who is still a LEAD (AGL-3458).\n *\n * A lead-routed form files a lead and no contact (AGL-3232), so an\n * automation welcoming that person — \"assign an owner, then book them a\n * call\" — has only the lead to act on. The owner is the lead's own\n * `ownerUid`, the field the Leads list's Owner column and filter read and a\n * conversion hands to the contact. Same transaction shape as a contact's:\n * the org document, the lead and every roster candidate are read inside it,\n * and the round-robin pointer moves in the commit that writes the owner, so\n * a lead and a contact assigned at the same moment take two different\n * members of the one rotation.\n *\n * Never rejects; a failure is logged and answered as `none`, which the\n * automation records as the step's error.\n */\nexport async function reassignLeadOwner(\n input: LeadReassignmentInput,\n): Promise<OwnerAssignment> {\n try {\n const resolved = await getOrgForHost(input.hostId)\n if (!resolved) return { outcome: 'none', reason: 'no-org' }\n const { orgId } = resolved\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const leadRef = orgRef.collection('leads').doc(input.leadId)\n const membersRef = orgRef.collection('members')\n let who = ''\n const verdict = await firestore.runTransaction(\n async (tx): Promise<OwnerAssignment> => {\n const [orgSnapshot, leadSnapshot] = await Promise.all([\n tx.get(orgRef),\n tx.get(leadRef),\n ])\n if (!leadSnapshot.exists) return { outcome: 'none', reason: 'no-lead' }\n const lead = (leadSnapshot.data() ?? {}) as Record<string, unknown>\n who = String(lead['name'] ?? '') || String(lead['email'] ?? '')\n const current = String(lead['ownerUid'] ?? '')\n const settings = readCrmAssignmentSettings(\n (orgSnapshot.data() ?? {}) as Record<string, unknown>,\n )\n const { resolveTarget } = rosterResolver(tx, membersRef, settings)\n const hit = await resolveTarget(input.assign)\n if (!hit) {\n return {\n outcome: 'none',\n reason: 'roundRobin' in input.assign ? 'empty-pool' : 'not-a-member',\n }\n }\n if (hit.uid === current) return { outcome: 'unchanged', ownerUid: current }\n tx.update(leadRef, {\n ownerUid: hit.uid,\n updatedAt: FieldValue.serverTimestamp(),\n })\n if (hit.advancePointer) {\n tx.update(orgRef, { [CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH]: hit.uid })\n }\n return {\n outcome: 'assigned',\n ownerUid: hit.uid,\n by: 'roundRobin' in input.assign ? 'roundRobin' : 'member',\n leadMirrored: false,\n notified: false,\n }\n },\n )\n if (verdict.outcome !== 'assigned') return verdict\n const notified = await notifyRecordAssigned({\n hostId: input.hostId,\n orgId,\n ownerUid: verdict.ownerUid,\n actorUid: input.actorUid,\n record: { kind: 'lead', id: input.leadId },\n who,\n })\n return { ...verdict, notified }\n } catch (error) {\n console.error('assignLeadOwner failed', input.hostId, input.leadId, error)\n return { outcome: 'none', reason: 'failed' }\n }\n}\n\nexport default assignOwnerForCapture\n"],"names":["assignmentRuleMatches","consentGroupForHost","contactFacetPath","CRM_ROUND_ROBIN_LAST_ASSIGNED_PATH","personKey","readContactFacet","readCrmAssignmentSettings","roundRobinOrder","firebaseAdmin","getHostDocAdmin","getOrgForHost","hostDisplayName","notifyUsers","restampCrmListFieldsAt","FieldValue","OWNER_ASSIGNMENT_REFUSALS","failed","LEAD_FILING_SOURCES","Set","LEAD_MIRROR_RETRY_MS","recordLink","hostId","record","section","kind","encodeURIComponent","id","notifyRecordAssigned","input","ownerUid","actorUid","lead","site","host","type","title","body","who","link","orgId","rosterResolver","tx","membersRef","settings","onRoster","Map","isMember","uid","known","get","doc","then","snapshot","exists","set","fromPool","pool","memberUids","lastAssignedUid","resolveTarget","target","advancePointer","memberUid","assignOwner","policy","resolved","outcome","reason","firestore","app","orgRef","collection","contactRef","contactId","leadKey","email","leadRef","contactName","verdict","runTransaction","orgSnapshot","contactSnapshot","contact","leadSnapshot","Promise","all","resolve","org","data","String","group","facet","groupId","current","decision","refusal","capture","source","formId","tags","rule","rules","when","hit","assign","by","ruleId","fallback","hostDefaultOwners","update","updatedAt","serverTimestamp","leadOwner","leadMirrored","Boolean","notified","has","mirrorOwnerOntoLeadLater","error","console","delaysMs","delay","setTimeout","path","assignOwnerForCapture","reassignContactOwner","reassignLeadOwner","leadId"],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type HostContactCreated } from '@aglyn/tenant-data-admin';
|
|
18
|
+
/** What the association did, for the spec and the log. */
|
|
19
|
+
export type CompanyAssociation = {
|
|
20
|
+
outcome: 'linked' | 'created';
|
|
21
|
+
companyId: string;
|
|
22
|
+
} | {
|
|
23
|
+
outcome: 'none';
|
|
24
|
+
reason: 'no-domain' | 'no-org' | 'ambiguous' | 'no-match' | 'failed';
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Link a NEW contact to the company its email domain names (AGL-2613).
|
|
28
|
+
*
|
|
29
|
+
* HubSpot associates a contact with a company on domain the moment the
|
|
30
|
+
* contact exists, and that is the moment this runs: once, from the capture
|
|
31
|
+
* door, for a person the org did not hold before. A repeat visit by somebody
|
|
32
|
+
* already held is not a new person and re-asks nothing; a door that already
|
|
33
|
+
* knew the company — the console's create drawer, an import row naming one
|
|
34
|
+
* — has said so in the facet and this is not called at all.
|
|
35
|
+
*
|
|
36
|
+
* ## Bounded: one query, one commit
|
|
37
|
+
*
|
|
38
|
+
* The query is `companies where domain == d and visibleTo array-contains-any
|
|
39
|
+
* (the capturing scope's read tokens)`, capped at two — the `(visibleTo
|
|
40
|
+
* CONTAINS, domain ASC)` composite serves it. Two rather than one because
|
|
41
|
+
* the answer has to be "exactly one": two companies at one domain visible
|
|
42
|
+
* to this site is an ambiguity a capture must not resolve by picking the
|
|
43
|
+
* first, and the contact waits for a person. The scope predicate is the
|
|
44
|
+
* console's own listener predicate, so a company this site could not open
|
|
45
|
+
* is not a match — an agency's client at `acme.com` is not linked to a
|
|
46
|
+
* sibling client's Acme.
|
|
47
|
+
*
|
|
48
|
+
* The write is one batch: the facet and the mirror on the contact, and the
|
|
49
|
+
* count on the company. The contact is fresh, so the plan is the trivial
|
|
50
|
+
* one — no previous link, nothing held elsewhere — and needs no read of the
|
|
51
|
+
* document.
|
|
52
|
+
*
|
|
53
|
+
* ## Creating is a setting, and off by default
|
|
54
|
+
*
|
|
55
|
+
* With `crm.autoCreateCompanies` on the org document, a domain no visible
|
|
56
|
+
* company carries becomes one, named after the domain and scoped exactly as
|
|
57
|
+
* the contact was — `crmScopeTokens`, the stamp every CRM creator uses —
|
|
58
|
+
* with the count starting at one. Public mailbox domains never reach this
|
|
59
|
+
* branch, because `companyDomainForEmail` answers `null` for them: a
|
|
60
|
+
* workspace's consumer list is not a list of accounts.
|
|
61
|
+
*
|
|
62
|
+
* ## The same posture as the capture
|
|
63
|
+
*
|
|
64
|
+
* Never throws. The capture has already succeeded and a form submission
|
|
65
|
+
* must not fail because the CRM could not file its author; a failure is
|
|
66
|
+
* logged and answered as `none`, and the person is exactly as capturable
|
|
67
|
+
* by hand as they were before this existed.
|
|
68
|
+
*/
|
|
69
|
+
export declare function associateCompanyByDomain(created: Pick<HostContactCreated, 'hostId' | 'contactId' | 'email'>): Promise<CompanyAssociation>;
|
|
70
|
+
export default associateCompanyByDomain;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { companyDomainForEmail, companyNameForDomain, CRM_COLLECTIONS, crmReadTokens, crmScopeTokens, nameSearchFields, orgAutoCreatesCompanies } from "@aglyn/aglyn/server";
|
|
18
|
+
import { consentGroupForSite, contactCompanyLinkFields, firebaseAdmin, getOrgForHost, restampCrmListFieldsAt, writeContactCompanyLink } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { COMPANY_CONTACTS_COUNT_FIELD, crmNewRecordListFields, planContactCompanyLink } from "@aglyn/aglyn/server";
|
|
20
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
21
|
+
/**
|
|
22
|
+
* Link a NEW contact to the company its email domain names (AGL-2613).
|
|
23
|
+
*
|
|
24
|
+
* HubSpot associates a contact with a company on domain the moment the
|
|
25
|
+
* contact exists, and that is the moment this runs: once, from the capture
|
|
26
|
+
* door, for a person the org did not hold before. A repeat visit by somebody
|
|
27
|
+
* already held is not a new person and re-asks nothing; a door that already
|
|
28
|
+
* knew the company — the console's create drawer, an import row naming one
|
|
29
|
+
* — has said so in the facet and this is not called at all.
|
|
30
|
+
*
|
|
31
|
+
* ## Bounded: one query, one commit
|
|
32
|
+
*
|
|
33
|
+
* The query is `companies where domain == d and visibleTo array-contains-any
|
|
34
|
+
* (the capturing scope's read tokens)`, capped at two — the `(visibleTo
|
|
35
|
+
* CONTAINS, domain ASC)` composite serves it. Two rather than one because
|
|
36
|
+
* the answer has to be "exactly one": two companies at one domain visible
|
|
37
|
+
* to this site is an ambiguity a capture must not resolve by picking the
|
|
38
|
+
* first, and the contact waits for a person. The scope predicate is the
|
|
39
|
+
* console's own listener predicate, so a company this site could not open
|
|
40
|
+
* is not a match — an agency's client at `acme.com` is not linked to a
|
|
41
|
+
* sibling client's Acme.
|
|
42
|
+
*
|
|
43
|
+
* The write is one batch: the facet and the mirror on the contact, and the
|
|
44
|
+
* count on the company. The contact is fresh, so the plan is the trivial
|
|
45
|
+
* one — no previous link, nothing held elsewhere — and needs no read of the
|
|
46
|
+
* document.
|
|
47
|
+
*
|
|
48
|
+
* ## Creating is a setting, and off by default
|
|
49
|
+
*
|
|
50
|
+
* With `crm.autoCreateCompanies` on the org document, a domain no visible
|
|
51
|
+
* company carries becomes one, named after the domain and scoped exactly as
|
|
52
|
+
* the contact was — `crmScopeTokens`, the stamp every CRM creator uses —
|
|
53
|
+
* with the count starting at one. Public mailbox domains never reach this
|
|
54
|
+
* branch, because `companyDomainForEmail` answers `null` for them: a
|
|
55
|
+
* workspace's consumer list is not a list of accounts.
|
|
56
|
+
*
|
|
57
|
+
* ## The same posture as the capture
|
|
58
|
+
*
|
|
59
|
+
* Never throws. The capture has already succeeded and a form submission
|
|
60
|
+
* must not fail because the CRM could not file its author; a failure is
|
|
61
|
+
* logged and answered as `none`, and the person is exactly as capturable
|
|
62
|
+
* by hand as they were before this existed.
|
|
63
|
+
*/ export async function associateCompanyByDomain(created) {
|
|
64
|
+
const domain = companyDomainForEmail(created.email);
|
|
65
|
+
if (!domain) return {
|
|
66
|
+
outcome: 'none',
|
|
67
|
+
reason: 'no-domain'
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
const resolved = await getOrgForHost(created.hostId);
|
|
71
|
+
if (!resolved) return {
|
|
72
|
+
outcome: 'none',
|
|
73
|
+
reason: 'no-org'
|
|
74
|
+
};
|
|
75
|
+
const org = resolved.org;
|
|
76
|
+
const group = await consentGroupForSite(created.hostId, org);
|
|
77
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
78
|
+
const orgRef = firestore.collection('orgs').doc(resolved.orgId);
|
|
79
|
+
const companiesRef = orgRef.collection(CRM_COLLECTIONS.companies);
|
|
80
|
+
const contactRef = orgRef.collection('contacts').doc(created.contactId);
|
|
81
|
+
const matches = await companiesRef.where('domain', '==', domain).where('visibleTo', 'array-contains-any', crmReadTokens(group)).limit(2).get();
|
|
82
|
+
if (matches.size > 1) return {
|
|
83
|
+
outcome: 'none',
|
|
84
|
+
reason: 'ambiguous'
|
|
85
|
+
};
|
|
86
|
+
if (matches.size === 1) {
|
|
87
|
+
const companyId = matches.docs[0].id;
|
|
88
|
+
await writeContactCompanyLink({
|
|
89
|
+
firestore,
|
|
90
|
+
contactRef,
|
|
91
|
+
contact: null,
|
|
92
|
+
companiesRef,
|
|
93
|
+
groupId: group.groupId,
|
|
94
|
+
companyId
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
outcome: 'linked',
|
|
98
|
+
companyId
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!orgAutoCreatesCompanies(org)) {
|
|
102
|
+
return {
|
|
103
|
+
outcome: 'none',
|
|
104
|
+
reason: 'no-match'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/*
|
|
108
|
+
* The company and the link in one commit, so a company never exists
|
|
109
|
+
* with a count of one and no contact naming it — or the reverse. The
|
|
110
|
+
* count is written as a value rather than an increment because the
|
|
111
|
+
* document is being created in the same batch, and the plan's `+1` is
|
|
112
|
+
* exactly what a fresh company with one contact stores.
|
|
113
|
+
*/ const plan = planContactCompanyLink({
|
|
114
|
+
companyId: null,
|
|
115
|
+
companyIds: [],
|
|
116
|
+
heldElsewhere: []
|
|
117
|
+
}, companiesRef.doc().id);
|
|
118
|
+
if (!(plan == null ? void 0 : plan.companyId)) return {
|
|
119
|
+
outcome: 'none',
|
|
120
|
+
reason: 'failed'
|
|
121
|
+
};
|
|
122
|
+
const companyRef = companiesRef.doc(plan.companyId);
|
|
123
|
+
const batch = firestore.batch();
|
|
124
|
+
const company = _extends({}, nameSearchFields(companyNameForDomain(domain)), {
|
|
125
|
+
domain,
|
|
126
|
+
visibleTo: crmScopeTokens(org, group),
|
|
127
|
+
hostId: created.hostId,
|
|
128
|
+
[COMPANY_CONTACTS_COUNT_FIELD]: 1,
|
|
129
|
+
createdAt: FieldValue.serverTimestamp(),
|
|
130
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
131
|
+
});
|
|
132
|
+
batch.set(companyRef, _extends({}, company, crmNewRecordListFields('companies', company)));
|
|
133
|
+
batch.update(contactRef, _extends({}, contactCompanyLinkFields(plan, group.groupId), {
|
|
134
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
135
|
+
}));
|
|
136
|
+
await batch.commit();
|
|
137
|
+
// The company is what the Contacts list filters by (AGL-3321).
|
|
138
|
+
await restampCrmListFieldsAt(contactRef, 'contacts');
|
|
139
|
+
return {
|
|
140
|
+
outcome: 'created',
|
|
141
|
+
companyId: plan.companyId
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('associateCompanyByDomain failed', created.hostId, created.contactId, error);
|
|
145
|
+
return {
|
|
146
|
+
outcome: 'none',
|
|
147
|
+
reason: 'failed'
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export default associateCompanyByDomain;
|
|
152
|
+
|
|
153
|
+
//# sourceMappingURL=associate-company-by-domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/associate-company-by-domain.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n companyDomainForEmail,\n companyNameForDomain,\n CRM_COLLECTIONS,\n crmReadTokens,\n crmScopeTokens,\n nameSearchFields,\n orgAutoCreatesCompanies,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n contactCompanyLinkFields,\n firebaseAdmin,\n getOrgForHost,\n type HostContactCreated,\n restampCrmListFieldsAt,\n writeContactCompanyLink,\n} from '@aglyn/tenant-data-admin'\nimport {\n COMPANY_CONTACTS_COUNT_FIELD,\n crmNewRecordListFields,\n planContactCompanyLink,\n} from '@aglyn/aglyn/server'\nimport { FieldValue } from 'firebase-admin/firestore'\n\n/** What the association did, for the spec and the log. */\nexport type CompanyAssociation =\n | { outcome: 'linked' | 'created'; companyId: string }\n | {\n outcome: 'none'\n reason: 'no-domain' | 'no-org' | 'ambiguous' | 'no-match' | 'failed'\n }\n\n/**\n * Link a NEW contact to the company its email domain names (AGL-2613).\n *\n * HubSpot associates a contact with a company on domain the moment the\n * contact exists, and that is the moment this runs: once, from the capture\n * door, for a person the org did not hold before. A repeat visit by somebody\n * already held is not a new person and re-asks nothing; a door that already\n * knew the company — the console's create drawer, an import row naming one\n * — has said so in the facet and this is not called at all.\n *\n * ## Bounded: one query, one commit\n *\n * The query is `companies where domain == d and visibleTo array-contains-any\n * (the capturing scope's read tokens)`, capped at two — the `(visibleTo\n * CONTAINS, domain ASC)` composite serves it. Two rather than one because\n * the answer has to be \"exactly one\": two companies at one domain visible\n * to this site is an ambiguity a capture must not resolve by picking the\n * first, and the contact waits for a person. The scope predicate is the\n * console's own listener predicate, so a company this site could not open\n * is not a match — an agency's client at `acme.com` is not linked to a\n * sibling client's Acme.\n *\n * The write is one batch: the facet and the mirror on the contact, and the\n * count on the company. The contact is fresh, so the plan is the trivial\n * one — no previous link, nothing held elsewhere — and needs no read of the\n * document.\n *\n * ## Creating is a setting, and off by default\n *\n * With `crm.autoCreateCompanies` on the org document, a domain no visible\n * company carries becomes one, named after the domain and scoped exactly as\n * the contact was — `crmScopeTokens`, the stamp every CRM creator uses —\n * with the count starting at one. Public mailbox domains never reach this\n * branch, because `companyDomainForEmail` answers `null` for them: a\n * workspace's consumer list is not a list of accounts.\n *\n * ## The same posture as the capture\n *\n * Never throws. The capture has already succeeded and a form submission\n * must not fail because the CRM could not file its author; a failure is\n * logged and answered as `none`, and the person is exactly as capturable\n * by hand as they were before this existed.\n */\nexport async function associateCompanyByDomain(\n created: Pick<HostContactCreated, 'hostId' | 'contactId' | 'email'>,\n): Promise<CompanyAssociation> {\n const domain = companyDomainForEmail(created.email)\n if (!domain) return { outcome: 'none', reason: 'no-domain' }\n try {\n const resolved = await getOrgForHost(created.hostId)\n if (!resolved) return { outcome: 'none', reason: 'no-org' }\n const org = resolved.org as Record<string, unknown>\n const group = await consentGroupForSite(created.hostId, org)\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(resolved.orgId)\n const companiesRef = orgRef.collection(CRM_COLLECTIONS.companies)\n const contactRef = orgRef.collection('contacts').doc(created.contactId)\n\n const matches = await companiesRef\n .where('domain', '==', domain)\n .where('visibleTo', 'array-contains-any', crmReadTokens(group))\n .limit(2)\n .get()\n if (matches.size > 1) return { outcome: 'none', reason: 'ambiguous' }\n if (matches.size === 1) {\n const companyId = matches.docs[0].id\n await writeContactCompanyLink({\n firestore,\n contactRef,\n contact: null,\n companiesRef,\n groupId: group.groupId,\n companyId,\n })\n return { outcome: 'linked', companyId }\n }\n if (!orgAutoCreatesCompanies(org)) {\n return { outcome: 'none', reason: 'no-match' }\n }\n\n /*\n * The company and the link in one commit, so a company never exists\n * with a count of one and no contact naming it — or the reverse. The\n * count is written as a value rather than an increment because the\n * document is being created in the same batch, and the plan's `+1` is\n * exactly what a fresh company with one contact stores.\n */\n const plan = planContactCompanyLink(\n { companyId: null, companyIds: [], heldElsewhere: [] },\n companiesRef.doc().id,\n )\n if (!plan?.companyId) return { outcome: 'none', reason: 'failed' }\n const companyRef = companiesRef.doc(plan.companyId)\n const batch = firestore.batch()\n const company: Record<string, unknown> = {\n ...nameSearchFields(companyNameForDomain(domain)),\n domain,\n visibleTo: crmScopeTokens(org, group),\n hostId: created.hostId,\n [COMPANY_CONTACTS_COUNT_FIELD]: 1,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n batch.set(companyRef, {\n ...company,\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', company),\n })\n batch.update(contactRef, {\n ...contactCompanyLinkFields(plan, group.groupId),\n updatedAt: FieldValue.serverTimestamp(),\n })\n await batch.commit()\n // The company is what the Contacts list filters by (AGL-3321).\n await restampCrmListFieldsAt(contactRef, 'contacts')\n return { outcome: 'created', companyId: plan.companyId }\n } catch (error) {\n console.error(\n 'associateCompanyByDomain failed',\n created.hostId,\n created.contactId,\n error,\n )\n return { outcome: 'none', reason: 'failed' }\n }\n}\n\nexport default associateCompanyByDomain\n"],"names":["companyDomainForEmail","companyNameForDomain","CRM_COLLECTIONS","crmReadTokens","crmScopeTokens","nameSearchFields","orgAutoCreatesCompanies","consentGroupForSite","contactCompanyLinkFields","firebaseAdmin","getOrgForHost","restampCrmListFieldsAt","writeContactCompanyLink","COMPANY_CONTACTS_COUNT_FIELD","crmNewRecordListFields","planContactCompanyLink","FieldValue","associateCompanyByDomain","created","domain","email","outcome","reason","resolved","hostId","org","group","firestore","app","orgRef","collection","doc","orgId","companiesRef","companies","contactRef","contactId","matches","where","limit","get","size","companyId","docs","id","contact","groupId","plan","companyIds","heldElsewhere","companyRef","batch","company","visibleTo","createdAt","serverTimestamp","updatedAt","set","update","commit","error","console"],"mappings":"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"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type CrmActivityDirection, type CrmTaskPriority } from '@aglyn/aglyn/server';
|
|
18
|
+
import type { PluginRecordEmailPrepareRequest, PluginRecordPreparedEmail } from '@aglyn/aglyn/plugin-manager/plugin-record-timeline';
|
|
19
|
+
import type { ServerStepAnswer, ServerStepRequest } from '@aglyn/aglyn/plugin-manager/plugin-server-steps';
|
|
20
|
+
import type { InteractionStepGuard } from '@aglyn/aglyn/app-utils/site-interactions';
|
|
21
|
+
import type { ContactLifecycleStage, CrmActivityKind, CrmTaskKind } from '@aglyn/aglyn/app-utils/crm-kinds';
|
|
22
|
+
import type { CRM_STEP_TYPES } from '../constants/bundle-common';
|
|
23
|
+
/**
|
|
24
|
+
* THE AUTOMATION STEPS THAT WRITE THE CRM (AGL-2605), run for the automation
|
|
25
|
+
* engine through the platform's server-step seam (`plugin-server-steps`,
|
|
26
|
+
* AGL-3080): `setContactStage`, `addContactTag`, `assignContactOwner`,
|
|
27
|
+
* `createCrmTask` and `logCrmActivity`.
|
|
28
|
+
*
|
|
29
|
+
* The engine keeps the step's guard, the run's order, its history and the
|
|
30
|
+
* nesting cap; this answers what the step did. A refusal is ANSWERED as
|
|
31
|
+
* `error`, never thrown, and its words are the line the run history carries —
|
|
32
|
+
* the same words the engine wrote when these steps were its own.
|
|
33
|
+
*
|
|
34
|
+
* The five share one shape no other step has: each begins by finding THE
|
|
35
|
+
* PERSON the event is about, and each writes either inside the site's facet on
|
|
36
|
+
* that person or a record stamped with the site's scope. One resolver and one
|
|
37
|
+
* scope expression, used five times, rather than five copies drifting apart.
|
|
38
|
+
*
|
|
39
|
+
* ## The person is the event's, never the step's
|
|
40
|
+
*
|
|
41
|
+
* A step carries no contact reference. The contact is whoever the event
|
|
42
|
+
* names — by `contactId` when the emitting door knew the document, and by
|
|
43
|
+
* `email` otherwise, which is what every pre-CRM event carries. Both lookups
|
|
44
|
+
* are scoped to what THIS site may see: a document the id names but the site
|
|
45
|
+
* cannot read is treated as absent, exactly as the scoped query would have
|
|
46
|
+
* treated it, so an event replayed against the wrong site reaches nobody.
|
|
47
|
+
*
|
|
48
|
+
* ## A missing person is a reported no-op
|
|
49
|
+
*
|
|
50
|
+
* Nothing is written and the step's error names the reason, which lands in
|
|
51
|
+
* the run summary beside the trigger — the same place "why didn't it fire?"
|
|
52
|
+
* is answered for every other step. Silence here would make a mis-wired
|
|
53
|
+
* trigger (a form with no email field) indistinguishable from a working one.
|
|
54
|
+
*
|
|
55
|
+
* ## The plan gate is the CRM's
|
|
56
|
+
*
|
|
57
|
+
* Every step is refused on a workspace whose plan does not carry the CRM,
|
|
58
|
+
* into the run history with the tier that carries it, so a Free workspace
|
|
59
|
+
* whose flow names a CRM step reads why the step did nothing rather than a
|
|
60
|
+
* log that says it ran. The org document is the one the run's gate already
|
|
61
|
+
* read, handed over with the step, so the check costs no read.
|
|
62
|
+
*
|
|
63
|
+
* The same module answers the record timeline's `prepareEmail` (AGL-2615): an
|
|
64
|
+
* automation's email addressed to the contact the event is about is filed on
|
|
65
|
+
* that contact's timeline — see {@link prepareCrmRecordEmail}.
|
|
66
|
+
*/
|
|
67
|
+
/** One of the steps this module runs. */
|
|
68
|
+
export type CrmStepType = (typeof CRM_STEP_TYPES)[number];
|
|
69
|
+
/**
|
|
70
|
+
* A CRM step, as it is stored: the CRM's own shapes of the steps it runs. The
|
|
71
|
+
* automation editor writes them and checks them; `apps/console/specs/
|
|
72
|
+
* crm-automation-steps.spec.ts` runs what it writes through this module.
|
|
73
|
+
*/
|
|
74
|
+
export type CrmAutomationStep = ({
|
|
75
|
+
type: 'setContactStage';
|
|
76
|
+
lifecycleStage: ContactLifecycleStage;
|
|
77
|
+
} | {
|
|
78
|
+
type: 'addContactTag';
|
|
79
|
+
tag: string;
|
|
80
|
+
} | {
|
|
81
|
+
type: 'assignContactOwner';
|
|
82
|
+
ownerUid?: string;
|
|
83
|
+
ownerEmail?: string;
|
|
84
|
+
roundRobin?: boolean;
|
|
85
|
+
} | {
|
|
86
|
+
type: 'createCrmTask';
|
|
87
|
+
title: string;
|
|
88
|
+
kind: CrmTaskKind;
|
|
89
|
+
/** `normal` when absent — the steps written before priorities were (AGL-3517). */
|
|
90
|
+
priority?: CrmTaskPriority;
|
|
91
|
+
dueInDays: number;
|
|
92
|
+
assigneeUid?: string;
|
|
93
|
+
assigneeEmail?: string;
|
|
94
|
+
} | {
|
|
95
|
+
type: 'logCrmActivity';
|
|
96
|
+
kind: CrmActivityKind;
|
|
97
|
+
body: string;
|
|
98
|
+
/** Which way a call or an email went (AGL-3517). */
|
|
99
|
+
direction?: CrmActivityDirection;
|
|
100
|
+
}) & {
|
|
101
|
+
when?: InteractionStepGuard | null;
|
|
102
|
+
} & {
|
|
103
|
+
type: CrmStepType;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Runs one CRM step for the person the event names: the executor the CRM
|
|
107
|
+
* registers on the server-step seam for every type in `CRM_STEP_TYPES`.
|
|
108
|
+
*
|
|
109
|
+
* Never throws for a plan without the CRM, a missing person, an unknown owner
|
|
110
|
+
* or a site with no org — those are answered as `error` so the run records
|
|
111
|
+
* them; a Firestore failure propagates, and the engine records it the same
|
|
112
|
+
* way.
|
|
113
|
+
*/
|
|
114
|
+
export declare function runCrmAutomationStep(request: ServerStepRequest, nowMs?: number): Promise<ServerStepAnswer>;
|
|
115
|
+
/**
|
|
116
|
+
* Whether — and where — an email a plugin is about to send lands on a
|
|
117
|
+
* timeline: the CRM's answer to the record timeline's `prepareEmail`.
|
|
118
|
+
*
|
|
119
|
+
* An automation's `sendEmail` step is not a CRM step: it mails whatever
|
|
120
|
+
* address the event carries, to a person who may be nobody the CRM knows. It
|
|
121
|
+
* earns a row on exactly one condition, that the message is ADDRESSED TO THE
|
|
122
|
+
* PERSON the sender means — a contact, else the lead a lead-routed form filed
|
|
123
|
+
* (AGL-3458), whose row lands on the lead's timeline — the person the request's link finds, at the
|
|
124
|
+
* address the row holds. A welcome sequence to a new contact is that; an
|
|
125
|
+
* internal alert routed to a merchant's own address through `toField` is not,
|
|
126
|
+
* and a row for it would put the merchant's inbox on a customer's history.
|
|
127
|
+
*
|
|
128
|
+
* Prepared ahead of the send, for the reason the console route mints its id
|
|
129
|
+
* first: the delivery webhook has nothing but the tags on the message to find
|
|
130
|
+
* the row with. Nothing is written until `file` is called, once the provider
|
|
131
|
+
* accepted the message. A workspace whose plan does not carry the CRM, a
|
|
132
|
+
* record at the activity ceiling and a site with no org earn no row — the
|
|
133
|
+
* message still goes, because the ceiling bounds the log and not the mail.
|
|
134
|
+
*
|
|
135
|
+
* **Never throws**, and neither does `file`. The row is bookkeeping beside a
|
|
136
|
+
* send, and a lookup that fails must not become a message that never left —
|
|
137
|
+
* the posture every meter beside a send takes. A failure is logged and the
|
|
138
|
+
* message goes out untagged.
|
|
139
|
+
*/
|
|
140
|
+
export declare function prepareCrmRecordEmail(request: PluginRecordEmailPrepareRequest): Promise<PluginRecordPreparedEmail | null>;
|