@aglyn/plugins-crm 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +57 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +29 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/activity-list.d.ts +98 -0
- package/src/lib/components/activity-list.js +321 -0
- package/src/lib/components/activity-list.js.map +1 -0
- package/src/lib/components/activity-queries.d.ts +117 -0
- package/src/lib/components/activity-queries.js +198 -0
- package/src/lib/components/activity-queries.js.map +1 -0
- package/src/lib/components/add-to-list-button.d.ts +25 -0
- package/src/lib/components/add-to-list-button.js +64 -0
- package/src/lib/components/add-to-list-button.js.map +1 -0
- package/src/lib/components/add-to-list-dialog.d.ts +24 -0
- package/src/lib/components/add-to-list-dialog.js +327 -0
- package/src/lib/components/add-to-list-dialog.js.map +1 -0
- package/src/lib/components/assignment-rule-drawer.d.ts +43 -0
- package/src/lib/components/assignment-rule-drawer.js +293 -0
- package/src/lib/components/assignment-rule-drawer.js.map +1 -0
- package/src/lib/components/companies-bulk-bar.d.ts +29 -0
- package/src/lib/components/companies-bulk-bar.js +274 -0
- package/src/lib/components/companies-bulk-bar.js.map +1 -0
- package/src/lib/components/companies-section.d.ts +54 -0
- package/src/lib/components/companies-section.js +522 -0
- package/src/lib/components/companies-section.js.map +1 -0
- package/src/lib/components/company-contacts-card.d.ts +49 -0
- package/src/lib/components/company-contacts-card.js +435 -0
- package/src/lib/components/company-contacts-card.js.map +1 -0
- package/src/lib/components/company-deals-card.d.ts +20 -0
- package/src/lib/components/company-deals-card.js +38 -0
- package/src/lib/components/company-deals-card.js.map +1 -0
- package/src/lib/components/company-detail-page.d.ts +20 -0
- package/src/lib/components/company-detail-page.js +165 -0
- package/src/lib/components/company-detail-page.js.map +1 -0
- package/src/lib/components/company-edit-drawer.d.ts +65 -0
- package/src/lib/components/company-edit-drawer.js +433 -0
- package/src/lib/components/company-edit-drawer.js.map +1 -0
- package/src/lib/components/company-import-drawer.d.ts +42 -0
- package/src/lib/components/company-import-drawer.js +99 -0
- package/src/lib/components/company-import-drawer.js.map +1 -0
- package/src/lib/components/company-picker.d.ts +127 -0
- package/src/lib/components/company-picker.js +323 -0
- package/src/lib/components/company-picker.js.map +1 -0
- package/src/lib/components/company-properties-card.d.ts +47 -0
- package/src/lib/components/company-properties-card.js +289 -0
- package/src/lib/components/company-properties-card.js.map +1 -0
- package/src/lib/components/contact-address-fields.d.ts +29 -0
- package/src/lib/components/contact-address-fields.js +149 -0
- package/src/lib/components/contact-address-fields.js.map +1 -0
- package/src/lib/components/contact-associations-card.d.ts +72 -0
- package/src/lib/components/contact-associations-card.js +296 -0
- package/src/lib/components/contact-associations-card.js.map +1 -0
- package/src/lib/components/contact-custom-columns.d.ts +38 -0
- package/src/lib/components/contact-custom-columns.js +102 -0
- package/src/lib/components/contact-custom-columns.js.map +1 -0
- package/src/lib/components/contact-custom-fields-card.d.ts +39 -0
- package/src/lib/components/contact-custom-fields-card.js +204 -0
- package/src/lib/components/contact-custom-fields-card.js.map +1 -0
- package/src/lib/components/contact-deals-card.d.ts +20 -0
- package/src/lib/components/contact-deals-card.js +38 -0
- package/src/lib/components/contact-deals-card.js.map +1 -0
- package/src/lib/components/contact-detail-page.d.ts +46 -0
- package/src/lib/components/contact-detail-page.js +535 -0
- package/src/lib/components/contact-detail-page.js.map +1 -0
- package/src/lib/components/contact-duplicates-card.d.ts +42 -0
- package/src/lib/components/contact-duplicates-card.js +167 -0
- package/src/lib/components/contact-duplicates-card.js.map +1 -0
- package/src/lib/components/contact-field-drawer.d.ts +64 -0
- package/src/lib/components/contact-field-drawer.js +251 -0
- package/src/lib/components/contact-field-drawer.js.map +1 -0
- package/src/lib/components/contact-import-drawer.d.ts +46 -0
- package/src/lib/components/contact-import-drawer.js +149 -0
- package/src/lib/components/contact-import-drawer.js.map +1 -0
- package/src/lib/components/contact-known-by-card.d.ts +39 -0
- package/src/lib/components/contact-known-by-card.js +142 -0
- package/src/lib/components/contact-known-by-card.js.map +1 -0
- package/src/lib/components/contact-list-columns.d.ts +53 -0
- package/src/lib/components/contact-list-columns.js +288 -0
- package/src/lib/components/contact-list-columns.js.map +1 -0
- package/src/lib/components/contact-merge-dialog.d.ts +79 -0
- package/src/lib/components/contact-merge-dialog.js +405 -0
- package/src/lib/components/contact-merge-dialog.js.map +1 -0
- package/src/lib/components/contact-properties-card.d.ts +89 -0
- package/src/lib/components/contact-properties-card.js +492 -0
- package/src/lib/components/contact-properties-card.js.map +1 -0
- package/src/lib/components/contact-timeline-card.d.ts +68 -0
- package/src/lib/components/contact-timeline-card.js +435 -0
- package/src/lib/components/contact-timeline-card.js.map +1 -0
- package/src/lib/components/contacts-bulk-bar.d.ts +91 -0
- package/src/lib/components/contacts-bulk-bar.js +493 -0
- package/src/lib/components/contacts-bulk-bar.js.map +1 -0
- package/src/lib/components/contacts-section.d.ts +26 -0
- package/src/lib/components/contacts-section.js +1025 -0
- package/src/lib/components/contacts-section.js.map +1 -0
- package/src/lib/components/copy-capture-address-button.d.ts +33 -0
- package/src/lib/components/copy-capture-address-button.js +60 -0
- package/src/lib/components/copy-capture-address-button.js.map +1 -0
- package/src/lib/components/crm-attribution-zone.d.ts +38 -0
- package/src/lib/components/crm-attribution-zone.js +36 -0
- package/src/lib/components/crm-attribution-zone.js.map +1 -0
- package/src/lib/components/crm-booking-zone.d.ts +61 -0
- package/src/lib/components/crm-booking-zone.js +50 -0
- package/src/lib/components/crm-booking-zone.js.map +1 -0
- package/src/lib/components/crm-bulk-bar-frame.d.ts +47 -0
- package/src/lib/components/crm-bulk-bar-frame.js +137 -0
- package/src/lib/components/crm-bulk-bar-frame.js.map +1 -0
- package/src/lib/components/crm-call-actions.d.ts +65 -0
- package/src/lib/components/crm-call-actions.js +110 -0
- package/src/lib/components/crm-call-actions.js.map +1 -0
- package/src/lib/components/crm-column-menu.d.ts +21 -0
- package/src/lib/components/crm-column-menu.js +103 -0
- package/src/lib/components/crm-column-menu.js.map +1 -0
- package/src/lib/components/crm-console-page.d.ts +19 -0
- package/src/lib/components/crm-console-page.js +151 -0
- package/src/lib/components/crm-console-page.js.map +1 -0
- package/src/lib/components/crm-console-sections.d.ts +52 -0
- package/src/lib/components/crm-console-sections.js +83 -0
- package/src/lib/components/crm-console-sections.js.map +1 -0
- package/src/lib/components/crm-custom-field-control.d.ts +26 -0
- package/src/lib/components/crm-custom-field-control.js +131 -0
- package/src/lib/components/crm-custom-field-control.js.map +1 -0
- package/src/lib/components/crm-email-template-tools.d.ts +69 -0
- package/src/lib/components/crm-email-template-tools.js +240 -0
- package/src/lib/components/crm-email-template-tools.js.map +1 -0
- package/src/lib/components/crm-export-all-button.d.ts +39 -0
- package/src/lib/components/crm-export-all-button.js +98 -0
- package/src/lib/components/crm-export-all-button.js.map +1 -0
- package/src/lib/components/crm-filter-bar.d.ts +53 -0
- package/src/lib/components/crm-filter-bar.js +294 -0
- package/src/lib/components/crm-filter-bar.js.map +1 -0
- package/src/lib/components/crm-glance-card.d.ts +54 -0
- package/src/lib/components/crm-glance-card.js +219 -0
- package/src/lib/components/crm-glance-card.js.map +1 -0
- package/src/lib/components/crm-next-activity-column.d.ts +35 -0
- package/src/lib/components/crm-next-activity-column.js +100 -0
- package/src/lib/components/crm-next-activity-column.js.map +1 -0
- package/src/lib/components/crm-record-header.d.ts +97 -0
- package/src/lib/components/crm-record-header.js +160 -0
- package/src/lib/components/crm-record-header.js.map +1 -0
- package/src/lib/components/crm-record-insights-zone.d.ts +50 -0
- package/src/lib/components/crm-record-insights-zone.js +169 -0
- package/src/lib/components/crm-record-insights-zone.js.map +1 -0
- package/src/lib/components/crm-record-picker.d.ts +42 -0
- package/src/lib/components/crm-record-picker.js +104 -0
- package/src/lib/components/crm-record-picker.js.map +1 -0
- package/src/lib/components/crm-send-email-button.d.ts +39 -0
- package/src/lib/components/crm-send-email-button.js +81 -0
- package/src/lib/components/crm-send-email-button.js.map +1 -0
- package/src/lib/components/crm-send-email-dialog.d.ts +61 -0
- package/src/lib/components/crm-send-email-dialog.js +851 -0
- package/src/lib/components/crm-send-email-dialog.js.map +1 -0
- package/src/lib/components/crm-site-picker.d.ts +61 -0
- package/src/lib/components/crm-site-picker.js +99 -0
- package/src/lib/components/crm-site-picker.js.map +1 -0
- package/src/lib/components/crm-suite-lock.d.ts +29 -0
- package/src/lib/components/crm-suite-lock.js +114 -0
- package/src/lib/components/crm-suite-lock.js.map +1 -0
- package/src/lib/components/crm-tasks-due-card.d.ts +42 -0
- package/src/lib/components/crm-tasks-due-card.js +225 -0
- package/src/lib/components/crm-tasks-due-card.js.map +1 -0
- package/src/lib/components/crm-views-control.d.ts +65 -0
- package/src/lib/components/crm-views-control.js +518 -0
- package/src/lib/components/crm-views-control.js.map +1 -0
- package/src/lib/components/csv-import-drawer.d.ts +132 -0
- package/src/lib/components/csv-import-drawer.js +746 -0
- package/src/lib/components/csv-import-drawer.js.map +1 -0
- package/src/lib/components/deal-board.d.ts +70 -0
- package/src/lib/components/deal-board.js +478 -0
- package/src/lib/components/deal-board.js.map +1 -0
- package/src/lib/components/deal-detail-page.d.ts +18 -0
- package/src/lib/components/deal-detail-page.js +323 -0
- package/src/lib/components/deal-detail-page.js.map +1 -0
- package/src/lib/components/deal-edit-drawer.d.ts +59 -0
- package/src/lib/components/deal-edit-drawer.js +640 -0
- package/src/lib/components/deal-edit-drawer.js.map +1 -0
- package/src/lib/components/deal-import-drawer.d.ts +24 -0
- package/src/lib/components/deal-import-drawer.js +83 -0
- package/src/lib/components/deal-import-drawer.js.map +1 -0
- package/src/lib/components/deal-products-card.d.ts +48 -0
- package/src/lib/components/deal-products-card.js +614 -0
- package/src/lib/components/deal-products-card.js.map +1 -0
- package/src/lib/components/deal-properties-card.d.ts +29 -0
- package/src/lib/components/deal-properties-card.js +144 -0
- package/src/lib/components/deal-properties-card.js.map +1 -0
- package/src/lib/components/deal-stage-card.d.ts +24 -0
- package/src/lib/components/deal-stage-card.js +199 -0
- package/src/lib/components/deal-stage-card.js.map +1 -0
- package/src/lib/components/deals-bulk-bar.d.ts +31 -0
- package/src/lib/components/deals-bulk-bar.js +340 -0
- package/src/lib/components/deals-bulk-bar.js.map +1 -0
- package/src/lib/components/deals-section.d.ts +49 -0
- package/src/lib/components/deals-section.js +767 -0
- package/src/lib/components/deals-section.js.map +1 -0
- package/src/lib/components/email-capture-card.d.ts +42 -0
- package/src/lib/components/email-capture-card.js +134 -0
- package/src/lib/components/email-capture-card.js.map +1 -0
- package/src/lib/components/email-template-drawer.d.ts +33 -0
- package/src/lib/components/email-template-drawer.js +209 -0
- package/src/lib/components/email-template-drawer.js.map +1 -0
- package/src/lib/components/email-templates-card.d.ts +33 -0
- package/src/lib/components/email-templates-card.js +370 -0
- package/src/lib/components/email-templates-card.js.map +1 -0
- package/src/lib/components/erase-person-action.d.ts +92 -0
- package/src/lib/components/erase-person-action.js +261 -0
- package/src/lib/components/erase-person-action.js.map +1 -0
- package/src/lib/components/fields-section.d.ts +37 -0
- package/src/lib/components/fields-section.js +641 -0
- package/src/lib/components/fields-section.js.map +1 -0
- package/src/lib/components/form-contact-fields-card.d.ts +46 -0
- package/src/lib/components/form-contact-fields-card.js +253 -0
- package/src/lib/components/form-contact-fields-card.js.map +1 -0
- package/src/lib/components/insert-link-at-caret.d.ts +28 -0
- package/src/lib/components/insert-link-at-caret.js +37 -0
- package/src/lib/components/insert-link-at-caret.js.map +1 -0
- package/src/lib/components/lead-convert-dialog.d.ts +32 -0
- package/src/lib/components/lead-convert-dialog.js +493 -0
- package/src/lib/components/lead-convert-dialog.js.map +1 -0
- package/src/lib/components/lead-detail-page.d.ts +16 -0
- package/src/lib/components/lead-detail-page.js +163 -0
- package/src/lib/components/lead-detail-page.js.map +1 -0
- package/src/lib/components/lead-history-card.d.ts +32 -0
- package/src/lib/components/lead-history-card.js +165 -0
- package/src/lib/components/lead-history-card.js.map +1 -0
- package/src/lib/components/lead-import-drawer.d.ts +24 -0
- package/src/lib/components/lead-import-drawer.js +95 -0
- package/src/lib/components/lead-import-drawer.js.map +1 -0
- package/src/lib/components/lead-owner-select.d.ts +35 -0
- package/src/lib/components/lead-owner-select.js +91 -0
- package/src/lib/components/lead-owner-select.js.map +1 -0
- package/src/lib/components/lead-properties-card.d.ts +61 -0
- package/src/lib/components/lead-properties-card.js +395 -0
- package/src/lib/components/lead-properties-card.js.map +1 -0
- package/src/lib/components/lead-status-chip.d.ts +12 -0
- package/src/lib/components/lead-status-chip.js +43 -0
- package/src/lib/components/lead-status-chip.js.map +1 -0
- package/src/lib/components/lead-surfaces-note.d.ts +126 -0
- package/src/lib/components/lead-surfaces-note.js +262 -0
- package/src/lib/components/lead-surfaces-note.js.map +1 -0
- package/src/lib/components/lead-unqualify-dialog.d.ts +24 -0
- package/src/lib/components/lead-unqualify-dialog.js +125 -0
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -0
- package/src/lib/components/leads-bulk-bar.d.ts +58 -0
- package/src/lib/components/leads-bulk-bar.js +307 -0
- package/src/lib/components/leads-bulk-bar.js.map +1 -0
- package/src/lib/components/leads-section.d.ts +23 -0
- package/src/lib/components/leads-section.js +664 -0
- package/src/lib/components/leads-section.js.map +1 -0
- package/src/lib/components/linked-deals-card.d.ts +32 -0
- package/src/lib/components/linked-deals-card.js +163 -0
- package/src/lib/components/linked-deals-card.js.map +1 -0
- package/src/lib/components/log-activity-dialog.d.ts +60 -0
- package/src/lib/components/log-activity-dialog.js +368 -0
- package/src/lib/components/log-activity-dialog.js.map +1 -0
- package/src/lib/components/lost-reason-dialog.d.ts +26 -0
- package/src/lib/components/lost-reason-dialog.js +95 -0
- package/src/lib/components/lost-reason-dialog.js.map +1 -0
- package/src/lib/components/new-contact-drawer.d.ts +78 -0
- package/src/lib/components/new-contact-drawer.js +343 -0
- package/src/lib/components/new-contact-drawer.js.map +1 -0
- package/src/lib/components/org-lead-surfaces-note.d.ts +57 -0
- package/src/lib/components/org-lead-surfaces-note.js +172 -0
- package/src/lib/components/org-lead-surfaces-note.js.map +1 -0
- package/src/lib/components/owner-avatar.d.ts +18 -0
- package/src/lib/components/owner-avatar.js +61 -0
- package/src/lib/components/owner-avatar.js.map +1 -0
- package/src/lib/components/pipeline-stages-dialog.d.ts +43 -0
- package/src/lib/components/pipeline-stages-dialog.js +339 -0
- package/src/lib/components/pipeline-stages-dialog.js.map +1 -0
- package/src/lib/components/pipelines-dialog.d.ts +55 -0
- package/src/lib/components/pipelines-dialog.js +517 -0
- package/src/lib/components/pipelines-dialog.js.map +1 -0
- package/src/lib/components/recent-activity-feed.d.ts +39 -0
- package/src/lib/components/recent-activity-feed.js +103 -0
- package/src/lib/components/recent-activity-feed.js.map +1 -0
- package/src/lib/components/recipes-card.d.ts +39 -0
- package/src/lib/components/recipes-card.js +485 -0
- package/src/lib/components/recipes-card.js.map +1 -0
- package/src/lib/components/record-activity-card.d.ts +26 -0
- package/src/lib/components/record-activity-card.js +121 -0
- package/src/lib/components/record-activity-card.js.map +1 -0
- package/src/lib/components/record-files-card.d.ts +67 -0
- package/src/lib/components/record-files-card.js +290 -0
- package/src/lib/components/record-files-card.js.map +1 -0
- package/src/lib/components/record-tasks-card.d.ts +32 -0
- package/src/lib/components/record-tasks-card.js +306 -0
- package/src/lib/components/record-tasks-card.js.map +1 -0
- package/src/lib/components/reports/activity-card.d.ts +57 -0
- package/src/lib/components/reports/activity-card.js +281 -0
- package/src/lib/components/reports/activity-card.js.map +1 -0
- package/src/lib/components/reports/closed-deals-card.d.ts +20 -0
- package/src/lib/components/reports/closed-deals-card.js +194 -0
- package/src/lib/components/reports/closed-deals-card.js.map +1 -0
- package/src/lib/components/reports/contacts-mix-card.d.ts +27 -0
- package/src/lib/components/reports/contacts-mix-card.js +161 -0
- package/src/lib/components/reports/contacts-mix-card.js.map +1 -0
- package/src/lib/components/reports/contacts-trend-card.d.ts +31 -0
- package/src/lib/components/reports/contacts-trend-card.js +144 -0
- package/src/lib/components/reports/contacts-trend-card.js.map +1 -0
- package/src/lib/components/reports/forecast-card.d.ts +22 -0
- package/src/lib/components/reports/forecast-card.js +251 -0
- package/src/lib/components/reports/forecast-card.js.map +1 -0
- package/src/lib/components/reports/lead-counts.d.ts +32 -0
- package/src/lib/components/reports/lead-counts.js +44 -0
- package/src/lib/components/reports/lead-counts.js.map +1 -0
- package/src/lib/components/reports/lead-funnel-card.d.ts +38 -0
- package/src/lib/components/reports/lead-funnel-card.js +375 -0
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -0
- package/src/lib/components/reports/pipeline-card.d.ts +19 -0
- package/src/lib/components/reports/pipeline-card.js +338 -0
- package/src/lib/components/reports/pipeline-card.js.map +1 -0
- package/src/lib/components/reports/report-bar-chart.d.ts +39 -0
- package/src/lib/components/reports/report-bar-chart.js +133 -0
- package/src/lib/components/reports/report-bar-chart.js.map +1 -0
- package/src/lib/components/reports/report-breakdown.d.ts +32 -0
- package/src/lib/components/reports/report-breakdown.js +105 -0
- package/src/lib/components/reports/report-breakdown.js.map +1 -0
- package/src/lib/components/reports/report-export.d.ts +37 -0
- package/src/lib/components/reports/report-export.js +66 -0
- package/src/lib/components/reports/report-export.js.map +1 -0
- package/src/lib/components/reports/report-format.d.ts +36 -0
- package/src/lib/components/reports/report-format.js +43 -0
- package/src/lib/components/reports/report-format.js.map +1 -0
- package/src/lib/components/reports/report-scope.d.ts +81 -0
- package/src/lib/components/reports/report-scope.js +58 -0
- package/src/lib/components/reports/report-scope.js.map +1 -0
- package/src/lib/components/reports/report-stat-tile.d.ts +47 -0
- package/src/lib/components/reports/report-stat-tile.js +96 -0
- package/src/lib/components/reports/report-stat-tile.js.map +1 -0
- package/src/lib/components/reports/source-conversion-card.d.ts +28 -0
- package/src/lib/components/reports/source-conversion-card.js +205 -0
- package/src/lib/components/reports/source-conversion-card.js.map +1 -0
- package/src/lib/components/reports/tasks-card.d.ts +26 -0
- package/src/lib/components/reports/tasks-card.js +279 -0
- package/src/lib/components/reports/tasks-card.js.map +1 -0
- package/src/lib/components/reports/use-aggregate-read.d.ts +99 -0
- package/src/lib/components/reports/use-aggregate-read.js +195 -0
- package/src/lib/components/reports/use-aggregate-read.js.map +1 -0
- package/src/lib/components/reports/won-lost-by-owner-card.d.ts +30 -0
- package/src/lib/components/reports/won-lost-by-owner-card.js +168 -0
- package/src/lib/components/reports/won-lost-by-owner-card.js.map +1 -0
- package/src/lib/components/reports-section.d.ts +42 -0
- package/src/lib/components/reports-section.js +308 -0
- package/src/lib/components/reports-section.js.map +1 -0
- package/src/lib/components/sending-addresses-card.d.ts +49 -0
- package/src/lib/components/sending-addresses-card.js +262 -0
- package/src/lib/components/sending-addresses-card.js.map +1 -0
- package/src/lib/components/settings-section.d.ts +151 -0
- package/src/lib/components/settings-section.js +783 -0
- package/src/lib/components/settings-section.js.map +1 -0
- package/src/lib/components/task-cells.d.ts +51 -0
- package/src/lib/components/task-cells.js +139 -0
- package/src/lib/components/task-cells.js.map +1 -0
- package/src/lib/components/task-edit-drawer.d.ts +53 -0
- package/src/lib/components/task-edit-drawer.js +483 -0
- package/src/lib/components/task-edit-drawer.js.map +1 -0
- package/src/lib/components/task-import-drawer.d.ts +24 -0
- package/src/lib/components/task-import-drawer.js +82 -0
- package/src/lib/components/task-import-drawer.js.map +1 -0
- package/src/lib/components/task-snooze-menu.d.ts +65 -0
- package/src/lib/components/task-snooze-menu.js +207 -0
- package/src/lib/components/task-snooze-menu.js.map +1 -0
- package/src/lib/components/tasks-bulk-bar.d.ts +25 -0
- package/src/lib/components/tasks-bulk-bar.js +348 -0
- package/src/lib/components/tasks-bulk-bar.js.map +1 -0
- package/src/lib/components/tasks-calendar.d.ts +38 -0
- package/src/lib/components/tasks-calendar.js +228 -0
- package/src/lib/components/tasks-calendar.js.map +1 -0
- package/src/lib/components/tasks-section.d.ts +27 -0
- package/src/lib/components/tasks-section.js +579 -0
- package/src/lib/components/tasks-section.js.map +1 -0
- package/src/lib/components/use-contact-campaign-emails.d.ts +32 -0
- package/src/lib/components/use-contact-campaign-emails.js +83 -0
- package/src/lib/components/use-contact-campaign-emails.js.map +1 -0
- package/src/lib/components/use-crm-api.d.ts +44 -0
- package/src/lib/components/use-crm-api.js +84 -0
- package/src/lib/components/use-crm-api.js.map +1 -0
- package/src/lib/components/use-crm-inbound-address.d.ts +60 -0
- package/src/lib/components/use-crm-inbound-address.js +124 -0
- package/src/lib/components/use-crm-inbound-address.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +24 -0
- package/src/lib/components/use-emails-hub-path.js +59 -0
- package/src/lib/components/use-emails-hub-path.js.map +1 -0
- package/src/lib/components/use-member-email-aliases.d.ts +54 -0
- package/src/lib/components/use-member-email-aliases.js +186 -0
- package/src/lib/components/use-member-email-aliases.js.map +1 -0
- package/src/lib/components/use-org-members.d.ts +40 -0
- package/src/lib/components/use-org-members.js +100 -0
- package/src/lib/components/use-org-members.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +109 -0
- package/src/lib/constants/api-routes.js +74 -0
- package/src/lib/constants/api-routes.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +14 -0
- package/src/lib/constants/bundle-common.js +14 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/constants/company-filters.d.ts +18 -0
- package/src/lib/constants/company-filters.js +51 -0
- package/src/lib/constants/company-filters.js.map +1 -0
- package/src/lib/constants/contact-filters.d.ts +38 -0
- package/src/lib/constants/contact-filters.js +294 -0
- package/src/lib/constants/contact-filters.js.map +1 -0
- package/src/lib/hooks/use-contact-field-definitions.d.ts +48 -0
- package/src/lib/hooks/use-contact-field-definitions.js +72 -0
- package/src/lib/hooks/use-contact-field-definitions.js.map +1 -0
- package/src/lib/hooks/use-contact-update.d.ts +25 -0
- package/src/lib/hooks/use-contact-update.js +80 -0
- package/src/lib/hooks/use-contact-update.js.map +1 -0
- package/src/lib/hooks/use-crm-activity-logger.d.ts +73 -0
- package/src/lib/hooks/use-crm-activity-logger.js +101 -0
- package/src/lib/hooks/use-crm-activity-logger.js.map +1 -0
- package/src/lib/hooks/use-crm-bulk-apply.d.ts +52 -0
- package/src/lib/hooks/use-crm-bulk-apply.js +112 -0
- package/src/lib/hooks/use-crm-bulk-apply.js.map +1 -0
- package/src/lib/hooks/use-crm-default-view.d.ts +24 -0
- package/src/lib/hooks/use-crm-default-view.js +64 -0
- package/src/lib/hooks/use-crm-default-view.js.map +1 -0
- package/src/lib/hooks/use-crm-email-templates.d.ts +59 -0
- package/src/lib/hooks/use-crm-email-templates.js +103 -0
- package/src/lib/hooks/use-crm-email-templates.js.map +1 -0
- package/src/lib/hooks/use-crm-hub-path.d.ts +29 -0
- package/src/lib/hooks/use-crm-hub-path.js +41 -0
- package/src/lib/hooks/use-crm-hub-path.js.map +1 -0
- package/src/lib/hooks/use-crm-org-mount.d.ts +97 -0
- package/src/lib/hooks/use-crm-org-mount.js +215 -0
- package/src/lib/hooks/use-crm-org-mount.js.map +1 -0
- package/src/lib/hooks/use-crm-record-names.d.ts +52 -0
- package/src/lib/hooks/use-crm-record-names.js +126 -0
- package/src/lib/hooks/use-crm-record-names.js.map +1 -0
- package/src/lib/hooks/use-crm-records-quota.d.ts +68 -0
- package/src/lib/hooks/use-crm-records-quota.js +118 -0
- package/src/lib/hooks/use-crm-records-quota.js.map +1 -0
- package/src/lib/hooks/use-crm-saved-view.d.ts +96 -0
- package/src/lib/hooks/use-crm-saved-view.js +329 -0
- package/src/lib/hooks/use-crm-saved-view.js.map +1 -0
- package/src/lib/hooks/use-crm-scope.d.ts +117 -0
- package/src/lib/hooks/use-crm-scope.js +147 -0
- package/src/lib/hooks/use-crm-scope.js.map +1 -0
- package/src/lib/hooks/use-crm-tasks.d.ts +85 -0
- package/src/lib/hooks/use-crm-tasks.js +153 -0
- package/src/lib/hooks/use-crm-tasks.js.map +1 -0
- package/src/lib/hooks/use-crm-view-grid.d.ts +55 -0
- package/src/lib/hooks/use-crm-view-grid.js +170 -0
- package/src/lib/hooks/use-crm-view-grid.js.map +1 -0
- package/src/lib/hooks/use-crm-views.d.ts +42 -0
- package/src/lib/hooks/use-crm-views.js +84 -0
- package/src/lib/hooks/use-crm-views.js.map +1 -0
- package/src/lib/hooks/use-deal-stage-api.d.ts +69 -0
- package/src/lib/hooks/use-deal-stage-api.js +78 -0
- package/src/lib/hooks/use-deal-stage-api.js.map +1 -0
- package/src/lib/hooks/use-deals.d.ts +47 -0
- package/src/lib/hooks/use-deals.js +105 -0
- package/src/lib/hooks/use-deals.js.map +1 -0
- package/src/lib/hooks/use-org-leads.d.ts +53 -0
- package/src/lib/hooks/use-org-leads.js +114 -0
- package/src/lib/hooks/use-org-leads.js.map +1 -0
- package/src/lib/hooks/use-org-member-directory.d.ts +36 -0
- package/src/lib/hooks/use-org-member-directory.js +116 -0
- package/src/lib/hooks/use-org-member-directory.js.map +1 -0
- package/src/lib/hooks/use-org-member-options.d.ts +38 -0
- package/src/lib/hooks/use-org-member-options.js +131 -0
- package/src/lib/hooks/use-org-member-options.js.map +1 -0
- package/src/lib/hooks/use-pipeline.d.ts +71 -0
- package/src/lib/hooks/use-pipeline.js +159 -0
- package/src/lib/hooks/use-pipeline.js.map +1 -0
- package/src/lib/model/companies-bulk-writes.d.ts +40 -0
- package/src/lib/model/companies-bulk-writes.js +103 -0
- package/src/lib/model/companies-bulk-writes.js.map +1 -0
- package/src/lib/model/companies-csv.d.ts +36 -0
- package/src/lib/model/companies-csv.js +38 -0
- package/src/lib/model/companies-csv.js.map +1 -0
- package/src/lib/model/companies.d.ts +99 -0
- package/src/lib/model/companies.js +168 -0
- package/src/lib/model/companies.js.map +1 -0
- package/src/lib/model/company-delete-route.d.ts +43 -0
- package/src/lib/model/company-delete-route.js +31 -0
- package/src/lib/model/company-delete-route.js.map +1 -0
- package/src/lib/model/company-delete.d.ts +42 -0
- package/src/lib/model/company-delete.js +52 -0
- package/src/lib/model/company-delete.js.map +1 -0
- package/src/lib/model/contact-duplicates.d.ts +58 -0
- package/src/lib/model/contact-duplicates.js +67 -0
- package/src/lib/model/contact-duplicates.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +113 -0
- package/src/lib/model/contact-record.js +70 -0
- package/src/lib/model/contact-record.js.map +1 -0
- package/src/lib/model/contact-update.d.ts +93 -0
- package/src/lib/model/contact-update.js +50 -0
- package/src/lib/model/contact-update.js.map +1 -0
- package/src/lib/model/contacts-bulk-writes.d.ts +173 -0
- package/src/lib/model/contacts-bulk-writes.js +196 -0
- package/src/lib/model/contacts-bulk-writes.js.map +1 -0
- package/src/lib/model/contacts-csv.d.ts +54 -0
- package/src/lib/model/contacts-csv.js +67 -0
- package/src/lib/model/contacts-csv.js.map +1 -0
- package/src/lib/model/contacts-list-seed.d.ts +51 -0
- package/src/lib/model/contacts-list-seed.js +63 -0
- package/src/lib/model/contacts-list-seed.js.map +1 -0
- package/src/lib/model/crm-activity-report.d.ts +27 -0
- package/src/lib/model/crm-activity-report.js +56 -0
- package/src/lib/model/crm-activity-report.js.map +1 -0
- package/src/lib/model/crm-api.d.ts +49 -0
- package/src/lib/model/crm-api.js +47 -0
- package/src/lib/model/crm-api.js.map +1 -0
- package/src/lib/model/crm-bulk-writes.d.ts +147 -0
- package/src/lib/model/crm-bulk-writes.js +207 -0
- package/src/lib/model/crm-bulk-writes.js.map +1 -0
- package/src/lib/model/crm-company-import.d.ts +154 -0
- package/src/lib/model/crm-company-import.js +331 -0
- package/src/lib/model/crm-company-import.js.map +1 -0
- package/src/lib/model/crm-custom-draft.d.ts +62 -0
- package/src/lib/model/crm-custom-draft.js +67 -0
- package/src/lib/model/crm-custom-draft.js.map +1 -0
- package/src/lib/model/crm-deal-import.d.ts +112 -0
- package/src/lib/model/crm-deal-import.js +263 -0
- package/src/lib/model/crm-deal-import.js.map +1 -0
- package/src/lib/model/crm-import.d.ts +244 -0
- package/src/lib/model/crm-import.js +466 -0
- package/src/lib/model/crm-import.js.map +1 -0
- package/src/lib/model/crm-lead-import.d.ts +105 -0
- package/src/lib/model/crm-lead-import.js +255 -0
- package/src/lib/model/crm-lead-import.js.map +1 -0
- package/src/lib/model/crm-record-routes.d.ts +21 -0
- package/src/lib/model/crm-record-routes.js +70 -0
- package/src/lib/model/crm-record-routes.js.map +1 -0
- package/src/lib/model/crm-routes.d.ts +101 -0
- package/src/lib/model/crm-routes.js +92 -0
- package/src/lib/model/crm-routes.js.map +1 -0
- package/src/lib/model/crm-task-import.d.ts +94 -0
- package/src/lib/model/crm-task-import.js +275 -0
- package/src/lib/model/crm-task-import.js.map +1 -0
- package/src/lib/model/crm-view-param.d.ts +43 -0
- package/src/lib/model/crm-view-param.js +49 -0
- package/src/lib/model/crm-view-param.js.map +1 -0
- package/src/lib/model/deal-board-model.d.ts +241 -0
- package/src/lib/model/deal-board-model.js +439 -0
- package/src/lib/model/deal-board-model.js.map +1 -0
- package/src/lib/model/deal-form-model.d.ts +114 -0
- package/src/lib/model/deal-form-model.js +218 -0
- package/src/lib/model/deal-form-model.js.map +1 -0
- package/src/lib/model/deals-csv.d.ts +32 -0
- package/src/lib/model/deals-csv.js +37 -0
- package/src/lib/model/deals-csv.js.map +1 -0
- package/src/lib/model/import-column-shapes.d.ts +36 -0
- package/src/lib/model/import-column-shapes.js +76 -0
- package/src/lib/model/import-column-shapes.js.map +1 -0
- package/src/lib/model/lead-company-suggestion.d.ts +61 -0
- package/src/lib/model/lead-company-suggestion.js +65 -0
- package/src/lib/model/lead-company-suggestion.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +29 -0
- package/src/lib/model/lead-filters.js +39 -0
- package/src/lib/model/lead-filters.js.map +1 -0
- package/src/lib/model/lead-surfaces.d.ts +58 -0
- package/src/lib/model/lead-surfaces.js +67 -0
- package/src/lib/model/lead-surfaces.js.map +1 -0
- package/src/lib/model/leads-csv.d.ts +40 -0
- package/src/lib/model/leads-csv.js +40 -0
- package/src/lib/model/leads-csv.js.map +1 -0
- package/src/lib/model/next-activity-api.d.ts +31 -0
- package/src/lib/model/next-activity-api.js +57 -0
- package/src/lib/model/next-activity-api.js.map +1 -0
- package/src/lib/model/next-activity.d.ts +64 -0
- package/src/lib/model/next-activity.js +47 -0
- package/src/lib/model/next-activity.js.map +1 -0
- package/src/lib/model/person-key-web.d.ts +43 -0
- package/src/lib/model/person-key-web.js +52 -0
- package/src/lib/model/person-key-web.js.map +1 -0
- package/src/lib/model/record-facts.d.ts +282 -0
- package/src/lib/model/record-facts.js +434 -0
- package/src/lib/model/record-facts.js.map +1 -0
- package/src/lib/model/task-api.d.ts +28 -0
- package/src/lib/model/task-api.js +57 -0
- package/src/lib/model/task-api.js.map +1 -0
- package/src/lib/model/task-calendar.d.ts +108 -0
- package/src/lib/model/task-calendar.js +139 -0
- package/src/lib/model/task-calendar.js.map +1 -0
- package/src/lib/model/task-routes.d.ts +177 -0
- package/src/lib/model/task-routes.js +197 -0
- package/src/lib/model/task-routes.js.map +1 -0
- package/src/lib/model/task-scope.d.ts +45 -0
- package/src/lib/model/task-scope.js +51 -0
- package/src/lib/model/task-scope.js.map +1 -0
- package/src/lib/model/task-views.d.ts +149 -0
- package/src/lib/model/task-views.js +286 -0
- package/src/lib/model/task-views.js.map +1 -0
- package/src/lib/model/tasks-csv.d.ts +28 -0
- package/src/lib/model/tasks-csv.js +28 -0
- package/src/lib/model/tasks-csv.js.map +1 -0
- package/src/lib/plugin.d.ts +32 -0
- package/src/lib/plugin.js +208 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/companies-import.d.ts +57 -0
- package/src/lib/server/companies-import.js +242 -0
- package/src/lib/server/companies-import.js.map +1 -0
- package/src/lib/server/company-delete.d.ts +59 -0
- package/src/lib/server/company-delete.js +146 -0
- package/src/lib/server/company-delete.js.map +1 -0
- package/src/lib/server/contact-email-history.d.ts +82 -0
- package/src/lib/server/contact-email-history.js +264 -0
- package/src/lib/server/contact-email-history.js.map +1 -0
- package/src/lib/server/contact-profile.d.ts +41 -0
- package/src/lib/server/contact-profile.js +45 -0
- package/src/lib/server/contact-profile.js.map +1 -0
- package/src/lib/server/contact-update.d.ts +92 -0
- package/src/lib/server/contact-update.js +512 -0
- package/src/lib/server/contact-update.js.map +1 -0
- package/src/lib/server/contacts-import.d.ts +73 -0
- package/src/lib/server/contacts-import.js +263 -0
- package/src/lib/server/contacts-import.js.map +1 -0
- package/src/lib/server/contacts-merge.d.ts +86 -0
- package/src/lib/server/contacts-merge.js +206 -0
- package/src/lib/server/contacts-merge.js.map +1 -0
- package/src/lib/server/deals-import.d.ts +76 -0
- package/src/lib/server/deals-import.js +246 -0
- package/src/lib/server/deals-import.js.map +1 -0
- package/src/lib/server/email-send.d.ts +119 -0
- package/src/lib/server/email-send.js +667 -0
- package/src/lib/server/email-send.js.map +1 -0
- package/src/lib/server/email-template-duplicate.d.ts +19 -0
- package/src/lib/server/email-template-duplicate.js +206 -0
- package/src/lib/server/email-template-duplicate.js.map +1 -0
- package/src/lib/server/erase-person.d.ts +108 -0
- package/src/lib/server/erase-person.js +309 -0
- package/src/lib/server/erase-person.js.map +1 -0
- package/src/lib/server/import-context.d.ts +85 -0
- package/src/lib/server/import-context.js +181 -0
- package/src/lib/server/import-context.js.map +1 -0
- package/src/lib/server/inbound-address.d.ts +61 -0
- package/src/lib/server/inbound-address.js +188 -0
- package/src/lib/server/inbound-address.js.map +1 -0
- package/src/lib/server/lead-convert.d.ts +78 -0
- package/src/lib/server/lead-convert.js +250 -0
- package/src/lib/server/lead-convert.js.map +1 -0
- package/src/lib/server/leads-import.d.ts +84 -0
- package/src/lib/server/leads-import.js +261 -0
- package/src/lib/server/leads-import.js.map +1 -0
- package/src/lib/server/next-activity-routes.d.ts +33 -0
- package/src/lib/server/next-activity-routes.js +110 -0
- package/src/lib/server/next-activity-routes.js.map +1 -0
- package/src/lib/server/org-activity.d.ts +61 -0
- package/src/lib/server/org-activity.js +120 -0
- package/src/lib/server/org-activity.js.map +1 -0
- package/src/lib/server/org-caller.d.ts +110 -0
- package/src/lib/server/org-caller.js +154 -0
- package/src/lib/server/org-caller.js.map +1 -0
- package/src/lib/server/recipe-routes.d.ts +75 -0
- package/src/lib/server/recipe-routes.js +442 -0
- package/src/lib/server/recipe-routes.js.map +1 -0
- package/src/lib/server/record-facts.d.ts +65 -0
- package/src/lib/server/record-facts.js +337 -0
- package/src/lib/server/record-facts.js.map +1 -0
- package/src/lib/server/record-timeline.d.ts +51 -0
- package/src/lib/server/record-timeline.js +215 -0
- package/src/lib/server/record-timeline.js.map +1 -0
- package/src/lib/server/suite-gate.d.ts +42 -0
- package/src/lib/server/suite-gate.js +87 -0
- package/src/lib/server/suite-gate.js.map +1 -0
- package/src/lib/server/task-routes.d.ts +132 -0
- package/src/lib/server/task-routes.js +699 -0
- package/src/lib/server/task-routes.js.map +1 -0
- package/src/lib/server/tasks-import.d.ts +54 -0
- package/src/lib/server/tasks-import.js +157 -0
- package/src/lib/server/tasks-import.js.map +1 -0
- package/src/lib/server-deal-stage.d.ts +88 -0
- package/src/lib/server-deal-stage.js +316 -0
- package/src/lib/server-deal-stage.js.map +1 -0
- package/src/lib/server.d.ts +149 -0
- package/src/lib/server.js +615 -0
- package/src/lib/server.js.map +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* `POST /api/crm/company-delete` — unlink the contacts that name a company,
|
|
19
|
+
* then delete it (AGL-2804).
|
|
20
|
+
*
|
|
21
|
+
* Body: `{ hostId, companyId }` under a site, or `{ orgId, companyId }` at
|
|
22
|
+
* the organization level. Answers `CompanyDeleteResponse`.
|
|
23
|
+
*
|
|
24
|
+
* ## Why a route
|
|
25
|
+
*
|
|
26
|
+
* Firestore does not cascade. A company deleted on its own leaves every
|
|
27
|
+
* contact at it naming a record that no longer exists: their page links to
|
|
28
|
+
* nothing and the `companyIds` mirror the company list queries keeps
|
|
29
|
+
* matching a ghost. So a delete is a detach pass and then a delete, and the
|
|
30
|
+
* detach clears each holder's facet that named the company — which the rules
|
|
31
|
+
* refuse a client, because they cannot tell a company link inside a facet
|
|
32
|
+
* from an owner or a stage beside it.
|
|
33
|
+
*
|
|
34
|
+
* ## Bounded, and honest past the bound
|
|
35
|
+
*
|
|
36
|
+
* One pass unlinks at most `COMPANY_DETACH_LIMIT` contacts, a batch's worth.
|
|
37
|
+
* A company past that is left standing with a pass's worth unlinked, and the
|
|
38
|
+
* answer says more remain, so the next delete continues where this one
|
|
39
|
+
* stopped. A company is never deleted with a link still on a contact.
|
|
40
|
+
*
|
|
41
|
+
* ## Who may call it
|
|
42
|
+
*
|
|
43
|
+
* A CRM writer (`authorizeCrmWriter`) whose reach is the whole organization.
|
|
44
|
+
* The contacts are found by the mirror, and that query cannot carry a scope
|
|
45
|
+
* clause beside its `array-contains`: a member scoped to particular sites
|
|
46
|
+
* could not read them in the browser either, and the route does not unlink
|
|
47
|
+
* people on behalf of someone who may not see them.
|
|
48
|
+
*
|
|
49
|
+
* ## The plan
|
|
50
|
+
*
|
|
51
|
+
* Companies are the CRM's, and the CRM is included from Starter (AGL-2851):
|
|
52
|
+
* a workspace whose plan does not carry it is refused once the writer is
|
|
53
|
+
* known, staff included, before anything is read or unlinked
|
|
54
|
+
* (`suite-gate.ts`).
|
|
55
|
+
*/
|
|
56
|
+
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
57
|
+
/** What a member scoped to particular sites is told. */
|
|
58
|
+
export declare const COMPANY_DELETE_SCOPED_REFUSAL: string;
|
|
59
|
+
export declare const crmCompanyDeleteHandler: PluginApiHandler;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* `POST /api/crm/company-delete` — unlink the contacts that name a company,
|
|
18
|
+
* then delete it (AGL-2804).
|
|
19
|
+
*
|
|
20
|
+
* Body: `{ hostId, companyId }` under a site, or `{ orgId, companyId }` at
|
|
21
|
+
* the organization level. Answers `CompanyDeleteResponse`.
|
|
22
|
+
*
|
|
23
|
+
* ## Why a route
|
|
24
|
+
*
|
|
25
|
+
* Firestore does not cascade. A company deleted on its own leaves every
|
|
26
|
+
* contact at it naming a record that no longer exists: their page links to
|
|
27
|
+
* nothing and the `companyIds` mirror the company list queries keeps
|
|
28
|
+
* matching a ghost. So a delete is a detach pass and then a delete, and the
|
|
29
|
+
* detach clears each holder's facet that named the company — which the rules
|
|
30
|
+
* refuse a client, because they cannot tell a company link inside a facet
|
|
31
|
+
* from an owner or a stage beside it.
|
|
32
|
+
*
|
|
33
|
+
* ## Bounded, and honest past the bound
|
|
34
|
+
*
|
|
35
|
+
* One pass unlinks at most `COMPANY_DETACH_LIMIT` contacts, a batch's worth.
|
|
36
|
+
* A company past that is left standing with a pass's worth unlinked, and the
|
|
37
|
+
* answer says more remain, so the next delete continues where this one
|
|
38
|
+
* stopped. A company is never deleted with a link still on a contact.
|
|
39
|
+
*
|
|
40
|
+
* ## Who may call it
|
|
41
|
+
*
|
|
42
|
+
* A CRM writer (`authorizeCrmWriter`) whose reach is the whole organization.
|
|
43
|
+
* The contacts are found by the mirror, and that query cannot carry a scope
|
|
44
|
+
* clause beside its `array-contains`: a member scoped to particular sites
|
|
45
|
+
* could not read them in the browser either, and the route does not unlink
|
|
46
|
+
* people on behalf of someone who may not see them.
|
|
47
|
+
*
|
|
48
|
+
* ## The plan
|
|
49
|
+
*
|
|
50
|
+
* Companies are the CRM's, and the CRM is included from Starter (AGL-2851):
|
|
51
|
+
* a workspace whose plan does not carry it is refused once the writer is
|
|
52
|
+
* known, staff included, before anything is read or unlinked
|
|
53
|
+
* (`suite-gate.ts`).
|
|
54
|
+
*/ import { CONTACT_COMPANY_IDS_FIELD, contactFacetPath, contactGroupsNamingCompany, CRM_COLLECTIONS, isOrgWideMember } from "@aglyn/aglyn/server";
|
|
55
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
56
|
+
import { FieldValue } from "firebase-admin/firestore";
|
|
57
|
+
import { COMPANY_DETACH_LIMIT } from "../model/company-delete-route.js";
|
|
58
|
+
import { typed } from "./contact-profile.js";
|
|
59
|
+
import { readCrmRouteScope } from "./org-caller.js";
|
|
60
|
+
import { authorizeCrmWriter, canReach } from "./task-routes.js";
|
|
61
|
+
/** What a member scoped to particular sites is told. */ export const COMPANY_DELETE_SCOPED_REFUSAL = 'Your access is limited to specific sites, so the contacts at this company ' + 'could not be read to unlink them. Ask an organization administrator to delete it.';
|
|
62
|
+
/**
|
|
63
|
+
* A contact's update when a company it names is deleted: the id out of the
|
|
64
|
+
* mirror, and out of every holder's facet that named it. Another holder's
|
|
65
|
+
* link to a different company is not read or written.
|
|
66
|
+
*/ function companyDetachFields(contact, companyId) {
|
|
67
|
+
const update = {
|
|
68
|
+
[CONTACT_COMPANY_IDS_FIELD]: FieldValue.arrayRemove(companyId),
|
|
69
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
70
|
+
};
|
|
71
|
+
for (const groupId of contactGroupsNamingCompany(contact, companyId)){
|
|
72
|
+
update[contactFacetPath(groupId, 'companyId')] = FieldValue.delete();
|
|
73
|
+
}
|
|
74
|
+
return update;
|
|
75
|
+
}
|
|
76
|
+
export const crmCompanyDeleteHandler = async (req, res)=>{
|
|
77
|
+
var _req_body;
|
|
78
|
+
if (req.method !== 'POST') {
|
|
79
|
+
res.setHeader('Allow', 'POST');
|
|
80
|
+
res.status(405).json({
|
|
81
|
+
error: 'Method not allowed'
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const body = (_req_body = req.body) != null ? _req_body : {};
|
|
86
|
+
const scope = readCrmRouteScope(body);
|
|
87
|
+
const companyId = typed(body['companyId'], 200);
|
|
88
|
+
if (!scope || !companyId || companyId.includes('/')) {
|
|
89
|
+
res.status(400).json({
|
|
90
|
+
error: 'Missing hostId or companyId'
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const writer = await authorizeCrmWriter(req, scope, {
|
|
96
|
+
suiteAct: 'Deleting a company'
|
|
97
|
+
});
|
|
98
|
+
if (writer.ok === false) {
|
|
99
|
+
res.status(writer.status).json(writer.body);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!(writer.staff || writer.level === 'org' || isOrgWideMember(writer.member))) {
|
|
103
|
+
res.status(403).json({
|
|
104
|
+
error: COMPANY_DELETE_SCOPED_REFUSAL
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
109
|
+
const orgRef = firestore.collection('orgs').doc(writer.orgId);
|
|
110
|
+
const companyRef = orgRef.collection(CRM_COLLECTIONS.companies).doc(companyId);
|
|
111
|
+
const company = await companyRef.get();
|
|
112
|
+
if (!company.exists || !canReach(writer, company.get('visibleTo'))) {
|
|
113
|
+
res.status(404).json({
|
|
114
|
+
error: 'That company no longer exists.'
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// One past the bound, so "more remain" is a fact from the probe row
|
|
119
|
+
// rather than a guess from a full page.
|
|
120
|
+
const probe = await orgRef.collection('contacts').where(CONTACT_COMPANY_IDS_FIELD, 'array-contains', companyId).limit(COMPANY_DETACH_LIMIT + 1).get();
|
|
121
|
+
const linked = probe.docs.slice(0, COMPANY_DETACH_LIMIT);
|
|
122
|
+
const moreRemain = probe.docs.length > COMPANY_DETACH_LIMIT;
|
|
123
|
+
if (linked.length) {
|
|
124
|
+
const batch = firestore.batch();
|
|
125
|
+
for (const snapshot of linked){
|
|
126
|
+
batch.update(snapshot.ref, companyDetachFields(snapshot.data(), companyId));
|
|
127
|
+
}
|
|
128
|
+
await batch.commit();
|
|
129
|
+
}
|
|
130
|
+
if (!moreRemain) await companyRef.delete();
|
|
131
|
+
const answer = {
|
|
132
|
+
ok: true,
|
|
133
|
+
deleted: !moreRemain,
|
|
134
|
+
detached: linked.length,
|
|
135
|
+
moreRemain
|
|
136
|
+
};
|
|
137
|
+
res.status(200).json(answer);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('[crm] company-delete failed', companyId, error);
|
|
140
|
+
res.status(500).json({
|
|
141
|
+
error: 'The company could not be deleted.'
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
//# sourceMappingURL=company-delete.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/company-delete.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `POST /api/crm/company-delete` — unlink the contacts that name a company,\n * then delete it (AGL-2804).\n *\n * Body: `{ hostId, companyId }` under a site, or `{ orgId, companyId }` at\n * the organization level. Answers `CompanyDeleteResponse`.\n *\n * ## Why a route\n *\n * Firestore does not cascade. A company deleted on its own leaves every\n * contact at it naming a record that no longer exists: their page links to\n * nothing and the `companyIds` mirror the company list queries keeps\n * matching a ghost. So a delete is a detach pass and then a delete, and the\n * detach clears each holder's facet that named the company — which the rules\n * refuse a client, because they cannot tell a company link inside a facet\n * from an owner or a stage beside it.\n *\n * ## Bounded, and honest past the bound\n *\n * One pass unlinks at most `COMPANY_DETACH_LIMIT` contacts, a batch's worth.\n * A company past that is left standing with a pass's worth unlinked, and the\n * answer says more remain, so the next delete continues where this one\n * stopped. A company is never deleted with a link still on a contact.\n *\n * ## Who may call it\n *\n * A CRM writer (`authorizeCrmWriter`) whose reach is the whole organization.\n * The contacts are found by the mirror, and that query cannot carry a scope\n * clause beside its `array-contains`: a member scoped to particular sites\n * could not read them in the browser either, and the route does not unlink\n * people on behalf of someone who may not see them.\n *\n * ## The plan\n *\n * Companies are the CRM's, and the CRM is included from Starter (AGL-2851):\n * a workspace whose plan does not carry it is refused once the writer is\n * known, staff included, before anything is read or unlinked\n * (`suite-gate.ts`).\n */\n\nimport {\n CONTACT_COMPANY_IDS_FIELD,\n contactFacetPath,\n contactGroupsNamingCompany,\n CRM_COLLECTIONS,\n isOrgWideMember,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n COMPANY_DETACH_LIMIT,\n type CompanyDeleteResponse,\n} from '../model/company-delete-route'\nimport { typed } from './contact-profile'\nimport { readCrmRouteScope } from './org-caller'\nimport { authorizeCrmWriter, canReach } from './task-routes'\n\n/** What a member scoped to particular sites is told. */\nexport const COMPANY_DELETE_SCOPED_REFUSAL =\n 'Your access is limited to specific sites, so the contacts at this company ' +\n 'could not be read to unlink them. Ask an organization administrator to delete it.'\n\n/**\n * A contact's update when a company it names is deleted: the id out of the\n * mirror, and out of every holder's facet that named it. Another holder's\n * link to a different company is not read or written.\n */\nfunction companyDetachFields(\n contact: Record<string, unknown>,\n companyId: string,\n): Record<string, unknown> {\n const update: Record<string, unknown> = {\n [CONTACT_COMPANY_IDS_FIELD]: FieldValue.arrayRemove(companyId),\n updatedAt: FieldValue.serverTimestamp(),\n }\n for (const groupId of contactGroupsNamingCompany(contact, companyId)) {\n update[contactFacetPath(groupId, 'companyId')] = FieldValue.delete()\n }\n return update\n}\n\nexport const crmCompanyDeleteHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n const companyId = typed(body['companyId'], 200)\n if (!scope || !companyId || companyId.includes('/')) {\n res.status(400).json({ error: 'Missing hostId or companyId' })\n return\n }\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: 'Deleting a company' })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n if (!(writer.staff || writer.level === 'org' || isOrgWideMember(writer.member))) {\n res.status(403).json({ error: COMPANY_DELETE_SCOPED_REFUSAL })\n return\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const companyRef = orgRef.collection(CRM_COLLECTIONS.companies).doc(companyId)\n const company = await companyRef.get()\n if (!company.exists || !canReach(writer, company.get('visibleTo'))) {\n res.status(404).json({ error: 'That company no longer exists.' })\n return\n }\n\n // One past the bound, so \"more remain\" is a fact from the probe row\n // rather than a guess from a full page.\n const probe = await orgRef\n .collection('contacts')\n .where(CONTACT_COMPANY_IDS_FIELD, 'array-contains', companyId)\n .limit(COMPANY_DETACH_LIMIT + 1)\n .get()\n const linked = probe.docs.slice(0, COMPANY_DETACH_LIMIT)\n const moreRemain = probe.docs.length > COMPANY_DETACH_LIMIT\n if (linked.length) {\n const batch = firestore.batch()\n for (const snapshot of linked) {\n batch.update(\n snapshot.ref,\n companyDetachFields(snapshot.data() as Record<string, unknown>, companyId),\n )\n }\n await batch.commit()\n }\n if (!moreRemain) await companyRef.delete()\n\n const answer: CompanyDeleteResponse = {\n ok: true,\n deleted: !moreRemain,\n detached: linked.length,\n moreRemain,\n }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] company-delete failed', companyId, error)\n res.status(500).json({ error: 'The company could not be deleted.' })\n }\n}\n"],"names":["CONTACT_COMPANY_IDS_FIELD","contactFacetPath","contactGroupsNamingCompany","CRM_COLLECTIONS","isOrgWideMember","firebaseAdmin","FieldValue","COMPANY_DETACH_LIMIT","typed","readCrmRouteScope","authorizeCrmWriter","canReach","COMPANY_DELETE_SCOPED_REFUSAL","companyDetachFields","contact","companyId","update","arrayRemove","updatedAt","serverTimestamp","groupId","delete","crmCompanyDeleteHandler","req","res","method","setHeader","status","json","error","body","scope","includes","writer","suiteAct","ok","staff","level","member","firestore","app","orgRef","collection","doc","orgId","companyRef","companies","company","get","exists","probe","where","limit","linked","docs","slice","moreRemain","length","batch","snapshot","ref","data","commit","answer","deleted","detached","console"],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* `POST /api/crm/contact-email-history` (AGL-2616): the campaign mail one
|
|
19
|
+
* contact was sent, as the timeline draws it.
|
|
20
|
+
*
|
|
21
|
+
* ## Why a server route
|
|
22
|
+
*
|
|
23
|
+
* The per-recipient delivery log — `emailDeliveries/{key}/messages` — is
|
|
24
|
+
* closed to every client by the rules, and rightly: it is keyed by an
|
|
25
|
+
* address hash across every org on the platform, and a row carries the
|
|
26
|
+
* recipient's address, the provider and the links they followed. What a
|
|
27
|
+
* contact's timeline needs is one slice of it — this person, this group's
|
|
28
|
+
* campaigns — projected to the fields a timeline shows. That projection is
|
|
29
|
+
* this route, and the authorization in front of it is the contacts read
|
|
30
|
+
* rule restated for the Admin SDK, which evaluates no rules of its own.
|
|
31
|
+
*
|
|
32
|
+
* ## What one open of a contact page costs
|
|
33
|
+
*
|
|
34
|
+
* Bounded by the PERSON, never by the campaign ledger: one keyed query down
|
|
35
|
+
* the person's own message subcollection, newest first and capped at
|
|
36
|
+
* {@link EMAIL_DELIVERY_READ_LIMIT}, then one `getAll` for the names of the
|
|
37
|
+
* distinct emails those rows name. Nothing here walks a campaign's
|
|
38
|
+
* recipients — that direction is the campaign report's, and is a
|
|
39
|
+
* collection-group query this route never issues.
|
|
40
|
+
*
|
|
41
|
+
* ## Only this group's campaigns
|
|
42
|
+
*
|
|
43
|
+
* The log holds every message ever sent to the address, from every site on
|
|
44
|
+
* the platform. The rows handed back are those a campaign on one of the
|
|
45
|
+
* reading group's sites sent — the `hostId` tag `campaign-send` stamps,
|
|
46
|
+
* filtered against the consent group's `hostIds` — so a sibling business
|
|
47
|
+
* sharing the same contact row never sees the mail another business sent
|
|
48
|
+
* the person, and transactional mail (a receipt, an invite, which carry no
|
|
49
|
+
* site) is not on a CRM timeline at all. The filter runs over the newest
|
|
50
|
+
* fifty messages, so a person mailed heavily by several orgs can have this
|
|
51
|
+
* group's older campaigns fall past the window; the response says what the
|
|
52
|
+
* window was.
|
|
53
|
+
*/
|
|
54
|
+
import { type ContactCampaignEmail, type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
55
|
+
import { type EmailDeliveryRecord } from '@aglyn/tenant-data-admin/server/email-delivery-log';
|
|
56
|
+
/** The route key, as `registerCrmConsoleApi` registers it. */
|
|
57
|
+
export declare const CONTACT_EMAIL_HISTORY_ROUTE = "crm/contact-email-history";
|
|
58
|
+
/** What the route answers with. */
|
|
59
|
+
export interface ContactEmailHistoryResponse {
|
|
60
|
+
ok: true;
|
|
61
|
+
/** Newest first, by the instant each was sent. */
|
|
62
|
+
emails: ContactCampaignEmail[];
|
|
63
|
+
/**
|
|
64
|
+
* True when the log could not be read, so an empty list means "unknown"
|
|
65
|
+
* rather than "nothing was sent" — the two lead a reader to opposite
|
|
66
|
+
* conclusions about the relationship.
|
|
67
|
+
*/
|
|
68
|
+
lookupFailed: boolean;
|
|
69
|
+
/** How many of the person's newest messages the campaign filter ran over. */
|
|
70
|
+
limit: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* One delivery-log row as the timeline reads it.
|
|
74
|
+
*
|
|
75
|
+
* Exported for the spec, and pure: it is the projection that decides what
|
|
76
|
+
* leaves the server — the address, the provider, the followed links and the
|
|
77
|
+
* bounce detail stay behind. `sentAtMs` prefers the provider's own `sent`
|
|
78
|
+
* instant and falls back to the first event seen, which the log guarantees
|
|
79
|
+
* on every row, so an entry can always be placed.
|
|
80
|
+
*/
|
|
81
|
+
export declare function contactCampaignEmailFromDelivery(row: EmailDeliveryRecord, campaignName: string | null): ContactCampaignEmail | null;
|
|
82
|
+
export declare const contactEmailHistoryHandler: PluginApiHandler;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* `POST /api/crm/contact-email-history` (AGL-2616): the campaign mail one
|
|
19
|
+
* contact was sent, as the timeline draws it.
|
|
20
|
+
*
|
|
21
|
+
* ## Why a server route
|
|
22
|
+
*
|
|
23
|
+
* The per-recipient delivery log — `emailDeliveries/{key}/messages` — is
|
|
24
|
+
* closed to every client by the rules, and rightly: it is keyed by an
|
|
25
|
+
* address hash across every org on the platform, and a row carries the
|
|
26
|
+
* recipient's address, the provider and the links they followed. What a
|
|
27
|
+
* contact's timeline needs is one slice of it — this person, this group's
|
|
28
|
+
* campaigns — projected to the fields a timeline shows. That projection is
|
|
29
|
+
* this route, and the authorization in front of it is the contacts read
|
|
30
|
+
* rule restated for the Admin SDK, which evaluates no rules of its own.
|
|
31
|
+
*
|
|
32
|
+
* ## What one open of a contact page costs
|
|
33
|
+
*
|
|
34
|
+
* Bounded by the PERSON, never by the campaign ledger: one keyed query down
|
|
35
|
+
* the person's own message subcollection, newest first and capped at
|
|
36
|
+
* {@link EMAIL_DELIVERY_READ_LIMIT}, then one `getAll` for the names of the
|
|
37
|
+
* distinct emails those rows name. Nothing here walks a campaign's
|
|
38
|
+
* recipients — that direction is the campaign report's, and is a
|
|
39
|
+
* collection-group query this route never issues.
|
|
40
|
+
*
|
|
41
|
+
* ## Only this group's campaigns
|
|
42
|
+
*
|
|
43
|
+
* The log holds every message ever sent to the address, from every site on
|
|
44
|
+
* the platform. The rows handed back are those a campaign on one of the
|
|
45
|
+
* reading group's sites sent — the `hostId` tag `campaign-send` stamps,
|
|
46
|
+
* filtered against the consent group's `hostIds` — so a sibling business
|
|
47
|
+
* sharing the same contact row never sees the mail another business sent
|
|
48
|
+
* the person, and transactional mail (a receipt, an invite, which carry no
|
|
49
|
+
* site) is not on a CRM timeline at all. The filter runs over the newest
|
|
50
|
+
* fifty messages, so a person mailed heavily by several orgs can have this
|
|
51
|
+
* group's older campaigns fall past the window; the response says what the
|
|
52
|
+
* window was.
|
|
53
|
+
*/ import { memberCanSee } from "@aglyn/aglyn/server";
|
|
54
|
+
import { consentGroupForSite, firebaseAdmin, getOrgForHost, memberHasOrgPermission, orgDataCollectionForHost, resolveOrgMembership } from "@aglyn/tenant-data-admin";
|
|
55
|
+
// The leaf, for the reason the webhook imports it from the leaf: a spec that
|
|
56
|
+
// mocks the admin barrel wholesale must not silently replace the reader.
|
|
57
|
+
import { EMAIL_DELIVERY_READ_LIMIT, readEmailDeliveryHistory } from "@aglyn/tenant-data-admin/server/email-delivery-log";
|
|
58
|
+
import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
59
|
+
import { crmSuiteRefusal } from "./suite-gate.js";
|
|
60
|
+
/** The route key, as `registerCrmConsoleApi` registers it. */ export const CONTACT_EMAIL_HISTORY_ROUTE = 'crm/contact-email-history';
|
|
61
|
+
const refuse = (status, error)=>({
|
|
62
|
+
ok: false,
|
|
63
|
+
status,
|
|
64
|
+
error
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* Who is asking, and whether they may read this site's CRM at all — the
|
|
68
|
+
* rules' `canReadScopedPeople` restated, minus the per-document half that
|
|
69
|
+
* the handler asks against the contact once it has it.
|
|
70
|
+
*
|
|
71
|
+
* `data.manage` is the key, because it is the key the contacts read rule
|
|
72
|
+
* gates on: an org viewer holding no grant may open no contact in the
|
|
73
|
+
* console and may not read one here. Staff pass on the claim alone, as the
|
|
74
|
+
* rules admit them.
|
|
75
|
+
*/ async function authorizeCrmReader(req, hostId) {
|
|
76
|
+
var _req_headers_authorization, _ref;
|
|
77
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
78
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
79
|
+
if (!idToken) return refuse(401, 'Unauthenticated');
|
|
80
|
+
let decoded;
|
|
81
|
+
try {
|
|
82
|
+
decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// A refused credential is the caller's 401; a failure to check one is
|
|
85
|
+
// ours and keeps a 5xx (AGL-2852).
|
|
86
|
+
if (isRefusedIdToken(error)) return refuse(401, 'Unauthenticated');
|
|
87
|
+
console.error('[crm] contact-email-history could not verify the reader', error);
|
|
88
|
+
return refuse(500, 'The sign-in could not be checked. Try again.');
|
|
89
|
+
}
|
|
90
|
+
const staff = decoded.staff === true;
|
|
91
|
+
const resolved = await getOrgForHost(hostId).catch(()=>null);
|
|
92
|
+
if (!resolved) {
|
|
93
|
+
return refuse(404, 'This site has no organization, so it has no CRM.');
|
|
94
|
+
}
|
|
95
|
+
const { orgId, org } = resolved;
|
|
96
|
+
const membership = await resolveOrgMembership(decoded.uid, orgId).catch(()=>null);
|
|
97
|
+
const member = (_ref = membership == null ? void 0 : membership.member) != null ? _ref : null;
|
|
98
|
+
if (!staff) {
|
|
99
|
+
const suspended = (member == null ? void 0 : member.orgSuspended) === true;
|
|
100
|
+
if (!member || suspended) {
|
|
101
|
+
return refuse(403, 'Your organization role does not allow reading the CRM.');
|
|
102
|
+
}
|
|
103
|
+
if (!await memberHasOrgPermission(orgId, member, 'data.manage')) {
|
|
104
|
+
return refuse(403, 'Your organization role does not allow reading the CRM.');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
uid: decoded.uid,
|
|
110
|
+
staff,
|
|
111
|
+
orgId,
|
|
112
|
+
org: org != null ? org : {},
|
|
113
|
+
member
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** A finite, positive instant, or nothing. */ const instant = (value)=>{
|
|
117
|
+
const parsed = Number(value);
|
|
118
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* One delivery-log row as the timeline reads it.
|
|
122
|
+
*
|
|
123
|
+
* Exported for the spec, and pure: it is the projection that decides what
|
|
124
|
+
* leaves the server — the address, the provider, the followed links and the
|
|
125
|
+
* bounce detail stay behind. `sentAtMs` prefers the provider's own `sent`
|
|
126
|
+
* instant and falls back to the first event seen, which the log guarantees
|
|
127
|
+
* on every row, so an entry can always be placed.
|
|
128
|
+
*/ export function contactCampaignEmailFromDelivery(row, campaignName) {
|
|
129
|
+
var _row_timestamps, _instant, _row_subject, _row_openCount, _row_clickCount;
|
|
130
|
+
if (!row.campaignId || !row.hostId) return null;
|
|
131
|
+
const stamps = (_row_timestamps = row.timestamps) != null ? _row_timestamps : {};
|
|
132
|
+
const sentAtMs = (_instant = instant(stamps.sent)) != null ? _instant : instant(row.firstSeenAtMs);
|
|
133
|
+
if (!sentAtMs) return null;
|
|
134
|
+
const delivered = instant(stamps.delivered);
|
|
135
|
+
const opened = instant(stamps.opened);
|
|
136
|
+
const clicked = instant(stamps.clicked);
|
|
137
|
+
const bounced = instant(stamps.bounced);
|
|
138
|
+
const complained = instant(stamps.complained);
|
|
139
|
+
return _extends({
|
|
140
|
+
messageId: row.messageId,
|
|
141
|
+
hostId: row.hostId,
|
|
142
|
+
campaignId: row.campaignId,
|
|
143
|
+
campaignName,
|
|
144
|
+
subject: (_row_subject = row.subject) != null ? _row_subject : null,
|
|
145
|
+
sentAtMs
|
|
146
|
+
}, delivered ? {
|
|
147
|
+
deliveredAtMs: delivered
|
|
148
|
+
} : {}, opened ? {
|
|
149
|
+
openedAtMs: opened
|
|
150
|
+
} : {}, clicked ? {
|
|
151
|
+
clickedAtMs: clicked
|
|
152
|
+
} : {}, bounced ? {
|
|
153
|
+
bouncedAtMs: bounced
|
|
154
|
+
} : {}, complained ? {
|
|
155
|
+
complainedAtMs: complained
|
|
156
|
+
} : {}, {
|
|
157
|
+
openCount: Number((_row_openCount = row.openCount) != null ? _row_openCount : 0),
|
|
158
|
+
clickCount: Number((_row_clickCount = row.clickCount) != null ? _row_clickCount : 0)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The names of the emails these rows name, in one `getAll`.
|
|
163
|
+
*
|
|
164
|
+
* Keyed reads rather than a query: the ids are known, a `getAll` is one round
|
|
165
|
+
* trip however many there are, and it needs no index. An email the team has
|
|
166
|
+
* since deleted reads as `null`, which the timeline draws as the subject the
|
|
167
|
+
* person received with no report to link to.
|
|
168
|
+
*/ async function readCampaignNames(rows) {
|
|
169
|
+
const names = new Map();
|
|
170
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
171
|
+
const refs = [];
|
|
172
|
+
for (const row of rows){
|
|
173
|
+
const key = `${row.hostId}/${row.campaignId}`;
|
|
174
|
+
if (names.has(key)) continue;
|
|
175
|
+
names.set(key, null);
|
|
176
|
+
refs.push(firestore.collection('hosts').doc(String(row.hostId)).collection('campaigns').doc(String(row.campaignId)));
|
|
177
|
+
}
|
|
178
|
+
if (!refs.length) return names;
|
|
179
|
+
const snapshots = await firestore.getAll(...refs);
|
|
180
|
+
for (const snapshot of snapshots){
|
|
181
|
+
var _ref, _snapshot_get, _snapshot_get1;
|
|
182
|
+
var _snapshot_ref_parent_parent;
|
|
183
|
+
if (!snapshot.exists) continue;
|
|
184
|
+
const hostId = (_ref = (_snapshot_ref_parent_parent = snapshot.ref.parent.parent) == null ? void 0 : _snapshot_ref_parent_parent.id) != null ? _ref : '';
|
|
185
|
+
const name = String((_snapshot_get = snapshot.get('displayName')) != null ? _snapshot_get : '').trim() || String((_snapshot_get1 = snapshot.get('subject')) != null ? _snapshot_get1 : '').trim();
|
|
186
|
+
names.set(`${hostId}/${snapshot.id}`, name || null);
|
|
187
|
+
}
|
|
188
|
+
return names;
|
|
189
|
+
}
|
|
190
|
+
export const contactEmailHistoryHandler = async (req, res)=>{
|
|
191
|
+
var _ref, _ref1;
|
|
192
|
+
var _req_body, _req_body1;
|
|
193
|
+
if (req.method !== 'POST') {
|
|
194
|
+
res.setHeader('Allow', 'POST');
|
|
195
|
+
res.status(405).json({
|
|
196
|
+
error: 'Method not allowed'
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const hostId = String((_ref = (_req_body = req.body) == null ? void 0 : _req_body.hostId) != null ? _ref : '').trim();
|
|
201
|
+
const contactId = String((_ref1 = (_req_body1 = req.body) == null ? void 0 : _req_body1.contactId) != null ? _ref1 : '').trim();
|
|
202
|
+
if (!hostId || !contactId) {
|
|
203
|
+
res.status(400).json({
|
|
204
|
+
error: 'Missing hostId or contactId'
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const reader = await authorizeCrmReader(req, hostId);
|
|
209
|
+
if (reader.ok === false) {
|
|
210
|
+
res.status(reader.status).json({
|
|
211
|
+
error: reader.error
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// The plan after the reader, before any contact or log is read (AGL-2851).
|
|
216
|
+
const suite = crmSuiteRefusal(reader.org, "A contact's campaign email history");
|
|
217
|
+
if (suite) {
|
|
218
|
+
res.status(suite.status).json(suite.body);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
var _snapshot_get;
|
|
223
|
+
/*
|
|
224
|
+
* The contact by id, then the per-document half of the read rule: the
|
|
225
|
+
* Admin SDK evaluates no rules, so a scoped member who can guess an id
|
|
226
|
+
* must be refused here exactly as their listener would refuse the row.
|
|
227
|
+
* "Not visible" and "not there" are one answer on purpose — a 403 would
|
|
228
|
+
* confirm the id exists.
|
|
229
|
+
*/ const contactsRef = await orgDataCollectionForHost(hostId, 'contacts');
|
|
230
|
+
const snapshot = await contactsRef.doc(contactId).get();
|
|
231
|
+
const visibleTo = snapshot.exists ? snapshot.get('visibleTo') : undefined;
|
|
232
|
+
if (!snapshot.exists || !(reader.staff || memberCanSee(reader.member, visibleTo))) {
|
|
233
|
+
res.status(404).json({
|
|
234
|
+
error: 'Unknown contact'
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const email = String((_snapshot_get = snapshot.get('email')) != null ? _snapshot_get : '');
|
|
239
|
+
const group = await consentGroupForSite(hostId, reader.org);
|
|
240
|
+
const history = await readEmailDeliveryHistory(email, {
|
|
241
|
+
limit: EMAIL_DELIVERY_READ_LIMIT
|
|
242
|
+
});
|
|
243
|
+
const rows = history.rows.filter((row)=>Boolean(row.campaignId) && Boolean(row.hostId) && group.hostIds.includes(String(row.hostId)));
|
|
244
|
+
const names = await readCampaignNames(rows);
|
|
245
|
+
const emails = rows.map((row)=>{
|
|
246
|
+
var _names_get;
|
|
247
|
+
return contactCampaignEmailFromDelivery(row, (_names_get = names.get(`${row.hostId}/${row.campaignId}`)) != null ? _names_get : null);
|
|
248
|
+
}).filter((entry)=>entry !== null).sort((a, b)=>b.sentAtMs - a.sentAtMs);
|
|
249
|
+
const body = {
|
|
250
|
+
ok: true,
|
|
251
|
+
emails,
|
|
252
|
+
lookupFailed: history.lookupFailed,
|
|
253
|
+
limit: EMAIL_DELIVERY_READ_LIMIT
|
|
254
|
+
};
|
|
255
|
+
res.status(200).json(body);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('[crm] contact-email-history failed', hostId, contactId, error);
|
|
258
|
+
res.status(500).json({
|
|
259
|
+
error: 'The campaign history could not be read'
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
//# sourceMappingURL=contact-email-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/contact-email-history.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `POST /api/crm/contact-email-history` (AGL-2616): the campaign mail one\n * contact was sent, as the timeline draws it.\n *\n * ## Why a server route\n *\n * The per-recipient delivery log — `emailDeliveries/{key}/messages` — is\n * closed to every client by the rules, and rightly: it is keyed by an\n * address hash across every org on the platform, and a row carries the\n * recipient's address, the provider and the links they followed. What a\n * contact's timeline needs is one slice of it — this person, this group's\n * campaigns — projected to the fields a timeline shows. That projection is\n * this route, and the authorization in front of it is the contacts read\n * rule restated for the Admin SDK, which evaluates no rules of its own.\n *\n * ## What one open of a contact page costs\n *\n * Bounded by the PERSON, never by the campaign ledger: one keyed query down\n * the person's own message subcollection, newest first and capped at\n * {@link EMAIL_DELIVERY_READ_LIMIT}, then one `getAll` for the names of the\n * distinct emails those rows name. Nothing here walks a campaign's\n * recipients — that direction is the campaign report's, and is a\n * collection-group query this route never issues.\n *\n * ## Only this group's campaigns\n *\n * The log holds every message ever sent to the address, from every site on\n * the platform. The rows handed back are those a campaign on one of the\n * reading group's sites sent — the `hostId` tag `campaign-send` stamps,\n * filtered against the consent group's `hostIds` — so a sibling business\n * sharing the same contact row never sees the mail another business sent\n * the person, and transactional mail (a receipt, an invite, which carry no\n * site) is not on a CRM timeline at all. The filter runs over the newest\n * fifty messages, so a person mailed heavily by several orgs can have this\n * group's older campaigns fall past the window; the response says what the\n * window was.\n */\n\nimport {\n type AglynOrgMember,\n type ContactCampaignEmail,\n memberCanSee,\n type PluginApiHandler,\n type PluginApiRequest,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n firebaseAdmin,\n getOrgForHost,\n memberHasOrgPermission,\n orgDataCollectionForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\n// The leaf, for the reason the webhook imports it from the leaf: a spec that\n// mocks the admin barrel wholesale must not silently replace the reader.\nimport {\n EMAIL_DELIVERY_READ_LIMIT,\n type EmailDeliveryRecord,\n readEmailDeliveryHistory,\n} from '@aglyn/tenant-data-admin/server/email-delivery-log'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { crmSuiteRefusal } from './suite-gate'\n\n/** The route key, as `registerCrmConsoleApi` registers it. */\nexport const CONTACT_EMAIL_HISTORY_ROUTE = 'crm/contact-email-history'\n\n/** What the route answers with. */\nexport interface ContactEmailHistoryResponse {\n ok: true\n /** Newest first, by the instant each was sent. */\n emails: ContactCampaignEmail[]\n /**\n * True when the log could not be read, so an empty list means \"unknown\"\n * rather than \"nothing was sent\" — the two lead a reader to opposite\n * conclusions about the relationship.\n */\n lookupFailed: boolean\n /** How many of the person's newest messages the campaign filter ran over. */\n limit: number\n}\n\ntype Refusal = { ok: false; status: number; error: string }\n\ninterface Reader {\n ok: true\n uid: string\n staff: boolean\n orgId: string\n org: Record<string, unknown>\n member: Partial<AglynOrgMember> | null\n}\n\nconst refuse = (status: number, error: string): Refusal => ({\n ok: false,\n status,\n error,\n})\n\n/**\n * Who is asking, and whether they may read this site's CRM at all — the\n * rules' `canReadScopedPeople` restated, minus the per-document half that\n * the handler asks against the contact once it has it.\n *\n * `data.manage` is the key, because it is the key the contacts read rule\n * gates on: an org viewer holding no grant may open no contact in the\n * console and may not read one here. Staff pass on the claim alone, as the\n * rules admit them.\n */\nasync function authorizeCrmReader(\n req: PluginApiRequest,\n hostId: string,\n): Promise<Reader | Refusal> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return refuse(401, 'Unauthenticated')\n let decoded: { uid: string; staff?: unknown }\n try {\n decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n } catch (error) {\n // A refused credential is the caller's 401; a failure to check one is\n // ours and keeps a 5xx (AGL-2852).\n if (isRefusedIdToken(error)) return refuse(401, 'Unauthenticated')\n console.error('[crm] contact-email-history could not verify the reader', error)\n return refuse(500, 'The sign-in could not be checked. Try again.')\n }\n const staff = decoded.staff === true\n const resolved = await getOrgForHost(hostId).catch(() => null)\n if (!resolved) {\n return refuse(404, 'This site has no organization, so it has no CRM.')\n }\n const { orgId, org } = resolved\n const membership = await resolveOrgMembership(decoded.uid, orgId).catch(\n () => null,\n )\n const member = (membership?.member ?? null) as Partial<AglynOrgMember> | null\n if (!staff) {\n const suspended =\n (member as { orgSuspended?: boolean } | null)?.orgSuspended === true\n if (!member || suspended) {\n return refuse(403, 'Your organization role does not allow reading the CRM.')\n }\n if (!(await memberHasOrgPermission(orgId, member, 'data.manage'))) {\n return refuse(403, 'Your organization role does not allow reading the CRM.')\n }\n }\n return {\n ok: true,\n uid: decoded.uid,\n staff,\n orgId,\n org: (org ?? {}) as Record<string, unknown>,\n member,\n }\n}\n\n/** A finite, positive instant, or nothing. */\nconst instant = (value: unknown): number | undefined => {\n const parsed = Number(value)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined\n}\n\n/**\n * One delivery-log row as the timeline reads it.\n *\n * Exported for the spec, and pure: it is the projection that decides what\n * leaves the server — the address, the provider, the followed links and the\n * bounce detail stay behind. `sentAtMs` prefers the provider's own `sent`\n * instant and falls back to the first event seen, which the log guarantees\n * on every row, so an entry can always be placed.\n */\nexport function contactCampaignEmailFromDelivery(\n row: EmailDeliveryRecord,\n campaignName: string | null,\n): ContactCampaignEmail | null {\n if (!row.campaignId || !row.hostId) return null\n const stamps = row.timestamps ?? {}\n const sentAtMs = instant(stamps.sent) ?? instant(row.firstSeenAtMs)\n if (!sentAtMs) return null\n const delivered = instant(stamps.delivered)\n const opened = instant(stamps.opened)\n const clicked = instant(stamps.clicked)\n const bounced = instant(stamps.bounced)\n const complained = instant(stamps.complained)\n return {\n messageId: row.messageId,\n hostId: row.hostId,\n campaignId: row.campaignId,\n campaignName,\n subject: row.subject ?? null,\n sentAtMs,\n ...(delivered ? { deliveredAtMs: delivered } : {}),\n ...(opened ? { openedAtMs: opened } : {}),\n ...(clicked ? { clickedAtMs: clicked } : {}),\n ...(bounced ? { bouncedAtMs: bounced } : {}),\n ...(complained ? { complainedAtMs: complained } : {}),\n openCount: Number(row.openCount ?? 0),\n clickCount: Number(row.clickCount ?? 0),\n }\n}\n\n/**\n * The names of the emails these rows name, in one `getAll`.\n *\n * Keyed reads rather than a query: the ids are known, a `getAll` is one round\n * trip however many there are, and it needs no index. An email the team has\n * since deleted reads as `null`, which the timeline draws as the subject the\n * person received with no report to link to.\n */\nasync function readCampaignNames(\n rows: readonly EmailDeliveryRecord[],\n): Promise<Map<string, string | null>> {\n const names = new Map<string, string | null>()\n const firestore = firebaseAdmin.app().firestore()\n const refs: FirebaseFirestore.DocumentReference[] = []\n for (const row of rows) {\n const key = `${row.hostId}/${row.campaignId}`\n if (names.has(key)) continue\n names.set(key, null)\n refs.push(\n firestore\n .collection('hosts')\n .doc(String(row.hostId))\n .collection('campaigns')\n .doc(String(row.campaignId)),\n )\n }\n if (!refs.length) return names\n const snapshots = await firestore.getAll(...refs)\n for (const snapshot of snapshots) {\n if (!snapshot.exists) continue\n const hostId = snapshot.ref.parent.parent?.id ?? ''\n const name =\n String(snapshot.get('displayName') ?? '').trim() ||\n String(snapshot.get('subject') ?? '').trim()\n names.set(`${hostId}/${snapshot.id}`, name || null)\n }\n return names\n}\n\nexport const contactEmailHistoryHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const hostId = String(req.body?.hostId ?? '').trim()\n const contactId = String(req.body?.contactId ?? '').trim()\n if (!hostId || !contactId) {\n res.status(400).json({ error: 'Missing hostId or contactId' })\n return\n }\n const reader = await authorizeCrmReader(req, hostId)\n if (reader.ok === false) {\n res.status(reader.status).json({ error: reader.error })\n return\n }\n // The plan after the reader, before any contact or log is read (AGL-2851).\n const suite = crmSuiteRefusal(reader.org, \"A contact's campaign email history\")\n if (suite) {\n res.status(suite.status).json(suite.body)\n return\n }\n try {\n /*\n * The contact by id, then the per-document half of the read rule: the\n * Admin SDK evaluates no rules, so a scoped member who can guess an id\n * must be refused here exactly as their listener would refuse the row.\n * \"Not visible\" and \"not there\" are one answer on purpose — a 403 would\n * confirm the id exists.\n */\n const contactsRef = await orgDataCollectionForHost(hostId, 'contacts')\n const snapshot = await contactsRef.doc(contactId).get()\n const visibleTo = snapshot.exists\n ? (snapshot.get('visibleTo') as string[] | undefined)\n : undefined\n if (!snapshot.exists || !(reader.staff || memberCanSee(reader.member, visibleTo))) {\n res.status(404).json({ error: 'Unknown contact' })\n return\n }\n const email = String(snapshot.get('email') ?? '')\n const group = await consentGroupForSite(hostId, reader.org)\n const history = await readEmailDeliveryHistory(email, {\n limit: EMAIL_DELIVERY_READ_LIMIT,\n })\n const rows = history.rows.filter(\n (row) =>\n Boolean(row.campaignId) &&\n Boolean(row.hostId) &&\n group.hostIds.includes(String(row.hostId)),\n )\n const names = await readCampaignNames(rows)\n const emails = rows\n .map((row) =>\n contactCampaignEmailFromDelivery(\n row,\n names.get(`${row.hostId}/${row.campaignId}`) ?? null,\n ),\n )\n .filter((entry): entry is ContactCampaignEmail => entry !== null)\n .sort((a, b) => b.sentAtMs - a.sentAtMs)\n const body: ContactEmailHistoryResponse = {\n ok: true,\n emails,\n lookupFailed: history.lookupFailed,\n limit: EMAIL_DELIVERY_READ_LIMIT,\n }\n res.status(200).json(body)\n } catch (error) {\n console.error('[crm] contact-email-history failed', hostId, contactId, error)\n res.status(500).json({ error: 'The campaign history could not be read' })\n }\n}\n"],"names":["memberCanSee","consentGroupForSite","firebaseAdmin","getOrgForHost","memberHasOrgPermission","orgDataCollectionForHost","resolveOrgMembership","EMAIL_DELIVERY_READ_LIMIT","readEmailDeliveryHistory","isRefusedIdToken","crmSuiteRefusal","CONTACT_EMAIL_HISTORY_ROUTE","refuse","status","error","ok","authorizeCrmReader","req","hostId","authorization","String","headers","idToken","startsWith","slice","length","undefined","decoded","app","auth","verifyIdToken","console","staff","resolved","catch","orgId","org","membership","uid","member","suspended","orgSuspended","instant","value","parsed","Number","isFinite","contactCampaignEmailFromDelivery","row","campaignName","campaignId","stamps","timestamps","sentAtMs","sent","firstSeenAtMs","delivered","opened","clicked","bounced","complained","messageId","subject","deliveredAtMs","openedAtMs","clickedAtMs","bouncedAtMs","complainedAtMs","openCount","clickCount","readCampaignNames","rows","names","Map","firestore","refs","key","has","set","push","collection","doc","snapshots","getAll","snapshot","exists","ref","parent","id","name","get","trim","contactEmailHistoryHandler","res","method","setHeader","json","body","contactId","reader","suite","contactsRef","visibleTo","email","group","history","limit","filter","Boolean","hostIds","includes","emails","map","entry","sort","a","b","lookupFailed"],"mappings":"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"}
|