@aglyn/plugins-crm 1.0.0-beta.221 → 1.0.0-beta.222
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-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/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 +92 -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 +15 -1
- 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-recipes.d.ts +40 -0
- package/src/lib/model/crm-recipes.js +244 -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/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/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 +75 -33
- 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 +162 -0
- package/src/lib/server/assign-contact-owner.js +340 -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 +139 -0
- package/src/lib/server/automation-steps.js +472 -0
- package/src/lib/server/automation-steps.js.map +1 -0
- package/src/lib/server/capture-contact.js +65 -21
- 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-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/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 +22 -4
- package/src/lib/server/merge-contacts.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 +15 -7
- 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 +15 -2
- 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-routes.js +30 -16
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server-deal-stage.d.ts +5 -1
- package/src/lib/server-deal-stage.js +10 -5
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +8 -3
- package/src/lib/server.js +47 -43
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +55 -0
- package/src/lib/testing/memory-firestore.js +335 -0
- package/src/lib/testing/memory-firestore.js.map +1 -0
- package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
- package/src/lib/testing/stand-in-email-send-index.js +49 -0
- package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
- package/src/lib/transfer/activities.d.ts +30 -0
- package/src/lib/transfer/activities.js +132 -0
- package/src/lib/transfer/activities.js.map +1 -0
- package/src/lib/transfer/aliases.d.ts +22 -0
- package/src/lib/transfer/aliases.js +1342 -0
- package/src/lib/transfer/aliases.js.map +1 -0
- package/src/lib/transfer/common.d.ts +222 -0
- package/src/lib/transfer/common.js +832 -0
- package/src/lib/transfer/common.js.map +1 -0
- package/src/lib/transfer/companies.d.ts +43 -0
- package/src/lib/transfer/companies.js +549 -0
- package/src/lib/transfer/companies.js.map +1 -0
- package/src/lib/transfer/contacts.d.ts +31 -0
- package/src/lib/transfer/contacts.js +727 -0
- package/src/lib/transfer/contacts.js.map +1 -0
- package/src/lib/transfer/deals.d.ts +66 -0
- package/src/lib/transfer/deals.js +698 -0
- package/src/lib/transfer/deals.js.map +1 -0
- package/src/lib/transfer/email-templates-package.d.ts +40 -0
- package/src/lib/transfer/email-templates-package.js +76 -0
- package/src/lib/transfer/email-templates-package.js.map +1 -0
- package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
- package/src/lib/transfer/email-templates-package.server.js +176 -0
- package/src/lib/transfer/email-templates-package.server.js.map +1 -0
- package/src/lib/transfer/fields.d.ts +102 -0
- package/src/lib/transfer/fields.js +1755 -0
- package/src/lib/transfer/fields.js.map +1 -0
- package/src/lib/transfer/leads.d.ts +30 -0
- package/src/lib/transfer/leads.js +582 -0
- package/src/lib/transfer/leads.js.map +1 -0
- package/src/lib/transfer/pipelines.d.ts +21 -0
- package/src/lib/transfer/pipelines.js +172 -0
- package/src/lib/transfer/pipelines.js.map +1 -0
- package/src/lib/transfer/register.d.ts +47 -0
- package/src/lib/transfer/register.js +277 -0
- package/src/lib/transfer/register.js.map +1 -0
- package/src/lib/transfer/tasks.d.ts +30 -0
- package/src/lib/transfer/tasks.js +418 -0
- package/src/lib/transfer/tasks.js.map +1 -0
- package/src/lib/transfer/ui.d.ts +20 -0
- package/src/lib/transfer/ui.js +42 -0
- package/src/lib/transfer/ui.js.map +1 -0
- package/src/lib/components/company-import-drawer.d.ts +0 -42
- package/src/lib/components/company-import-drawer.js +0 -99
- package/src/lib/components/company-import-drawer.js.map +0 -1
- package/src/lib/components/contact-import-drawer.d.ts +0 -46
- package/src/lib/components/contact-import-drawer.js +0 -149
- package/src/lib/components/contact-import-drawer.js.map +0 -1
- package/src/lib/components/crm-export-all-button.d.ts +0 -39
- package/src/lib/components/crm-export-all-button.js +0 -98
- package/src/lib/components/crm-export-all-button.js.map +0 -1
- package/src/lib/components/csv-import-drawer.d.ts +0 -139
- package/src/lib/components/csv-import-drawer.js +0 -757
- package/src/lib/components/csv-import-drawer.js.map +0 -1
- package/src/lib/components/deal-import-drawer.d.ts +0 -24
- package/src/lib/components/deal-import-drawer.js +0 -83
- package/src/lib/components/deal-import-drawer.js.map +0 -1
- package/src/lib/components/lead-import-drawer.d.ts +0 -27
- package/src/lib/components/lead-import-drawer.js +0 -118
- package/src/lib/components/lead-import-drawer.js.map +0 -1
- package/src/lib/components/lead-source-values-card.d.ts +0 -27
- package/src/lib/components/lead-source-values-card.js.map +0 -1
- package/src/lib/components/task-import-drawer.d.ts +0 -24
- package/src/lib/components/task-import-drawer.js +0 -82
- package/src/lib/components/task-import-drawer.js.map +0 -1
- package/src/lib/model/companies-csv.d.ts +0 -36
- package/src/lib/model/companies-csv.js +0 -38
- package/src/lib/model/companies-csv.js.map +0 -1
- package/src/lib/model/contacts-csv.d.ts +0 -54
- package/src/lib/model/contacts-csv.js +0 -67
- package/src/lib/model/contacts-csv.js.map +0 -1
- package/src/lib/model/crm-company-import.d.ts +0 -154
- package/src/lib/model/crm-company-import.js +0 -331
- package/src/lib/model/crm-company-import.js.map +0 -1
- package/src/lib/model/crm-csv.d.ts +0 -239
- package/src/lib/model/crm-csv.js +0 -467
- package/src/lib/model/crm-csv.js.map +0 -1
- package/src/lib/model/crm-deal-import.d.ts +0 -112
- package/src/lib/model/crm-deal-import.js +0 -263
- package/src/lib/model/crm-deal-import.js.map +0 -1
- package/src/lib/model/crm-import.d.ts +0 -250
- package/src/lib/model/crm-import.js +0 -467
- package/src/lib/model/crm-import.js.map +0 -1
- package/src/lib/model/crm-lead-import.d.ts +0 -138
- package/src/lib/model/crm-lead-import.js +0 -446
- package/src/lib/model/crm-lead-import.js.map +0 -1
- package/src/lib/model/crm-task-import.d.ts +0 -94
- package/src/lib/model/crm-task-import.js +0 -275
- package/src/lib/model/crm-task-import.js.map +0 -1
- package/src/lib/model/deals-csv.d.ts +0 -32
- package/src/lib/model/deals-csv.js +0 -36
- package/src/lib/model/deals-csv.js.map +0 -1
- package/src/lib/model/import-column-shapes.d.ts +0 -36
- package/src/lib/model/import-column-shapes.js +0 -76
- package/src/lib/model/import-column-shapes.js.map +0 -1
- package/src/lib/model/lead-source-values.d.ts +0 -82
- package/src/lib/model/lead-source-values.js +0 -149
- package/src/lib/model/lead-source-values.js.map +0 -1
- package/src/lib/model/leads-csv.d.ts +0 -40
- package/src/lib/model/leads-csv.js +0 -40
- package/src/lib/model/leads-csv.js.map +0 -1
- package/src/lib/model/tasks-csv.d.ts +0 -28
- package/src/lib/model/tasks-csv.js +0 -28
- package/src/lib/model/tasks-csv.js.map +0 -1
- package/src/lib/server/companies-import.d.ts +0 -57
- package/src/lib/server/companies-import.js +0 -242
- package/src/lib/server/companies-import.js.map +0 -1
- package/src/lib/server/contacts-import.d.ts +0 -109
- package/src/lib/server/contacts-import.js +0 -393
- package/src/lib/server/contacts-import.js.map +0 -1
- package/src/lib/server/deals-import.d.ts +0 -76
- package/src/lib/server/deals-import.js +0 -248
- package/src/lib/server/deals-import.js.map +0 -1
- package/src/lib/server/export-route.d.ts +0 -60
- package/src/lib/server/export-route.js +0 -447
- package/src/lib/server/export-route.js.map +0 -1
- package/src/lib/server/import-context.d.ts +0 -87
- package/src/lib/server/import-context.js +0 -182
- package/src/lib/server/import-context.js.map +0 -1
- package/src/lib/server/lead-source-values.d.ts +0 -50
- package/src/lib/server/lead-source-values.js +0 -191
- package/src/lib/server/lead-source-values.js.map +0 -1
- package/src/lib/server/leads-import.d.ts +0 -84
- package/src/lib/server/leads-import.js +0 -367
- package/src/lib/server/leads-import.js.map +0 -1
- package/src/lib/server/tasks-import.d.ts +0 -54
- package/src/lib/server/tasks-import.js +0 -160
- package/src/lib/server/tasks-import.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-origin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n PluginRecordOriginReport,\n PluginRecordOriginRequest,\n PluginRecordOriginWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-origin'\nimport {\n consentGroupForHost,\n CONTACT_FACETS_FIELD,\n contactFacetPath,\n crmLeadSourceForOrigin,\n crmPicklistKey,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n findContactByEmail,\n firebaseAdmin,\n getOrgForHost,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { readLeadSourcePicklist } from './lead-source-picklist'\n\n/*==========================================\n * A PERSON'S ORIGIN, AS A LEAD SOURCE (AGL-3519).\n *\n * The CRM's side of the platform's record-origin seam, and what its own\n * capture door calls after filing a person: a door's word for how it met\n * somebody — a form, a booking, a sequence — becomes the Lead source its\n * built-in value names (`CRM_LEAD_SOURCE_ORIGINS`), as the org spells it.\n *\n * - ORIGINAL SOURCE. A record that already names a lead source keeps it;\n * only an empty one is stamped, and a person the org cleared stays clear\n * to every door that meets them again (`firstTouchOnly`).\n * - A DEACTIVATED value stamps nothing: the org has said not to file\n * people under it.\n * - Both records the address is, as the site sees them: the lead, when the\n * site may see it, and the site's own facet on the contact — a holder's\n * knowledge of the person, like the rest of its profile.\n *\n * Each stamp is a transaction that reads the field and writes only into an\n * empty one, so a rep's pick made a moment earlier is never overwritten;\n * the list fields follow (`leadSourceKey`, `facetKeys`).\n *=========================================*/\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** Where one stamp lands, for a caller that already holds the org. */\nexport interface CrmRecordOriginInput extends PluginRecordOriginRequest {\n org?: Record<string, unknown> | null\n}\n\n/** Stamps the origin on the lead and the site's contact facet the address is; how many took it. */\nexport async function stampCrmRecordOrigin(\n input: CrmRecordOriginInput,\n firestore: Firestore = firebaseAdmin.app().firestore(),\n): Promise<PluginRecordOriginReport> {\n const email = normalizeContactEmail(input.email)\n const key = email ? personKey(email) : null\n if (!email || !key || !input.hostId) return { records: 0 }\n let orgId = input.orgId\n let org = input.org ?? null\n if (!orgId || !org) {\n const resolved = await getOrgForHost(input.hostId)\n if (!resolved || (orgId && resolved.orgId !== orgId)) return { records: 0 }\n orgId = resolved.orgId\n org = resolved.org as Record<string, unknown>\n }\n const label = crmLeadSourceForOrigin(await readLeadSourcePicklist(firestore, orgId), input.origin)\n if (!label) return { records: 0 }\n const orgRef = firestore.collection('orgs').doc(orgId)\n let records = 0\n\n /* THE LEAD, keyed on the person and seen by the site. */\n const leadRef = orgRef.collection('leads').doc(key)\n const leadStamped = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(leadRef)\n if (!snapshot.exists) return false\n const lead = snapshot.data() ?? {}\n if (!visibleToHost(lead['visibleTo'] as string[] | undefined, input.hostId)) return false\n if (typeof lead['leadSource'] === 'string' && lead['leadSource'].trim()) return false\n if (input.firstTouchOnly && Number(lead['submissionCount'] ?? 1) > 1) return false\n tx.update(leadRef, {\n leadSource: label,\n leadSourceKey: crmPicklistKey(label),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n })\n if (leadStamped) {\n records += 1\n await restampCrmListFieldsAt(leadRef, 'leads')\n }\n\n /* THE CONTACT, on the site's own facet — present only once the site holds the person. */\n const found = await findContactByEmail(orgRef.collection('contacts'), email)\n if (found) {\n const groupId = consentGroupForHost(org, input.hostId).groupId\n const contactStamped = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(found.ref)\n const facets = (snapshot.data() ?? {})[CONTACT_FACETS_FIELD] as Record<string, unknown> | undefined\n const facet = facets?.[groupId] as Record<string, unknown> | undefined\n if (!facet || typeof facet !== 'object') return false\n if (typeof facet['leadSource'] === 'string' && facet['leadSource'].trim()) return false\n const interactions = Array.isArray(facet['interactions']) ? facet['interactions'].length : 0\n if (input.firstTouchOnly && interactions > 1) return false\n tx.update(found.ref, {\n [contactFacetPath(groupId, 'leadSource')]: label,\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n })\n if (contactStamped) {\n records += 1\n await restampCrmListFieldsAt(found.ref, 'contacts')\n }\n }\n return { records }\n}\n\n/** The writer the CRM registers on the platform's record-origin seam. Never throws. */\nexport const crmRecordOriginWriter: PluginRecordOriginWriter = {\n async stamp(request) {\n try {\n return await stampCrmRecordOrigin(request)\n } catch (error) {\n console.error('[crm] the lead source could not be stamped', request.hostId, error)\n return { records: 0 }\n }\n },\n}\n"],"names":["consentGroupForHost","CONTACT_FACETS_FIELD","contactFacetPath","crmLeadSourceForOrigin","crmPicklistKey","normalizeContactEmail","personKey","visibleToHost","findContactByEmail","firebaseAdmin","getOrgForHost","restampCrmListFieldsAt","FieldValue","readLeadSourcePicklist","stampCrmRecordOrigin","input","firestore","app","email","key","hostId","records","orgId","org","resolved","label","origin","orgRef","collection","doc","leadRef","leadStamped","runTransaction","tx","snapshot","lead","get","exists","data","trim","firstTouchOnly","Number","update","leadSource","leadSourceKey","updatedAt","serverTimestamp","found","groupId","contactStamped","ref","facets","facet","interactions","Array","isArray","length","crmRecordOriginWriter","stamp","request","error","console"],"mappings":"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"}
|
|
@@ -15,12 +15,13 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { registerPluginRecordTimelineWriter } from "@aglyn/aglyn/plugin-manager/plugin-record-timeline";
|
|
18
|
-
import { checkEntitlement, consentGroupForHost, CRM_ACTIVITY_LOG_FULL_MESSAGE, CRM_COLLECTIONS, crmActivityLogHasRoom, crmScopeTokens, crmTaskListFields, crmTaskReminderAfterEdit, isCrmActivityKind, isCrmTaskKind, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
|
|
18
|
+
import { checkEntitlement, consentGroupForHost, CRM_ACTIVITY_LOG_FULL_MESSAGE, CRM_COLLECTIONS, crmActivityLogHasRoom, crmScopeTokens, crmTaskLabelsForNew, crmTaskListFields, crmTaskReminderAfterEdit, isCrmActivityKind, isCrmTaskKind, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
|
|
19
19
|
import { countCrmActivitiesForRecord, createCrmEmailActivity, crmActivityRef, crmCapturedEmailActivityRef, findContactByEmail, firebaseAdmin, readLeadForHost, recomputeCrmNextTaskAt, recordCrmEmailDelivery } from "@aglyn/tenant-data-admin";
|
|
20
20
|
import { createHash } from "crypto";
|
|
21
21
|
import { FieldValue } from "firebase-admin/firestore";
|
|
22
22
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
23
23
|
import { buildCrmCapturedEmailActivity, crmCapturedEmailKey } from "../model/crm-inbound.js";
|
|
24
|
+
import { readCrmTaskPicklists } from "./read-picklist.js";
|
|
24
25
|
import { CRM_SUITE_FEATURE } from "./suite-gate.js";
|
|
25
26
|
/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */ const ALREADY_EXISTS = 6;
|
|
26
27
|
/** The longest task title the CRM keeps, as its own forms do. */ const TASK_TITLE_MAX = 200;
|
|
@@ -274,6 +275,11 @@ export function createCrmRecordTimelineWriter(deps) {
|
|
|
274
275
|
const notes = String((_request_notes = request.notes) != null ? _request_notes : '').trim().slice(0, NOTE_MAX);
|
|
275
276
|
// `null` is "whoever holds the record", which admission read.
|
|
276
277
|
const assigneeUid = request.assigneeUid === null ? (_admitted_holderUid = admitted.holderUid) != null ? _admitted_holderUid : '' : String((_request_assigneeUid = request.assigneeUid) != null ? _request_assigneeUid : '').trim();
|
|
278
|
+
// The org's labels for the meanings it is filed with (AGL-3517).
|
|
279
|
+
const labels = crmTaskLabelsForNew(await readCrmTaskPicklists(firestore, orgRef.id), {
|
|
280
|
+
kind: request.kind,
|
|
281
|
+
priority: 'normal'
|
|
282
|
+
});
|
|
277
283
|
const task = _extends({
|
|
278
284
|
title
|
|
279
285
|
}, notes ? {
|
|
@@ -281,7 +287,8 @@ export function createCrmRecordTimelineWriter(deps) {
|
|
|
281
287
|
} : {}, {
|
|
282
288
|
kind: request.kind,
|
|
283
289
|
priority: 'normal',
|
|
284
|
-
status: 'open'
|
|
290
|
+
status: 'open'
|
|
291
|
+
}, labels, {
|
|
285
292
|
dueAtMs: request.dueAtMs,
|
|
286
293
|
// The reminder a person's task gets (AGL-2659): the due time.
|
|
287
294
|
remindAtMs: crmTaskReminderAfterEdit({
|
|
@@ -338,6 +345,12 @@ export function createCrmRecordTimelineWriter(deps) {
|
|
|
338
345
|
id: ref.id,
|
|
339
346
|
created: outcome === 'advanced'
|
|
340
347
|
};
|
|
348
|
+
},
|
|
349
|
+
// An automation's email to the person its event is about (AGL-2615):
|
|
350
|
+
// see `automation-steps.ts`, loaded with the first one.
|
|
351
|
+
async prepareEmail (request) {
|
|
352
|
+
const { prepareCrmRecordEmail } = await import("./automation-steps.js");
|
|
353
|
+
return prepareCrmRecordEmail(request);
|
|
341
354
|
}
|
|
342
355
|
};
|
|
343
356
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-timeline.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginRecordTimelineWriter,\n type PluginRecordActivityRequest,\n type PluginRecordDeliveryRequest,\n type PluginRecordEntryContext,\n type PluginRecordTaskRequest,\n type PluginRecordTimelineWriter,\n type PluginRecordWrite,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport {\n checkEntitlement,\n consentGroupForHost,\n CRM_ACTIVITY_LOG_FULL_MESSAGE,\n CRM_COLLECTIONS,\n crmActivityLogHasRoom,\n crmScopeTokens,\n crmTaskListFields,\n crmTaskReminderAfterEdit,\n isCrmActivityKind,\n isCrmTaskKind,\n readContactFacet,\n visibleToHost,\n type CrmActivity,\n type CrmActivityLink,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport {\n countCrmActivitiesForRecord,\n createCrmEmailActivity,\n crmActivityRef,\n crmCapturedEmailActivityRef,\n findContactByEmail,\n firebaseAdmin,\n readLeadForHost,\n recomputeCrmNextTaskAt,\n recordCrmEmailDelivery,\n} from '@aglyn/tenant-data-admin'\nimport { createHash } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { buildCrmCapturedEmailActivity, crmCapturedEmailKey } from '../model/crm-inbound'\nimport { CRM_SUITE_FEATURE } from './suite-gate'\n\n/**\n * THE CRM'S WRITER ON THE CORE'S RECORD-TIMELINE SEAM (AGL-2981).\n *\n * Another plugin files an activity or a task on a contact, company or deal —\n * an email a sequence sent, the reply it read, the call it asks for — and\n * this writes it as the CRM writes its own:\n *\n * 1. the workspace's plan carries the CRM (`features.crm`), as at every\n * CRM door; a Free workspace keeps no records to file on;\n * 2. the entry carries the scope a record made on that site carries\n * (`crmScopeTokens`), so exactly the people who see the record see it;\n * 3. an EMAIL is filed under the id the capture address files a copy of the\n * same message by (`cap_` + the digest of its `Message-ID`), and written\n * with `create()`: the send, a copy the rep forwarded to the capture\n * address, and a second run of the caller are one row;\n * 4. anything else — a note, a task — is filed under the caller's own key,\n * hashed with the caller's plugin id, and written once the same way;\n * 5. a record at its activity ceiling is refused with the sentence every\n * CRM writer answers with, before anything is written;\n * 6. a bounce or a complaint on a filed email (AGL-3245) advances that\n * email's own delivery state — the row the send was filed under, found\n * by its `Message-ID` — exactly as the campaign webhook advances one,\n * so the timeline reads Sent, then Bounced, with what the server said.\n *\n * Each entry names the plugin that filed it (`sourcePluginId`), and a task's\n * `nextTaskAtMs` is recomputed on the records it names, as every server\n * writer of a task does.\n *\n * ## A record found rather than named\n *\n * A caller that holds no id of the CRM's — a booking made through a link a\n * rep dropped from a record, or one taken off the widget cold (AGL-2660) —\n * hands back the record the link carried, or the booker's address, and the\n * entry is filed where the CRM finds it: the record the reference names\n * when this site may see it, otherwise the contact the address names\n * through the org's address index, narrowed to the site. A deal carries its\n * contact and company onto the entry; a contact its company, from the\n * site's own facet. A task with no assignee of the caller's goes to whoever\n * holds that record.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CrmRecordTimelineDeps {\n firestore(): Firestore\n}\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/** The longest task title the CRM keeps, as its own forms do. */\nconst TASK_TITLE_MAX = 200\n/** The longest note body or task note an entry filed here keeps. */\nconst NOTE_MAX = 2_000\n\nconst refuse = (status: 400 | 403 | 404 | 409, error: string): PluginRecordWrite => ({\n ok: false,\n status,\n error,\n})\n\n/** The record fields of a link, only the ones it names. */\nfunction linkFields(link: PluginRecordEntryContext['link']): CrmActivityLink {\n return {\n ...(link?.contactId ? { contactId: String(link.contactId) } : {}),\n ...(link?.companyId ? { companyId: String(link.companyId) } : {}),\n // A lead (AGL-3234): host-scoped by path, so the entry carries the\n // site's scope like any record created from that site.\n ...(link?.leadId ? { leadId: String(link.leadId) } : {}),\n }\n}\n\n/** What a reference handed back by a caller may name: the records a booking link is dropped from. */\nconst FOUND_RECORD_KINDS: ReadonlySet<string> = new Set(['contact', 'lead', 'deal'])\n/**\n * A record id as the CRM mints them: Firestore's for a contact and a deal, a\n * person key for a lead. A reference arrives on a public request, so one\n * that could not be an id is passed over rather than read.\n */\nconst RECORD_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\n/** The record an entry lands on, and who holds it. */\ninterface FoundRecord {\n link: CrmActivityLink\n holderUid?: string\n}\n\n/** A contact's link and holder, from this site's facet on it. */\nfunction contactRecord(\n contact: FirebaseFirestore.DocumentSnapshot,\n groupId: string,\n): FoundRecord {\n const facet = readContactFacet(contact.data() ?? {}, groupId)\n return {\n link: { contactId: contact.id, ...(facet.companyId ? { companyId: facet.companyId } : {}) },\n ...(facet.ownerUid ? { holderUid: facet.ownerUid } : {}),\n }\n}\n\n/**\n * The record a caller's reference names, then the contact its address does,\n * as `hostId` may see them; `null` when neither finds one. See the module\n * comment.\n */\nasync function findRecord(\n orgRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n groupId: string,\n link: PluginRecordEntryContext['link'],\n): Promise<FoundRecord | null> {\n const contacts = orgRef.collection('contacts')\n const kind = String(link?.record?.kind ?? '')\n const id = String(link?.record?.id ?? '')\n if (FOUND_RECORD_KINDS.has(kind) && RECORD_ID_PATTERN.test(id)) {\n if (kind === 'contact') {\n const contact = await contacts.doc(id).get()\n if (contact.exists && visibleToHost(contact.get('visibleTo'), hostId)) {\n return contactRecord(contact, groupId)\n }\n } else if (kind === 'deal') {\n const deal = await orgRef.collection(CRM_COLLECTIONS.deals).doc(id).get()\n if (deal.exists && visibleToHost(deal.get('visibleTo'), hostId)) {\n const contactId = String(deal.get('contactId') ?? '')\n const companyId = String(deal.get('companyId') ?? '')\n const ownerUid = String(deal.get('ownerUid') ?? '')\n return {\n link: { dealId: deal.id, ...(contactId ? { contactId } : {}), ...(companyId ? { companyId } : {}) },\n ...(ownerUid ? { holderUid: ownerUid } : {}),\n }\n }\n } else {\n // A lead carries its own `visibleTo` (AGL-3275), and the site has to be\n // allowed to see it, or one brand's booking would name another's lead.\n const lead = await readLeadForHost(hostId, id)\n if (lead && visibleToHost(lead.get('visibleTo'), hostId)) {\n const ownerUid = String(lead.get('ownerUid') ?? '')\n return { link: { leadId: lead.id }, ...(ownerUid ? { holderUid: ownerUid } : {}) }\n }\n }\n }\n const email = String(link?.email ?? '').trim()\n if (!email) return null\n const hit = await findContactByEmail(contacts, email, { hostId })\n return hit ? contactRecord(hit, groupId) : null\n}\n\n/** Who holds a record a caller named by id: its contact's site facet, or its lead. */\nasync function holderOf(\n orgRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n groupId: string,\n link: CrmActivityLink,\n): Promise<string | undefined> {\n if (link.contactId) {\n const contact = await orgRef.collection('contacts').doc(link.contactId).get()\n return contact.exists ? contactRecord(contact, groupId).holderUid : undefined\n }\n if (link.leadId) {\n const lead = await readLeadForHost(hostId, link.leadId)\n return String(lead?.get('ownerUid') ?? '') || undefined\n }\n return undefined\n}\n\n/** The document id a caller's key files under: its own namespace, never a raw key. */\nfunction keyedId(sourcePluginId: string, key: string): string {\n return `plg_${createHash('sha256').update(`${sourcePluginId}:${key}`).digest('hex').slice(0, 28)}`\n}\n\nexport function createCrmRecordTimelineWriter(deps: CrmRecordTimelineDeps): PluginRecordTimelineWriter {\n /**\n * The org, its plan, the link and the scope an entry is stamped with — and,\n * when `wantHolder`, who holds the record it lands on.\n */\n async function admit(\n context: PluginRecordEntryContext,\n wantHolder = false,\n ): Promise<\n | PluginRecordWrite\n | {\n firestore: Firestore\n orgRef: FirebaseFirestore.DocumentReference\n link: CrmActivityLink\n visibleTo: string[]\n holderUid?: string\n }\n > {\n const orgId = String(context.orgId ?? '').trim()\n const hostId = String(context.hostId ?? '').trim()\n const named = linkFields(context.link)\n const isNamed = Boolean(named.contactId || named.companyId || named.leadId)\n if (!orgId || !hostId || !String(context.sourcePluginId ?? '').trim()) {\n return refuse(400, 'An entry names its organization, its site and the plugin filing it.')\n }\n if (!isNamed && !context.link?.record && !String(context.link?.email ?? '').trim()) {\n return refuse(400, 'An entry is filed on a contact, a company or a lead, or names how to find one.')\n }\n const firestore = deps.firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const snapshot = await orgRef.get()\n if (!snapshot.exists) return refuse(404, 'That organization no longer exists.')\n const org = (snapshot.data() ?? {}) as Record<string, unknown>\n if (!checkEntitlement(org, CRM_SUITE_FEATURE)) {\n return refuse(403, \"This workspace's plan doesn't include the CRM.\")\n }\n const group = consentGroupForHost(org, hostId)\n let link = named\n let holderUid: string | undefined\n if (isNamed) {\n if (wantHolder) holderUid = await holderOf(orgRef, hostId, group.groupId, named)\n } else {\n const found = await findRecord(orgRef, hostId, group.groupId, context.link)\n if (!found) return refuse(404, 'Nothing this site keeps matches the record or the address.')\n link = found.link\n holderUid = found.holderUid\n }\n return {\n firestore,\n orgRef,\n link,\n visibleTo: crmScopeTokens(org, group),\n ...(holderUid ? { holderUid } : {}),\n }\n }\n\n /** Creates a keyed entry once: `created: false` for one already filed. */\n async function createOnce(\n ref: FirebaseFirestore.DocumentReference,\n data: Record<string, unknown>,\n ): Promise<boolean> {\n try {\n await ref.create({\n ...data,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n } catch (error) {\n if ((error as { code?: unknown })?.code === ALREADY_EXISTS) return false\n throw error\n }\n }\n\n return {\n async logActivity(request: PluginRecordActivityRequest): Promise<PluginRecordWrite> {\n if (!isCrmActivityKind(request.kind)) return refuse(400, `\"${String(request.kind)}\" isn't an activity.`)\n const admitted = await admit(request)\n if ('ok' in admitted) return admitted\n const { firestore, orgRef, link, visibleTo } = admitted\n const orgId = orgRef.id\n\n let ref: FirebaseFirestore.DocumentReference\n let activity: CrmActivity\n if (request.kind === 'email') {\n const email = request.email\n const key = email ? crmCapturedEmailKey(email.messageId, null) : null\n if (!email || !key) return refuse(400, 'An email is filed by its Message-ID.')\n ref = crmCapturedEmailActivityRef(firestore, orgId, key)\n activity = buildCrmCapturedEmailActivity({\n direction: email.direction === 'inbound' ? 'inbound' : 'outbound',\n subject: email.subject,\n excerpt: request.body,\n from: email.from,\n to: email.to,\n messageId: email.messageId,\n inReplyTo: email.inReplyTo ?? null,\n atMs: request.atMs,\n byUid: String(request.byUid ?? ''),\n byName: request.byName ?? null,\n link,\n hostId: request.hostId,\n visibleTo,\n })\n } else {\n const key = String(request.dedupeKey ?? '').trim()\n if (!key) return refuse(400, 'An entry that is not an email names its own key.')\n ref = crmActivityRef(firestore, orgId, keyedId(request.sourcePluginId, key))\n const byName = String(request.byName ?? '').trim()\n activity = {\n kind: request.kind,\n body: String(request.body ?? '').slice(0, NOTE_MAX),\n atMs: request.atMs,\n byUid: String(request.byUid ?? ''),\n ...(byName ? { byName } : {}),\n ...link,\n hostId: request.hostId,\n visibleTo,\n }\n }\n // Filed before: answered off the one read, ahead of the ceiling's count.\n if ((await ref.get()).exists) return { ok: true, id: ref.id, created: false }\n if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, link))) {\n return refuse(409, CRM_ACTIVITY_LOG_FULL_MESSAGE)\n }\n const stamped = { ...activity, sourcePluginId: request.sourcePluginId }\n const created =\n request.kind === 'email'\n ? (await createCrmEmailActivity(ref, stamped)) === 'created'\n : await createOnce(ref, stamped as unknown as Record<string, unknown>)\n return { ok: true, id: ref.id, created }\n },\n\n async createTask(request: PluginRecordTaskRequest): Promise<PluginRecordWrite> {\n const title = String(request.title ?? '').replace(/\\s+/g, ' ').trim().slice(0, TASK_TITLE_MAX)\n const key = String(request.dedupeKey ?? '').trim()\n if (!title || !key) return refuse(400, 'A task has a title and the caller’s own key.')\n if (!isCrmTaskKind(request.kind)) return refuse(400, `\"${String(request.kind)}\" isn't a task.`)\n if (!Number.isFinite(request.dueAtMs)) return refuse(400, 'A task has a due time.')\n const admitted = await admit(request, request.assigneeUid === null)\n if ('ok' in admitted) return admitted\n const { firestore, orgRef, link, visibleTo } = admitted\n const ref = orgRef.collection(CRM_COLLECTIONS.tasks).doc(keyedId(request.sourcePluginId, key))\n const notes = String(request.notes ?? '').trim().slice(0, NOTE_MAX)\n // `null` is \"whoever holds the record\", which admission read.\n const assigneeUid =\n request.assigneeUid === null\n ? (admitted.holderUid ?? '')\n : String(request.assigneeUid ?? '').trim()\n const task: CrmTask = {\n title,\n ...(notes ? { notes } : {}),\n kind: request.kind,\n priority: 'normal',\n status: 'open',\n dueAtMs: request.dueAtMs,\n // The reminder a person's task gets (AGL-2659): the due time.\n remindAtMs: crmTaskReminderAfterEdit({ dueAtMs: request.dueAtMs, previous: null }),\n ...(assigneeUid ? { assigneeUid } : {}),\n createdByUid: String(request.createdByUid ?? ''),\n sourcePluginId: request.sourcePluginId,\n ...link,\n hostId: request.hostId,\n visibleTo,\n }\n // What the Tasks list searches by (AGL-3321), beside the task.\n const created = await createOnce(ref, { ...task, ...crmTaskListFields(task) })\n if (created) {\n // The records the task names carry `nextTaskAtMs` (AGL-2661); a figure\n // that could not move is the Fields section's recompute's.\n await recomputeCrmNextTaskAt(firestore, orgRef.id, [link]).catch((error: unknown) => {\n console.error('[crm] next activity could not be recomputed', orgRef.id, error)\n })\n }\n return { ok: true, id: ref.id, created }\n },\n\n async recordEmailDelivery(request: PluginRecordDeliveryRequest): Promise<PluginRecordWrite> {\n const orgId = String(request.orgId ?? '').trim()\n const key = crmCapturedEmailKey(request.messageId, null)\n if (!orgId || !key) return refuse(400, 'A delivery names its organization and the email’s Message-ID.')\n if (request.state !== 'bounced' && request.state !== 'complained') {\n return refuse(400, `\"${String(request.state)}\" isn't a delivery failure.`)\n }\n const firestore = deps.firestore()\n const ref = crmCapturedEmailActivityRef(firestore, orgId, key)\n const outcome = await recordCrmEmailDelivery(firestore, {\n orgId,\n activityId: ref.id,\n state: request.state,\n atMs: request.atMs,\n detail: request.detail ?? null,\n })\n if (outcome === 'missing') return refuse(404, 'No email by that Message-ID is filed here.')\n if (outcome === 'failed') return refuse(409, 'The delivery state could not be written.')\n return { ok: true, id: ref.id, created: outcome === 'advanced' }\n },\n }\n}\n\n/** The platform's own dependencies. Specs build their own. */\nexport function defaultCrmRecordTimelineDeps(): CrmRecordTimelineDeps {\n return { firestore: () => firebaseAdmin.app().firestore() }\n}\n\n/** Registers the CRM as the workspace's record system on the timeline seam. */\nexport function registerCrmRecordTimelineWriter(\n deps: CrmRecordTimelineDeps = defaultCrmRecordTimelineDeps(),\n): void {\n registerPluginRecordTimelineWriter(createCrmRecordTimelineWriter(deps), { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginRecordTimelineWriter","checkEntitlement","consentGroupForHost","CRM_ACTIVITY_LOG_FULL_MESSAGE","CRM_COLLECTIONS","crmActivityLogHasRoom","crmScopeTokens","crmTaskListFields","crmTaskReminderAfterEdit","isCrmActivityKind","isCrmTaskKind","readContactFacet","visibleToHost","countCrmActivitiesForRecord","createCrmEmailActivity","crmActivityRef","crmCapturedEmailActivityRef","findContactByEmail","firebaseAdmin","readLeadForHost","recomputeCrmNextTaskAt","recordCrmEmailDelivery","createHash","FieldValue","BUNDLE_ID","buildCrmCapturedEmailActivity","crmCapturedEmailKey","CRM_SUITE_FEATURE","ALREADY_EXISTS","TASK_TITLE_MAX","NOTE_MAX","refuse","status","error","ok","linkFields","link","contactId","String","companyId","leadId","FOUND_RECORD_KINDS","Set","RECORD_ID_PATTERN","contactRecord","contact","groupId","facet","data","id","ownerUid","holderUid","findRecord","orgRef","hostId","contacts","collection","kind","record","has","test","doc","get","exists","deal","deals","dealId","lead","email","trim","hit","holderOf","undefined","keyedId","sourcePluginId","key","update","digest","slice","createCrmRecordTimelineWriter","deps","admit","context","wantHolder","snapshot","orgId","named","isNamed","Boolean","firestore","org","group","found","visibleTo","createOnce","ref","create","createdAt","serverTimestamp","updatedAt","code","logActivity","request","admitted","activity","messageId","direction","subject","excerpt","body","from","to","inReplyTo","atMs","byUid","byName","dedupeKey","created","stamped","createTask","title","replace","Number","isFinite","dueAtMs","assigneeUid","tasks","notes","task","priority","remindAtMs","previous","createdByUid","catch","console","recordEmailDelivery","state","outcome","activityId","detail","defaultCrmRecordTimelineDeps","app","registerCrmRecordTimelineWriter","pluginId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-timeline.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginRecordTimelineWriter,\n type PluginRecordActivityRequest,\n type PluginRecordDeliveryRequest,\n type PluginRecordEntryContext,\n type PluginRecordTaskRequest,\n type PluginRecordTimelineWriter,\n type PluginRecordWrite,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-timeline'\nimport {\n checkEntitlement,\n consentGroupForHost,\n CRM_ACTIVITY_LOG_FULL_MESSAGE,\n CRM_COLLECTIONS,\n crmActivityLogHasRoom,\n crmScopeTokens,\n crmTaskLabelsForNew,\n crmTaskListFields,\n crmTaskReminderAfterEdit,\n isCrmActivityKind,\n isCrmTaskKind,\n readContactFacet,\n visibleToHost,\n type CrmActivity,\n type CrmActivityLink,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport {\n countCrmActivitiesForRecord,\n createCrmEmailActivity,\n crmActivityRef,\n crmCapturedEmailActivityRef,\n findContactByEmail,\n firebaseAdmin,\n readLeadForHost,\n recomputeCrmNextTaskAt,\n recordCrmEmailDelivery,\n} from '@aglyn/tenant-data-admin'\nimport { createHash } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { buildCrmCapturedEmailActivity, crmCapturedEmailKey } from '../model/crm-inbound'\nimport { readCrmTaskPicklists } from './read-picklist'\nimport { CRM_SUITE_FEATURE } from './suite-gate'\n\n/**\n * THE CRM'S WRITER ON THE CORE'S RECORD-TIMELINE SEAM (AGL-2981).\n *\n * Another plugin files an activity or a task on a contact, company or deal —\n * an email a sequence sent, the reply it read, the call it asks for — and\n * this writes it as the CRM writes its own:\n *\n * 1. the workspace's plan carries the CRM (`features.crm`), as at every\n * CRM door; a Free workspace keeps no records to file on;\n * 2. the entry carries the scope a record made on that site carries\n * (`crmScopeTokens`), so exactly the people who see the record see it;\n * 3. an EMAIL is filed under the id the capture address files a copy of the\n * same message by (`cap_` + the digest of its `Message-ID`), and written\n * with `create()`: the send, a copy the rep forwarded to the capture\n * address, and a second run of the caller are one row;\n * 4. anything else — a note, a task — is filed under the caller's own key,\n * hashed with the caller's plugin id, and written once the same way;\n * 5. a record at its activity ceiling is refused with the sentence every\n * CRM writer answers with, before anything is written;\n * 6. a bounce or a complaint on a filed email (AGL-3245) advances that\n * email's own delivery state — the row the send was filed under, found\n * by its `Message-ID` — exactly as the campaign webhook advances one,\n * so the timeline reads Sent, then Bounced, with what the server said.\n *\n * Each entry names the plugin that filed it (`sourcePluginId`), and a task's\n * `nextTaskAtMs` is recomputed on the records it names, as every server\n * writer of a task does.\n *\n * ## A record found rather than named\n *\n * A caller that holds no id of the CRM's — a booking made through a link a\n * rep dropped from a record, or one taken off the widget cold (AGL-2660) —\n * hands back the record the link carried, or the booker's address, and the\n * entry is filed where the CRM finds it: the record the reference names\n * when this site may see it, otherwise the contact the address names\n * through the org's address index, narrowed to the site. A deal carries its\n * contact and company onto the entry; a contact its company, from the\n * site's own facet. A task with no assignee of the caller's goes to whoever\n * holds that record.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CrmRecordTimelineDeps {\n firestore(): Firestore\n}\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/** The longest task title the CRM keeps, as its own forms do. */\nconst TASK_TITLE_MAX = 200\n/** The longest note body or task note an entry filed here keeps. */\nconst NOTE_MAX = 2_000\n\nconst refuse = (status: 400 | 403 | 404 | 409, error: string): PluginRecordWrite => ({\n ok: false,\n status,\n error,\n})\n\n/** The record fields of a link, only the ones it names. */\nfunction linkFields(link: PluginRecordEntryContext['link']): CrmActivityLink {\n return {\n ...(link?.contactId ? { contactId: String(link.contactId) } : {}),\n ...(link?.companyId ? { companyId: String(link.companyId) } : {}),\n // A lead (AGL-3234): host-scoped by path, so the entry carries the\n // site's scope like any record created from that site.\n ...(link?.leadId ? { leadId: String(link.leadId) } : {}),\n }\n}\n\n/** What a reference handed back by a caller may name: the records a booking link is dropped from. */\nconst FOUND_RECORD_KINDS: ReadonlySet<string> = new Set(['contact', 'lead', 'deal'])\n/**\n * A record id as the CRM mints them: Firestore's for a contact and a deal, a\n * person key for a lead. A reference arrives on a public request, so one\n * that could not be an id is passed over rather than read.\n */\nconst RECORD_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\n\n/** The record an entry lands on, and who holds it. */\ninterface FoundRecord {\n link: CrmActivityLink\n holderUid?: string\n}\n\n/** A contact's link and holder, from this site's facet on it. */\nfunction contactRecord(\n contact: FirebaseFirestore.DocumentSnapshot,\n groupId: string,\n): FoundRecord {\n const facet = readContactFacet(contact.data() ?? {}, groupId)\n return {\n link: { contactId: contact.id, ...(facet.companyId ? { companyId: facet.companyId } : {}) },\n ...(facet.ownerUid ? { holderUid: facet.ownerUid } : {}),\n }\n}\n\n/**\n * The record a caller's reference names, then the contact its address does,\n * as `hostId` may see them; `null` when neither finds one. See the module\n * comment.\n */\nasync function findRecord(\n orgRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n groupId: string,\n link: PluginRecordEntryContext['link'],\n): Promise<FoundRecord | null> {\n const contacts = orgRef.collection('contacts')\n const kind = String(link?.record?.kind ?? '')\n const id = String(link?.record?.id ?? '')\n if (FOUND_RECORD_KINDS.has(kind) && RECORD_ID_PATTERN.test(id)) {\n if (kind === 'contact') {\n const contact = await contacts.doc(id).get()\n if (contact.exists && visibleToHost(contact.get('visibleTo'), hostId)) {\n return contactRecord(contact, groupId)\n }\n } else if (kind === 'deal') {\n const deal = await orgRef.collection(CRM_COLLECTIONS.deals).doc(id).get()\n if (deal.exists && visibleToHost(deal.get('visibleTo'), hostId)) {\n const contactId = String(deal.get('contactId') ?? '')\n const companyId = String(deal.get('companyId') ?? '')\n const ownerUid = String(deal.get('ownerUid') ?? '')\n return {\n link: { dealId: deal.id, ...(contactId ? { contactId } : {}), ...(companyId ? { companyId } : {}) },\n ...(ownerUid ? { holderUid: ownerUid } : {}),\n }\n }\n } else {\n // A lead carries its own `visibleTo` (AGL-3275), and the site has to be\n // allowed to see it, or one brand's booking would name another's lead.\n const lead = await readLeadForHost(hostId, id)\n if (lead && visibleToHost(lead.get('visibleTo'), hostId)) {\n const ownerUid = String(lead.get('ownerUid') ?? '')\n return { link: { leadId: lead.id }, ...(ownerUid ? { holderUid: ownerUid } : {}) }\n }\n }\n }\n const email = String(link?.email ?? '').trim()\n if (!email) return null\n const hit = await findContactByEmail(contacts, email, { hostId })\n return hit ? contactRecord(hit, groupId) : null\n}\n\n/** Who holds a record a caller named by id: its contact's site facet, or its lead. */\nasync function holderOf(\n orgRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n groupId: string,\n link: CrmActivityLink,\n): Promise<string | undefined> {\n if (link.contactId) {\n const contact = await orgRef.collection('contacts').doc(link.contactId).get()\n return contact.exists ? contactRecord(contact, groupId).holderUid : undefined\n }\n if (link.leadId) {\n const lead = await readLeadForHost(hostId, link.leadId)\n return String(lead?.get('ownerUid') ?? '') || undefined\n }\n return undefined\n}\n\n/** The document id a caller's key files under: its own namespace, never a raw key. */\nfunction keyedId(sourcePluginId: string, key: string): string {\n return `plg_${createHash('sha256').update(`${sourcePluginId}:${key}`).digest('hex').slice(0, 28)}`\n}\n\nexport function createCrmRecordTimelineWriter(deps: CrmRecordTimelineDeps): PluginRecordTimelineWriter {\n /**\n * The org, its plan, the link and the scope an entry is stamped with — and,\n * when `wantHolder`, who holds the record it lands on.\n */\n async function admit(\n context: PluginRecordEntryContext,\n wantHolder = false,\n ): Promise<\n | PluginRecordWrite\n | {\n firestore: Firestore\n orgRef: FirebaseFirestore.DocumentReference\n link: CrmActivityLink\n visibleTo: string[]\n holderUid?: string\n }\n > {\n const orgId = String(context.orgId ?? '').trim()\n const hostId = String(context.hostId ?? '').trim()\n const named = linkFields(context.link)\n const isNamed = Boolean(named.contactId || named.companyId || named.leadId)\n if (!orgId || !hostId || !String(context.sourcePluginId ?? '').trim()) {\n return refuse(400, 'An entry names its organization, its site and the plugin filing it.')\n }\n if (!isNamed && !context.link?.record && !String(context.link?.email ?? '').trim()) {\n return refuse(400, 'An entry is filed on a contact, a company or a lead, or names how to find one.')\n }\n const firestore = deps.firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const snapshot = await orgRef.get()\n if (!snapshot.exists) return refuse(404, 'That organization no longer exists.')\n const org = (snapshot.data() ?? {}) as Record<string, unknown>\n if (!checkEntitlement(org, CRM_SUITE_FEATURE)) {\n return refuse(403, \"This workspace's plan doesn't include the CRM.\")\n }\n const group = consentGroupForHost(org, hostId)\n let link = named\n let holderUid: string | undefined\n if (isNamed) {\n if (wantHolder) holderUid = await holderOf(orgRef, hostId, group.groupId, named)\n } else {\n const found = await findRecord(orgRef, hostId, group.groupId, context.link)\n if (!found) return refuse(404, 'Nothing this site keeps matches the record or the address.')\n link = found.link\n holderUid = found.holderUid\n }\n return {\n firestore,\n orgRef,\n link,\n visibleTo: crmScopeTokens(org, group),\n ...(holderUid ? { holderUid } : {}),\n }\n }\n\n /** Creates a keyed entry once: `created: false` for one already filed. */\n async function createOnce(\n ref: FirebaseFirestore.DocumentReference,\n data: Record<string, unknown>,\n ): Promise<boolean> {\n try {\n await ref.create({\n ...data,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n })\n return true\n } catch (error) {\n if ((error as { code?: unknown })?.code === ALREADY_EXISTS) return false\n throw error\n }\n }\n\n return {\n async logActivity(request: PluginRecordActivityRequest): Promise<PluginRecordWrite> {\n if (!isCrmActivityKind(request.kind)) return refuse(400, `\"${String(request.kind)}\" isn't an activity.`)\n const admitted = await admit(request)\n if ('ok' in admitted) return admitted\n const { firestore, orgRef, link, visibleTo } = admitted\n const orgId = orgRef.id\n\n let ref: FirebaseFirestore.DocumentReference\n let activity: CrmActivity\n if (request.kind === 'email') {\n const email = request.email\n const key = email ? crmCapturedEmailKey(email.messageId, null) : null\n if (!email || !key) return refuse(400, 'An email is filed by its Message-ID.')\n ref = crmCapturedEmailActivityRef(firestore, orgId, key)\n activity = buildCrmCapturedEmailActivity({\n direction: email.direction === 'inbound' ? 'inbound' : 'outbound',\n subject: email.subject,\n excerpt: request.body,\n from: email.from,\n to: email.to,\n messageId: email.messageId,\n inReplyTo: email.inReplyTo ?? null,\n atMs: request.atMs,\n byUid: String(request.byUid ?? ''),\n byName: request.byName ?? null,\n link,\n hostId: request.hostId,\n visibleTo,\n })\n } else {\n const key = String(request.dedupeKey ?? '').trim()\n if (!key) return refuse(400, 'An entry that is not an email names its own key.')\n ref = crmActivityRef(firestore, orgId, keyedId(request.sourcePluginId, key))\n const byName = String(request.byName ?? '').trim()\n activity = {\n kind: request.kind,\n body: String(request.body ?? '').slice(0, NOTE_MAX),\n atMs: request.atMs,\n byUid: String(request.byUid ?? ''),\n ...(byName ? { byName } : {}),\n ...link,\n hostId: request.hostId,\n visibleTo,\n }\n }\n // Filed before: answered off the one read, ahead of the ceiling's count.\n if ((await ref.get()).exists) return { ok: true, id: ref.id, created: false }\n if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, link))) {\n return refuse(409, CRM_ACTIVITY_LOG_FULL_MESSAGE)\n }\n const stamped = { ...activity, sourcePluginId: request.sourcePluginId }\n const created =\n request.kind === 'email'\n ? (await createCrmEmailActivity(ref, stamped)) === 'created'\n : await createOnce(ref, stamped as unknown as Record<string, unknown>)\n return { ok: true, id: ref.id, created }\n },\n\n async createTask(request: PluginRecordTaskRequest): Promise<PluginRecordWrite> {\n const title = String(request.title ?? '').replace(/\\s+/g, ' ').trim().slice(0, TASK_TITLE_MAX)\n const key = String(request.dedupeKey ?? '').trim()\n if (!title || !key) return refuse(400, 'A task has a title and the caller’s own key.')\n if (!isCrmTaskKind(request.kind)) return refuse(400, `\"${String(request.kind)}\" isn't a task.`)\n if (!Number.isFinite(request.dueAtMs)) return refuse(400, 'A task has a due time.')\n const admitted = await admit(request, request.assigneeUid === null)\n if ('ok' in admitted) return admitted\n const { firestore, orgRef, link, visibleTo } = admitted\n const ref = orgRef.collection(CRM_COLLECTIONS.tasks).doc(keyedId(request.sourcePluginId, key))\n const notes = String(request.notes ?? '').trim().slice(0, NOTE_MAX)\n // `null` is \"whoever holds the record\", which admission read.\n const assigneeUid =\n request.assigneeUid === null\n ? (admitted.holderUid ?? '')\n : String(request.assigneeUid ?? '').trim()\n // The org's labels for the meanings it is filed with (AGL-3517).\n const labels = crmTaskLabelsForNew(await readCrmTaskPicklists(firestore, orgRef.id), {\n kind: request.kind,\n priority: 'normal',\n })\n const task: CrmTask = {\n title,\n ...(notes ? { notes } : {}),\n kind: request.kind,\n priority: 'normal',\n status: 'open',\n ...labels,\n dueAtMs: request.dueAtMs,\n // The reminder a person's task gets (AGL-2659): the due time.\n remindAtMs: crmTaskReminderAfterEdit({ dueAtMs: request.dueAtMs, previous: null }),\n ...(assigneeUid ? { assigneeUid } : {}),\n createdByUid: String(request.createdByUid ?? ''),\n sourcePluginId: request.sourcePluginId,\n ...link,\n hostId: request.hostId,\n visibleTo,\n }\n // What the Tasks list searches by (AGL-3321), beside the task.\n const created = await createOnce(ref, { ...task, ...crmTaskListFields(task) })\n if (created) {\n // The records the task names carry `nextTaskAtMs` (AGL-2661); a figure\n // that could not move is the Fields section's recompute's.\n await recomputeCrmNextTaskAt(firestore, orgRef.id, [link]).catch((error: unknown) => {\n console.error('[crm] next activity could not be recomputed', orgRef.id, error)\n })\n }\n return { ok: true, id: ref.id, created }\n },\n\n async recordEmailDelivery(request: PluginRecordDeliveryRequest): Promise<PluginRecordWrite> {\n const orgId = String(request.orgId ?? '').trim()\n const key = crmCapturedEmailKey(request.messageId, null)\n if (!orgId || !key) return refuse(400, 'A delivery names its organization and the email’s Message-ID.')\n if (request.state !== 'bounced' && request.state !== 'complained') {\n return refuse(400, `\"${String(request.state)}\" isn't a delivery failure.`)\n }\n const firestore = deps.firestore()\n const ref = crmCapturedEmailActivityRef(firestore, orgId, key)\n const outcome = await recordCrmEmailDelivery(firestore, {\n orgId,\n activityId: ref.id,\n state: request.state,\n atMs: request.atMs,\n detail: request.detail ?? null,\n })\n if (outcome === 'missing') return refuse(404, 'No email by that Message-ID is filed here.')\n if (outcome === 'failed') return refuse(409, 'The delivery state could not be written.')\n return { ok: true, id: ref.id, created: outcome === 'advanced' }\n },\n\n // An automation's email to the person its event is about (AGL-2615):\n // see `automation-steps.ts`, loaded with the first one.\n async prepareEmail(request) {\n const { prepareCrmRecordEmail } = await import('./automation-steps')\n return prepareCrmRecordEmail(request)\n },\n }\n}\n\n/** The platform's own dependencies. Specs build their own. */\nexport function defaultCrmRecordTimelineDeps(): CrmRecordTimelineDeps {\n return { firestore: () => firebaseAdmin.app().firestore() }\n}\n\n/** Registers the CRM as the workspace's record system on the timeline seam. */\nexport function registerCrmRecordTimelineWriter(\n deps: CrmRecordTimelineDeps = defaultCrmRecordTimelineDeps(),\n): void {\n registerPluginRecordTimelineWriter(createCrmRecordTimelineWriter(deps), { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginRecordTimelineWriter","checkEntitlement","consentGroupForHost","CRM_ACTIVITY_LOG_FULL_MESSAGE","CRM_COLLECTIONS","crmActivityLogHasRoom","crmScopeTokens","crmTaskLabelsForNew","crmTaskListFields","crmTaskReminderAfterEdit","isCrmActivityKind","isCrmTaskKind","readContactFacet","visibleToHost","countCrmActivitiesForRecord","createCrmEmailActivity","crmActivityRef","crmCapturedEmailActivityRef","findContactByEmail","firebaseAdmin","readLeadForHost","recomputeCrmNextTaskAt","recordCrmEmailDelivery","createHash","FieldValue","BUNDLE_ID","buildCrmCapturedEmailActivity","crmCapturedEmailKey","readCrmTaskPicklists","CRM_SUITE_FEATURE","ALREADY_EXISTS","TASK_TITLE_MAX","NOTE_MAX","refuse","status","error","ok","linkFields","link","contactId","String","companyId","leadId","FOUND_RECORD_KINDS","Set","RECORD_ID_PATTERN","contactRecord","contact","groupId","facet","data","id","ownerUid","holderUid","findRecord","orgRef","hostId","contacts","collection","kind","record","has","test","doc","get","exists","deal","deals","dealId","lead","email","trim","hit","holderOf","undefined","keyedId","sourcePluginId","key","update","digest","slice","createCrmRecordTimelineWriter","deps","admit","context","wantHolder","snapshot","orgId","named","isNamed","Boolean","firestore","org","group","found","visibleTo","createOnce","ref","create","createdAt","serverTimestamp","updatedAt","code","logActivity","request","admitted","activity","messageId","direction","subject","excerpt","body","from","to","inReplyTo","atMs","byUid","byName","dedupeKey","created","stamped","createTask","title","replace","Number","isFinite","dueAtMs","assigneeUid","tasks","notes","labels","priority","task","remindAtMs","previous","createdByUid","catch","console","recordEmailDelivery","state","outcome","activityId","detail","prepareEmail","prepareCrmRecordEmail","defaultCrmRecordTimelineDeps","app","registerCrmRecordTimelineWriter","pluginId"],"mappings":"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"}
|
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
*
|
|
30
30
|
* Capture — a form, a sign-up, a booking, an order — does not come through
|
|
31
31
|
* these routes, and records what it records on every plan. The compliance
|
|
32
|
-
* doors are every plan's too: `crm/erase-person
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* doors are every plan's too: `crm/erase-person` here, and the `crm.contacts`
|
|
33
|
+
* and `crm.leads` exports of the transfer framework, which ask no plan,
|
|
34
|
+
* because exporting and erasing the people a workspace holds are
|
|
35
|
+
* obligations rather than CRM features.
|
|
35
36
|
*
|
|
36
37
|
* ## After authorization, never before
|
|
37
38
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/suite-gate.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 * THE CRM'S PLAN GATE, for the routes the console calls (AGL-2787, AGL-2851).\n *\n * The CRM is included from Starter: `features.crm` is false on Free, and a\n * Free workspace has none of it — no section, no read and no write. Three\n * other doors onto the CRM's records ask the same question: the shell draws\n * every CRM section locked and refuses its body, the `/v1` dispatcher refuses\n * the CRM's resources, and the Firestore rules refuse a client's CRM writes.\n * These routes are the fourth door, and every `crm/*` read and write asks it.\n * A lock drawn on a page is advice to a script holding a member's token, so\n * the same question is asked where the read or the write happens.\n *\n * ## What is never asked\n *\n * Capture — a form, a sign-up, a booking, an order — does not come through\n * these routes, and records what it records on every plan. The compliance\n * doors are every plan's too: `crm/erase-person
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/suite-gate.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 * THE CRM'S PLAN GATE, for the routes the console calls (AGL-2787, AGL-2851).\n *\n * The CRM is included from Starter: `features.crm` is false on Free, and a\n * Free workspace has none of it — no section, no read and no write. Three\n * other doors onto the CRM's records ask the same question: the shell draws\n * every CRM section locked and refuses its body, the `/v1` dispatcher refuses\n * the CRM's resources, and the Firestore rules refuse a client's CRM writes.\n * These routes are the fourth door, and every `crm/*` read and write asks it.\n * A lock drawn on a page is advice to a script holding a member's token, so\n * the same question is asked where the read or the write happens.\n *\n * ## What is never asked\n *\n * Capture — a form, a sign-up, a booking, an order — does not come through\n * these routes, and records what it records on every plan. The compliance\n * doors are every plan's too: `crm/erase-person` here, and the `crm.contacts`\n * and `crm.leads` exports of the transfer framework, which ask no plan,\n * because exporting and erasing the people a workspace holds are\n * obligations rather than CRM features.\n *\n * ## After authorization, never before\n *\n * Each route asks once it knows who is calling and has read the org to\n * answer that — the order the shell refuses in. A reader who may not open a\n * surface is not shown its upgrade path, and a caller with no standing in\n * the workspace learns nothing about its plan.\n *\n * ## One refusal shape\n *\n * 403, as the REST refusal is, with its two machine-readable facts in the\n * console routes' flat body: `reason: 'plan_required'` is the REST\n * `error.type` and `code: 'crm'` its `error.code`. Beside them, a sentence\n * the console relays unchanged, as it relays every route refusal. The plan\n * it names comes from the plan tables, so it cannot name a tier that has\n * stopped carrying the suite.\n *\n * ## Staff are refused too\n *\n * The plan is a fact about the workspace, not about the person asking. A\n * support engineer acting inside a Free workspace would otherwise create\n * suite records that the workspace then holds without the suite.\n */\n\nimport { checkEntitlement, planLabelGrantingFeature } from '@aglyn/aglyn/server'\n\n/** The entitlement the suite is sold under — the REST refusal's `error.code`. */\nexport const CRM_SUITE_FEATURE = 'crm'\n\n/** The REST refusal's `error.type`, as a console route's `reason`. */\nexport const CRM_SUITE_REFUSAL_REASON = 'plan_required'\n\n/** What a route answers a workspace whose plan does not carry the suite. */\nexport interface CrmSuiteRefusal {\n status: 403\n body: {\n error: string\n reason: typeof CRM_SUITE_REFUSAL_REASON\n code: typeof CRM_SUITE_FEATURE\n }\n}\n\n/**\n * The refusal's sentence, for an act named as a phrase that takes \"is\" —\n * \"Importing a CSV file\", \"A CRM recipe\".\n */\nexport function crmSuiteRefusalMessage(act: string): string {\n const plan = planLabelGrantingFeature(CRM_SUITE_FEATURE)\n return (\n `${act} is part of the CRM, which is not included in your current ` +\n 'plan. Manage your plan and add-ons from Billing.' +\n (plan ? ` Included from ${plan}.` : '')\n )\n}\n\n/**\n * `null` when the org's plan carries the suite, else the refusal to send.\n *\n * Resolved through `checkEntitlement`, so a per-org\n * `entitlements.features.crm` grant or revocation, and a paid plan whose\n * subscription has died, answer here exactly as they answer everywhere else.\n */\nexport function crmSuiteRefusal(org: unknown, act: string): CrmSuiteRefusal | null {\n if (checkEntitlement(org as Parameters<typeof checkEntitlement>[0], CRM_SUITE_FEATURE)) {\n return null\n }\n return {\n status: 403,\n body: {\n error: crmSuiteRefusalMessage(act),\n reason: CRM_SUITE_REFUSAL_REASON,\n code: CRM_SUITE_FEATURE,\n },\n }\n}\n"],"names":["checkEntitlement","planLabelGrantingFeature","CRM_SUITE_FEATURE","CRM_SUITE_REFUSAL_REASON","crmSuiteRefusalMessage","act","plan","crmSuiteRefusal","org","status","body","error","reason","code"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0CC,GAED,SAASA,gBAAgB,EAAEC,wBAAwB,QAAQ,sBAAqB;AAEhF,+EAA+E,GAC/E,OAAO,MAAMC,oBAAoB,MAAK;AAEtC,oEAAoE,GACpE,OAAO,MAAMC,2BAA2B,gBAAe;AAYvD;;;CAGC,GACD,OAAO,SAASC,uBAAuBC,GAAW;IAChD,MAAMC,OAAOL,yBAAyBC;IACtC,OACE,GAAGG,IAAI,2DAA2D,CAAC,GACnE,qDACCC,CAAAA,OAAO,CAAC,eAAe,EAAEA,KAAK,CAAC,CAAC,GAAG,EAAC;AAEzC;AAEA;;;;;;CAMC,GACD,OAAO,SAASC,gBAAgBC,GAAY,EAAEH,GAAW;IACvD,IAAIL,iBAAiBQ,KAA+CN,oBAAoB;QACtF,OAAO;IACT;IACA,OAAO;QACLO,QAAQ;QACRC,MAAM;YACJC,OAAOP,uBAAuBC;YAC9BO,QAAQT;YACRU,MAAMX;QACR;IACF;AACF"}
|
|
@@ -14,12 +14,14 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import { consentGroupForHost, CRM_COLLECTIONS, crmScopeTokens, crmTaskListFields, crmTaskReminderAfterEdit, crmTaskReminderPending, crmTaskReminderWhen, isOrgWideMember, memberCanSee } from "@aglyn/aglyn/server";
|
|
17
|
+
*/ import { consentGroupForHost, CRM_COLLECTIONS, crmScopeTokens, crmTaskListFields, crmTaskStatusWrite, crmTaskReminderAfterEdit, crmTaskReminderPending, crmTaskReminderWhen, isOrgWideMember, memberCanSee } from "@aglyn/aglyn/server";
|
|
18
18
|
import { crmNextActivityLinksOf, firebaseAdmin, getOrgForHost, memberHasOrgPermission, notifyUsers, recomputeCrmNextTaskAt, resolveOrgMembership } from "@aglyn/tenant-data-admin";
|
|
19
19
|
import { emitHostEvent } from "@aglyn/tenant-runtime";
|
|
20
20
|
import { FieldValue } from "firebase-admin/firestore";
|
|
21
21
|
import { CRM_TASK_BATCH_MAX, readCrmTaskFields } from "../model/task-routes.js";
|
|
22
22
|
import { CRM_ORG_TASK_SCOPE } from "../model/task-scope.js";
|
|
23
|
+
import { resolveCrmTaskPicklistWrite } from "../model/task-picklists.js";
|
|
24
|
+
import { readCrmTaskPicklists } from "./read-picklist.js";
|
|
23
25
|
import { digestTimeZone } from "../model/crm-digest.js";
|
|
24
26
|
import { authorizeOrgCaller, readCrmRouteScope } from "./org-caller.js";
|
|
25
27
|
import { crmSuiteRefusal } from "./suite-gate.js";
|
|
@@ -162,7 +164,7 @@ const ORG_REFUSAL = 'Editing tasks at the organization level requires the "Manag
|
|
|
162
164
|
* `dueAtMs` is the one field written as `null` when unset, because every
|
|
163
165
|
* view orders by it and a document missing the field drops out of an
|
|
164
166
|
* `orderBy` entirely.
|
|
165
|
-
*/ function storedFields(fields, mode) {
|
|
167
|
+
*/ function storedFields(fields, mode, picked) {
|
|
166
168
|
const absent = mode === 'update' ? FieldValue.delete() : undefined;
|
|
167
169
|
const optional = (value)=>value ? {
|
|
168
170
|
present: true,
|
|
@@ -171,10 +173,14 @@ const ORG_REFUSAL = 'Editing tasks at the organization level requires the "Manag
|
|
|
171
173
|
present: mode === 'update',
|
|
172
174
|
value: absent
|
|
173
175
|
};
|
|
176
|
+
// The meanings every query reads, with the org's labels beside them (AGL-3517).
|
|
174
177
|
const out = {
|
|
175
178
|
title: fields.title,
|
|
176
|
-
kind:
|
|
177
|
-
|
|
179
|
+
kind: picked.kind,
|
|
180
|
+
typeLabel: picked.typeLabel,
|
|
181
|
+
priority: picked.priority,
|
|
182
|
+
priorityLabel: picked.priorityLabel,
|
|
183
|
+
statusLabel: picked.statusLabel,
|
|
178
184
|
dueAtMs: fields.dueAtMs,
|
|
179
185
|
notes: fields.notes
|
|
180
186
|
};
|
|
@@ -286,7 +292,7 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
286
292
|
* and `createdByUid` are provenance and never rewritten. The notification
|
|
287
293
|
* opens under the site the caller is on, or — at the organization level —
|
|
288
294
|
* under the task's own site, and under the org hub for a task with none.
|
|
289
|
-
*/ async function updateTask(writer, scope, tasks, taskId, fields, onRoster) {
|
|
295
|
+
*/ async function updateTask(writer, scope, tasks, taskId, fields, onRoster, picklists) {
|
|
290
296
|
var _existing_get, _existing_get1;
|
|
291
297
|
if (fields.assigneeUid && !await onRoster(fields.assigneeUid)) {
|
|
292
298
|
return refuse(400, 'The assignee is not a member of this organization.');
|
|
@@ -296,9 +302,11 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
296
302
|
if (!canReach(writer, existing.get('visibleTo'))) {
|
|
297
303
|
return refuse(403, 'That task is not visible to you.');
|
|
298
304
|
}
|
|
305
|
+
const picked = resolveCrmTaskPicklistWrite(picklists, fields, existing.data());
|
|
306
|
+
if (picked.ok === false) return refuse(400, picked.error);
|
|
299
307
|
const previousAssignee = String((_existing_get = existing.get('assigneeUid')) != null ? _existing_get : '') || null;
|
|
300
308
|
const previousLinks = crmNextActivityLinksOf(existing.data());
|
|
301
|
-
const stored = storedFields(fields, 'update');
|
|
309
|
+
const stored = storedFields(fields, 'update', picked.write);
|
|
302
310
|
await tasks.doc(taskId).update(_extends({}, stored, reminderFields(fields, existing), crmTaskListFields(_extends({}, existing.data(), stored)), {
|
|
303
311
|
updatedAt: FieldValue.serverTimestamp()
|
|
304
312
|
}));
|
|
@@ -347,10 +355,12 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
347
355
|
* refused, which is the rules' `canCreateScoped()` said before the write
|
|
348
356
|
* instead of after. An organization task — no site — carries
|
|
349
357
|
* `CRM_ORG_TASK_SCOPE` and `hostId: null`.
|
|
350
|
-
*/ async function createTask(writer, scope, tasks, fields, onRoster) {
|
|
358
|
+
*/ async function createTask(writer, scope, tasks, fields, onRoster, picklists) {
|
|
351
359
|
if (fields.assigneeUid && !await onRoster(fields.assigneeUid)) {
|
|
352
360
|
return refuse(400, 'The assignee is not a member of this organization.');
|
|
353
361
|
}
|
|
362
|
+
const picked = resolveCrmTaskPicklistWrite(picklists, fields, null);
|
|
363
|
+
if (picked.ok === false) return refuse(400, picked.error);
|
|
354
364
|
const filed = await resolveFiledFrom(writer, scope);
|
|
355
365
|
if (filed.ok === false) return filed;
|
|
356
366
|
const { hostId } = filed;
|
|
@@ -361,7 +371,8 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
361
371
|
return refuse(403, 'Your access to this organization does not reach this site.');
|
|
362
372
|
}
|
|
363
373
|
const ref = tasks.doc();
|
|
364
|
-
|
|
374
|
+
const stored = storedFields(fields, 'create', picked.write);
|
|
375
|
+
await ref.set(_extends({}, stored, reminderFields(fields, null), crmTaskListFields(_extends({}, stored, {
|
|
365
376
|
visibleTo
|
|
366
377
|
})), {
|
|
367
378
|
status: 'open',
|
|
@@ -435,9 +446,10 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
435
446
|
}
|
|
436
447
|
const tasks = tasksCollection(writer.orgId);
|
|
437
448
|
const onRoster = rosterOf(writer.orgId);
|
|
449
|
+
const picklists = await readCrmTaskPicklists(firebaseAdmin.app().firestore(), writer.orgId);
|
|
438
450
|
let body;
|
|
439
451
|
if (taskId) {
|
|
440
|
-
const saved = await updateTask(writer, scope, tasks, taskId, fields, onRoster);
|
|
452
|
+
const saved = await updateTask(writer, scope, tasks, taskId, fields, onRoster, picklists);
|
|
441
453
|
if (saved.ok === false) {
|
|
442
454
|
res.status(saved.status).json(saved.body);
|
|
443
455
|
return;
|
|
@@ -448,7 +460,7 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
448
460
|
notified: saved.notified
|
|
449
461
|
};
|
|
450
462
|
} else {
|
|
451
|
-
const saved = await createTask(writer, scope, tasks, fields, onRoster);
|
|
463
|
+
const saved = await createTask(writer, scope, tasks, fields, onRoster, picklists);
|
|
452
464
|
if (saved.ok === false) {
|
|
453
465
|
res.status(saved.status).json(saved.body);
|
|
454
466
|
return;
|
|
@@ -511,9 +523,10 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
511
523
|
}
|
|
512
524
|
const tasks = tasksCollection(writer.orgId);
|
|
513
525
|
const onRoster = rosterOf(writer.orgId);
|
|
526
|
+
const picklists = await readCrmTaskPicklists(firebaseAdmin.app().firestore(), writer.orgId);
|
|
514
527
|
const results = [];
|
|
515
528
|
for (const { taskId, fields } of parsed){
|
|
516
|
-
const saved = await updateTask(writer, scope, tasks, taskId, fields, onRoster);
|
|
529
|
+
const saved = await updateTask(writer, scope, tasks, taskId, fields, onRoster, picklists);
|
|
517
530
|
results.push(saved.ok === false ? {
|
|
518
531
|
taskId,
|
|
519
532
|
ok: false,
|
|
@@ -548,7 +561,7 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
548
561
|
* Idempotent on purpose: two people ticking the same box, or one person
|
|
549
562
|
* double-clicking, completes a task once and fires its event once. The
|
|
550
563
|
* second call reads the stored `completedAtMs` back and writes nothing.
|
|
551
|
-
*/ async function completeTask(writer, scope, tasks, taskId) {
|
|
564
|
+
*/ async function completeTask(writer, scope, tasks, taskId, picklists) {
|
|
552
565
|
var _task_hostId;
|
|
553
566
|
const ref = tasks.doc(taskId);
|
|
554
567
|
const snapshot = await ref.get();
|
|
@@ -577,8 +590,7 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
577
590
|
};
|
|
578
591
|
}
|
|
579
592
|
const completedAtMs = Date.now();
|
|
580
|
-
await ref.update(_extends({
|
|
581
|
-
status: 'done',
|
|
593
|
+
await ref.update(_extends({}, crmTaskStatusWrite(picklists.status, true), {
|
|
582
594
|
completedAtMs,
|
|
583
595
|
completedByUid: writer.uid
|
|
584
596
|
}, crmTaskReminderPending(task) ? {
|
|
@@ -660,7 +672,8 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
660
672
|
res.status(writer.status).json(writer.body);
|
|
661
673
|
return;
|
|
662
674
|
}
|
|
663
|
-
const
|
|
675
|
+
const picklists = await readCrmTaskPicklists(firebaseAdmin.app().firestore(), writer.orgId);
|
|
676
|
+
const outcome = await completeTask(writer, scope, tasksCollection(writer.orgId), taskId, picklists);
|
|
664
677
|
if (outcome.ok === false) {
|
|
665
678
|
res.status(completeStatus(outcome)).json({
|
|
666
679
|
error: outcome.error
|
|
@@ -710,9 +723,10 @@ const tasksCollection = (orgId)=>firebaseAdmin.app().firestore().collection('org
|
|
|
710
723
|
return;
|
|
711
724
|
}
|
|
712
725
|
const tasks = tasksCollection(writer.orgId);
|
|
726
|
+
const picklists = await readCrmTaskPicklists(firebaseAdmin.app().firestore(), writer.orgId);
|
|
713
727
|
const results = [];
|
|
714
728
|
for (const taskId of taskIds){
|
|
715
|
-
results.push(await completeTask(writer, scope, tasks, taskId));
|
|
729
|
+
results.push(await completeTask(writer, scope, tasks, taskId, picklists));
|
|
716
730
|
}
|
|
717
731
|
const body = {
|
|
718
732
|
ok: true,
|