@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/merge-contacts.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 * MERGING TWO CONTACTS — the write (AGL-2625).\n *\n * `planContactMerge` says what the survivor becomes; this performs it, and\n * moves everything that pointed at the merged record. One function behind\n * two doors — the console's `crm/contacts-merge` route and\n * `POST /v1/contacts/{id}/merge` — so the REST caller and the record page\n * cannot merge two different ways.\n *\n * ## The order is the crash-safety argument\n *\n * 1. Every deal, task and activity naming the merged contact is repointed\n * at the survivor, and every lead converted into it. Batched, bounded,\n * idempotent: a re-run finds nothing left to move.\n * 2. Then, in ONE transaction: the survivor takes the plan, every address\n * it now answers to is indexed at it, and the merged document is\n * deleted — with both records re-read inside the transaction, so two\n * merges racing on one pair cannot both succeed and a plan is never\n * computed on a document that changed under it.\n *\n * Children first because a process that dies between the two steps leaves\n * children pointing at a record that EXISTS — the survivor — while the merged\n * document is still there for a retry to fold in. The other order would\n * leave deals pointing at a deleted contact, which is a record nobody can\n * open from anywhere.\n *\n * ## What it does not move\n *\n * List memberships, suppression entries and the delivery log are keyed by\n * ADDRESS, not by contact id, and stay under the merged address; a capture\n * on that address resolves to the survivor through the index, which is the\n * fact those keys need. A company's contacts count drops only for a company\n * both records named — the survivor still names the rest.\n */\n\nimport {\n CONTACT_EMAIL_INDEX_COLLECTION,\n contactEmails,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport type { HostActivityActor } from '@aglyn/aglyn/app-utils/activity-presenter'\nimport { planContactMerge } from '../model/contact-merge'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { settleCompanyContactsCounts } from '@aglyn/tenant-data-admin/server/contact-company-link'\nimport { recomputeCrmNextTaskAt } from '@aglyn/tenant-data-admin/server/crm-next-activity'\nimport { restampCrmListFieldsAt } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { logHostActivity } from '@aglyn/tenant-data-admin/server/organizations'\n\n/** How many pointing rows one repoint pass reads — and one batch writes. */\nconst REPOINT_PAGE = 400\n/** A ceiling on passes, so a query that keeps answering cannot loop forever. */\nconst REPOINT_PASSES = 25\n\nexport interface MergeContactsOptions {\n firestore: FirebaseFirestore.Firestore\n /** `orgs/{orgId}` — the org whose contacts both records are. */\n orgRef: FirebaseFirestore.DocumentReference\n /** The record that stays, and keeps its address as the identity. */\n survivorId: string\n /** The record folded in and deleted. */\n mergedId: string\n actor: HostActivityActor\n /**\n * The site whose console did the work: where the activity feed entry\n * lands, and the site a timeline note is filed under. `null` for a door\n * with no site — the REST API — which logs no host entry.\n */\n hostId: string | null\n /** The actor's display name for the timeline note, when the door knows it. */\n actorName?: string | null\n}\n\nexport interface MergeContactsRepointed {\n deals: number\n tasks: number\n activities: number\n leads: number\n}\n\nexport type MergeContactsResult =\n | {\n ok: true\n survivorId: string\n survivorEmail: string\n mergedId: string\n mergedEmail: string\n /** Every address the survivor answers to now, primary first. */\n emails: string[]\n repointed: MergeContactsRepointed\n }\n | { ok: false; reason: 'same-record' | 'survivor-missing' | 'merged-missing' }\n\n/**\n * Point every row of `collection` whose `contactId` is `from` at `to`.\n *\n * Page, batch, repeat: each pass reads the rows still pointing at the\n * merged record — the ones a previous pass moved no longer match — so the\n * loop ends when a pass finds nothing. `updatedAt` moves with the pointer\n * because an integration syncing by `updatedAfter` has to see the change.\n */\nasync function repointContactId(\n firestore: FirebaseFirestore.Firestore,\n collection: FirebaseFirestore.CollectionReference,\n from: string,\n to: string,\n): Promise<number> {\n let moved = 0\n for (let pass = 0; pass < REPOINT_PASSES; pass += 1) {\n const page = await collection\n .where('contactId', '==', from)\n .limit(REPOINT_PAGE)\n .get()\n if (page.empty) break\n const batch = firestore.batch()\n for (const row of page.docs) {\n batch.update(row.ref, {\n contactId: to,\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n await batch.commit()\n moved += page.size\n if (page.size < REPOINT_PAGE) break\n }\n return moved\n}\n\n/**\n * The leads converted into the merged contact, restamped at the survivor.\n *\n * A lead lives at `orgs/{orgId}/leads/{personKey(email)}` (AGL-3275), so there\n * is still no query — one document per address, asked for directly. This used\n * to be bounded by sites TIMES addresses because a lead lived under each site;\n * it is bounded by addresses now, and the sites drop out of the signature\n * because the collection no longer has one per site to walk.\n */\nasync function repointLeads(\n orgRef: FirebaseFirestore.DocumentReference,\n emails: readonly string[],\n from: string,\n to: string,\n): Promise<number> {\n let moved = 0\n /*\n * ONE ROW PER ADDRESS, on the org (AGL-3275). This walked the sites because\n * there was a lead under each of them; re-pointing the same document once\n * per site would now be the same write repeated, and the second would find\n * `convertedContactId` already moved and skip — a silent undercount rather\n * than a wrong answer, but an undercount the caller reports to the operator.\n */\n for (const email of emails) {\n const key = personKey(email)\n if (!key) continue\n const leadRef = orgRef.collection('leads').doc(key)\n const lead = await leadRef.get()\n if (!lead.exists || lead.get('convertedContactId') !== from) continue\n await leadRef.update({\n convertedContactId: to,\n updatedAt: FieldValue.serverTimestamp(),\n })\n moved += 1\n }\n return moved\n}\n\nexport async function mergeContacts(\n options: MergeContactsOptions,\n): Promise<MergeContactsResult> {\n const { firestore, orgRef, survivorId, mergedId, actor, hostId } = options\n if (!survivorId || !mergedId || survivorId === mergedId) {\n return { ok: false, reason: 'same-record' }\n }\n const contactsRef = orgRef.collection('contacts')\n const survivorRef = contactsRef.doc(survivorId)\n const mergedRef = contactsRef.doc(mergedId)\n const [survivorBefore, mergedBefore] = await Promise.all([\n survivorRef.get(),\n mergedRef.get(),\n ])\n if (!survivorBefore.exists) return { ok: false, reason: 'survivor-missing' }\n if (!mergedBefore.exists) return { ok: false, reason: 'merged-missing' }\n const mergedData = (mergedBefore.data() ?? {}) as Record<string, unknown>\n const survivorData = (survivorBefore.data() ?? {}) as Record<string, unknown>\n\n /*==========================================\n * 1. THE CHILDREN — before the parent, for the reason the header gives.\n *=========================================*/\n const [deals, tasks, activities] = await Promise.all([\n repointContactId(firestore, orgRef.collection(CRM_COLLECTIONS.deals), mergedId, survivorId),\n repointContactId(firestore, orgRef.collection(CRM_COLLECTIONS.tasks), mergedId, survivorId),\n repointContactId(\n firestore,\n orgRef.collection(CRM_COLLECTIONS.activities),\n mergedId,\n survivorId,\n ),\n ])\n const leads = await repointLeads(\n orgRef,\n contactEmails(mergedData),\n mergedId,\n survivorId,\n )\n\n /*==========================================\n * 2. THE SWAP — one transaction over both documents and the index.\n *=========================================*/\n const index = orgRef.collection(CONTACT_EMAIL_INDEX_COLLECTION)\n const swapped = await firestore.runTransaction(async (transaction) => {\n const [survivor, merged] = await Promise.all([\n transaction.get(survivorRef),\n transaction.get(mergedRef),\n ])\n if (!survivor.exists) return { reason: 'survivor-missing' as const }\n if (!merged.exists) return { reason: 'merged-missing' as const }\n const plan = planContactMerge(\n (survivor.data() ?? {}) as Record<string, unknown>,\n (merged.data() ?? {}) as Record<string, unknown>,\n )\n transaction.set(\n survivorRef,\n { ...plan.survivor, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n for (const email of plan.emails) {\n const key = personKey(email)\n if (!key) continue\n transaction.set(\n index.doc(key),\n { email, contactId: survivorId, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n }\n transaction.delete(mergedRef)\n return { plan, survivor: (survivor.data() ?? {}) as Record<string, unknown> }\n })\n if ('reason' in swapped) return { ok: false, reason: swapped.reason }\n const { plan } = swapped\n // The survivor now carries both records' addresses, holders and facets —\n // what the Contacts list searches and filters by (AGL-3321).\n await restampCrmListFieldsAt(survivorRef, 'contacts')\n\n /*\n * The companies both records named counted two people and now count one\n * (AGL-2613). Settled after the swap the way the capture settles its\n * counts: a figure that could not move is logged, and the company page's\n * live aggregate corrects it.\n */\n await settleCompanyContactsCounts(orgRef.collection(CRM_COLLECTIONS.companies), {\n companyId: null,\n mirror: null,\n counts: plan.companyCounts,\n })\n /*\n * The survivor now owns the merged record's open tasks too, so its next\n * activity is the earliest of both (AGL-2661). Settled the same way: a\n * figure that could not move is logged, and the Fields section's\n * recompute corrects it.\n */\n await recomputeCrmNextTaskAt(firestore, orgRef.id, [{ contactId: survivorId }]).catch(\n (error: unknown) => {\n console.error('contact merge next activity could not be recomputed', survivorId, error)\n },\n )\n\n const mergedEmail = contactEmails(mergedData)[0] ?? ''\n const survivorEmail = plan.emails[0] ?? ''\n // The canonical name — the one identity every holder shares — for the\n // feed; the address stands in for a person nobody has named.\n const survivorName =\n typeof swapped.survivor['name'] === 'string' ? swapped.survivor['name'] : ''\n\n /*\n * THE TIMELINE NOTE, on the survivor: the merge is an event in this\n * person's history, and the record page is where a reader looks for one.\n * Filed under the calling site, or the survivor's first site for a door\n * with none; its own catch, because a note that could not be written must\n * not read as a merge that did not happen.\n */\n try {\n await orgRef.collection(CRM_COLLECTIONS.activities).add({\n kind: 'note',\n body: `Merged with ${mergedEmail}`,\n atMs: Date.now(),\n byUid: actor.uid,\n ...(options.actorName || actor.email\n ? { byName: options.actorName || actor.email }\n : {}),\n contactId: survivorId,\n visibleTo: plan.visibleTo,\n hostId: hostId ?? String(survivorData['hostId'] ?? ''),\n createdByUid: actor.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n } catch (error) {\n console.error('[contact-merge] timeline note failed', survivorId, error)\n }\n\n // The audit line (AGL-2622), in the feed of the site whose console did\n // the work. A door with no site — the API — has no host feed to write.\n if (hostId) {\n await logHostActivity(hostId, actor, `Merged with ${mergedEmail}`, {\n type: 'contact',\n id: survivorId,\n name: survivorName || survivorEmail,\n })\n }\n\n return {\n ok: true,\n survivorId,\n survivorEmail,\n mergedId,\n mergedEmail,\n emails: plan.emails,\n repointed: { deals, tasks, activities, leads },\n }\n}\n"],"names":["CONTACT_EMAIL_INDEX_COLLECTION","contactEmails","planContactMerge","CRM_COLLECTIONS","personKey","FieldValue","settleCompanyContactsCounts","recomputeCrmNextTaskAt","restampCrmListFieldsAt","logHostActivity","REPOINT_PAGE","REPOINT_PASSES","repointContactId","firestore","collection","from","to","moved","pass","page","where","limit","get","empty","batch","row","docs","update","ref","contactId","updatedAt","serverTimestamp","commit","size","repointLeads","orgRef","emails","email","key","leadRef","doc","lead","exists","convertedContactId","mergeContacts","options","mergedBefore","survivorBefore","plan","survivorId","mergedId","actor","hostId","ok","reason","contactsRef","survivorRef","mergedRef","Promise","all","mergedData","data","survivorData","deals","tasks","activities","leads","index","swapped","runTransaction","transaction","survivor","merged","set","merge","delete","companies","companyId","mirror","counts","companyCounts","id","catch","error","console","mergedEmail","survivorEmail","survivorName","add","kind","body","atMs","Date","now","byUid","uid","actorName","byName","visibleTo","String","createdByUid","createdAt","type","name","repointed"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/merge-contacts.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 * MERGING TWO CONTACTS — the write (AGL-2625).\n *\n * `planContactMerge` says what the survivor becomes; this performs it, and\n * moves everything that pointed at the merged record. One function behind\n * two doors — the console's `crm/contacts-merge` route and\n * `POST /v1/contacts/{id}/merge` — so the REST caller and the record page\n * cannot merge two different ways.\n *\n * ## The order is the crash-safety argument\n *\n * 1. Every deal, task and activity naming the merged contact is repointed\n * at the survivor — on a deal, its contact roles too (AGL-3521) — and\n * every lead converted into it, and every contact that reports to it\n * (AGL-3515), in each holder's facet. Batched,\n * bounded, idempotent: a re-run finds nothing left to move.\n * 2. Then, in ONE transaction: the survivor takes the plan, every address\n * it now answers to is indexed at it, and the merged document is\n * deleted — with both records re-read inside the transaction, so two\n * merges racing on one pair cannot both succeed and a plan is never\n * computed on a document that changed under it.\n *\n * Children first because a process that dies between the two steps leaves\n * children pointing at a record that EXISTS — the survivor — while the merged\n * document is still there for a retry to fold in. The other order would\n * leave deals pointing at a deleted contact, which is a record nobody can\n * open from anywhere.\n *\n * ## What it does not move\n *\n * List memberships, suppression entries and the delivery log are keyed by\n * ADDRESS, not by contact id, and stay under the merged address; a capture\n * on that address resolves to the survivor through the index, which is the\n * fact those keys need. A company's contacts count drops only for a company\n * both records named — the survivor still names the rest.\n */\n\nimport { CONTACT_EMAIL_INDEX_COLLECTION, contactEmails } from '@aglyn/aglyn/app-utils/contacts'\nimport type { HostActivityActor } from '@aglyn/aglyn/app-utils/activity-presenter'\nimport { planContactMerge } from '../model/contact-merge'\nimport { contactFacetHolders, repointContactReportsTo } from './contact-reports-to'\nimport { sweepDealContactRoles } from './deal-contact-roles'\nimport { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { personKey } from '@aglyn/aglyn/app-utils/person-key'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { settleCompanyContactsCounts } from '@aglyn/tenant-data-admin/server/contact-company-link'\nimport { restampCrmListFieldsAt } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { logHostActivity } from '@aglyn/tenant-data-admin/server/organizations'\nimport { recomputeCrmNextTaskAt } from './crm-next-activity'\n\n/** How many pointing rows one repoint pass reads — and one batch writes. */\nconst REPOINT_PAGE = 400\n/** A ceiling on passes, so a query that keeps answering cannot loop forever. */\nconst REPOINT_PASSES = 25\n\nexport interface MergeContactsOptions {\n firestore: FirebaseFirestore.Firestore\n /** `orgs/{orgId}` — the org whose contacts both records are. */\n orgRef: FirebaseFirestore.DocumentReference\n /** The record that stays, and keeps its address as the identity. */\n survivorId: string\n /** The record folded in and deleted. */\n mergedId: string\n actor: HostActivityActor\n /**\n * The site whose console did the work: where the activity feed entry\n * lands, and the site a timeline note is filed under. `null` for a door\n * with no site — the REST API — which logs no host entry.\n */\n hostId: string | null\n /** The actor's display name for the timeline note, when the door knows it. */\n actorName?: string | null\n}\n\nexport interface MergeContactsRepointed {\n deals: number\n tasks: number\n activities: number\n leads: number\n}\n\nexport type MergeContactsResult =\n | {\n ok: true\n survivorId: string\n survivorEmail: string\n mergedId: string\n mergedEmail: string\n /** Every address the survivor answers to now, primary first. */\n emails: string[]\n repointed: MergeContactsRepointed\n }\n | { ok: false; reason: 'same-record' | 'survivor-missing' | 'merged-missing' }\n\n/**\n * Point every row of `collection` whose `contactId` is `from` at `to`.\n *\n * Page, batch, repeat: each pass reads the rows still pointing at the\n * merged record — the ones a previous pass moved no longer match — so the\n * loop ends when a pass finds nothing. `updatedAt` moves with the pointer\n * because an integration syncing by `updatedAfter` has to see the change.\n */\nasync function repointContactId(\n firestore: FirebaseFirestore.Firestore,\n collection: FirebaseFirestore.CollectionReference,\n from: string,\n to: string,\n): Promise<number> {\n let moved = 0\n for (let pass = 0; pass < REPOINT_PASSES; pass += 1) {\n const page = await collection\n .where('contactId', '==', from)\n .limit(REPOINT_PAGE)\n .get()\n if (page.empty) break\n const batch = firestore.batch()\n for (const row of page.docs) {\n batch.update(row.ref, {\n contactId: to,\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n await batch.commit()\n moved += page.size\n if (page.size < REPOINT_PAGE) break\n }\n return moved\n}\n\n/**\n * The leads converted into the merged contact, restamped at the survivor.\n *\n * A lead lives at `orgs/{orgId}/leads/{personKey(email)}` (AGL-3275), so there\n * is still no query — one document per address, asked for directly. This used\n * to be bounded by sites TIMES addresses because a lead lived under each site;\n * it is bounded by addresses now, and the sites drop out of the signature\n * because the collection no longer has one per site to walk.\n */\nasync function repointLeads(\n orgRef: FirebaseFirestore.DocumentReference,\n emails: readonly string[],\n from: string,\n to: string,\n): Promise<number> {\n let moved = 0\n /*\n * ONE ROW PER ADDRESS, on the org (AGL-3275). This walked the sites because\n * there was a lead under each of them; re-pointing the same document once\n * per site would now be the same write repeated, and the second would find\n * `convertedContactId` already moved and skip — a silent undercount rather\n * than a wrong answer, but an undercount the caller reports to the operator.\n */\n for (const email of emails) {\n const key = personKey(email)\n if (!key) continue\n const leadRef = orgRef.collection('leads').doc(key)\n const lead = await leadRef.get()\n if (!lead.exists || lead.get('convertedContactId') !== from) continue\n await leadRef.update({\n convertedContactId: to,\n updatedAt: FieldValue.serverTimestamp(),\n })\n moved += 1\n }\n return moved\n}\n\nexport async function mergeContacts(\n options: MergeContactsOptions,\n): Promise<MergeContactsResult> {\n const { firestore, orgRef, survivorId, mergedId, actor, hostId } = options\n if (!survivorId || !mergedId || survivorId === mergedId) {\n return { ok: false, reason: 'same-record' }\n }\n const contactsRef = orgRef.collection('contacts')\n const survivorRef = contactsRef.doc(survivorId)\n const mergedRef = contactsRef.doc(mergedId)\n const [survivorBefore, mergedBefore] = await Promise.all([\n survivorRef.get(),\n mergedRef.get(),\n ])\n if (!survivorBefore.exists) return { ok: false, reason: 'survivor-missing' }\n if (!mergedBefore.exists) return { ok: false, reason: 'merged-missing' }\n const mergedData = (mergedBefore.data() ?? {}) as Record<string, unknown>\n const survivorData = (survivorBefore.data() ?? {}) as Record<string, unknown>\n\n /*==========================================\n * 1. THE CHILDREN — before the parent, for the reason the header gives.\n *=========================================*/\n const [deals, tasks, activities] = await Promise.all([\n // A deal names the merged record as a contact role or as its Primary\n // (AGL-3521); both move, and a deal naming both records keeps one row.\n sweepDealContactRoles(\n firestore,\n orgRef.collection(CRM_COLLECTIONS.deals),\n mergedId,\n survivorId,\n 'contact merge',\n ),\n repointContactId(firestore, orgRef.collection(CRM_COLLECTIONS.tasks), mergedId, survivorId),\n repointContactId(\n firestore,\n orgRef.collection(CRM_COLLECTIONS.activities),\n mergedId,\n survivorId,\n ),\n ])\n const leads = await repointLeads(\n orgRef,\n contactEmails(mergedData),\n mergedId,\n survivorId,\n )\n /*\n * Whoever reported to the merged record reports to the survivor now. A\n * holder can point only at a contact it holds, so the holders of either\n * record are every facet a pointer can sit in; the survivor's own pointer\n * at the merged record is cleared rather than turned on itself.\n */\n await repointContactReportsTo(\n firestore,\n contactsRef,\n [...contactFacetHolders(mergedData), ...contactFacetHolders(survivorData)],\n mergedId,\n survivorId,\n 'contact merge',\n )\n\n /*==========================================\n * 2. THE SWAP — one transaction over both documents and the index.\n *=========================================*/\n const index = orgRef.collection(CONTACT_EMAIL_INDEX_COLLECTION)\n const swapped = await firestore.runTransaction(async (transaction) => {\n const [survivor, merged] = await Promise.all([\n transaction.get(survivorRef),\n transaction.get(mergedRef),\n ])\n if (!survivor.exists) return { reason: 'survivor-missing' as const }\n if (!merged.exists) return { reason: 'merged-missing' as const }\n const plan = planContactMerge(\n (survivor.data() ?? {}) as Record<string, unknown>,\n (merged.data() ?? {}) as Record<string, unknown>,\n { survivorId, mergedId },\n )\n transaction.set(\n survivorRef,\n { ...plan.survivor, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n for (const email of plan.emails) {\n const key = personKey(email)\n if (!key) continue\n transaction.set(\n index.doc(key),\n { email, contactId: survivorId, updatedAt: FieldValue.serverTimestamp() },\n { merge: true },\n )\n }\n transaction.delete(mergedRef)\n return { plan, survivor: (survivor.data() ?? {}) as Record<string, unknown> }\n })\n if ('reason' in swapped) return { ok: false, reason: swapped.reason }\n const { plan } = swapped\n // The survivor now carries both records' addresses, holders and facets —\n // what the Contacts list searches and filters by (AGL-3321).\n await restampCrmListFieldsAt(survivorRef, 'contacts')\n\n /*\n * The companies both records named counted two people and now count one\n * (AGL-2613). Settled after the swap the way the capture settles its\n * counts: a figure that could not move is logged, and the company page's\n * live aggregate corrects it.\n */\n await settleCompanyContactsCounts(orgRef.collection(CRM_COLLECTIONS.companies), {\n companyId: null,\n mirror: null,\n counts: plan.companyCounts,\n })\n /*\n * The survivor now owns the merged record's open tasks too, so its next\n * activity is the earliest of both (AGL-2661). Settled the same way: a\n * figure that could not move is logged, and the Fields section's\n * recompute corrects it.\n */\n await recomputeCrmNextTaskAt(firestore, orgRef.id, [{ contactId: survivorId }]).catch(\n (error: unknown) => {\n console.error('contact merge next activity could not be recomputed', survivorId, error)\n },\n )\n\n const mergedEmail = contactEmails(mergedData)[0] ?? ''\n const survivorEmail = plan.emails[0] ?? ''\n // The canonical name — the one identity every holder shares — for the\n // feed; the address stands in for a person nobody has named.\n const survivorName =\n typeof swapped.survivor['name'] === 'string' ? swapped.survivor['name'] : ''\n\n /*\n * THE TIMELINE NOTE, on the survivor: the merge is an event in this\n * person's history, and the record page is where a reader looks for one.\n * Filed under the calling site, or the survivor's first site for a door\n * with none; its own catch, because a note that could not be written must\n * not read as a merge that did not happen.\n */\n try {\n await orgRef.collection(CRM_COLLECTIONS.activities).add({\n kind: 'note',\n body: `Merged with ${mergedEmail}`,\n atMs: Date.now(),\n byUid: actor.uid,\n ...(options.actorName || actor.email\n ? { byName: options.actorName || actor.email }\n : {}),\n contactId: survivorId,\n visibleTo: plan.visibleTo,\n hostId: hostId ?? String(survivorData['hostId'] ?? ''),\n createdByUid: actor.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n } catch (error) {\n console.error('[contact-merge] timeline note failed', survivorId, error)\n }\n\n // The audit line (AGL-2622), in the feed of the site whose console did\n // the work. A door with no site — the API — has no host feed to write.\n if (hostId) {\n await logHostActivity(hostId, actor, `Merged with ${mergedEmail}`, {\n type: 'contact',\n id: survivorId,\n name: survivorName || survivorEmail,\n })\n }\n\n return {\n ok: true,\n survivorId,\n survivorEmail,\n mergedId,\n mergedEmail,\n emails: plan.emails,\n repointed: { deals, tasks, activities, leads },\n }\n}\n"],"names":["CONTACT_EMAIL_INDEX_COLLECTION","contactEmails","planContactMerge","contactFacetHolders","repointContactReportsTo","sweepDealContactRoles","CRM_COLLECTIONS","personKey","FieldValue","settleCompanyContactsCounts","restampCrmListFieldsAt","logHostActivity","recomputeCrmNextTaskAt","REPOINT_PAGE","REPOINT_PASSES","repointContactId","firestore","collection","from","to","moved","pass","page","where","limit","get","empty","batch","row","docs","update","ref","contactId","updatedAt","serverTimestamp","commit","size","repointLeads","orgRef","emails","email","key","leadRef","doc","lead","exists","convertedContactId","mergeContacts","options","mergedBefore","survivorBefore","plan","survivorId","mergedId","actor","hostId","ok","reason","contactsRef","survivorRef","mergedRef","Promise","all","mergedData","data","survivorData","deals","tasks","activities","leads","index","swapped","runTransaction","transaction","survivor","merged","set","merge","delete","companies","companyId","mirror","counts","companyCounts","id","catch","error","console","mergedEmail","survivorEmail","survivorName","add","kind","body","atMs","Date","now","byUid","uid","actorName","byName","visibleTo","String","createdByUid","createdAt","type","name","repointed"],"mappings":"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"}
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
-
import { firebaseAdmin
|
|
17
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
18
18
|
import { CRM_NEXT_ACTIVITY_LINKS_MAX } from "../model/next-activity.js";
|
|
19
19
|
import { readCrmRouteScope } from "./org-caller.js";
|
|
20
20
|
import { authorizeCrmWriter } from "./task-routes.js";
|
|
21
|
+
import { recomputeCrmNextTaskAt, sweepCrmNextTaskAt } from "./crm-next-activity.js";
|
|
21
22
|
/**
|
|
22
23
|
* `POST crm/next-activity` — recompute `nextTaskAtMs` (AGL-2661).
|
|
23
24
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/next-activity-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/next-activity-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n} from '@aglyn/tenant-data-admin'\nimport {\n CRM_NEXT_ACTIVITY_LINKS_MAX,\n type CrmNextActivityResponse,\n} from '../model/next-activity'\nimport { readCrmRouteScope } from './org-caller'\nimport { authorizeCrmWriter } from './task-routes'\nimport { type CrmNextActivityLink } from '../model/crm-next-activity'\nimport { recomputeCrmNextTaskAt, sweepCrmNextTaskAt } from './crm-next-activity'\n\n/**\n * `POST crm/next-activity` — recompute `nextTaskAtMs` (AGL-2661).\n *\n * Body: `{ hostId | orgId, links: [{ contactId?, companyId?, dealId? }] }`\n * after a client-direct task write, or `{ hostId | orgId, all: true }` for\n * the Fields section's whole-organization recompute. Authorized the way the\n * task routes are — a member who may write this CRM — and never more\n * revealing than a task write: the route stores a figure derived from\n * tasks the caller could already read, and answers with counts.\n *\n * The links are not checked against the caller's scope. A scoped member\n * naming a record they cannot see gets that record's figure recomputed\n * from its own tasks, which is the figure it should carry anyway; nothing\n * about the record comes back.\n */\nexport const crmNextActivityHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n try {\n const writer = await authorizeCrmWriter(req, scope)\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n const firestore = firebaseAdmin.app().firestore()\n if (body['all'] === true) {\n const sweep = await sweepCrmNextTaskAt(firestore, writer.orgId)\n const answer: CrmNextActivityResponse = {\n ok: true,\n records: sweep.scheduled + sweep.cleared,\n missing: 0,\n tasks: sweep.tasks,\n cleared: sweep.cleared,\n truncated: sweep.truncated,\n }\n res.status(200).json(answer)\n return\n }\n const links = readLinks(body['links'])\n if (!links) {\n res.status(400).json({ error: 'Name the records to recompute, or ask for all of them.' })\n return\n }\n const result = await recomputeCrmNextTaskAt(firestore, writer.orgId, links)\n const answer: CrmNextActivityResponse = { ok: true, ...result }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] next-activity failed', error)\n res.status(500).json({ error: 'The next activity could not be recomputed.' })\n }\n}\n\n/** The links off the body, held to ids: a string each, trimmed, capped. */\nfunction readLinks(raw: unknown): CrmNextActivityLink[] | null {\n if (!Array.isArray(raw)) return null\n const links: CrmNextActivityLink[] = []\n for (const entry of raw.slice(0, CRM_NEXT_ACTIVITY_LINKS_MAX)) {\n const record = (entry ?? {}) as Record<string, unknown>\n const link: CrmNextActivityLink = {}\n for (const field of ['contactId', 'companyId', 'dealId'] as const) {\n const id = String(record[field] ?? '').trim().slice(0, 128)\n if (id) link[field] = id\n }\n if (Object.keys(link).length) links.push(link)\n }\n return links\n}\n"],"names":["firebaseAdmin","CRM_NEXT_ACTIVITY_LINKS_MAX","readCrmRouteScope","authorizeCrmWriter","recomputeCrmNextTaskAt","sweepCrmNextTaskAt","crmNextActivityHandler","req","res","method","status","json","error","body","scope","writer","ok","firestore","app","sweep","orgId","answer","records","scheduled","cleared","missing","tasks","truncated","links","readLinks","result","console","raw","Array","isArray","entry","slice","record","link","field","id","String","trim","Object","keys","length","push"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,SACEA,aAAa,QACR,2BAA0B;AACjC,SACEC,2BAA2B,QAEtB,4BAAwB;AAC/B,SAASC,iBAAiB,QAAQ,kBAAc;AAChD,SAASC,kBAAkB,QAAQ,mBAAe;AAElD,SAASC,sBAAsB,EAAEC,kBAAkB,QAAQ,yBAAqB;AAEhF;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMC,yBAA2C,OAAOC,KAAKC;QAKpDD;IAJd,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzBD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACnD;IACF;IACA,MAAMC,QAAQN,YAAAA,IAAIM,IAAI,YAARN,YAAY,CAAC;IAC3B,MAAMO,QAAQZ,kBAAkBW;IAChC,IAAI,CAACC,OAAO;QACVN,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAiB;QAC/C;IACF;IACA,IAAI;QACF,MAAMG,SAAS,MAAMZ,mBAAmBI,KAAKO;QAC7C,IAAIC,OAAOC,EAAE,KAAK,OAAO;YACvBR,IAAIE,MAAM,CAACK,OAAOL,MAAM,EAAEC,IAAI,CAACI,OAAOF,IAAI;YAC1C;QACF;QACA,MAAMI,YAAYjB,cAAckB,GAAG,GAAGD,SAAS;QAC/C,IAAIJ,IAAI,CAAC,MAAM,KAAK,MAAM;YACxB,MAAMM,QAAQ,MAAMd,mBAAmBY,WAAWF,OAAOK,KAAK;YAC9D,MAAMC,SAAkC;gBACtCL,IAAI;gBACJM,SAASH,MAAMI,SAAS,GAAGJ,MAAMK,OAAO;gBACxCC,SAAS;gBACTC,OAAOP,MAAMO,KAAK;gBAClBF,SAASL,MAAMK,OAAO;gBACtBG,WAAWR,MAAMQ,SAAS;YAC5B;YACAnB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAACU;YACrB;QACF;QACA,MAAMO,QAAQC,UAAUhB,IAAI,CAAC,QAAQ;QACrC,IAAI,CAACe,OAAO;YACVpB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAyD;YACvF;QACF;QACA,MAAMkB,SAAS,MAAM1B,uBAAuBa,WAAWF,OAAOK,KAAK,EAAEQ;QACrE,MAAMP,SAAkC;YAAEL,IAAI;WAASc;QACvDtB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAACU;IACvB,EAAE,OAAOT,OAAO;QACdmB,QAAQnB,KAAK,CAAC,8BAA8BA;QAC5CJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA6C;IAC7E;AACF,EAAC;AAED,yEAAyE,GACzE,SAASiB,UAAUG,GAAY;IAC7B,IAAI,CAACC,MAAMC,OAAO,CAACF,MAAM,OAAO;IAChC,MAAMJ,QAA+B,EAAE;IACvC,KAAK,MAAMO,SAASH,IAAII,KAAK,CAAC,GAAGnC,6BAA8B;QAC7D,MAAMoC,SAAUF,gBAAAA,QAAS,CAAC;QAC1B,MAAMG,OAA4B,CAAC;QACnC,KAAK,MAAMC,SAAS;YAAC;YAAa;YAAa;SAAS,CAAW;gBAC/CF;YAAlB,MAAMG,KAAKC,QAAOJ,gBAAAA,MAAM,CAACE,MAAM,YAAbF,gBAAiB,IAAIK,IAAI,GAAGN,KAAK,CAAC,GAAG;YACvD,IAAII,IAAIF,IAAI,CAACC,MAAM,GAAGC;QACxB;QACA,IAAIG,OAAOC,IAAI,CAACN,MAAMO,MAAM,EAAEjB,MAAMkB,IAAI,CAACR;IAC3C;IACA,OAAOV;AACT"}
|
|
@@ -0,0 +1,53 @@
|
|
|
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 { PluginPersonRecordsEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure';
|
|
18
|
+
/**
|
|
19
|
+
* THE CRM'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080): the records
|
|
20
|
+
* eraser of the plugin that keeps people.
|
|
21
|
+
*
|
|
22
|
+
* `locate` names the person's contacts — every contact document whose address
|
|
23
|
+
* is theirs — before anything is erased, so every other plugin can find what
|
|
24
|
+
* it keeps by them. `erase` runs after every other eraser:
|
|
25
|
+
*
|
|
26
|
+
* 1. each contact and its satellites — each linked company's contact count
|
|
27
|
+
* moved down once, deals unlinked (a deal is the team's own pipeline
|
|
28
|
+
* record and stays; the person leaves its contact roles, AGL-3521), tasks and activities deleted, every other contact's
|
|
29
|
+
* reports-to that named the person cleared (AGL-3515) — then the contact
|
|
30
|
+
* document itself, WHOLE. Not the CRM's delete, which is a detach that
|
|
31
|
+
* leaves the row for the other holders: every site's facet, every consent
|
|
32
|
+
* entry and every attribution goes with it, and with the facets every
|
|
33
|
+
* profile field — the birthdate, the phones, the other address, the
|
|
34
|
+
* assistant — that Salesforce's standard fields put there;
|
|
35
|
+
* 2. the person's lead: the organization's row (AGL-3275), and every legacy
|
|
36
|
+
* per-site row still standing on a site of this workspace — and no other
|
|
37
|
+
* workspace's;
|
|
38
|
+
* 3. once those rows are gone, `host.records.removed` names them, so a plugin
|
|
39
|
+
* keeping a figure over leads — a form counting the people it filed —
|
|
40
|
+
* drops by this person and by nothing else (AGL-3330).
|
|
41
|
+
*
|
|
42
|
+
* Each step is best-effort against the others: a failure is logged and counted
|
|
43
|
+
* as zero, and the report says what happened. A DRY RUN counts the contacts
|
|
44
|
+
* and the lead rows and writes nothing.
|
|
45
|
+
*/
|
|
46
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
47
|
+
export interface CrmPersonEraserDeps {
|
|
48
|
+
firestore(): Firestore;
|
|
49
|
+
}
|
|
50
|
+
export declare function createCrmPersonEraser(deps: CrmPersonEraserDeps): PluginPersonRecordsEraser;
|
|
51
|
+
/** The records eraser over the platform's own Firestore, as the declarations register it. */
|
|
52
|
+
export declare function crmPersonEraser(): PluginPersonRecordsEraser;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
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 { CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
|
|
18
|
+
import { runPluginEventHandlers } from "@aglyn/aglyn/plugin-manager/plugin-events";
|
|
19
|
+
import { companyContactsCountFields } from "@aglyn/tenant-data-admin/server/contact-company-link";
|
|
20
|
+
import { deleteWhereEquals } from "@aglyn/tenant-data-admin/server/paged-sweeps";
|
|
21
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
22
|
+
import { contactFacetHolders, repointContactReportsTo } from "./contact-reports-to.js";
|
|
23
|
+
import { sweepDealContactRoles } from "./deal-contact-roles.js";
|
|
24
|
+
/** The label every log line of this eraser carries — never the address. */ const LABEL = 'crm person eraser';
|
|
25
|
+
async function contactsOf(firestore, target) {
|
|
26
|
+
const contacts = firestore.collection('orgs').doc(target.orgId).collection('contacts');
|
|
27
|
+
return (await contacts.where('email', '==', target.email).get()).docs;
|
|
28
|
+
}
|
|
29
|
+
export function createCrmPersonEraser(deps) {
|
|
30
|
+
async function locate(target) {
|
|
31
|
+
return (await contactsOf(deps.firestore(), target)).map((contact)=>String(contact.id));
|
|
32
|
+
}
|
|
33
|
+
async function erase(request) {
|
|
34
|
+
const db = deps.firestore();
|
|
35
|
+
const orgRef = db.collection('orgs').doc(request.orgId);
|
|
36
|
+
const report = {
|
|
37
|
+
contacts: 0,
|
|
38
|
+
companyLinks: 0,
|
|
39
|
+
deals: 0,
|
|
40
|
+
tasks: 0,
|
|
41
|
+
activities: 0,
|
|
42
|
+
leads: 0
|
|
43
|
+
};
|
|
44
|
+
const wanted = new Set(request.contactIds);
|
|
45
|
+
// The contacts `locate` named, read again: a dry run counts them, and a
|
|
46
|
+
// contact deleted since is simply not there to erase.
|
|
47
|
+
const contacts = (await contactsOf(db, request)).filter((contact)=>wanted.has(contact.id));
|
|
48
|
+
const hostIds = (await db.collection('hosts').where('orgId', '==', request.orgId).get()).docs.map((doc)=>String(doc.id));
|
|
49
|
+
if (request.dryRun) {
|
|
50
|
+
let leads = (await orgRef.collection('leads').doc(request.key).get()).exists ? 1 : 0;
|
|
51
|
+
for (const hostId of hostIds){
|
|
52
|
+
const lead = await db.collection('hosts').doc(hostId).collection('leads').doc(request.key).get();
|
|
53
|
+
if (lead.exists) leads += 1;
|
|
54
|
+
}
|
|
55
|
+
return _extends({}, report, {
|
|
56
|
+
contacts: contacts.length,
|
|
57
|
+
leads
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
for (const contact of contacts){
|
|
61
|
+
var _contact_data;
|
|
62
|
+
const contactId = String(contact.id);
|
|
63
|
+
const companyIds = contact.get('companyIds');
|
|
64
|
+
const linked = Array.isArray(companyIds) ? companyIds.filter((id)=>typeof id === 'string' && !!id) : [];
|
|
65
|
+
for (const companyId of new Set(linked)){
|
|
66
|
+
try {
|
|
67
|
+
await orgRef.collection(CRM_COLLECTIONS.companies).doc(companyId).update(companyContactsCountFields(-1));
|
|
68
|
+
report.companyLinks += 1;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`${LABEL}: company count could not move for ${companyId}`, error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Off every deal: its contact role, and its Primary with `contactId` (AGL-3521).
|
|
74
|
+
report.deals += await sweepDealContactRoles(db, orgRef.collection(CRM_COLLECTIONS.deals), contactId, null, LABEL);
|
|
75
|
+
report.tasks += await deleteWhereEquals(db, orgRef.collection(CRM_COLLECTIONS.tasks), 'contactId', contactId, LABEL);
|
|
76
|
+
report.activities += await deleteWhereEquals(db, orgRef.collection(CRM_COLLECTIONS.activities), 'contactId', contactId, LABEL);
|
|
77
|
+
// A pointer at an erased person is a trace of them: cleared in every
|
|
78
|
+
// holder's facet that could hold one — the holders of this contact.
|
|
79
|
+
await repointContactReportsTo(db, orgRef.collection('contacts'), contactFacetHolders((_contact_data = contact.data()) != null ? _contact_data : {}), contactId, null, LABEL);
|
|
80
|
+
try {
|
|
81
|
+
await contact.ref.delete();
|
|
82
|
+
report.contacts += 1;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`${LABEL}: contact delete failed for ${contactId}`, error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/*
|
|
88
|
+
* THE ORG ROW, ONCE (AGL-3275) — and then every legacy row still standing.
|
|
89
|
+
* A lead is one document for the whole org now, but until every site is
|
|
90
|
+
* folded a person can still be held at `hosts/{hostId}/leads`, and an
|
|
91
|
+
* erasure that deleted only the org row would leave that copy behind. So
|
|
92
|
+
* both are swept, and `leads` is what was actually destroyed rather than
|
|
93
|
+
* how many places were looked at.
|
|
94
|
+
*/ const erasedLeads = [];
|
|
95
|
+
try {
|
|
96
|
+
const orgLead = orgRef.collection('leads').doc(request.key);
|
|
97
|
+
const snapshot = await orgLead.get();
|
|
98
|
+
if (snapshot.exists) {
|
|
99
|
+
var _snapshot_data;
|
|
100
|
+
erasedLeads.push({
|
|
101
|
+
id: request.key,
|
|
102
|
+
data: (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {}
|
|
103
|
+
});
|
|
104
|
+
await orgLead.delete();
|
|
105
|
+
report.leads += 1;
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error(`${LABEL}: org lead delete failed`, error);
|
|
109
|
+
}
|
|
110
|
+
for (const hostId of hostIds){
|
|
111
|
+
try {
|
|
112
|
+
const lead = db.collection('hosts').doc(hostId).collection('leads').doc(request.key);
|
|
113
|
+
const snapshot = await lead.get();
|
|
114
|
+
if (snapshot.exists) {
|
|
115
|
+
var _snapshot_data1;
|
|
116
|
+
erasedLeads.push({
|
|
117
|
+
id: request.key,
|
|
118
|
+
data: (_snapshot_data1 = snapshot.data()) != null ? _snapshot_data1 : {}
|
|
119
|
+
});
|
|
120
|
+
await lead.delete();
|
|
121
|
+
report.leads += 1;
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`${LABEL}: legacy lead delete failed for ${hostId}`, error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (erasedLeads.length) {
|
|
128
|
+
// Isolated per plugin by the seam, and never the erasure's failure.
|
|
129
|
+
await runPluginEventHandlers('host.records.removed', {
|
|
130
|
+
orgId: request.orgId,
|
|
131
|
+
hostIds,
|
|
132
|
+
collection: 'leads',
|
|
133
|
+
records: erasedLeads
|
|
134
|
+
}).catch((error)=>console.error(`${LABEL}: records-removed event failed`, error));
|
|
135
|
+
}
|
|
136
|
+
return report;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
locate,
|
|
140
|
+
erase
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** The records eraser over the platform's own Firestore, as the declarations register it. */ export function crmPersonEraser() {
|
|
144
|
+
return createCrmPersonEraser({
|
|
145
|
+
firestore: ()=>firebaseAdmin.app().firestore()
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=person-eraser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/person-eraser.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 { CRM_COLLECTIONS } from '@aglyn/aglyn/app-utils/crm'\nimport { runPluginEventHandlers } from '@aglyn/aglyn/plugin-manager/plugin-events'\nimport type {\n PluginPersonErasureReport,\n PluginPersonErasureRequest,\n PluginPersonErasureTarget,\n PluginPersonRecordsEraser,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { companyContactsCountFields } from '@aglyn/tenant-data-admin/server/contact-company-link'\nimport { deleteWhereEquals } from '@aglyn/tenant-data-admin/server/paged-sweeps'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { contactFacetHolders, repointContactReportsTo } from './contact-reports-to'\nimport { sweepDealContactRoles } from './deal-contact-roles'\n\n/**\n * THE CRM'S SHARE OF A PERSON ERASURE (AGL-2623, AGL-3080): the records\n * eraser of the plugin that keeps people.\n *\n * `locate` names the person's contacts — every contact document whose address\n * is theirs — before anything is erased, so every other plugin can find what\n * it keeps by them. `erase` runs after every other eraser:\n *\n * 1. each contact and its satellites — each linked company's contact count\n * moved down once, deals unlinked (a deal is the team's own pipeline\n * record and stays; the person leaves its contact roles, AGL-3521), tasks and activities deleted, every other contact's\n * reports-to that named the person cleared (AGL-3515) — then the contact\n * document itself, WHOLE. Not the CRM's delete, which is a detach that\n * leaves the row for the other holders: every site's facet, every consent\n * entry and every attribution goes with it, and with the facets every\n * profile field — the birthdate, the phones, the other address, the\n * assistant — that Salesforce's standard fields put there;\n * 2. the person's lead: the organization's row (AGL-3275), and every legacy\n * per-site row still standing on a site of this workspace — and no other\n * workspace's;\n * 3. once those rows are gone, `host.records.removed` names them, so a plugin\n * keeping a figure over leads — a form counting the people it filed —\n * drops by this person and by nothing else (AGL-3330).\n *\n * Each step is best-effort against the others: a failure is logged and counted\n * as zero, and the report says what happened. A DRY RUN counts the contacts\n * and the lead rows and writes nothing.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CrmPersonEraserDeps {\n firestore(): Firestore\n}\n\n/** The label every log line of this eraser carries — never the address. */\nconst LABEL = 'crm person eraser'\n\nasync function contactsOf(\n firestore: Firestore,\n target: PluginPersonErasureTarget,\n): Promise<FirebaseFirestore.QueryDocumentSnapshot[]> {\n const contacts = firestore.collection('orgs').doc(target.orgId).collection('contacts')\n return (await contacts.where('email', '==', target.email).get()).docs\n}\n\nexport function createCrmPersonEraser(deps: CrmPersonEraserDeps): PluginPersonRecordsEraser {\n async function locate(target: PluginPersonErasureTarget): Promise<readonly string[]> {\n return (await contactsOf(deps.firestore(), target)).map((contact) => String(contact.id))\n }\n\n async function erase(request: PluginPersonErasureRequest): Promise<PluginPersonErasureReport> {\n const db = deps.firestore()\n const orgRef = db.collection('orgs').doc(request.orgId)\n const report = {\n contacts: 0,\n companyLinks: 0,\n deals: 0,\n tasks: 0,\n activities: 0,\n leads: 0,\n }\n const wanted = new Set(request.contactIds)\n // The contacts `locate` named, read again: a dry run counts them, and a\n // contact deleted since is simply not there to erase.\n const contacts = (await contactsOf(db, request)).filter((contact) => wanted.has(contact.id))\n const hostIds = (await db.collection('hosts').where('orgId', '==', request.orgId).get()).docs.map(\n (doc) => String(doc.id),\n )\n\n if (request.dryRun) {\n let leads = (await orgRef.collection('leads').doc(request.key).get()).exists ? 1 : 0\n for (const hostId of hostIds) {\n const lead = await db.collection('hosts').doc(hostId).collection('leads').doc(request.key).get()\n if (lead.exists) leads += 1\n }\n return { ...report, contacts: contacts.length, leads }\n }\n\n for (const contact of contacts) {\n const contactId = String(contact.id)\n const companyIds: unknown = contact.get('companyIds')\n const linked = Array.isArray(companyIds)\n ? companyIds.filter((id): id is string => typeof id === 'string' && !!id)\n : []\n for (const companyId of new Set(linked)) {\n try {\n await orgRef\n .collection(CRM_COLLECTIONS.companies)\n .doc(companyId)\n .update(companyContactsCountFields(-1))\n report.companyLinks += 1\n } catch (error) {\n console.error(`${LABEL}: company count could not move for ${companyId}`, error)\n }\n }\n // Off every deal: its contact role, and its Primary with `contactId` (AGL-3521).\n report.deals += await sweepDealContactRoles(\n db,\n orgRef.collection(CRM_COLLECTIONS.deals),\n contactId,\n null,\n LABEL,\n )\n report.tasks += await deleteWhereEquals(db, orgRef.collection(CRM_COLLECTIONS.tasks), 'contactId', contactId, LABEL)\n report.activities += await deleteWhereEquals(\n db,\n orgRef.collection(CRM_COLLECTIONS.activities),\n 'contactId',\n contactId,\n LABEL,\n )\n // A pointer at an erased person is a trace of them: cleared in every\n // holder's facet that could hold one — the holders of this contact.\n await repointContactReportsTo(\n db,\n orgRef.collection('contacts'),\n contactFacetHolders(contact.data() ?? {}),\n contactId,\n null,\n LABEL,\n )\n try {\n await contact.ref.delete()\n report.contacts += 1\n } catch (error) {\n console.error(`${LABEL}: contact delete failed for ${contactId}`, error)\n }\n }\n\n /*\n * THE ORG ROW, ONCE (AGL-3275) — and then every legacy row still standing.\n * A lead is one document for the whole org now, but until every site is\n * folded a person can still be held at `hosts/{hostId}/leads`, and an\n * erasure that deleted only the org row would leave that copy behind. So\n * both are swept, and `leads` is what was actually destroyed rather than\n * how many places were looked at.\n */\n const erasedLeads: Array<{ id: string; data: Record<string, unknown> }> = []\n try {\n const orgLead = orgRef.collection('leads').doc(request.key)\n const snapshot = await orgLead.get()\n if (snapshot.exists) {\n erasedLeads.push({ id: request.key, data: snapshot.data() ?? {} })\n await orgLead.delete()\n report.leads += 1\n }\n } catch (error) {\n console.error(`${LABEL}: org lead delete failed`, error)\n }\n for (const hostId of hostIds) {\n try {\n const lead = db.collection('hosts').doc(hostId).collection('leads').doc(request.key)\n const snapshot = await lead.get()\n if (snapshot.exists) {\n erasedLeads.push({ id: request.key, data: snapshot.data() ?? {} })\n await lead.delete()\n report.leads += 1\n }\n } catch (error) {\n console.error(`${LABEL}: legacy lead delete failed for ${hostId}`, error)\n }\n }\n if (erasedLeads.length) {\n // Isolated per plugin by the seam, and never the erasure's failure.\n await runPluginEventHandlers('host.records.removed', {\n orgId: request.orgId,\n hostIds,\n collection: 'leads',\n records: erasedLeads,\n }).catch((error) => console.error(`${LABEL}: records-removed event failed`, error))\n }\n return report\n }\n\n return { locate, erase }\n}\n\n/** The records eraser over the platform's own Firestore, as the declarations register it. */\nexport function crmPersonEraser(): PluginPersonRecordsEraser {\n return createCrmPersonEraser({ firestore: () => firebaseAdmin.app().firestore() })\n}\n"],"names":["CRM_COLLECTIONS","runPluginEventHandlers","companyContactsCountFields","deleteWhereEquals","firebaseAdmin","contactFacetHolders","repointContactReportsTo","sweepDealContactRoles","LABEL","contactsOf","firestore","target","contacts","collection","doc","orgId","where","email","get","docs","createCrmPersonEraser","deps","locate","map","contact","String","id","erase","request","db","orgRef","report","companyLinks","deals","tasks","activities","leads","wanted","Set","contactIds","filter","has","hostIds","dryRun","key","exists","hostId","lead","length","contactId","companyIds","linked","Array","isArray","companyId","companies","update","error","console","data","ref","delete","erasedLeads","orgLead","snapshot","push","records","catch","crmPersonEraser","app"],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { PluginPersonRecords } from '@aglyn/aglyn/plugin-manager/plugin-person-records';
|
|
18
|
+
/**
|
|
19
|
+
* THE CRM AS THE PLUGIN THAT KEEPS PEOPLE, asked by the plugins that do not
|
|
20
|
+
* (AGL-3080) — the CRM's side of `plugin-person-records`.
|
|
21
|
+
*
|
|
22
|
+
* Two kinds of record are a person here, in the words `plugin-record-routes`
|
|
23
|
+
* addresses them by: a `contact`, the person once they are known, filed at
|
|
24
|
+
* `orgs/{orgId}/contacts/{id}`; and a `lead`, the person before anybody has
|
|
25
|
+
* qualified them, at `orgs/{orgId}/leads/{personKey}` (AGL-3275). One person
|
|
26
|
+
* is one record (AGL-3232), so a lookup by address answers the contact when
|
|
27
|
+
* there is one and the lead only for a caller that asked for any kind.
|
|
28
|
+
*
|
|
29
|
+
* The record's `data` is the stored document. Beside the fields the platform
|
|
30
|
+
* defines (`marketingConsent…`, `emailState`, `visibleTo`), a contact keeps
|
|
31
|
+
* what each consent group knows of the person in `facets.{groupId}` — the
|
|
32
|
+
* sources, stage, owner, company, address and tags — and a lead keeps the
|
|
33
|
+
* same facts at its top level.
|
|
34
|
+
*/
|
|
35
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
36
|
+
/** The person kinds, as `plugin-record-routes` addresses them. */
|
|
37
|
+
export declare const CRM_PERSON_KINDS: {
|
|
38
|
+
readonly contact: "contact";
|
|
39
|
+
readonly lead: "lead";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* The most leads a saved Leads view is taken from: the site's most recently
|
|
43
|
+
* seen, the Leads list's own window (AGL-3234).
|
|
44
|
+
*/
|
|
45
|
+
export declare const CRM_LEADS_VIEW_WINDOW = 200;
|
|
46
|
+
export interface CrmPersonRecordsDeps {
|
|
47
|
+
firestore(): Firestore;
|
|
48
|
+
/** The organization a site belongs to, or `null` for one with none. */
|
|
49
|
+
orgIdForHost(hostId: string): Promise<string | null>;
|
|
50
|
+
/** The consent group a site files its facet under. */
|
|
51
|
+
groupIdForHost(hostId: string): Promise<string>;
|
|
52
|
+
/**
|
|
53
|
+
* The addresses a saved Contacts view selects among one site's contacts,
|
|
54
|
+
* read the way the dynamic-list sweep reads one, and whether the read
|
|
55
|
+
* reached the whole view before its budget.
|
|
56
|
+
*/
|
|
57
|
+
contactViewEmails(input: {
|
|
58
|
+
hostId: string;
|
|
59
|
+
viewId: string;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
emails: string[];
|
|
62
|
+
complete: boolean;
|
|
63
|
+
}>;
|
|
64
|
+
}
|
|
65
|
+
export declare function defaultCrmPersonRecordsDeps(): CrmPersonRecordsDeps;
|
|
66
|
+
export declare function createCrmPersonRecords(deps: CrmPersonRecordsDeps): PluginPersonRecords;
|
|
67
|
+
/** The service as the CRM registers it, its storage the admin app's. */
|
|
68
|
+
export declare const crmPersonRecords: Required<PluginPersonRecords>;
|
|
69
|
+
export {};
|