@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,393 +0,0 @@
|
|
|
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
|
-
*/ /**
|
|
18
|
-
* `POST /api/crm/contacts-import` — one chunk of a contact file, written
|
|
19
|
-
* (AGL-2602).
|
|
20
|
-
*
|
|
21
|
-
* The browser has already read the file and applied the operator's column
|
|
22
|
-
* mapping; what arrives here is up to {@link CONTACT_IMPORT_CHUNK_SIZE} raw
|
|
23
|
-
* rows in the field vocabulary `crm-import.ts` defines. This route judges
|
|
24
|
-
* each one and writes it, and it is the ONLY place either happens: the
|
|
25
|
-
* drawer shows a preview from the same mapper but stores nothing, so a
|
|
26
|
-
* stale tab cannot store something the server would have refused.
|
|
27
|
-
*
|
|
28
|
-
* ## Every row goes through the door every capture goes through
|
|
29
|
-
*
|
|
30
|
-
* `captureHostContact` — the runtime's wrapper over `upsertHostContact`,
|
|
31
|
-
* the one writer of the contacts collection for every door that is not the
|
|
32
|
-
* v1 API: forms, checkout, bookings, the newsletter box — is the writer
|
|
33
|
-
* here too, so a row this file creates raises `contactCreated` like any
|
|
34
|
-
* other capture (AGL-2605). That is what makes an import
|
|
35
|
-
* dedupe on the address the way a second form submission does, land in the
|
|
36
|
-
* capturing group's facet the way a form capture does, record consent
|
|
37
|
-
* against the capturing site, and stop at a hard band where a form
|
|
38
|
-
* capture stops. A bulk path with its own `add()` would be the fastest way
|
|
39
|
-
* to grow an unscoped, unbanded, unconsented copy of the address book.
|
|
40
|
-
*
|
|
41
|
-
* What the door did not know how to write until now — a phone, a title, a
|
|
42
|
-
* company, an owner, a stage, custom values — it now takes as `facet`, and
|
|
43
|
-
* what it did not say until now — created or merged or refused — it now
|
|
44
|
-
* returns as a verdict. Both changes are the door's, so the v1 API and the
|
|
45
|
-
* manual add can reach them next.
|
|
46
|
-
*
|
|
47
|
-
* ## Every lookup paid once per request, not once per row (AGL-3423)
|
|
48
|
-
*
|
|
49
|
-
* A request has sixty seconds, and a row that reads for itself spends them
|
|
50
|
-
* a round trip at a time: the door alone was a dozen serial reads and three
|
|
51
|
-
* aggregate counts per new person, and a two-hundred-row chunk of an
|
|
52
|
-
* Apollo export ran out of time before it answered. So nothing here is
|
|
53
|
-
* read per row:
|
|
54
|
-
*
|
|
55
|
-
* - The door's own lookups — the site, the consent group, who each address
|
|
56
|
-
* already is, who the site erased, the room in the records band — come
|
|
57
|
-
* from one `prepareContactCaptureBatch` for the whole chunk, which every
|
|
58
|
-
* row's capture is handed. The door still makes every decision.
|
|
59
|
-
* - An owner is named by email and stored by uid, so the org's roster is
|
|
60
|
-
* read once and every row resolves against it.
|
|
61
|
-
* - A company is named by name and stored by id, so the distinct names are
|
|
62
|
-
* looked up together, thirty to an `in` query, and the missing ones are
|
|
63
|
-
* created in one batched write — an Apollo file names a different company
|
|
64
|
-
* on nearly every row, and that was a read, a count and a write each.
|
|
65
|
-
*
|
|
66
|
-
* What is left per row is the row's own writes and what a new person sets
|
|
67
|
-
* off, and those run {@link CONTACT_IMPORT_CONCURRENCY} at a time. A row is
|
|
68
|
-
* started only inside {@link CONTACT_IMPORT_TIME_BUDGET_MS}; the rest are
|
|
69
|
-
* reported `not-reached`, so a slow chunk answers with what it did instead
|
|
70
|
-
* of being cut off at the limit with no answer at all.
|
|
71
|
-
*
|
|
72
|
-
* ## A retry is a merge, never a second person
|
|
73
|
-
*
|
|
74
|
-
* The door dedupes on the address, through the index and then the `email`
|
|
75
|
-
* query, so a row that landed before a request was cut off is found by the
|
|
76
|
-
* next attempt and merged into — the one-person-one-record rule a second
|
|
77
|
-
* form submission meets. A company is found by its `nameLower` the same way.
|
|
78
|
-
*
|
|
79
|
-
* ## Who may call it
|
|
80
|
-
*
|
|
81
|
-
* `resolveImportContext` (`import-context.ts`) — the two gates every CSV
|
|
82
|
-
* import route asks, shared with the companies import so one permission
|
|
83
|
-
* has one spelling.
|
|
84
|
-
*/ import { CRM_COLLECTIONS, crmNewRecordListFields, nameSearchFields, visibleToTokens } from "@aglyn/aglyn/server";
|
|
85
|
-
import { CONTACT_IMPORT_CHUNK_SIZE, CONTACT_IMPORT_MAX_BODY_BYTES, normalizeContactImportRow } from "../model/crm-import.js";
|
|
86
|
-
import { firebaseAdmin, prepareContactCaptureBatch } from "@aglyn/tenant-data-admin";
|
|
87
|
-
import { captureHostContact } from "@aglyn/tenant-runtime";
|
|
88
|
-
// The leaf, so a spec that stands a partial barrel in still reaches it.
|
|
89
|
-
import { countUndeliverableEmails, findUndeliverableEmails, IMPORT_MAIL_CHECK_GRACE_MS, settleWithin } from "@aglyn/tenant-data-admin/server/capture-email-check";
|
|
90
|
-
import { FieldValue } from "firebase-admin/firestore";
|
|
91
|
-
import { ownerDirectory, readImportRows, resolveImportContext } from "./import-context.js";
|
|
92
|
-
/** The one sentence every imported contact's timeline opens with. */ export const CONTACT_IMPORT_INTERACTION_SUMMARY = 'Imported from CSV';
|
|
93
|
-
/**
|
|
94
|
-
* How many rows are written at once. Enough to overlap the round trips a
|
|
95
|
-
* row still pays; few enough that a chunk never has more writes in flight
|
|
96
|
-
* than one client connection handles comfortably.
|
|
97
|
-
*/ export const CONTACT_IMPORT_CONCURRENCY = 8;
|
|
98
|
-
/**
|
|
99
|
-
* How long after the request arrived a row may still be STARTED. The
|
|
100
|
-
* function is stopped at sixty seconds; this leaves the rows already in
|
|
101
|
-
* flight, and the mail-server check's grace, room to finish before then.
|
|
102
|
-
*/ export const CONTACT_IMPORT_TIME_BUDGET_MS = 40000;
|
|
103
|
-
/** The most values one Firestore `in` filter may carry. */ const IN_FILTER_LIMIT = 30;
|
|
104
|
-
/** The most writes one batch may carry. */ const BATCH_WRITE_LIMIT = 500;
|
|
105
|
-
/**
|
|
106
|
-
* The holder's live custom-field definitions.
|
|
107
|
-
*
|
|
108
|
-
* Read in full and filtered in memory: a holder has a handful of fields,
|
|
109
|
-
* and a `where` on `retiredAt` would need an index for a collection that
|
|
110
|
-
* fits in one page. Definitions the caller's scope cannot see are left out
|
|
111
|
-
* for the same reason a value under an undefined key is — a value written
|
|
112
|
-
* under a field the holder's own form will never show is a value nobody
|
|
113
|
-
* can edit.
|
|
114
|
-
*/ async function loadFieldDefinitions(orgId, readTokens) {
|
|
115
|
-
const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection(CRM_COLLECTIONS.contactFields).limit(200).get();
|
|
116
|
-
return snapshot.docs.map((doc)=>doc.data()).filter((field)=>!!field.key && !field.retiredAt && visibleToTokens(field.visibleTo, readTokens));
|
|
117
|
-
}
|
|
118
|
-
function pagesOf(items, size) {
|
|
119
|
-
const out = [];
|
|
120
|
-
for(let at = 0; at < items.length; at += size)out.push(items.slice(at, at + size));
|
|
121
|
-
return out;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* The company each row names, in this scope, with the missing ones created.
|
|
125
|
-
*
|
|
126
|
-
* Looked up by `nameLower`, the search twin `nameSearchFields` writes on
|
|
127
|
-
* every company, thirty names to an `in` query and every page at once, and
|
|
128
|
-
* narrowed in memory to the ones the caller's scope can see — two clients
|
|
129
|
-
* of one agency may each have an "Acme" and must each get their own. A
|
|
130
|
-
* name nobody in scope holds is created once for the request, with the
|
|
131
|
-
* same scope stamp every CRM creator uses, so the company lands exactly
|
|
132
|
-
* where a contact captured on this site would; the creates go out in one
|
|
133
|
-
* batched write.
|
|
134
|
-
*
|
|
135
|
-
* THE RECORDS BAND (AGL-2611). A company is a record of the same band the
|
|
136
|
-
* contact behind its row will meet at the door, so the band is walked in
|
|
137
|
-
* file order — the row's company, then the row's contact when the address
|
|
138
|
-
* is new — from the capture batch's one tally. On an org at a full hard
|
|
139
|
-
* band the row gets no company rather than a company it was not allowed to
|
|
140
|
-
* hold, and a later row's company never takes the room an earlier row's
|
|
141
|
-
* contact needed. Nothing is reported here: the contact is refused
|
|
142
|
-
* `audience-band` at its door and the row is listed skipped, which is the
|
|
143
|
-
* one message the operator needs. A plan with an overage rate always
|
|
144
|
-
* admits — a rate is what makes the band meter instead of refuse.
|
|
145
|
-
*
|
|
146
|
-
* Keyed by `nameLower`; a name absent from the map gets no company.
|
|
147
|
-
*/ async function resolveCompanies(context, capture, rows) {
|
|
148
|
-
const firestore = firebaseAdmin.app().firestore();
|
|
149
|
-
const collection = firestore.collection('orgs').doc(context.orgId).collection(CRM_COLLECTIONS.companies);
|
|
150
|
-
const named = new Map();
|
|
151
|
-
for (const { row } of rows){
|
|
152
|
-
if (!row.companyName) continue;
|
|
153
|
-
const fields = nameSearchFields(row.companyName);
|
|
154
|
-
if (!named.has(fields.nameLower)) named.set(fields.nameLower, fields);
|
|
155
|
-
}
|
|
156
|
-
const companies = new Map();
|
|
157
|
-
await Promise.all(pagesOf([
|
|
158
|
-
...named.keys()
|
|
159
|
-
], IN_FILTER_LIMIT).map(async (page)=>{
|
|
160
|
-
const matches = await collection.where('nameLower', 'in', page).get();
|
|
161
|
-
for (const doc of matches.docs){
|
|
162
|
-
var _doc_get, _ref, _doc_get1;
|
|
163
|
-
var _named_get;
|
|
164
|
-
const nameLower = String((_doc_get = doc.get('nameLower')) != null ? _doc_get : '');
|
|
165
|
-
if (!named.has(nameLower) || companies.has(nameLower)) continue;
|
|
166
|
-
if (!visibleToTokens(doc.get('visibleTo'), context.readTokens)) continue;
|
|
167
|
-
// The name as the record spells it, not as this row typed it, so the
|
|
168
|
-
// contact's facet reads the same as the company page.
|
|
169
|
-
companies.set(nameLower, {
|
|
170
|
-
id: doc.id,
|
|
171
|
-
name: String((_ref = (_doc_get1 = doc.get('name')) != null ? _doc_get1 : (_named_get = named.get(nameLower)) == null ? void 0 : _named_get.name) != null ? _ref : '')
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}));
|
|
175
|
-
const creates = [];
|
|
176
|
-
for (const { row } of rows){
|
|
177
|
-
if (row.companyName) {
|
|
178
|
-
const nameLower = nameSearchFields(row.companyName).nameLower;
|
|
179
|
-
if (!companies.has(nameLower) && capture.admit()) {
|
|
180
|
-
const fields = named.get(nameLower);
|
|
181
|
-
const ref = collection.doc();
|
|
182
|
-
creates.push({
|
|
183
|
-
ref,
|
|
184
|
-
record: _extends({}, fields, {
|
|
185
|
-
hostId: context.hostId,
|
|
186
|
-
visibleTo: context.scopeTokens,
|
|
187
|
-
createdByUid: context.uid,
|
|
188
|
-
createdAt: FieldValue.serverTimestamp(),
|
|
189
|
-
updatedAt: FieldValue.serverTimestamp()
|
|
190
|
-
}, crmNewRecordListFields('companies', _extends({}, fields, {
|
|
191
|
-
visibleTo: context.scopeTokens
|
|
192
|
-
})))
|
|
193
|
-
});
|
|
194
|
-
companies.set(nameLower, {
|
|
195
|
-
id: ref.id,
|
|
196
|
-
name: row.companyName
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
if (capture.peek(row.email) === null && capture.erased(row.email) !== true) {
|
|
201
|
-
capture.reserve(row.email);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
for (const page of pagesOf(creates, BATCH_WRITE_LIMIT)){
|
|
205
|
-
const batch = firestore.batch();
|
|
206
|
-
for (const { ref, record } of page)batch.set(ref, record);
|
|
207
|
-
await batch.commit();
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
companies,
|
|
211
|
-
created: creates.length
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* `POST crm/contacts-import` — `{ hostId, rows }` → a {@link ContactImportChunkResult}.
|
|
216
|
-
*
|
|
217
|
-
* Rows are written {@link CONTACT_IMPORT_CONCURRENCY} at a time, which the
|
|
218
|
-
* band allows because it is no longer counted per row: the capture batch
|
|
219
|
-
* counted once, and every create is admitted from its tally — reserved in
|
|
220
|
-
* file order before any row is written, so which rows the band keeps does
|
|
221
|
-
* not depend on which write happened to finish first.
|
|
222
|
-
*/ export const crmContactsImportHandler = async (req, res)=>{
|
|
223
|
-
const startedAt = Date.now();
|
|
224
|
-
if (req.method !== 'POST') {
|
|
225
|
-
res.setHeader('Allow', 'POST');
|
|
226
|
-
return res.status(405).json({
|
|
227
|
-
error: 'Method not allowed'
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
const read = readImportRows(req, {
|
|
231
|
-
maxBodyBytes: CONTACT_IMPORT_MAX_BODY_BYTES,
|
|
232
|
-
chunkSize: CONTACT_IMPORT_CHUNK_SIZE
|
|
233
|
-
});
|
|
234
|
-
if ('error' in read) return res.status(read.status).json({
|
|
235
|
-
error: read.error
|
|
236
|
-
});
|
|
237
|
-
try {
|
|
238
|
-
const context = await resolveImportContext(req);
|
|
239
|
-
if (context.ok === false) return res.status(context.status).json(context.body);
|
|
240
|
-
const fields = await loadFieldDefinitions(context.orgId, context.readTokens);
|
|
241
|
-
const skipped = [];
|
|
242
|
-
const dropped = {};
|
|
243
|
-
const normalized = [];
|
|
244
|
-
/*
|
|
245
|
-
* A duplicate WITHIN the request is skipped rather than merged: the
|
|
246
|
-
* second row would merge onto the first's record and the count would
|
|
247
|
-
* read one created and one merged for one person in one file, which is
|
|
248
|
-
* a number the operator cannot reconcile against the rows they sent.
|
|
249
|
-
* Across requests the door's own dedupe answers, and reports a merge.
|
|
250
|
-
*/ const seen = new Set();
|
|
251
|
-
read.rows.forEach((raw, index)=>{
|
|
252
|
-
const verdict = normalizeContactImportRow(raw, fields);
|
|
253
|
-
if (verdict.ok === false) {
|
|
254
|
-
skipped.push({
|
|
255
|
-
index,
|
|
256
|
-
email: verdict.input,
|
|
257
|
-
reason: verdict.reason
|
|
258
|
-
});
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
if (seen.has(verdict.row.email)) {
|
|
262
|
-
skipped.push({
|
|
263
|
-
index,
|
|
264
|
-
email: verdict.row.email,
|
|
265
|
-
reason: 'duplicate'
|
|
266
|
-
});
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
seen.add(verdict.row.email);
|
|
270
|
-
for (const entry of verdict.row.dropped){
|
|
271
|
-
var _dropped_entry_field;
|
|
272
|
-
dropped[entry.field] = ((_dropped_entry_field = dropped[entry.field]) != null ? _dropped_entry_field : 0) + 1;
|
|
273
|
-
}
|
|
274
|
-
normalized.push({
|
|
275
|
-
index,
|
|
276
|
-
row: verdict.row
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
// The addresses' mail servers (AGL-3328), asked while the rows are
|
|
280
|
-
// written — see the leads import. The check `upsertHostContact` queues
|
|
281
|
-
// stamps the records; this is the count the result states.
|
|
282
|
-
const mailServers = findUndeliverableEmails(normalized.map((entry)=>entry.row.email));
|
|
283
|
-
const imported = [];
|
|
284
|
-
const [owners, capture] = await Promise.all([
|
|
285
|
-
ownerDirectory(context.orgId, normalized.map((entry)=>entry.row)),
|
|
286
|
-
prepareContactCaptureBatch(context.hostId, normalized.map((entry)=>entry.row.email))
|
|
287
|
-
]);
|
|
288
|
-
const { companies, created: companiesCreated } = await resolveCompanies(context, capture, normalized);
|
|
289
|
-
const ownersUnresolved = new Set();
|
|
290
|
-
let created = 0;
|
|
291
|
-
let merged = 0;
|
|
292
|
-
const writeRow = async ({ index, row })=>{
|
|
293
|
-
let ownerUid;
|
|
294
|
-
if (row.ownerEmail) {
|
|
295
|
-
ownerUid = owners.get(row.ownerEmail);
|
|
296
|
-
if (!ownerUid) ownersUnresolved.add(row.ownerEmail);
|
|
297
|
-
}
|
|
298
|
-
const company = row.companyName ? companies.get(nameSearchFields(row.companyName).nameLower) : undefined;
|
|
299
|
-
const verdict = await captureHostContact(_extends({
|
|
300
|
-
hostId: context.hostId,
|
|
301
|
-
email: row.email
|
|
302
|
-
}, row.name ? {
|
|
303
|
-
name: row.name
|
|
304
|
-
} : {}, {
|
|
305
|
-
source: 'import',
|
|
306
|
-
actor: {
|
|
307
|
-
kind: 'member',
|
|
308
|
-
uid: context.uid,
|
|
309
|
-
email: context.email
|
|
310
|
-
},
|
|
311
|
-
interaction: {
|
|
312
|
-
summary: CONTACT_IMPORT_INTERACTION_SUMMARY
|
|
313
|
-
},
|
|
314
|
-
marketingConsent: row.marketingConsent,
|
|
315
|
-
tags: row.tags,
|
|
316
|
-
facet: _extends({}, row.phone ? {
|
|
317
|
-
phone: row.phone
|
|
318
|
-
} : {}, row.jobTitle ? {
|
|
319
|
-
jobTitle: row.jobTitle
|
|
320
|
-
} : {}, company ? {
|
|
321
|
-
companyId: company.id,
|
|
322
|
-
companyName: company.name
|
|
323
|
-
} : {}, row.address ? {
|
|
324
|
-
address: row.address
|
|
325
|
-
} : {}, ownerUid ? {
|
|
326
|
-
ownerUid
|
|
327
|
-
} : {}, row.lifecycleStage ? {
|
|
328
|
-
lifecycleStage: row.lifecycleStage
|
|
329
|
-
} : {}, Object.keys(row.custom).length ? {
|
|
330
|
-
custom: row.custom
|
|
331
|
-
} : {}),
|
|
332
|
-
// An import is the merchant's own act and files nobody under a
|
|
333
|
-
// campaign; the picker on the profile is where that happens.
|
|
334
|
-
campaignIds: [],
|
|
335
|
-
batch: capture
|
|
336
|
-
}));
|
|
337
|
-
if ('refused' in verdict) {
|
|
338
|
-
skipped.push({
|
|
339
|
-
index,
|
|
340
|
-
email: row.email,
|
|
341
|
-
reason: verdict.refused === 'band' ? 'audience-band' : verdict.refused === 'invalid-email' ? 'invalid-email' : verdict.refused === 'erased' ? 'erased' : 'write-failed'
|
|
342
|
-
});
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
imported.push(row.email);
|
|
346
|
-
if (verdict.created) created += 1;
|
|
347
|
-
else merged += 1;
|
|
348
|
-
};
|
|
349
|
-
// A fixed pool drawing from one queue in file order; a row is started
|
|
350
|
-
// only inside the budget, and whatever is left is reported, not dropped.
|
|
351
|
-
let next = 0;
|
|
352
|
-
const worker = async ()=>{
|
|
353
|
-
while(next < normalized.length){
|
|
354
|
-
if (Date.now() - startedAt >= CONTACT_IMPORT_TIME_BUDGET_MS) return;
|
|
355
|
-
const entry = normalized[next];
|
|
356
|
-
next += 1;
|
|
357
|
-
await writeRow(entry);
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
await Promise.all(Array.from({
|
|
361
|
-
length: Math.min(CONTACT_IMPORT_CONCURRENCY, normalized.length)
|
|
362
|
-
}, worker));
|
|
363
|
-
for (const { index, row } of normalized.slice(next)){
|
|
364
|
-
skipped.push({
|
|
365
|
-
index,
|
|
366
|
-
email: row.email,
|
|
367
|
-
reason: 'not-reached'
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
const noMailServer = countUndeliverableEmails(imported, await settleWithin(mailServers, IMPORT_MAIL_CHECK_GRACE_MS));
|
|
371
|
-
const result = _extends({
|
|
372
|
-
received: read.rows.length,
|
|
373
|
-
created,
|
|
374
|
-
merged,
|
|
375
|
-
skipped: skipped.sort((a, b)=>a.index - b.index),
|
|
376
|
-
dropped,
|
|
377
|
-
companiesCreated,
|
|
378
|
-
ownersUnresolved: [
|
|
379
|
-
...ownersUnresolved
|
|
380
|
-
]
|
|
381
|
-
}, noMailServer !== undefined ? {
|
|
382
|
-
noMailServer
|
|
383
|
-
} : {});
|
|
384
|
-
return res.status(200).json(result);
|
|
385
|
-
} catch (error) {
|
|
386
|
-
console.error('crm/contacts-import failed', error);
|
|
387
|
-
return res.status(500).json({
|
|
388
|
-
error: 'The import could not continue.'
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
//# sourceMappingURL=contacts-import.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/contacts-import.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 * `POST /api/crm/contacts-import` — one chunk of a contact file, written\n * (AGL-2602).\n *\n * The browser has already read the file and applied the operator's column\n * mapping; what arrives here is up to {@link CONTACT_IMPORT_CHUNK_SIZE} raw\n * rows in the field vocabulary `crm-import.ts` defines. This route judges\n * each one and writes it, and it is the ONLY place either happens: the\n * drawer shows a preview from the same mapper but stores nothing, so a\n * stale tab cannot store something the server would have refused.\n *\n * ## Every row goes through the door every capture goes through\n *\n * `captureHostContact` — the runtime's wrapper over `upsertHostContact`,\n * the one writer of the contacts collection for every door that is not the\n * v1 API: forms, checkout, bookings, the newsletter box — is the writer\n * here too, so a row this file creates raises `contactCreated` like any\n * other capture (AGL-2605). That is what makes an import\n * dedupe on the address the way a second form submission does, land in the\n * capturing group's facet the way a form capture does, record consent\n * against the capturing site, and stop at a hard band where a form\n * capture stops. A bulk path with its own `add()` would be the fastest way\n * to grow an unscoped, unbanded, unconsented copy of the address book.\n *\n * What the door did not know how to write until now — a phone, a title, a\n * company, an owner, a stage, custom values — it now takes as `facet`, and\n * what it did not say until now — created or merged or refused — it now\n * returns as a verdict. Both changes are the door's, so the v1 API and the\n * manual add can reach them next.\n *\n * ## Every lookup paid once per request, not once per row (AGL-3423)\n *\n * A request has sixty seconds, and a row that reads for itself spends them\n * a round trip at a time: the door alone was a dozen serial reads and three\n * aggregate counts per new person, and a two-hundred-row chunk of an\n * Apollo export ran out of time before it answered. So nothing here is\n * read per row:\n *\n * - The door's own lookups — the site, the consent group, who each address\n * already is, who the site erased, the room in the records band — come\n * from one `prepareContactCaptureBatch` for the whole chunk, which every\n * row's capture is handed. The door still makes every decision.\n * - An owner is named by email and stored by uid, so the org's roster is\n * read once and every row resolves against it.\n * - A company is named by name and stored by id, so the distinct names are\n * looked up together, thirty to an `in` query, and the missing ones are\n * created in one batched write — an Apollo file names a different company\n * on nearly every row, and that was a read, a count and a write each.\n *\n * What is left per row is the row's own writes and what a new person sets\n * off, and those run {@link CONTACT_IMPORT_CONCURRENCY} at a time. A row is\n * started only inside {@link CONTACT_IMPORT_TIME_BUDGET_MS}; the rest are\n * reported `not-reached`, so a slow chunk answers with what it did instead\n * of being cut off at the limit with no answer at all.\n *\n * ## A retry is a merge, never a second person\n *\n * The door dedupes on the address, through the index and then the `email`\n * query, so a row that landed before a request was cut off is found by the\n * next attempt and merged into — the one-person-one-record rule a second\n * form submission meets. A company is found by its `nameLower` the same way.\n *\n * ## Who may call it\n *\n * `resolveImportContext` (`import-context.ts`) — the two gates every CSV\n * import route asks, shared with the companies import so one permission\n * has one spelling.\n */\n\nimport {\n type ContactFieldDefinition,\n CRM_COLLECTIONS,\n crmNewRecordListFields,\n nameSearchFields,\n type PluginApiHandler,\n visibleToTokens,\n} from '@aglyn/aglyn/server'\nimport {\n CONTACT_IMPORT_CHUNK_SIZE,\n CONTACT_IMPORT_MAX_BODY_BYTES,\n type ContactImportChunkResult,\n type ContactImportRawRow,\n type ContactImportRow,\n type ContactImportSkippedRow,\n normalizeContactImportRow,\n} from '../model/crm-import'\nimport {\n type ContactCaptureBatch,\n firebaseAdmin,\n prepareContactCaptureBatch,\n} from '@aglyn/tenant-data-admin'\nimport { captureHostContact } from '@aglyn/tenant-runtime'\n// The leaf, so a spec that stands a partial barrel in still reaches it.\nimport {\n countUndeliverableEmails,\n findUndeliverableEmails,\n IMPORT_MAIL_CHECK_GRACE_MS,\n settleWithin,\n} from '@aglyn/tenant-data-admin/server/capture-email-check'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n type ImportContext,\n ownerDirectory,\n readImportRows,\n resolveImportContext,\n} from './import-context'\n\n/** The one sentence every imported contact's timeline opens with. */\nexport const CONTACT_IMPORT_INTERACTION_SUMMARY = 'Imported from CSV'\n\n/**\n * How many rows are written at once. Enough to overlap the round trips a\n * row still pays; few enough that a chunk never has more writes in flight\n * than one client connection handles comfortably.\n */\nexport const CONTACT_IMPORT_CONCURRENCY = 8\n\n/**\n * How long after the request arrived a row may still be STARTED. The\n * function is stopped at sixty seconds; this leaves the rows already in\n * flight, and the mail-server check's grace, room to finish before then.\n */\nexport const CONTACT_IMPORT_TIME_BUDGET_MS = 40_000\n\n/** The most values one Firestore `in` filter may carry. */\nconst IN_FILTER_LIMIT = 30\n\n/** The most writes one batch may carry. */\nconst BATCH_WRITE_LIMIT = 500\n\n/**\n * The holder's live custom-field definitions.\n *\n * Read in full and filtered in memory: a holder has a handful of fields,\n * and a `where` on `retiredAt` would need an index for a collection that\n * fits in one page. Definitions the caller's scope cannot see are left out\n * for the same reason a value under an undefined key is — a value written\n * under a field the holder's own form will never show is a value nobody\n * can edit.\n */\nasync function loadFieldDefinitions(\n orgId: string,\n readTokens: readonly string[],\n): Promise<ContactFieldDefinition[]> {\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(CRM_COLLECTIONS.contactFields)\n .limit(200)\n .get()\n return snapshot.docs\n .map((doc) => doc.data() as ContactFieldDefinition)\n .filter(\n (field) =>\n !!field.key &&\n !field.retiredAt &&\n visibleToTokens(field.visibleTo, readTokens),\n )\n}\n\n/** A company an import row named, as the CRM stores it. */\ninterface ImportCompany {\n id: string\n name: string\n}\n\nfunction pagesOf<T>(items: readonly T[], size: number): T[][] {\n const out: T[][] = []\n for (let at = 0; at < items.length; at += size) out.push(items.slice(at, at + size))\n return out\n}\n\n/**\n * The company each row names, in this scope, with the missing ones created.\n *\n * Looked up by `nameLower`, the search twin `nameSearchFields` writes on\n * every company, thirty names to an `in` query and every page at once, and\n * narrowed in memory to the ones the caller's scope can see — two clients\n * of one agency may each have an \"Acme\" and must each get their own. A\n * name nobody in scope holds is created once for the request, with the\n * same scope stamp every CRM creator uses, so the company lands exactly\n * where a contact captured on this site would; the creates go out in one\n * batched write.\n *\n * THE RECORDS BAND (AGL-2611). A company is a record of the same band the\n * contact behind its row will meet at the door, so the band is walked in\n * file order — the row's company, then the row's contact when the address\n * is new — from the capture batch's one tally. On an org at a full hard\n * band the row gets no company rather than a company it was not allowed to\n * hold, and a later row's company never takes the room an earlier row's\n * contact needed. Nothing is reported here: the contact is refused\n * `audience-band` at its door and the row is listed skipped, which is the\n * one message the operator needs. A plan with an overage rate always\n * admits — a rate is what makes the band meter instead of refuse.\n *\n * Keyed by `nameLower`; a name absent from the map gets no company.\n */\nasync function resolveCompanies(\n context: Extract<ImportContext, { ok: true }>,\n capture: ContactCaptureBatch,\n rows: readonly { row: ContactImportRow }[],\n): Promise<{ companies: Map<string, ImportCompany>; created: number }> {\n const firestore = firebaseAdmin.app().firestore()\n const collection = firestore\n .collection('orgs')\n .doc(context.orgId)\n .collection(CRM_COLLECTIONS.companies)\n const named = new Map<string, ReturnType<typeof nameSearchFields>>()\n for (const { row } of rows) {\n if (!row.companyName) continue\n const fields = nameSearchFields(row.companyName)\n if (!named.has(fields.nameLower)) named.set(fields.nameLower, fields)\n }\n\n const companies = new Map<string, ImportCompany>()\n await Promise.all(\n pagesOf([...named.keys()], IN_FILTER_LIMIT).map(async (page) => {\n const matches = await collection.where('nameLower', 'in', page).get()\n for (const doc of matches.docs) {\n const nameLower = String(doc.get('nameLower') ?? '')\n if (!named.has(nameLower) || companies.has(nameLower)) continue\n if (!visibleToTokens(doc.get('visibleTo'), context.readTokens)) continue\n // The name as the record spells it, not as this row typed it, so the\n // contact's facet reads the same as the company page.\n companies.set(nameLower, {\n id: doc.id,\n name: String(doc.get('name') ?? named.get(nameLower)?.name ?? ''),\n })\n }\n }),\n )\n\n const creates: { ref: FirebaseFirestore.DocumentReference; record: Record<string, unknown> }[] = []\n for (const { row } of rows) {\n if (row.companyName) {\n const nameLower = nameSearchFields(row.companyName).nameLower\n if (!companies.has(nameLower) && capture.admit()) {\n const fields = named.get(nameLower) as ReturnType<typeof nameSearchFields>\n const ref = collection.doc()\n creates.push({\n ref,\n record: {\n ...fields,\n hostId: context.hostId,\n visibleTo: context.scopeTokens,\n createdByUid: context.uid,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n // What the Companies list searches and filters by (AGL-3321).\n ...crmNewRecordListFields('companies', { ...fields, visibleTo: context.scopeTokens }),\n },\n })\n companies.set(nameLower, { id: ref.id, name: row.companyName })\n }\n }\n if (capture.peek(row.email) === null && capture.erased(row.email) !== true) {\n capture.reserve(row.email)\n }\n }\n for (const page of pagesOf(creates, BATCH_WRITE_LIMIT)) {\n const batch = firestore.batch()\n for (const { ref, record } of page) batch.set(ref, record)\n await batch.commit()\n }\n return { companies, created: creates.length }\n}\n\n/**\n * `POST crm/contacts-import` — `{ hostId, rows }` → a {@link ContactImportChunkResult}.\n *\n * Rows are written {@link CONTACT_IMPORT_CONCURRENCY} at a time, which the\n * band allows because it is no longer counted per row: the capture batch\n * counted once, and every create is admitted from its tally — reserved in\n * file order before any row is written, so which rows the band keeps does\n * not depend on which write happened to finish first.\n */\nexport const crmContactsImportHandler: PluginApiHandler = async (req, res) => {\n const startedAt = Date.now()\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const read = readImportRows<ContactImportRawRow>(req, {\n maxBodyBytes: CONTACT_IMPORT_MAX_BODY_BYTES,\n chunkSize: CONTACT_IMPORT_CHUNK_SIZE,\n })\n if ('error' in read) return res.status(read.status).json({ error: read.error })\n try {\n const context = await resolveImportContext(req)\n if (context.ok === false) return res.status(context.status).json(context.body)\n\n const fields = await loadFieldDefinitions(context.orgId, context.readTokens)\n const skipped: ContactImportSkippedRow[] = []\n const dropped: Record<string, number> = {}\n const normalized: { index: number; row: ContactImportRow }[] = []\n /*\n * A duplicate WITHIN the request is skipped rather than merged: the\n * second row would merge onto the first's record and the count would\n * read one created and one merged for one person in one file, which is\n * a number the operator cannot reconcile against the rows they sent.\n * Across requests the door's own dedupe answers, and reports a merge.\n */\n const seen = new Set<string>()\n read.rows.forEach((raw, index) => {\n const verdict = normalizeContactImportRow(raw, fields)\n if (verdict.ok === false) {\n skipped.push({ index, email: verdict.input, reason: verdict.reason })\n return\n }\n if (seen.has(verdict.row.email)) {\n skipped.push({ index, email: verdict.row.email, reason: 'duplicate' })\n return\n }\n seen.add(verdict.row.email)\n for (const entry of verdict.row.dropped) {\n dropped[entry.field] = (dropped[entry.field] ?? 0) + 1\n }\n normalized.push({ index, row: verdict.row })\n })\n\n // The addresses' mail servers (AGL-3328), asked while the rows are\n // written — see the leads import. The check `upsertHostContact` queues\n // stamps the records; this is the count the result states.\n const mailServers = findUndeliverableEmails(normalized.map((entry) => entry.row.email))\n const imported: string[] = []\n const [owners, capture] = await Promise.all([\n ownerDirectory(\n context.orgId,\n normalized.map((entry) => entry.row),\n ),\n prepareContactCaptureBatch(\n context.hostId,\n normalized.map((entry) => entry.row.email),\n ),\n ])\n const { companies, created: companiesCreated } = await resolveCompanies(\n context,\n capture,\n normalized,\n )\n const ownersUnresolved = new Set<string>()\n let created = 0\n let merged = 0\n\n const writeRow = async ({ index, row }: { index: number; row: ContactImportRow }) => {\n let ownerUid: string | undefined\n if (row.ownerEmail) {\n ownerUid = owners.get(row.ownerEmail)\n if (!ownerUid) ownersUnresolved.add(row.ownerEmail)\n }\n const company = row.companyName\n ? companies.get(nameSearchFields(row.companyName).nameLower)\n : undefined\n const verdict = await captureHostContact({\n hostId: context.hostId,\n email: row.email,\n ...(row.name ? { name: row.name } : {}),\n source: 'import',\n actor: { kind: 'member', uid: context.uid, email: context.email },\n interaction: { summary: CONTACT_IMPORT_INTERACTION_SUMMARY },\n marketingConsent: row.marketingConsent,\n tags: row.tags,\n facet: {\n ...(row.phone ? { phone: row.phone } : {}),\n ...(row.jobTitle ? { jobTitle: row.jobTitle } : {}),\n // The link AND the name: the merge fields read the facet's\n // `companyName`, and a link written alone renders as nothing.\n ...(company ? { companyId: company.id, companyName: company.name } : {}),\n ...(row.address ? { address: row.address } : {}),\n ...(ownerUid ? { ownerUid } : {}),\n ...(row.lifecycleStage ? { lifecycleStage: row.lifecycleStage } : {}),\n ...(Object.keys(row.custom).length ? { custom: row.custom } : {}),\n },\n // An import is the merchant's own act and files nobody under a\n // campaign; the picker on the profile is where that happens.\n campaignIds: [],\n batch: capture,\n })\n if ('refused' in verdict) {\n skipped.push({\n index,\n email: row.email,\n reason:\n verdict.refused === 'band'\n ? 'audience-band'\n : verdict.refused === 'invalid-email'\n ? 'invalid-email'\n : verdict.refused === 'erased'\n ? 'erased'\n : 'write-failed',\n })\n return\n }\n imported.push(row.email)\n if (verdict.created) created += 1\n else merged += 1\n }\n\n // A fixed pool drawing from one queue in file order; a row is started\n // only inside the budget, and whatever is left is reported, not dropped.\n let next = 0\n const worker = async () => {\n while (next < normalized.length) {\n if (Date.now() - startedAt >= CONTACT_IMPORT_TIME_BUDGET_MS) return\n const entry = normalized[next]\n next += 1\n await writeRow(entry)\n }\n }\n await Promise.all(\n Array.from(\n { length: Math.min(CONTACT_IMPORT_CONCURRENCY, normalized.length) },\n worker,\n ),\n )\n for (const { index, row } of normalized.slice(next)) {\n skipped.push({ index, email: row.email, reason: 'not-reached' })\n }\n\n const noMailServer = countUndeliverableEmails(\n imported,\n await settleWithin(mailServers, IMPORT_MAIL_CHECK_GRACE_MS),\n )\n\n const result: ContactImportChunkResult = {\n received: read.rows.length,\n created,\n merged,\n skipped: skipped.sort((a, b) => a.index - b.index),\n dropped,\n companiesCreated,\n ownersUnresolved: [...ownersUnresolved],\n ...(noMailServer !== undefined ? { noMailServer } : {}),\n }\n return res.status(200).json(result)\n } catch (error) {\n console.error('crm/contacts-import failed', error)\n return res.status(500).json({ error: 'The import could not continue.' })\n }\n}\n"],"names":["CRM_COLLECTIONS","crmNewRecordListFields","nameSearchFields","visibleToTokens","CONTACT_IMPORT_CHUNK_SIZE","CONTACT_IMPORT_MAX_BODY_BYTES","normalizeContactImportRow","firebaseAdmin","prepareContactCaptureBatch","captureHostContact","countUndeliverableEmails","findUndeliverableEmails","IMPORT_MAIL_CHECK_GRACE_MS","settleWithin","FieldValue","ownerDirectory","readImportRows","resolveImportContext","CONTACT_IMPORT_INTERACTION_SUMMARY","CONTACT_IMPORT_CONCURRENCY","CONTACT_IMPORT_TIME_BUDGET_MS","IN_FILTER_LIMIT","BATCH_WRITE_LIMIT","loadFieldDefinitions","orgId","readTokens","snapshot","app","firestore","collection","doc","contactFields","limit","get","docs","map","data","filter","field","key","retiredAt","visibleTo","pagesOf","items","size","out","at","length","push","slice","resolveCompanies","context","capture","rows","companies","named","Map","row","companyName","fields","has","nameLower","set","Promise","all","keys","page","matches","where","String","id","name","creates","admit","ref","record","hostId","scopeTokens","createdByUid","uid","createdAt","serverTimestamp","updatedAt","peek","email","erased","reserve","batch","commit","created","crmContactsImportHandler","req","res","startedAt","Date","now","method","setHeader","status","json","error","read","maxBodyBytes","chunkSize","ok","body","skipped","dropped","normalized","seen","Set","forEach","raw","index","verdict","input","reason","add","entry","mailServers","imported","owners","companiesCreated","ownersUnresolved","merged","writeRow","ownerUid","ownerEmail","company","undefined","source","actor","kind","interaction","summary","marketingConsent","tags","facet","phone","jobTitle","companyId","address","lifecycleStage","Object","custom","campaignIds","refused","next","worker","Array","from","Math","min","noMailServer","result","received","sort","a","b","console"],"mappings":"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"}
|
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
/**
|
|
18
|
-
* `POST /api/crm/deals-import` — one chunk of a deals file, written
|
|
19
|
-
* (AGL-2662).
|
|
20
|
-
*
|
|
21
|
-
* The browser has already read the file and applied the operator's column
|
|
22
|
-
* mapping; what arrives here is up to {@link DEAL_IMPORT_CHUNK_SIZE} raw
|
|
23
|
-
* rows in the vocabulary `crm-deal-import.ts` defines. This route judges
|
|
24
|
-
* each one through the same normalizer, resolves the two names a deal is
|
|
25
|
-
* filed under, and writes it with the stamp every CRM creator writes.
|
|
26
|
-
*
|
|
27
|
-
* ## The pipeline and the stage are looked up ONCE per request
|
|
28
|
-
*
|
|
29
|
-
* The org's pipelines are read once — the active ones the caller's scope
|
|
30
|
-
* can see — and every row resolves against that list by name. A row
|
|
31
|
-
* naming no pipeline lands in the default one (or the only one); a row
|
|
32
|
-
* naming no stage lands in its pipeline's first open stage. A name that
|
|
33
|
-
* matches nothing refuses the row by name, before any write, so the
|
|
34
|
-
* operator's skipped file says exactly which cell to fix.
|
|
35
|
-
*
|
|
36
|
-
* ## A deal's status is its stage's kind
|
|
37
|
-
*
|
|
38
|
-
* Landing in a `won` or `lost` stage closes the deal on creation, stamped
|
|
39
|
-
* with the same clock as `stageChangedAtMs`, the way the REST create does.
|
|
40
|
-
* No `dealWon` event fires for an imported win: the deal was won before it
|
|
41
|
-
* was filed here, and an automation on the event is for what happens next.
|
|
42
|
-
*
|
|
43
|
-
* ## The band is counted once per request
|
|
44
|
-
*
|
|
45
|
-
* A deal is a CRM record and meets the records band (AGL-2611), judged
|
|
46
|
-
* the way the companies import judges it: one aggregate at the first
|
|
47
|
-
* create, re-judged locally as the request creates more.
|
|
48
|
-
*/
|
|
49
|
-
import { type CrmDealStage, type CrmPipeline, type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
50
|
-
import { type DealImportRow } from '../model/crm-deal-import';
|
|
51
|
-
/** One pipeline the request can file into, with its stages by name. */
|
|
52
|
-
interface PipelineChoice {
|
|
53
|
-
id: string;
|
|
54
|
-
pipeline: CrmPipeline;
|
|
55
|
-
stages: CrmDealStage[];
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Where one row lands: its pipeline and its stage, or which of the two
|
|
59
|
-
* names the org does not have.
|
|
60
|
-
*/
|
|
61
|
-
export declare function placeDealImportRow(choices: readonly PipelineChoice[], row: Pick<DealImportRow, 'pipeline' | 'stage'>): {
|
|
62
|
-
ok: true;
|
|
63
|
-
pipeline: PipelineChoice;
|
|
64
|
-
stage: CrmDealStage;
|
|
65
|
-
} | {
|
|
66
|
-
ok: false;
|
|
67
|
-
reason: 'unknown-pipeline' | 'unknown-stage';
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* `POST crm/deals-import` — `{ hostId, rows }` → a {@link DealImportChunkResult}.
|
|
71
|
-
*
|
|
72
|
-
* Rows are written one after another so the band's local count stays
|
|
73
|
-
* honest across the request.
|
|
74
|
-
*/
|
|
75
|
-
export declare const crmDealsImportHandler: PluginApiHandler;
|
|
76
|
-
export {};
|