@aglyn/plugins-crm 1.0.0-beta.218 → 1.0.0-beta.220
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 -0
- package/package.json +14 -14
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.js +12 -4
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/company-detail-page.js +11 -0
- package/src/lib/components/company-detail-page.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +12 -2
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-import-drawer.d.ts +1 -1
- package/src/lib/components/company-picker.js +10 -3
- package/src/lib/components/company-picker.js.map +1 -1
- package/src/lib/components/contact-associations-card.js +4 -3
- package/src/lib/components/contact-associations-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.d.ts +4 -4
- package/src/lib/components/contact-detail-page.js +50 -53
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-list-columns.d.ts +3 -0
- package/src/lib/components/contact-list-columns.js +21 -5
- package/src/lib/components/contact-list-columns.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +2 -1
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-timeline-card.js +4 -3
- package/src/lib/components/contact-timeline-card.js.map +1 -1
- package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
- package/src/lib/components/contacts-bulk-bar.js +24 -51
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +9 -3
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-booking-zone.d.ts +8 -2
- package/src/lib/components/crm-booking-zone.js.map +1 -1
- package/src/lib/components/crm-export-all-button.d.ts +1 -1
- package/src/lib/components/crm-export-all-button.js.map +1 -1
- package/src/lib/components/crm-glance-card.js +2 -1
- package/src/lib/components/crm-glance-card.js.map +1 -1
- package/src/lib/components/crm-send-email-dialog.js +9 -6
- package/src/lib/components/crm-send-email-dialog.js.map +1 -1
- package/src/lib/components/csv-import-drawer.js +2 -2
- package/src/lib/components/csv-import-drawer.js.map +1 -1
- package/src/lib/components/deal-detail-page.js +18 -2
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.js +10 -0
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deal-products-card.js +27 -16
- package/src/lib/components/deal-products-card.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.js +13 -5
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/lead-campaigns-card.d.ts +4 -4
- package/src/lib/components/lead-campaigns-card.js +15 -8
- package/src/lib/components/lead-campaigns-card.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +25 -5
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-properties-card.js +10 -0
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-status-chip.js +3 -1
- package/src/lib/components/lead-status-chip.js.map +1 -1
- package/src/lib/components/lead-surfaces-note.js +4 -5
- package/src/lib/components/lead-surfaces-note.js.map +1 -1
- package/src/lib/components/lead-unqualify-dialog.js +6 -0
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.js +34 -11
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +41 -8
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/new-lead-drawer.js +5 -4
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/components/org-lead-surfaces-note.js +6 -2
- package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
- package/src/lib/components/recipes-card.js +9 -4
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-sharing-card.d.ts +57 -0
- package/src/lib/components/record-sharing-card.js +244 -0
- package/src/lib/components/record-sharing-card.js.map +1 -0
- package/src/lib/components/reports/activity-card.js +4 -3
- package/src/lib/components/reports/activity-card.js.map +1 -1
- package/src/lib/components/reports/closed-deals-card.js +4 -3
- package/src/lib/components/reports/closed-deals-card.js.map +1 -1
- package/src/lib/components/reports/contacts-mix-card.js +3 -2
- package/src/lib/components/reports/contacts-mix-card.js.map +1 -1
- package/src/lib/components/reports/contacts-trend-card.js +2 -1
- package/src/lib/components/reports/contacts-trend-card.js.map +1 -1
- package/src/lib/components/reports/forecast-card.js +3 -2
- package/src/lib/components/reports/forecast-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.js +8 -6
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/pipeline-card.js +4 -3
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-format.d.ts +1 -1
- package/src/lib/components/reports/report-format.js.map +1 -1
- package/src/lib/components/reports/report-scope.d.ts +1 -1
- package/src/lib/components/reports/report-scope.js.map +1 -1
- package/src/lib/components/reports/source-conversion-card.js +2 -1
- package/src/lib/components/reports/source-conversion-card.js.map +1 -1
- package/src/lib/components/reports/tasks-card.js +2 -1
- package/src/lib/components/reports/tasks-card.js.map +1 -1
- package/src/lib/components/reports/won-lost-by-owner-card.js +3 -2
- package/src/lib/components/reports/won-lost-by-owner-card.js.map +1 -1
- package/src/lib/components/reports-section.js +4 -4
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/settings-section.js +7 -0
- package/src/lib/components/settings-section.js.map +1 -1
- package/src/lib/components/share-records-dialog.d.ts +28 -0
- package/src/lib/components/share-records-dialog.js +184 -0
- package/src/lib/components/share-records-dialog.js.map +1 -0
- package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
- package/src/lib/components/sharing-rule-drawer.js +374 -0
- package/src/lib/components/sharing-rule-drawer.js.map +1 -0
- package/src/lib/components/sharing-rules-card.d.ts +24 -0
- package/src/lib/components/sharing-rules-card.js +287 -0
- package/src/lib/components/sharing-rules-card.js.map +1 -0
- package/src/lib/components/use-crm-api.d.ts +1 -1
- package/src/lib/components/use-crm-api.js.map +1 -1
- package/src/lib/components/use-site-route-context.d.ts +25 -0
- package/src/lib/components/use-site-route-context.js +58 -0
- package/src/lib/components/use-site-route-context.js.map +1 -0
- package/src/lib/constants/api-routes.d.ts +31 -0
- package/src/lib/constants/api-routes.js +27 -1
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +2 -0
- package/src/lib/constants/bundle-common.js +1 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +41 -0
- package/src/lib/declarations.console-server.js +115 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.d.ts +12 -0
- package/src/lib/declarations.server.js +84 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-contact-remove.d.ts +35 -0
- package/src/lib/hooks/use-contact-remove.js +77 -0
- package/src/lib/hooks/use-contact-remove.js.map +1 -0
- package/src/lib/hooks/use-crm-campaigns.d.ts +2 -2
- package/src/lib/hooks/use-crm-campaigns.js +3 -3
- package/src/lib/hooks/use-crm-campaigns.js.map +1 -1
- package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
- package/src/lib/hooks/use-crm-sharing.js +109 -0
- package/src/lib/hooks/use-crm-sharing.js.map +1 -0
- package/src/lib/model/campaign-filing-activity.d.ts +1 -1
- package/src/lib/model/campaign-filing-activity.js +1 -1
- package/src/lib/model/campaign-filing-activity.js.map +1 -1
- package/src/lib/model/companies-csv.d.ts +1 -1
- package/src/lib/model/companies-csv.js +2 -2
- package/src/lib/model/companies-csv.js.map +1 -1
- package/src/lib/model/contact-holder.d.ts +68 -0
- package/src/lib/model/contact-holder.js +129 -0
- package/src/lib/model/contact-holder.js.map +1 -0
- package/src/lib/model/contact-interaction-links.d.ts +46 -0
- package/src/lib/model/contact-interaction-links.js +65 -0
- package/src/lib/model/contact-interaction-links.js.map +1 -0
- package/src/lib/model/contact-merge.d.ts +136 -0
- package/src/lib/model/contact-merge.js +412 -0
- package/src/lib/model/contact-merge.js.map +1 -0
- package/src/lib/model/contact-record.d.ts +7 -1
- package/src/lib/model/contact-record.js +7 -6
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-remove.d.ts +48 -0
- package/src/lib/model/contact-remove.js +22 -0
- package/src/lib/model/contact-remove.js.map +1 -0
- package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
- package/src/lib/model/contacts-bulk-writes.js +2 -82
- package/src/lib/model/contacts-bulk-writes.js.map +1 -1
- package/src/lib/model/contacts-csv.d.ts +2 -2
- package/src/lib/model/contacts-csv.js +2 -2
- package/src/lib/model/contacts-csv.js.map +1 -1
- package/src/lib/model/crm-csv.d.ts +239 -0
- package/src/lib/model/crm-csv.js +467 -0
- package/src/lib/model/crm-csv.js.map +1 -0
- package/src/lib/model/crm-digest.d.ts +227 -0
- package/src/lib/model/crm-digest.js +336 -0
- package/src/lib/model/crm-digest.js.map +1 -0
- package/src/lib/model/crm-import.d.ts +7 -1
- package/src/lib/model/crm-import.js +2 -1
- package/src/lib/model/crm-import.js.map +1 -1
- package/src/lib/model/crm-inbound.d.ts +162 -0
- package/src/lib/model/crm-inbound.js +208 -0
- package/src/lib/model/crm-inbound.js.map +1 -0
- package/src/lib/model/crm-lead-import.d.ts +1 -1
- package/src/lib/model/crm-lead-import.js +4 -4
- package/src/lib/model/crm-lead-import.js.map +1 -1
- package/src/lib/model/crm-record-routes.d.ts +5 -0
- package/src/lib/model/crm-record-routes.js +7 -0
- package/src/lib/model/crm-record-routes.js.map +1 -1
- package/src/lib/model/crm-reports.d.ts +467 -0
- package/src/lib/model/crm-reports.js +667 -0
- package/src/lib/model/crm-reports.js.map +1 -0
- package/src/lib/model/crm-routes.d.ts +6 -5
- package/src/lib/model/crm-routes.js +7 -7
- package/src/lib/model/crm-routes.js.map +1 -1
- package/src/lib/model/crm-search-sources.d.ts +15 -0
- package/src/lib/model/crm-search-sources.js +162 -0
- package/src/lib/model/crm-search-sources.js.map +1 -0
- package/src/lib/model/crm-sharing-api.d.ts +32 -0
- package/src/lib/model/crm-sharing-api.js +59 -0
- package/src/lib/model/crm-sharing-api.js.map +1 -0
- package/src/lib/model/crm-sharing.d.ts +333 -0
- package/src/lib/model/crm-sharing.js +603 -0
- package/src/lib/model/crm-sharing.js.map +1 -0
- package/src/lib/model/deals-csv.d.ts +1 -1
- package/src/lib/model/deals-csv.js +4 -5
- package/src/lib/model/deals-csv.js.map +1 -1
- package/src/lib/model/lead-filters.d.ts +5 -4
- package/src/lib/model/lead-filters.js +7 -5
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/leads-csv.d.ts +1 -1
- package/src/lib/model/leads-csv.js +1 -1
- package/src/lib/model/leads-csv.js.map +1 -1
- package/src/lib/model/tasks-csv.d.ts +1 -1
- package/src/lib/model/tasks-csv.js +1 -1
- package/src/lib/model/tasks-csv.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +75 -0
- package/src/lib/plan-entitlements.js +56 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +39 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
- package/src/lib/server/api-v1/contacts-merge.js +120 -0
- package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
- package/src/lib/server/api-v1/contacts.d.ts +18 -0
- package/src/lib/server/api-v1/contacts.js +1076 -0
- package/src/lib/server/api-v1/contacts.js.map +1 -0
- package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
- package/src/lib/server/api-v1/crm-activities.js +269 -0
- package/src/lib/server/api-v1/crm-activities.js.map +1 -0
- package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
- package/src/lib/server/api-v1/crm-companies.js +391 -0
- package/src/lib/server/api-v1/crm-companies.js.map +1 -0
- package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
- package/src/lib/server/api-v1/crm-deals.js +631 -0
- package/src/lib/server/api-v1/crm-deals.js.map +1 -0
- package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
- package/src/lib/server/api-v1/crm-email-templates.js +336 -0
- package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
- package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
- package/src/lib/server/api-v1/crm-leads.js +833 -0
- package/src/lib/server/api-v1/crm-leads.js.map +1 -0
- package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
- package/src/lib/server/api-v1/crm-pipelines.js +146 -0
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
- package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
- package/src/lib/server/api-v1/crm-shared.js +436 -0
- package/src/lib/server/api-v1/crm-shared.js.map +1 -0
- package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
- package/src/lib/server/api-v1/crm-tasks.js +398 -0
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +29 -0
- package/src/lib/server/api-v1/openapi.js +985 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/usage.d.ts +26 -0
- package/src/lib/server/api-v1/usage.js +73 -0
- package/src/lib/server/api-v1/usage.js.map +1 -0
- package/src/lib/server/campaign-filing-activity.d.ts +1 -1
- package/src/lib/server/campaign-filing-activity.js +7 -11
- package/src/lib/server/campaign-filing-activity.js.map +1 -1
- package/src/lib/server/capture-contact.js +15 -4
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/consent-group-participant.js +1 -1
- package/src/lib/server/consent-group-participant.js.map +1 -1
- package/src/lib/server/contact-email-engagement.d.ts +33 -0
- package/src/lib/server/contact-email-engagement.js +146 -0
- package/src/lib/server/contact-email-engagement.js.map +1 -0
- package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
- package/src/lib/server/contact-lifecycle-floor.js +91 -0
- package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
- package/src/lib/server/contact-remove.d.ts +75 -0
- package/src/lib/server/contact-remove.js +244 -0
- package/src/lib/server/contact-remove.js.map +1 -0
- package/src/lib/server/contact-update.js +4 -3
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/contacts-import.d.ts +47 -11
- package/src/lib/server/contacts-import.js +171 -71
- package/src/lib/server/contacts-import.js.map +1 -1
- package/src/lib/server/contacts-merge.d.ts +1 -1
- package/src/lib/server/contacts-merge.js +3 -2
- package/src/lib/server/contacts-merge.js.map +1 -1
- package/src/lib/server/container-detach.d.ts +31 -0
- package/src/lib/server/container-detach.js +115 -0
- package/src/lib/server/container-detach.js.map +1 -0
- package/src/lib/server/convert-host-lead.d.ts +189 -0
- package/src/lib/server/convert-host-lead.js +518 -0
- package/src/lib/server/convert-host-lead.js.map +1 -0
- package/src/lib/server/crm-inbound-email.d.ts +162 -0
- package/src/lib/server/crm-inbound-email.js +259 -0
- package/src/lib/server/crm-inbound-email.js.map +1 -0
- package/src/lib/server/crm-records-meter.d.ts +50 -0
- package/src/lib/server/crm-records-meter.js +126 -0
- package/src/lib/server/crm-records-meter.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +108 -0
- package/src/lib/server/crm-sharing.js +790 -0
- package/src/lib/server/crm-sharing.js.map +1 -0
- package/src/lib/server/daily-digest-route.d.ts +101 -0
- package/src/lib/server/daily-digest-route.js +578 -0
- package/src/lib/server/daily-digest-route.js.map +1 -0
- package/src/lib/server/export-route.d.ts +60 -0
- package/src/lib/server/export-route.js +447 -0
- package/src/lib/server/export-route.js.map +1 -0
- package/src/lib/server/inbound-address.js +5 -3
- package/src/lib/server/inbound-address.js.map +1 -1
- package/src/lib/server/inbound-route.d.ts +74 -0
- package/src/lib/server/inbound-route.js +260 -0
- package/src/lib/server/inbound-route.js.map +1 -0
- package/src/lib/server/lead-campaign-carry.js +3 -3
- package/src/lib/server/lead-campaign-carry.js.map +1 -1
- package/src/lib/server/lead-convert.d.ts +1 -1
- package/src/lib/server/lead-convert.js +2 -2
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +1 -1
- package/src/lib/server/lead-create.js +10 -15
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-nurturing.d.ts +23 -0
- package/src/lib/server/lead-nurturing.js +82 -0
- package/src/lib/server/lead-nurturing.js.map +1 -0
- package/src/lib/server/leads-import.js +7 -7
- package/src/lib/server/leads-import.js.map +1 -1
- package/src/lib/server/merge-contacts.d.ts +55 -0
- package/src/lib/server/merge-contacts.js +274 -0
- package/src/lib/server/merge-contacts.js.map +1 -0
- package/src/lib/server/pipeline-record-index.d.ts +36 -0
- package/src/lib/server/pipeline-record-index.js +87 -0
- package/src/lib/server/pipeline-record-index.js.map +1 -0
- package/src/lib/server/record-email-state.js +23 -0
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-facts.js +2 -1
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/record-timeline.d.ts +12 -0
- package/src/lib/server/record-timeline.js +127 -15
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/task-reminders-route.d.ts +82 -0
- package/src/lib/server/task-reminders-route.js +499 -0
- package/src/lib/server/task-reminders-route.js.map +1 -0
- package/src/lib/server/task-routes.d.ts +2 -0
- package/src/lib/server/task-routes.js +22 -3
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server-deal-stage.js +2 -1
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +5 -2
- package/src/lib/server.js +47 -2
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
- package/src/lib/testing/stand-in-site-record-routes.js +55 -0
- package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
- package/src/lib/usage-axes.d.ts +35 -0
- package/src/lib/usage-axes.js +75 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/components/use-emails-hub-path.d.ts +0 -24
- package/src/lib/components/use-emails-hub-path.js +0 -59
- package/src/lib/components/use-emails-hub-path.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-facts.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 registerPluginRecordFactsReader,\n type PluginRecordFactsRead,\n type PluginRecordFactsReader,\n type PluginRecordFactsRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-facts'\nimport {\n CRM_COLLECTIONS,\n contactPrimaryGroup,\n crmReadTokens,\n isOrgWideMember,\n type ConsentGroup,\n type ContactFieldDefinition,\n type CrmActivity,\n type CrmCompany,\n type CrmDeal,\n type CrmPipeline,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n firebaseAdmin,\n getOrgDoc,\n memberHasOrgPermission,\n readLeadForHost,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n CRM_IMPORT_FACTS_RESOURCE,\n CRM_RECORD_FACTS_RESOURCES,\n companyFacts,\n contactFacts,\n dealFacts,\n importFacts,\n isCrmImportFactsCollection,\n leadFacts,\n type CrmRecordFactsKind,\n} from '../model/record-facts'\nimport { crmSuiteRefusal } from './suite-gate'\n\n/**\n * THE CRM'S READERS ON THE CORE'S RECORD-FACTS SEAM (AGL-2917).\n *\n * Another plugin asks for a contact, a company, a deal, a lead or an import's\n * field catalog by resource name, and gets the facts `model/record-facts.ts`\n * reports — read here with the rules every other CRM door applies, for the\n * member the caller names:\n *\n * 1. the site belongs to the organization;\n * 2. the member reaches it — an org-wide member at the organization level, a\n * member of that site under it — and holds `data.manage`, the key the\n * whole CRM is gated on. A verified staff caller passes, as at every CRM\n * route;\n * 3. the organization's plan carries the CRM, asked after the caller is\n * known, the order `suite-gate.ts` gives;\n * 4. the record exists and is visible where it is read: under a site, its\n * `visibleTo` meets the site's read tokens, as the console's listeners\n * filter; at the organization level every row is the member's;\n * 5. what hangs off it — its logged activity, its tasks, its deals — is held\n * to the same visibility, row by row.\n *\n * A caller in the same process skips the plugin API dispatcher, and with it\n * the dispatcher's per-site enablement, release flag, lockdown and rate\n * limit; those are the caller's to establish before it asks (the AI plugin's\n * CRM admission does). Nothing here writes.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\ntype Refusal = { ok: false; status: 400 | 403 | 404; error: string }\n\n/** The org collection a contact lives in, beside the CRM's own. */\nconst CONTACTS_COLLECTION = 'contacts'\n\n/** Logged activities read per record before visibility and the timeline cut. */\nexport const CRM_FACTS_ACTIVITIES_READ = 40\n/** Tasks read per record, the record page's own window. */\nexport const CRM_FACTS_TASKS_READ = 50\n/** Deals read per contact or company. */\nexport const CRM_FACTS_DEALS_READ = 20\n/** Custom field definitions an import catalog reads. */\nexport const CRM_FACTS_FIELD_DEFINITIONS_READ = 100\n\nexport const CRM_FACTS_UNKNOWN_SITE = 'Unknown site'\nexport const CRM_FACTS_NOT_A_MEMBER = 'You are not a member of that organization'\nexport const CRM_FACTS_SITE_REFUSAL = 'Reading CRM records requires the data.manage permission on this site'\nexport const CRM_FACTS_ORG_REFUSAL =\n 'Reading CRM records across the organization requires the data.manage permission for the whole organization'\nexport const CRM_FACTS_LEAD_NEEDS_SITE = 'A lead is read on the site that captured it'\n\nconst NOT_FOUND: Record<CrmRecordFactsKind | 'import', string> = {\n contact: 'This contact could not be found. It may have been deleted.',\n company: 'This company could not be found. It may have been deleted.',\n deal: 'This deal could not be found. It may have been deleted.',\n lead: 'This lead could not be found. It may have been removed.',\n import: 'There is no such import.',\n}\n\n/** Where a read runs, once the caller has been admitted. */\ninterface CrmFactsScope {\n orgId: string\n hostId: string | null\n org: Data\n /** The site's consent group under a site; `null` at the organization level. */\n group: ConsentGroup | null\n /** Whether a row's `visibleTo` is readable here. */\n visible: (visibleTo: unknown) => boolean\n}\n\nfunction firestore(): Firestore {\n return firebaseAdmin.app().firestore()\n}\n\n/**\n * Rules 1 to 3: the site, the member and the plan. A refusal names what the\n * member lacks in the words the console's own CRM doors use.\n */\nexport async function crmFactsScope(request: PluginRecordFactsRequest): Promise<CrmFactsScope | Refusal> {\n const orgId = String(request.orgId ?? '').trim()\n const hostId = request.hostId ? String(request.hostId).trim() : null\n if (!orgId) return { ok: false, status: 400, error: 'Open a workspace first' }\n if (hostId && (await resolveOrgIdForHost(hostId)) !== orgId) {\n return { ok: false, status: 404, error: CRM_FACTS_UNKNOWN_SITE }\n }\n if (!request.staff) {\n const membership = await resolveOrgMembership(request.uid, orgId).catch(() => null)\n const member = membership?.orgId === orgId ? membership.member : null\n if (!member) return { ok: false, status: 403, error: CRM_FACTS_NOT_A_MEMBER }\n const reaches = hostId\n ? isOrgWideMember(member) || Boolean(member.hostAccess?.[hostId])\n : isOrgWideMember(member)\n const allowed = reaches && (await memberHasOrgPermission(orgId, member, 'data.manage'))\n if (!allowed) {\n return { ok: false, status: 403, error: hostId ? CRM_FACTS_SITE_REFUSAL : CRM_FACTS_ORG_REFUSAL }\n }\n }\n const org = ((request.org as Data | null) ?? ((await getOrgDoc(orgId)) as Data | null)) || null\n if (!org) return { ok: false, status: 404, error: 'Unknown organization' }\n const suite = crmSuiteRefusal(org, 'Reading a CRM record')\n if (suite) return { ok: false, status: 403, error: suite.body.error }\n if (!hostId) return { orgId, hostId: null, org, group: null, visible: () => true }\n const group = await consentGroupForSite(hostId, org)\n const readable = new Set<string>(crmReadTokens(group))\n return {\n orgId,\n hostId,\n org,\n group,\n visible: (visibleTo) =>\n Array.isArray(visibleTo) && visibleTo.some((token) => readable.has(String(token))),\n }\n}\n\nconst orgCollection = (orgId: string, name: string) =>\n firestore().collection('orgs').doc(orgId).collection(name)\n\n/** Rows of an org collection naming one record, held to the scope's visibility. */\nasync function rowsNaming<T>(\n scope: CrmFactsScope,\n input: { collection: string; field: string; id: string; orderBy: string; direction: 'asc' | 'desc'; limit: number },\n): Promise<T[]> {\n const snapshot = await orgCollection(scope.orgId, input.collection)\n .where(input.field, '==', input.id)\n .orderBy(input.orderBy, input.direction)\n .limit(input.limit)\n .get()\n return snapshot.docs\n .map((doc) => (doc.data() ?? {}) as Data)\n .filter((row) => scope.visible(row['visibleTo'])) as T[]\n}\n\n/** The pipelines some deals name, by id, read once each. */\nasync function pipelinesFor(scope: CrmFactsScope, deals: ReadonlyArray<Partial<CrmDeal>>): Promise<Map<string, CrmPipeline>> {\n const ids = [...new Set(deals.map((deal) => String(deal.pipelineId ?? '')).filter(Boolean))]\n const snapshots = await Promise.all(ids.map((id) => orgCollection(scope.orgId, CRM_COLLECTIONS.pipelines).doc(id).get()))\n return new Map(\n snapshots\n .filter((snapshot) => snapshot.exists)\n .map((snapshot) => [snapshot.id, (snapshot.data() ?? {}) as CrmPipeline]),\n )\n}\n\n/** A record document the scope may read, or `null`. */\nasync function visibleDoc(scope: CrmFactsScope, collection: string, id: string): Promise<Data | null> {\n if (!id) return null\n const snapshot = await orgCollection(scope.orgId, collection).doc(id).get()\n const data = snapshot.exists ? ((snapshot.data() ?? {}) as Data) : null\n return data && scope.visible(data['visibleTo']) ? data : null\n}\n\nconst refused = (kind: CrmRecordFactsKind | 'import', status: 400 | 404 = 404): Refusal => ({\n ok: false,\n status,\n error: NOT_FOUND[kind],\n})\n\n/** A reader that admits the caller, then reads the record. */\nfunction reader(read: (scope: CrmFactsScope, request: PluginRecordFactsRequest) => Promise<PluginRecordFactsRead>): PluginRecordFactsReader {\n return {\n read: async (request) => {\n const scope = await crmFactsScope(request)\n if ('ok' in scope) return scope\n return read(scope, request)\n },\n }\n}\n\nexport const crmContactFactsReader = reader(async (scope, request) => {\n const id = String(request.id ?? '').trim()\n const row = await visibleDoc(scope, CONTACTS_COLLECTION, id)\n if (!row) return refused('contact')\n const group = scope.group ?? contactPrimaryGroup(row, scope.org)\n const naming = { field: 'contactId', id }\n const [activities, tasks, deals] = await Promise.all([\n rowsNaming<CrmActivity>(scope, { ...naming, collection: CRM_COLLECTIONS.activities, orderBy: 'atMs', direction: 'desc', limit: CRM_FACTS_ACTIVITIES_READ }),\n rowsNaming<CrmTask>(scope, { ...naming, collection: CRM_COLLECTIONS.tasks, orderBy: 'dueAtMs', direction: 'asc', limit: CRM_FACTS_TASKS_READ }),\n rowsNaming<CrmDeal>(scope, { ...naming, collection: CRM_COLLECTIONS.deals, orderBy: 'updatedAt', direction: 'desc', limit: CRM_FACTS_DEALS_READ }),\n ])\n const pipelines = await pipelinesFor(scope, deals)\n return {\n ok: true,\n facts: { ...contactFacts({ row, group, activities, tasks, deals, pipelines, nowMs: request.now.getTime() }) },\n }\n})\n\nexport const crmCompanyFactsReader = reader(async (scope, request) => {\n const id = String(request.id ?? '').trim()\n const company = await visibleDoc(scope, CRM_COLLECTIONS.companies, id)\n if (!company) return refused('company')\n const naming = { field: 'companyId', id }\n const [activities, tasks, deals] = await Promise.all([\n rowsNaming<CrmActivity>(scope, { ...naming, collection: CRM_COLLECTIONS.activities, orderBy: 'atMs', direction: 'desc', limit: CRM_FACTS_ACTIVITIES_READ }),\n rowsNaming<CrmTask>(scope, { ...naming, collection: CRM_COLLECTIONS.tasks, orderBy: 'dueAtMs', direction: 'asc', limit: CRM_FACTS_TASKS_READ }),\n rowsNaming<CrmDeal>(scope, { ...naming, collection: CRM_COLLECTIONS.deals, orderBy: 'updatedAt', direction: 'desc', limit: CRM_FACTS_DEALS_READ }),\n ])\n const pipelines = await pipelinesFor(scope, deals)\n return {\n ok: true,\n facts: {\n ...companyFacts({ company: company as Partial<CrmCompany>, activities, tasks, deals, pipelines, nowMs: request.now.getTime() }),\n },\n }\n})\n\nexport const crmDealFactsReader = reader(async (scope, request) => {\n const id = String(request.id ?? '').trim()\n const deal = (await visibleDoc(scope, CRM_COLLECTIONS.deals, id)) as (Partial<CrmDeal> & Data) | null\n if (!deal) return refused('deal')\n const naming = { field: 'dealId', id }\n const [activities, tasks, pipelines] = await Promise.all([\n rowsNaming<CrmActivity>(scope, { ...naming, collection: CRM_COLLECTIONS.activities, orderBy: 'atMs', direction: 'desc', limit: CRM_FACTS_ACTIVITIES_READ }),\n rowsNaming<CrmTask>(scope, { ...naming, collection: CRM_COLLECTIONS.tasks, orderBy: 'dueAtMs', direction: 'asc', limit: CRM_FACTS_TASKS_READ }),\n pipelinesFor(scope, [deal]),\n ])\n return {\n ok: true,\n facts: {\n ...dealFacts({\n deal,\n pipeline: pipelines.get(String(deal.pipelineId ?? '')) ?? null,\n activities,\n tasks,\n nowMs: request.now.getTime(),\n }),\n },\n }\n})\n\nexport const crmLeadFactsReader = reader(async (scope, request) => {\n if (!scope.hostId) return { ok: false, status: 400, error: CRM_FACTS_LEAD_NEEDS_SITE }\n const id = String(request.id ?? '').trim()\n if (!id) return refused('lead')\n const snapshot = await readLeadForHost(scope.hostId, id)\n if (!snapshot) return refused('lead')\n const activities = await rowsNaming<CrmActivity>(scope, {\n field: 'leadId',\n id,\n collection: CRM_COLLECTIONS.activities,\n orderBy: 'atMs',\n direction: 'desc',\n limit: CRM_FACTS_ACTIVITIES_READ,\n })\n return { ok: true, facts: { ...leadFacts({ lead: (snapshot.data() ?? {}) as Data, activities }) } }\n})\n\nexport const crmImportFactsReader = reader(async (scope, request) => {\n const collection = String(request.id ?? '').trim()\n if (!isCrmImportFactsCollection(collection)) return refused('import', 400)\n const snapshot = await orgCollection(scope.orgId, CRM_COLLECTIONS.contactFields)\n .limit(CRM_FACTS_FIELD_DEFINITIONS_READ)\n .get()\n const definitions = snapshot.docs\n .map((doc) => (doc.data() ?? {}) as Partial<ContactFieldDefinition> & Data)\n .filter((definition) => scope.visible(definition['visibleTo']))\n return { ok: true, facts: { ...importFacts(collection, definitions) } }\n})\n\n/** Every reader this plugin registers, by resource name. */\nexport const CRM_FACTS_READERS: Readonly<Record<string, PluginRecordFactsReader>> = {\n [CRM_RECORD_FACTS_RESOURCES.contact]: crmContactFactsReader,\n [CRM_RECORD_FACTS_RESOURCES.company]: crmCompanyFactsReader,\n [CRM_RECORD_FACTS_RESOURCES.deal]: crmDealFactsReader,\n [CRM_RECORD_FACTS_RESOURCES.lead]: crmLeadFactsReader,\n [CRM_IMPORT_FACTS_RESOURCE]: crmImportFactsReader,\n}\n\n/**\n * Registers the readers; the console surface calls it, the one process that\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerCrmRecordFactsReaders(): void {\n for (const [resource, factsReader] of Object.entries(CRM_FACTS_READERS)) {\n registerPluginRecordFactsReader(resource, factsReader, { pluginId: BUNDLE_ID })\n }\n}\n"],"names":["registerPluginRecordFactsReader","CRM_COLLECTIONS","contactPrimaryGroup","crmReadTokens","isOrgWideMember","consentGroupForSite","firebaseAdmin","getOrgDoc","memberHasOrgPermission","readLeadForHost","resolveOrgIdForHost","resolveOrgMembership","BUNDLE_ID","CRM_IMPORT_FACTS_RESOURCE","CRM_RECORD_FACTS_RESOURCES","companyFacts","contactFacts","dealFacts","importFacts","isCrmImportFactsCollection","leadFacts","crmSuiteRefusal","CONTACTS_COLLECTION","CRM_FACTS_ACTIVITIES_READ","CRM_FACTS_TASKS_READ","CRM_FACTS_DEALS_READ","CRM_FACTS_FIELD_DEFINITIONS_READ","CRM_FACTS_UNKNOWN_SITE","CRM_FACTS_NOT_A_MEMBER","CRM_FACTS_SITE_REFUSAL","CRM_FACTS_ORG_REFUSAL","CRM_FACTS_LEAD_NEEDS_SITE","NOT_FOUND","contact","company","deal","lead","import","firestore","app","crmFactsScope","request","orgId","String","trim","hostId","ok","status","error","staff","member","membership","uid","catch","reaches","Boolean","hostAccess","allowed","org","suite","body","group","visible","readable","Set","visibleTo","Array","isArray","some","token","has","orgCollection","name","collection","doc","rowsNaming","scope","input","snapshot","where","field","id","orderBy","direction","limit","get","docs","map","data","filter","row","pipelinesFor","deals","ids","pipelineId","snapshots","Promise","all","pipelines","Map","exists","visibleDoc","refused","kind","reader","read","crmContactFactsReader","naming","activities","tasks","facts","nowMs","now","getTime","crmCompanyFactsReader","companies","crmDealFactsReader","pipeline","crmLeadFactsReader","crmImportFactsReader","contactFields","definitions","definition","CRM_FACTS_READERS","registerCrmRecordFactsReaders","resource","factsReader","Object","entries","pluginId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/record-facts.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 registerPluginRecordFactsReader,\n type PluginRecordFactsRead,\n type PluginRecordFactsReader,\n type PluginRecordFactsRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-facts'\nimport {\n CRM_COLLECTIONS,\n crmReadTokens,\n isOrgWideMember,\n type ConsentGroup,\n type ContactFieldDefinition,\n type CrmActivity,\n type CrmCompany,\n type CrmDeal,\n type CrmPipeline,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport {\n consentGroupForSite,\n firebaseAdmin,\n getOrgDoc,\n memberHasOrgPermission,\n readLeadForHost,\n resolveOrgIdForHost,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n CRM_IMPORT_FACTS_RESOURCE,\n CRM_RECORD_FACTS_RESOURCES,\n companyFacts,\n contactFacts,\n dealFacts,\n importFacts,\n isCrmImportFactsCollection,\n leadFacts,\n type CrmRecordFactsKind,\n} from '../model/record-facts'\nimport { crmSuiteRefusal } from './suite-gate'\nimport { contactPrimaryGroup } from '../model/contact-holder'\n\n/**\n * THE CRM'S READERS ON THE CORE'S RECORD-FACTS SEAM (AGL-2917).\n *\n * Another plugin asks for a contact, a company, a deal, a lead or an import's\n * field catalog by resource name, and gets the facts `model/record-facts.ts`\n * reports — read here with the rules every other CRM door applies, for the\n * member the caller names:\n *\n * 1. the site belongs to the organization;\n * 2. the member reaches it — an org-wide member at the organization level, a\n * member of that site under it — and holds `data.manage`, the key the\n * whole CRM is gated on. A verified staff caller passes, as at every CRM\n * route;\n * 3. the organization's plan carries the CRM, asked after the caller is\n * known, the order `suite-gate.ts` gives;\n * 4. the record exists and is visible where it is read: under a site, its\n * `visibleTo` meets the site's read tokens, as the console's listeners\n * filter; at the organization level every row is the member's;\n * 5. what hangs off it — its logged activity, its tasks, its deals — is held\n * to the same visibility, row by row.\n *\n * A caller in the same process skips the plugin API dispatcher, and with it\n * the dispatcher's per-site enablement, release flag, lockdown and rate\n * limit; those are the caller's to establish before it asks (the AI plugin's\n * CRM admission does). Nothing here writes.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\ntype Refusal = { ok: false; status: 400 | 403 | 404; error: string }\n\n/** The org collection a contact lives in, beside the CRM's own. */\nconst CONTACTS_COLLECTION = 'contacts'\n\n/** Logged activities read per record before visibility and the timeline cut. */\nexport const CRM_FACTS_ACTIVITIES_READ = 40\n/** Tasks read per record, the record page's own window. */\nexport const CRM_FACTS_TASKS_READ = 50\n/** Deals read per contact or company. */\nexport const CRM_FACTS_DEALS_READ = 20\n/** Custom field definitions an import catalog reads. */\nexport const CRM_FACTS_FIELD_DEFINITIONS_READ = 100\n\nexport const CRM_FACTS_UNKNOWN_SITE = 'Unknown site'\nexport const CRM_FACTS_NOT_A_MEMBER = 'You are not a member of that organization'\nexport const CRM_FACTS_SITE_REFUSAL = 'Reading CRM records requires the data.manage permission on this site'\nexport const CRM_FACTS_ORG_REFUSAL =\n 'Reading CRM records across the organization requires the data.manage permission for the whole organization'\nexport const CRM_FACTS_LEAD_NEEDS_SITE = 'A lead is read on the site that captured it'\n\nconst NOT_FOUND: Record<CrmRecordFactsKind | 'import', string> = {\n contact: 'This contact could not be found. It may have been deleted.',\n company: 'This company could not be found. It may have been deleted.',\n deal: 'This deal could not be found. It may have been deleted.',\n lead: 'This lead could not be found. It may have been removed.',\n import: 'There is no such import.',\n}\n\n/** Where a read runs, once the caller has been admitted. */\ninterface CrmFactsScope {\n orgId: string\n hostId: string | null\n org: Data\n /** The site's consent group under a site; `null` at the organization level. */\n group: ConsentGroup | null\n /** Whether a row's `visibleTo` is readable here. */\n visible: (visibleTo: unknown) => boolean\n}\n\nfunction firestore(): Firestore {\n return firebaseAdmin.app().firestore()\n}\n\n/**\n * Rules 1 to 3: the site, the member and the plan. A refusal names what the\n * member lacks in the words the console's own CRM doors use.\n */\nexport async function crmFactsScope(request: PluginRecordFactsRequest): Promise<CrmFactsScope | Refusal> {\n const orgId = String(request.orgId ?? '').trim()\n const hostId = request.hostId ? String(request.hostId).trim() : null\n if (!orgId) return { ok: false, status: 400, error: 'Open a workspace first' }\n if (hostId && (await resolveOrgIdForHost(hostId)) !== orgId) {\n return { ok: false, status: 404, error: CRM_FACTS_UNKNOWN_SITE }\n }\n if (!request.staff) {\n const membership = await resolveOrgMembership(request.uid, orgId).catch(() => null)\n const member = membership?.orgId === orgId ? membership.member : null\n if (!member) return { ok: false, status: 403, error: CRM_FACTS_NOT_A_MEMBER }\n const reaches = hostId\n ? isOrgWideMember(member) || Boolean(member.hostAccess?.[hostId])\n : isOrgWideMember(member)\n const allowed = reaches && (await memberHasOrgPermission(orgId, member, 'data.manage'))\n if (!allowed) {\n return { ok: false, status: 403, error: hostId ? CRM_FACTS_SITE_REFUSAL : CRM_FACTS_ORG_REFUSAL }\n }\n }\n const org = ((request.org as Data | null) ?? ((await getOrgDoc(orgId)) as Data | null)) || null\n if (!org) return { ok: false, status: 404, error: 'Unknown organization' }\n const suite = crmSuiteRefusal(org, 'Reading a CRM record')\n if (suite) return { ok: false, status: 403, error: suite.body.error }\n if (!hostId) return { orgId, hostId: null, org, group: null, visible: () => true }\n const group = await consentGroupForSite(hostId, org)\n const readable = new Set<string>(crmReadTokens(group))\n return {\n orgId,\n hostId,\n org,\n group,\n visible: (visibleTo) =>\n Array.isArray(visibleTo) && visibleTo.some((token) => readable.has(String(token))),\n }\n}\n\nconst orgCollection = (orgId: string, name: string) =>\n firestore().collection('orgs').doc(orgId).collection(name)\n\n/** Rows of an org collection naming one record, held to the scope's visibility. */\nasync function rowsNaming<T>(\n scope: CrmFactsScope,\n input: { collection: string; field: string; id: string; orderBy: string; direction: 'asc' | 'desc'; limit: number },\n): Promise<T[]> {\n const snapshot = await orgCollection(scope.orgId, input.collection)\n .where(input.field, '==', input.id)\n .orderBy(input.orderBy, input.direction)\n .limit(input.limit)\n .get()\n return snapshot.docs\n .map((doc) => (doc.data() ?? {}) as Data)\n .filter((row) => scope.visible(row['visibleTo'])) as T[]\n}\n\n/** The pipelines some deals name, by id, read once each. */\nasync function pipelinesFor(scope: CrmFactsScope, deals: ReadonlyArray<Partial<CrmDeal>>): Promise<Map<string, CrmPipeline>> {\n const ids = [...new Set(deals.map((deal) => String(deal.pipelineId ?? '')).filter(Boolean))]\n const snapshots = await Promise.all(ids.map((id) => orgCollection(scope.orgId, CRM_COLLECTIONS.pipelines).doc(id).get()))\n return new Map(\n snapshots\n .filter((snapshot) => snapshot.exists)\n .map((snapshot) => [snapshot.id, (snapshot.data() ?? {}) as CrmPipeline]),\n )\n}\n\n/** A record document the scope may read, or `null`. */\nasync function visibleDoc(scope: CrmFactsScope, collection: string, id: string): Promise<Data | null> {\n if (!id) return null\n const snapshot = await orgCollection(scope.orgId, collection).doc(id).get()\n const data = snapshot.exists ? ((snapshot.data() ?? {}) as Data) : null\n return data && scope.visible(data['visibleTo']) ? data : null\n}\n\nconst refused = (kind: CrmRecordFactsKind | 'import', status: 400 | 404 = 404): Refusal => ({\n ok: false,\n status,\n error: NOT_FOUND[kind],\n})\n\n/** A reader that admits the caller, then reads the record. */\nfunction reader(read: (scope: CrmFactsScope, request: PluginRecordFactsRequest) => Promise<PluginRecordFactsRead>): PluginRecordFactsReader {\n return {\n read: async (request) => {\n const scope = await crmFactsScope(request)\n if ('ok' in scope) return scope\n return read(scope, request)\n },\n }\n}\n\nexport const crmContactFactsReader = reader(async (scope, request) => {\n const id = String(request.id ?? '').trim()\n const row = await visibleDoc(scope, CONTACTS_COLLECTION, id)\n if (!row) return refused('contact')\n const group = scope.group ?? contactPrimaryGroup(row, scope.org)\n const naming = { field: 'contactId', id }\n const [activities, tasks, deals] = await Promise.all([\n rowsNaming<CrmActivity>(scope, { ...naming, collection: CRM_COLLECTIONS.activities, orderBy: 'atMs', direction: 'desc', limit: CRM_FACTS_ACTIVITIES_READ }),\n rowsNaming<CrmTask>(scope, { ...naming, collection: CRM_COLLECTIONS.tasks, orderBy: 'dueAtMs', direction: 'asc', limit: CRM_FACTS_TASKS_READ }),\n rowsNaming<CrmDeal>(scope, { ...naming, collection: CRM_COLLECTIONS.deals, orderBy: 'updatedAt', direction: 'desc', limit: CRM_FACTS_DEALS_READ }),\n ])\n const pipelines = await pipelinesFor(scope, deals)\n return {\n ok: true,\n facts: { ...contactFacts({ row, group, activities, tasks, deals, pipelines, nowMs: request.now.getTime() }) },\n }\n})\n\nexport const crmCompanyFactsReader = reader(async (scope, request) => {\n const id = String(request.id ?? '').trim()\n const company = await visibleDoc(scope, CRM_COLLECTIONS.companies, id)\n if (!company) return refused('company')\n const naming = { field: 'companyId', id }\n const [activities, tasks, deals] = await Promise.all([\n rowsNaming<CrmActivity>(scope, { ...naming, collection: CRM_COLLECTIONS.activities, orderBy: 'atMs', direction: 'desc', limit: CRM_FACTS_ACTIVITIES_READ }),\n rowsNaming<CrmTask>(scope, { ...naming, collection: CRM_COLLECTIONS.tasks, orderBy: 'dueAtMs', direction: 'asc', limit: CRM_FACTS_TASKS_READ }),\n rowsNaming<CrmDeal>(scope, { ...naming, collection: CRM_COLLECTIONS.deals, orderBy: 'updatedAt', direction: 'desc', limit: CRM_FACTS_DEALS_READ }),\n ])\n const pipelines = await pipelinesFor(scope, deals)\n return {\n ok: true,\n facts: {\n ...companyFacts({ company: company as Partial<CrmCompany>, activities, tasks, deals, pipelines, nowMs: request.now.getTime() }),\n },\n }\n})\n\nexport const crmDealFactsReader = reader(async (scope, request) => {\n const id = String(request.id ?? '').trim()\n const deal = (await visibleDoc(scope, CRM_COLLECTIONS.deals, id)) as (Partial<CrmDeal> & Data) | null\n if (!deal) return refused('deal')\n const naming = { field: 'dealId', id }\n const [activities, tasks, pipelines] = await Promise.all([\n rowsNaming<CrmActivity>(scope, { ...naming, collection: CRM_COLLECTIONS.activities, orderBy: 'atMs', direction: 'desc', limit: CRM_FACTS_ACTIVITIES_READ }),\n rowsNaming<CrmTask>(scope, { ...naming, collection: CRM_COLLECTIONS.tasks, orderBy: 'dueAtMs', direction: 'asc', limit: CRM_FACTS_TASKS_READ }),\n pipelinesFor(scope, [deal]),\n ])\n return {\n ok: true,\n facts: {\n ...dealFacts({\n deal,\n pipeline: pipelines.get(String(deal.pipelineId ?? '')) ?? null,\n activities,\n tasks,\n nowMs: request.now.getTime(),\n }),\n },\n }\n})\n\nexport const crmLeadFactsReader = reader(async (scope, request) => {\n if (!scope.hostId) return { ok: false, status: 400, error: CRM_FACTS_LEAD_NEEDS_SITE }\n const id = String(request.id ?? '').trim()\n if (!id) return refused('lead')\n const snapshot = await readLeadForHost(scope.hostId, id)\n if (!snapshot) return refused('lead')\n const activities = await rowsNaming<CrmActivity>(scope, {\n field: 'leadId',\n id,\n collection: CRM_COLLECTIONS.activities,\n orderBy: 'atMs',\n direction: 'desc',\n limit: CRM_FACTS_ACTIVITIES_READ,\n })\n return { ok: true, facts: { ...leadFacts({ lead: (snapshot.data() ?? {}) as Data, activities }) } }\n})\n\nexport const crmImportFactsReader = reader(async (scope, request) => {\n const collection = String(request.id ?? '').trim()\n if (!isCrmImportFactsCollection(collection)) return refused('import', 400)\n const snapshot = await orgCollection(scope.orgId, CRM_COLLECTIONS.contactFields)\n .limit(CRM_FACTS_FIELD_DEFINITIONS_READ)\n .get()\n const definitions = snapshot.docs\n .map((doc) => (doc.data() ?? {}) as Partial<ContactFieldDefinition> & Data)\n .filter((definition) => scope.visible(definition['visibleTo']))\n return { ok: true, facts: { ...importFacts(collection, definitions) } }\n})\n\n/** Every reader this plugin registers, by resource name. */\nexport const CRM_FACTS_READERS: Readonly<Record<string, PluginRecordFactsReader>> = {\n [CRM_RECORD_FACTS_RESOURCES.contact]: crmContactFactsReader,\n [CRM_RECORD_FACTS_RESOURCES.company]: crmCompanyFactsReader,\n [CRM_RECORD_FACTS_RESOURCES.deal]: crmDealFactsReader,\n [CRM_RECORD_FACTS_RESOURCES.lead]: crmLeadFactsReader,\n [CRM_IMPORT_FACTS_RESOURCE]: crmImportFactsReader,\n}\n\n/**\n * Registers the readers; the console surface calls it, the one process that\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerCrmRecordFactsReaders(): void {\n for (const [resource, factsReader] of Object.entries(CRM_FACTS_READERS)) {\n registerPluginRecordFactsReader(resource, factsReader, { pluginId: BUNDLE_ID })\n }\n}\n"],"names":["registerPluginRecordFactsReader","CRM_COLLECTIONS","crmReadTokens","isOrgWideMember","consentGroupForSite","firebaseAdmin","getOrgDoc","memberHasOrgPermission","readLeadForHost","resolveOrgIdForHost","resolveOrgMembership","BUNDLE_ID","CRM_IMPORT_FACTS_RESOURCE","CRM_RECORD_FACTS_RESOURCES","companyFacts","contactFacts","dealFacts","importFacts","isCrmImportFactsCollection","leadFacts","crmSuiteRefusal","contactPrimaryGroup","CONTACTS_COLLECTION","CRM_FACTS_ACTIVITIES_READ","CRM_FACTS_TASKS_READ","CRM_FACTS_DEALS_READ","CRM_FACTS_FIELD_DEFINITIONS_READ","CRM_FACTS_UNKNOWN_SITE","CRM_FACTS_NOT_A_MEMBER","CRM_FACTS_SITE_REFUSAL","CRM_FACTS_ORG_REFUSAL","CRM_FACTS_LEAD_NEEDS_SITE","NOT_FOUND","contact","company","deal","lead","import","firestore","app","crmFactsScope","request","orgId","String","trim","hostId","ok","status","error","staff","member","membership","uid","catch","reaches","Boolean","hostAccess","allowed","org","suite","body","group","visible","readable","Set","visibleTo","Array","isArray","some","token","has","orgCollection","name","collection","doc","rowsNaming","scope","input","snapshot","where","field","id","orderBy","direction","limit","get","docs","map","data","filter","row","pipelinesFor","deals","ids","pipelineId","snapshots","Promise","all","pipelines","Map","exists","visibleDoc","refused","kind","reader","read","crmContactFactsReader","naming","activities","tasks","facts","nowMs","now","getTime","crmCompanyFactsReader","companies","crmDealFactsReader","pipeline","crmLeadFactsReader","crmImportFactsReader","contactFields","definitions","definition","CRM_FACTS_READERS","registerCrmRecordFactsReaders","resource","factsReader","Object","entries","pluginId"],"mappings":"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"}
|
|
@@ -42,6 +42,18 @@ import { type PluginRecordTimelineWriter } from '@aglyn/aglyn/plugin-manager/plu
|
|
|
42
42
|
* Each entry names the plugin that filed it (`sourcePluginId`), and a task's
|
|
43
43
|
* `nextTaskAtMs` is recomputed on the records it names, as every server
|
|
44
44
|
* writer of a task does.
|
|
45
|
+
*
|
|
46
|
+
* ## A record found rather than named
|
|
47
|
+
*
|
|
48
|
+
* A caller that holds no id of the CRM's — a booking made through a link a
|
|
49
|
+
* rep dropped from a record, or one taken off the widget cold (AGL-2660) —
|
|
50
|
+
* hands back the record the link carried, or the booker's address, and the
|
|
51
|
+
* entry is filed where the CRM finds it: the record the reference names
|
|
52
|
+
* when this site may see it, otherwise the contact the address names
|
|
53
|
+
* through the org's address index, narrowed to the site. A deal carries its
|
|
54
|
+
* contact and company onto the entry; a contact its company, from the
|
|
55
|
+
* site's own facet. A task with no assignee of the caller's goes to whoever
|
|
56
|
+
* holds that record.
|
|
45
57
|
*/
|
|
46
58
|
type Firestore = FirebaseFirestore.Firestore;
|
|
47
59
|
export interface CrmRecordTimelineDeps {
|
|
@@ -15,11 +15,12 @@ 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 {
|
|
19
|
-
import { countCrmActivitiesForRecord, createCrmEmailActivity, crmActivityRef, crmCapturedEmailActivityRef, firebaseAdmin, recomputeCrmNextTaskAt, recordCrmEmailDelivery } from "@aglyn/tenant-data-admin";
|
|
18
|
+
import { checkEntitlement, consentGroupForHost, CRM_ACTIVITY_LOG_FULL_MESSAGE, CRM_COLLECTIONS, crmActivityLogHasRoom, crmScopeTokens, crmTaskListFields, crmTaskReminderAfterEdit, isCrmActivityKind, isCrmTaskKind, readContactFacet, visibleToHost } from "@aglyn/aglyn/server";
|
|
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
|
+
import { buildCrmCapturedEmailActivity, crmCapturedEmailKey } from "../model/crm-inbound.js";
|
|
23
24
|
import { CRM_SUITE_FEATURE } from "./suite-gate.js";
|
|
24
25
|
/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */ const ALREADY_EXISTS = 6;
|
|
25
26
|
/** The longest task title the CRM keeps, as its own forms do. */ const TASK_TITLE_MAX = 200;
|
|
@@ -34,26 +35,123 @@ const refuse = (status, error)=>({
|
|
|
34
35
|
contactId: String(link.contactId)
|
|
35
36
|
} : {}, (link == null ? void 0 : link.companyId) ? {
|
|
36
37
|
companyId: String(link.companyId)
|
|
37
|
-
} : {}, (link == null ? void 0 : link.dealId) ? {
|
|
38
|
-
dealId: String(link.dealId)
|
|
39
38
|
} : {}, (link == null ? void 0 : link.leadId) ? {
|
|
40
39
|
leadId: String(link.leadId)
|
|
41
40
|
} : {});
|
|
42
41
|
}
|
|
42
|
+
/** What a reference handed back by a caller may name: the records a booking link is dropped from. */ const FOUND_RECORD_KINDS = new Set([
|
|
43
|
+
'contact',
|
|
44
|
+
'lead',
|
|
45
|
+
'deal'
|
|
46
|
+
]);
|
|
47
|
+
/**
|
|
48
|
+
* A record id as the CRM mints them: Firestore's for a contact and a deal, a
|
|
49
|
+
* person key for a lead. A reference arrives on a public request, so one
|
|
50
|
+
* that could not be an id is passed over rather than read.
|
|
51
|
+
*/ const RECORD_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
52
|
+
/** A contact's link and holder, from this site's facet on it. */ function contactRecord(contact, groupId) {
|
|
53
|
+
var _contact_data;
|
|
54
|
+
const facet = readContactFacet((_contact_data = contact.data()) != null ? _contact_data : {}, groupId);
|
|
55
|
+
return _extends({
|
|
56
|
+
link: _extends({
|
|
57
|
+
contactId: contact.id
|
|
58
|
+
}, facet.companyId ? {
|
|
59
|
+
companyId: facet.companyId
|
|
60
|
+
} : {})
|
|
61
|
+
}, facet.ownerUid ? {
|
|
62
|
+
holderUid: facet.ownerUid
|
|
63
|
+
} : {});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The record a caller's reference names, then the contact its address does,
|
|
67
|
+
* as `hostId` may see them; `null` when neither finds one. See the module
|
|
68
|
+
* comment.
|
|
69
|
+
*/ async function findRecord(orgRef, hostId, groupId, link) {
|
|
70
|
+
var _ref, _ref1, _ref2;
|
|
71
|
+
var _link_record, _link_record1;
|
|
72
|
+
const contacts = orgRef.collection('contacts');
|
|
73
|
+
const kind = String((_ref = link == null ? void 0 : (_link_record = link.record) == null ? void 0 : _link_record.kind) != null ? _ref : '');
|
|
74
|
+
const id = String((_ref1 = link == null ? void 0 : (_link_record1 = link.record) == null ? void 0 : _link_record1.id) != null ? _ref1 : '');
|
|
75
|
+
if (FOUND_RECORD_KINDS.has(kind) && RECORD_ID_PATTERN.test(id)) {
|
|
76
|
+
if (kind === 'contact') {
|
|
77
|
+
const contact = await contacts.doc(id).get();
|
|
78
|
+
if (contact.exists && visibleToHost(contact.get('visibleTo'), hostId)) {
|
|
79
|
+
return contactRecord(contact, groupId);
|
|
80
|
+
}
|
|
81
|
+
} else if (kind === 'deal') {
|
|
82
|
+
const deal = await orgRef.collection(CRM_COLLECTIONS.deals).doc(id).get();
|
|
83
|
+
if (deal.exists && visibleToHost(deal.get('visibleTo'), hostId)) {
|
|
84
|
+
var _deal_get, _deal_get1, _deal_get2;
|
|
85
|
+
const contactId = String((_deal_get = deal.get('contactId')) != null ? _deal_get : '');
|
|
86
|
+
const companyId = String((_deal_get1 = deal.get('companyId')) != null ? _deal_get1 : '');
|
|
87
|
+
const ownerUid = String((_deal_get2 = deal.get('ownerUid')) != null ? _deal_get2 : '');
|
|
88
|
+
return _extends({
|
|
89
|
+
link: _extends({
|
|
90
|
+
dealId: deal.id
|
|
91
|
+
}, contactId ? {
|
|
92
|
+
contactId
|
|
93
|
+
} : {}, companyId ? {
|
|
94
|
+
companyId
|
|
95
|
+
} : {})
|
|
96
|
+
}, ownerUid ? {
|
|
97
|
+
holderUid: ownerUid
|
|
98
|
+
} : {});
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// A lead carries its own `visibleTo` (AGL-3275), and the site has to be
|
|
102
|
+
// allowed to see it, or one brand's booking would name another's lead.
|
|
103
|
+
const lead = await readLeadForHost(hostId, id);
|
|
104
|
+
if (lead && visibleToHost(lead.get('visibleTo'), hostId)) {
|
|
105
|
+
var _lead_get;
|
|
106
|
+
const ownerUid = String((_lead_get = lead.get('ownerUid')) != null ? _lead_get : '');
|
|
107
|
+
return _extends({
|
|
108
|
+
link: {
|
|
109
|
+
leadId: lead.id
|
|
110
|
+
}
|
|
111
|
+
}, ownerUid ? {
|
|
112
|
+
holderUid: ownerUid
|
|
113
|
+
} : {});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const email = String((_ref2 = link == null ? void 0 : link.email) != null ? _ref2 : '').trim();
|
|
118
|
+
if (!email) return null;
|
|
119
|
+
const hit = await findContactByEmail(contacts, email, {
|
|
120
|
+
hostId
|
|
121
|
+
});
|
|
122
|
+
return hit ? contactRecord(hit, groupId) : null;
|
|
123
|
+
}
|
|
124
|
+
/** Who holds a record a caller named by id: its contact's site facet, or its lead. */ async function holderOf(orgRef, hostId, groupId, link) {
|
|
125
|
+
if (link.contactId) {
|
|
126
|
+
const contact = await orgRef.collection('contacts').doc(link.contactId).get();
|
|
127
|
+
return contact.exists ? contactRecord(contact, groupId).holderUid : undefined;
|
|
128
|
+
}
|
|
129
|
+
if (link.leadId) {
|
|
130
|
+
var _ref;
|
|
131
|
+
const lead = await readLeadForHost(hostId, link.leadId);
|
|
132
|
+
return String((_ref = lead == null ? void 0 : lead.get('ownerUid')) != null ? _ref : '') || undefined;
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
43
136
|
/** The document id a caller's key files under: its own namespace, never a raw key. */ function keyedId(sourcePluginId, key) {
|
|
44
137
|
return `plg_${createHash('sha256').update(`${sourcePluginId}:${key}`).digest('hex').slice(0, 28)}`;
|
|
45
138
|
}
|
|
46
139
|
export function createCrmRecordTimelineWriter(deps) {
|
|
47
|
-
/**
|
|
48
|
-
|
|
140
|
+
/**
|
|
141
|
+
* The org, its plan, the link and the scope an entry is stamped with — and,
|
|
142
|
+
* when `wantHolder`, who holds the record it lands on.
|
|
143
|
+
*/ async function admit(context, wantHolder = false) {
|
|
144
|
+
var _context_orgId, _context_hostId, _context_sourcePluginId, _ref, _snapshot_data;
|
|
145
|
+
var _context_link, _context_link1;
|
|
49
146
|
const orgId = String((_context_orgId = context.orgId) != null ? _context_orgId : '').trim();
|
|
50
147
|
const hostId = String((_context_hostId = context.hostId) != null ? _context_hostId : '').trim();
|
|
51
|
-
const
|
|
148
|
+
const named = linkFields(context.link);
|
|
149
|
+
const isNamed = Boolean(named.contactId || named.companyId || named.leadId);
|
|
52
150
|
if (!orgId || !hostId || !String((_context_sourcePluginId = context.sourcePluginId) != null ? _context_sourcePluginId : '').trim()) {
|
|
53
151
|
return refuse(400, 'An entry names its organization, its site and the plugin filing it.');
|
|
54
152
|
}
|
|
55
|
-
if (!
|
|
56
|
-
return refuse(400, 'An entry is filed on a contact, a company
|
|
153
|
+
if (!isNamed && !((_context_link = context.link) == null ? void 0 : _context_link.record) && !String((_ref = (_context_link1 = context.link) == null ? void 0 : _context_link1.email) != null ? _ref : '').trim()) {
|
|
154
|
+
return refuse(400, 'An entry is filed on a contact, a company or a lead, or names how to find one.');
|
|
57
155
|
}
|
|
58
156
|
const firestore = deps.firestore();
|
|
59
157
|
const orgRef = firestore.collection('orgs').doc(orgId);
|
|
@@ -63,12 +161,25 @@ export function createCrmRecordTimelineWriter(deps) {
|
|
|
63
161
|
if (!checkEntitlement(org, CRM_SUITE_FEATURE)) {
|
|
64
162
|
return refuse(403, "This workspace's plan doesn't include the CRM.");
|
|
65
163
|
}
|
|
66
|
-
|
|
164
|
+
const group = consentGroupForHost(org, hostId);
|
|
165
|
+
let link = named;
|
|
166
|
+
let holderUid;
|
|
167
|
+
if (isNamed) {
|
|
168
|
+
if (wantHolder) holderUid = await holderOf(orgRef, hostId, group.groupId, named);
|
|
169
|
+
} else {
|
|
170
|
+
const found = await findRecord(orgRef, hostId, group.groupId, context.link);
|
|
171
|
+
if (!found) return refuse(404, 'Nothing this site keeps matches the record or the address.');
|
|
172
|
+
link = found.link;
|
|
173
|
+
holderUid = found.holderUid;
|
|
174
|
+
}
|
|
175
|
+
return _extends({
|
|
67
176
|
firestore,
|
|
68
177
|
orgRef,
|
|
69
178
|
link,
|
|
70
|
-
visibleTo: crmScopeTokens(org,
|
|
71
|
-
}
|
|
179
|
+
visibleTo: crmScopeTokens(org, group)
|
|
180
|
+
}, holderUid ? {
|
|
181
|
+
holderUid
|
|
182
|
+
} : {});
|
|
72
183
|
}
|
|
73
184
|
/** Creates a keyed entry once: `created: false` for one already filed. */ async function createOnce(ref, data) {
|
|
74
185
|
try {
|
|
@@ -150,18 +261,19 @@ export function createCrmRecordTimelineWriter(deps) {
|
|
|
150
261
|
};
|
|
151
262
|
},
|
|
152
263
|
async createTask (request) {
|
|
153
|
-
var _request_title, _request_dedupeKey, _request_notes, _request_assigneeUid, _request_createdByUid;
|
|
264
|
+
var _request_title, _request_dedupeKey, _request_notes, _admitted_holderUid, _request_assigneeUid, _request_createdByUid;
|
|
154
265
|
const title = String((_request_title = request.title) != null ? _request_title : '').replace(/\s+/g, ' ').trim().slice(0, TASK_TITLE_MAX);
|
|
155
266
|
const key = String((_request_dedupeKey = request.dedupeKey) != null ? _request_dedupeKey : '').trim();
|
|
156
267
|
if (!title || !key) return refuse(400, 'A task has a title and the caller’s own key.');
|
|
157
268
|
if (!isCrmTaskKind(request.kind)) return refuse(400, `"${String(request.kind)}" isn't a task.`);
|
|
158
269
|
if (!Number.isFinite(request.dueAtMs)) return refuse(400, 'A task has a due time.');
|
|
159
|
-
const admitted = await admit(request);
|
|
270
|
+
const admitted = await admit(request, request.assigneeUid === null);
|
|
160
271
|
if ('ok' in admitted) return admitted;
|
|
161
272
|
const { firestore, orgRef, link, visibleTo } = admitted;
|
|
162
273
|
const ref = orgRef.collection(CRM_COLLECTIONS.tasks).doc(keyedId(request.sourcePluginId, key));
|
|
163
274
|
const notes = String((_request_notes = request.notes) != null ? _request_notes : '').trim().slice(0, NOTE_MAX);
|
|
164
|
-
|
|
275
|
+
// `null` is "whoever holds the record", which admission read.
|
|
276
|
+
const assigneeUid = request.assigneeUid === null ? (_admitted_holderUid = admitted.holderUid) != null ? _admitted_holderUid : '' : String((_request_assigneeUid = request.assigneeUid) != null ? _request_assigneeUid : '').trim();
|
|
165
277
|
const task = _extends({
|
|
166
278
|
title
|
|
167
279
|
}, notes ? {
|
|
@@ -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 buildCrmCapturedEmailActivity,\n checkEntitlement,\n consentGroupForHost,\n CRM_ACTIVITY_LOG_FULL_MESSAGE,\n CRM_COLLECTIONS,\n crmActivityLogHasRoom,\n crmCapturedEmailKey,\n crmScopeTokens,\n crmTaskListFields,\n crmTaskReminderAfterEdit,\n isCrmActivityKind,\n isCrmTaskKind,\n type CrmActivity,\n type CrmActivityLink,\n type CrmTask,\n} from '@aglyn/aglyn/server'\nimport {\n countCrmActivitiesForRecord,\n createCrmEmailActivity,\n crmActivityRef,\n crmCapturedEmailActivityRef,\n firebaseAdmin,\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 { 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\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 ...(link?.dealId ? { dealId: String(link.dealId) } : {}),\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/** 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 /** The org, its plan, the link and the scope an entry is stamped with. */\n async function admit(\n context: PluginRecordEntryContext,\n ): Promise<\n | PluginRecordWrite\n | { firestore: Firestore; orgRef: FirebaseFirestore.DocumentReference; link: CrmActivityLink; visibleTo: string[] }\n > {\n const orgId = String(context.orgId ?? '').trim()\n const hostId = String(context.hostId ?? '').trim()\n const link = linkFields(context.link)\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 (!link.contactId && !link.companyId && !link.dealId && !link.leadId) {\n return refuse(400, 'An entry is filed on a contact, a company, a deal or a lead.')\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 return {\n firestore,\n orgRef,\n link,\n visibleTo: crmScopeTokens(org, consentGroupForHost(org, hostId)),\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)\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 const assigneeUid = 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","buildCrmCapturedEmailActivity","checkEntitlement","consentGroupForHost","CRM_ACTIVITY_LOG_FULL_MESSAGE","CRM_COLLECTIONS","crmActivityLogHasRoom","crmCapturedEmailKey","crmScopeTokens","crmTaskListFields","crmTaskReminderAfterEdit","isCrmActivityKind","isCrmTaskKind","countCrmActivitiesForRecord","createCrmEmailActivity","crmActivityRef","crmCapturedEmailActivityRef","firebaseAdmin","recomputeCrmNextTaskAt","recordCrmEmailDelivery","createHash","FieldValue","BUNDLE_ID","CRM_SUITE_FEATURE","ALREADY_EXISTS","TASK_TITLE_MAX","NOTE_MAX","refuse","status","error","ok","linkFields","link","contactId","String","companyId","dealId","leadId","keyedId","sourcePluginId","key","update","digest","slice","createCrmRecordTimelineWriter","deps","admit","context","snapshot","orgId","trim","hostId","firestore","orgRef","collection","doc","get","exists","org","data","visibleTo","createOnce","ref","create","createdAt","serverTimestamp","updatedAt","code","logActivity","request","kind","admitted","id","activity","email","messageId","direction","subject","excerpt","body","from","to","inReplyTo","atMs","byUid","byName","dedupeKey","created","stamped","createTask","title","replace","Number","isFinite","dueAtMs","tasks","notes","assigneeUid","task","priority","remindAtMs","previous","createdByUid","catch","console","recordEmailDelivery","state","outcome","activityId","detail","defaultCrmRecordTimelineDeps","app","registerCrmRecordTimelineWriter","pluginId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,kCAAkC,QAO7B,qDAAoD;AAC3D,SACEC,6BAA6B,EAC7BC,gBAAgB,EAChBC,mBAAmB,EACnBC,6BAA6B,EAC7BC,eAAe,EACfC,qBAAqB,EACrBC,mBAAmB,EACnBC,cAAc,EACdC,iBAAiB,EACjBC,wBAAwB,EACxBC,iBAAiB,EACjBC,aAAa,QAIR,sBAAqB;AAC5B,SACEC,2BAA2B,EAC3BC,sBAAsB,EACtBC,cAAc,EACdC,2BAA2B,EAC3BC,aAAa,EACbC,sBAAsB,EACtBC,sBAAsB,QACjB,2BAA0B;AACjC,SAASC,UAAU,QAAQ,SAAQ;AACnC,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,iBAAiB,QAAQ,kBAAc;AAqChD,qFAAqF,GACrF,MAAMC,iBAAiB;AAEvB,+DAA+D,GAC/D,MAAMC,iBAAiB;AACvB,kEAAkE,GAClE,MAAMC,WAAW;AAEjB,MAAMC,SAAS,CAACC,QAA+BC,QAAsC,CAAA;QACnFC,IAAI;QACJF;QACAC;IACF,CAAA;AAEA,yDAAyD,GACzD,SAASE,WAAWC,IAAsC;IACxD,OAAO,aACDA,CAAAA,wBAAAA,KAAMC,SAAS,IAAG;QAAEA,WAAWC,OAAOF,KAAKC,SAAS;IAAE,IAAI,CAAC,GAC3DD,CAAAA,wBAAAA,KAAMG,SAAS,IAAG;QAAEA,WAAWD,OAAOF,KAAKG,SAAS;IAAE,IAAI,CAAC,GAC3DH,CAAAA,wBAAAA,KAAMI,MAAM,IAAG;QAAEA,QAAQF,OAAOF,KAAKI,MAAM;IAAE,IAAI,CAAC,GAGlDJ,CAAAA,wBAAAA,KAAMK,MAAM,IAAG;QAAEA,QAAQH,OAAOF,KAAKK,MAAM;IAAE,IAAI,CAAC;AAE1D;AAEA,oFAAoF,GACpF,SAASC,QAAQC,cAAsB,EAAEC,GAAW;IAClD,OAAO,CAAC,IAAI,EAAEpB,WAAW,UAAUqB,MAAM,CAAC,GAAGF,eAAe,CAAC,EAAEC,KAAK,EAAEE,MAAM,CAAC,OAAOC,KAAK,CAAC,GAAG,KAAK;AACpG;AAEA,OAAO,SAASC,8BAA8BC,IAA2B;IACvE,wEAAwE,GACxE,eAAeC,MACbC,OAAiC;YAKZA,gBACCA,iBAEWA,yBAUpBC;QAbb,MAAMC,QAAQf,QAAOa,iBAAAA,QAAQE,KAAK,YAAbF,iBAAiB,IAAIG,IAAI;QAC9C,MAAMC,SAASjB,QAAOa,kBAAAA,QAAQI,MAAM,YAAdJ,kBAAkB,IAAIG,IAAI;QAChD,MAAMlB,OAAOD,WAAWgB,QAAQf,IAAI;QACpC,IAAI,CAACiB,SAAS,CAACE,UAAU,CAACjB,QAAOa,0BAAAA,QAAQR,cAAc,YAAtBQ,0BAA0B,IAAIG,IAAI,IAAI;YACrE,OAAOvB,OAAO,KAAK;QACrB;QACA,IAAI,CAACK,KAAKC,SAAS,IAAI,CAACD,KAAKG,SAAS,IAAI,CAACH,KAAKI,MAAM,IAAI,CAACJ,KAAKK,MAAM,EAAE;YACtE,OAAOV,OAAO,KAAK;QACrB;QACA,MAAMyB,YAAYP,KAAKO,SAAS;QAChC,MAAMC,SAASD,UAAUE,UAAU,CAAC,QAAQC,GAAG,CAACN;QAChD,MAAMD,WAAW,MAAMK,OAAOG,GAAG;QACjC,IAAI,CAACR,SAASS,MAAM,EAAE,OAAO9B,OAAO,KAAK;QACzC,MAAM+B,OAAOV,iBAAAA,SAASW,IAAI,cAAbX,iBAAmB,CAAC;QACjC,IAAI,CAAC9C,iBAAiBwD,KAAKnC,oBAAoB;YAC7C,OAAOI,OAAO,KAAK;QACrB;QACA,OAAO;YACLyB;YACAC;YACArB;YACA4B,WAAWpD,eAAekD,KAAKvD,oBAAoBuD,KAAKP;QAC1D;IACF;IAEA,wEAAwE,GACxE,eAAeU,WACbC,GAAwC,EACxCH,IAA6B;QAE7B,IAAI;YACF,MAAMG,IAAIC,MAAM,CAAC,aACZJ;gBACHK,WAAW3C,WAAW4C,eAAe;gBACrCC,WAAW7C,WAAW4C,eAAe;;YAEvC,OAAO;QACT,EAAE,OAAOpC,OAAO;YACd,IAAI,CAACA,yBAAD,AAACA,MAA8BsC,IAAI,MAAK3C,gBAAgB,OAAO;YACnE,MAAMK;QACR;IACF;IAEA,OAAO;QACL,MAAMuC,aAAYC,OAAoC;YACpD,IAAI,CAAC1D,kBAAkB0D,QAAQC,IAAI,GAAG,OAAO3C,OAAO,KAAK,CAAC,CAAC,EAAEO,OAAOmC,QAAQC,IAAI,EAAE,oBAAoB,CAAC;YACvG,MAAMC,WAAW,MAAMzB,MAAMuB;YAC7B,IAAI,QAAQE,UAAU,OAAOA;YAC7B,MAAM,EAAEnB,SAAS,EAAEC,MAAM,EAAErB,IAAI,EAAE4B,SAAS,EAAE,GAAGW;YAC/C,MAAMtB,QAAQI,OAAOmB,EAAE;YAEvB,IAAIV;YACJ,IAAIW;YACJ,IAAIJ,QAAQC,IAAI,KAAK,SAAS;oBAYfI,kBAEGL,gBACNA;gBAdV,MAAMK,QAAQL,QAAQK,KAAK;gBAC3B,MAAMlC,MAAMkC,QAAQnE,oBAAoBmE,MAAMC,SAAS,EAAE,QAAQ;gBACjE,IAAI,CAACD,SAAS,CAAClC,KAAK,OAAOb,OAAO,KAAK;gBACvCmC,MAAM9C,4BAA4BoC,WAAWH,OAAOT;gBACpDiC,WAAWxE,8BAA8B;oBACvC2E,WAAWF,MAAME,SAAS,KAAK,YAAY,YAAY;oBACvDC,SAASH,MAAMG,OAAO;oBACtBC,SAAST,QAAQU,IAAI;oBACrBC,MAAMN,MAAMM,IAAI;oBAChBC,IAAIP,MAAMO,EAAE;oBACZN,WAAWD,MAAMC,SAAS;oBAC1BO,SAAS,GAAER,mBAAAA,MAAMQ,SAAS,YAAfR,mBAAmB;oBAC9BS,MAAMd,QAAQc,IAAI;oBAClBC,OAAOlD,QAAOmC,iBAAAA,QAAQe,KAAK,YAAbf,iBAAiB;oBAC/BgB,MAAM,GAAEhB,kBAAAA,QAAQgB,MAAM,YAAdhB,kBAAkB;oBAC1BrC;oBACAmB,QAAQkB,QAAQlB,MAAM;oBACtBS;gBACF;YACF,OAAO;oBACcS,oBAGGA,kBAGPA,eAECA;gBARhB,MAAM7B,MAAMN,QAAOmC,qBAAAA,QAAQiB,SAAS,YAAjBjB,qBAAqB,IAAInB,IAAI;gBAChD,IAAI,CAACV,KAAK,OAAOb,OAAO,KAAK;gBAC7BmC,MAAM/C,eAAeqC,WAAWH,OAAOX,QAAQ+B,QAAQ9B,cAAc,EAAEC;gBACvE,MAAM6C,SAASnD,QAAOmC,mBAAAA,QAAQgB,MAAM,YAAdhB,mBAAkB,IAAInB,IAAI;gBAChDuB,WAAW;oBACTH,MAAMD,QAAQC,IAAI;oBAClBS,MAAM7C,QAAOmC,gBAAAA,QAAQU,IAAI,YAAZV,gBAAgB,IAAI1B,KAAK,CAAC,GAAGjB;oBAC1CyD,MAAMd,QAAQc,IAAI;oBAClBC,OAAOlD,QAAOmC,kBAAAA,QAAQe,KAAK,YAAbf,kBAAiB;mBAC3BgB,SAAS;oBAAEA;gBAAO,IAAI,CAAC,GACxBrD;oBACHmB,QAAQkB,QAAQlB,MAAM;oBACtBS;;YAEJ;YACA,yEAAyE;YACzE,IAAI,AAAC,CAAA,MAAME,IAAIN,GAAG,EAAC,EAAGC,MAAM,EAAE,OAAO;gBAAE3B,IAAI;gBAAM0C,IAAIV,IAAIU,EAAE;gBAAEe,SAAS;YAAM;YAC5E,IAAI,CAACjF,sBAAsB,MAAMO,4BAA4BwC,QAAQrB,QAAQ;gBAC3E,OAAOL,OAAO,KAAKvB;YACrB;YACA,MAAMoF,UAAU,aAAKf;gBAAUlC,gBAAgB8B,QAAQ9B,cAAc;;YACrE,MAAMgD,UACJlB,QAAQC,IAAI,KAAK,UACb,AAAC,MAAMxD,uBAAuBgD,KAAK0B,aAAc,YACjD,MAAM3B,WAAWC,KAAK0B;YAC5B,OAAO;gBAAE1D,IAAI;gBAAM0C,IAAIV,IAAIU,EAAE;gBAAEe;YAAQ;QACzC;QAEA,MAAME,YAAWpB,OAAgC;gBAC1BA,gBACFA,oBAQEA,gBACMA,sBAWJA;YArBvB,MAAMqB,QAAQxD,QAAOmC,iBAAAA,QAAQqB,KAAK,YAAbrB,iBAAiB,IAAIsB,OAAO,CAAC,QAAQ,KAAKzC,IAAI,GAAGP,KAAK,CAAC,GAAGlB;YAC/E,MAAMe,MAAMN,QAAOmC,qBAAAA,QAAQiB,SAAS,YAAjBjB,qBAAqB,IAAInB,IAAI;YAChD,IAAI,CAACwC,SAAS,CAAClD,KAAK,OAAOb,OAAO,KAAK;YACvC,IAAI,CAACf,cAAcyD,QAAQC,IAAI,GAAG,OAAO3C,OAAO,KAAK,CAAC,CAAC,EAAEO,OAAOmC,QAAQC,IAAI,EAAE,eAAe,CAAC;YAC9F,IAAI,CAACsB,OAAOC,QAAQ,CAACxB,QAAQyB,OAAO,GAAG,OAAOnE,OAAO,KAAK;YAC1D,MAAM4C,WAAW,MAAMzB,MAAMuB;YAC7B,IAAI,QAAQE,UAAU,OAAOA;YAC7B,MAAM,EAAEnB,SAAS,EAAEC,MAAM,EAAErB,IAAI,EAAE4B,SAAS,EAAE,GAAGW;YAC/C,MAAMT,MAAMT,OAAOC,UAAU,CAACjD,gBAAgB0F,KAAK,EAAExC,GAAG,CAACjB,QAAQ+B,QAAQ9B,cAAc,EAAEC;YACzF,MAAMwD,QAAQ9D,QAAOmC,iBAAAA,QAAQ2B,KAAK,YAAb3B,iBAAiB,IAAInB,IAAI,GAAGP,KAAK,CAAC,GAAGjB;YAC1D,MAAMuE,cAAc/D,QAAOmC,uBAAAA,QAAQ4B,WAAW,YAAnB5B,uBAAuB,IAAInB,IAAI;YAC1D,MAAMgD,OAAgB;gBACpBR;eACIM,QAAQ;gBAAEA;YAAM,IAAI,CAAC;gBACzB1B,MAAMD,QAAQC,IAAI;gBAClB6B,UAAU;gBACVvE,QAAQ;gBACRkE,SAASzB,QAAQyB,OAAO;gBACxB,8DAA8D;gBAC9DM,YAAY1F,yBAAyB;oBAAEoF,SAASzB,QAAQyB,OAAO;oBAAEO,UAAU;gBAAK;eAC5EJ,cAAc;gBAAEA;YAAY,IAAI,CAAC;gBACrCK,cAAcpE,QAAOmC,wBAAAA,QAAQiC,YAAY,YAApBjC,wBAAwB;gBAC7C9B,gBAAgB8B,QAAQ9B,cAAc;eACnCP;gBACHmB,QAAQkB,QAAQlB,MAAM;gBACtBS;;YAEF,+DAA+D;YAC/D,MAAM2B,UAAU,MAAM1B,WAAWC,KAAK,aAAKoC,MAASzF,kBAAkByF;YACtE,IAAIX,SAAS;gBACX,uEAAuE;gBACvE,2DAA2D;gBAC3D,MAAMrE,uBAAuBkC,WAAWC,OAAOmB,EAAE,EAAE;oBAACxC;iBAAK,EAAEuE,KAAK,CAAC,CAAC1E;oBAChE2E,QAAQ3E,KAAK,CAAC,+CAA+CwB,OAAOmB,EAAE,EAAE3C;gBAC1E;YACF;YACA,OAAO;gBAAEC,IAAI;gBAAM0C,IAAIV,IAAIU,EAAE;gBAAEe;YAAQ;QACzC;QAEA,MAAMkB,qBAAoBpC,OAAoC;gBACvCA,gBAaXA;YAbV,MAAMpB,QAAQf,QAAOmC,iBAAAA,QAAQpB,KAAK,YAAboB,iBAAiB,IAAInB,IAAI;YAC9C,MAAMV,MAAMjC,oBAAoB8D,QAAQM,SAAS,EAAE;YACnD,IAAI,CAAC1B,SAAS,CAACT,KAAK,OAAOb,OAAO,KAAK;YACvC,IAAI0C,QAAQqC,KAAK,KAAK,aAAarC,QAAQqC,KAAK,KAAK,cAAc;gBACjE,OAAO/E,OAAO,KAAK,CAAC,CAAC,EAAEO,OAAOmC,QAAQqC,KAAK,EAAE,2BAA2B,CAAC;YAC3E;YACA,MAAMtD,YAAYP,KAAKO,SAAS;YAChC,MAAMU,MAAM9C,4BAA4BoC,WAAWH,OAAOT;YAC1D,MAAMmE,UAAU,MAAMxF,uBAAuBiC,WAAW;gBACtDH;gBACA2D,YAAY9C,IAAIU,EAAE;gBAClBkC,OAAOrC,QAAQqC,KAAK;gBACpBvB,MAAMd,QAAQc,IAAI;gBAClB0B,MAAM,GAAExC,kBAAAA,QAAQwC,MAAM,YAAdxC,kBAAkB;YAC5B;YACA,IAAIsC,YAAY,WAAW,OAAOhF,OAAO,KAAK;YAC9C,IAAIgF,YAAY,UAAU,OAAOhF,OAAO,KAAK;YAC7C,OAAO;gBAAEG,IAAI;gBAAM0C,IAAIV,IAAIU,EAAE;gBAAEe,SAASoB,YAAY;YAAW;QACjE;IACF;AACF;AAEA,4DAA4D,GAC5D,OAAO,SAASG;IACd,OAAO;QAAE1D,WAAW,IAAMnC,cAAc8F,GAAG,GAAG3D,SAAS;IAAG;AAC5D;AAEA,6EAA6E,GAC7E,OAAO,SAAS4D,gCACdnE,OAA8BiE,8BAA8B;IAE5D9G,mCAAmC4C,8BAA8BC,OAAO;QAAEoE,UAAU3F;IAAU;AAChG"}
|
|
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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"}
|
|
@@ -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
|
+
* A task's reminder at its own time (AGL-2659): `POST /api/crm/task-reminders`.
|
|
19
|
+
*
|
|
20
|
+
* The digest beside this route says once a morning what is late and what
|
|
21
|
+
* is due today; nothing said "the 3:00 call is at 3:00". Every hour this
|
|
22
|
+
* reads, per org, the open tasks whose `remindAtMs` has come and tells
|
|
23
|
+
* each one's assignee ONCE — a console notification per task
|
|
24
|
+
* (`content.taskReminder`, opening the record the task is for) and one
|
|
25
|
+
* email per member per run listing everything of theirs that fell due.
|
|
26
|
+
* The words and the rule live in `crm-task-reminders.ts`; this file is the
|
|
27
|
+
* reads, the sends and the sweep, in the digest's shape wherever the two
|
|
28
|
+
* do the same thing.
|
|
29
|
+
*
|
|
30
|
+
* ## Who is told, and who is not
|
|
31
|
+
*
|
|
32
|
+
* The task's ASSIGNEE, and nobody else — an unassigned task is a deliberate
|
|
33
|
+
* choice in the drawer, as the digest reads it. Only a member who may open
|
|
34
|
+
* the CRM (`data.manage`, resolved through their custom role), only in an
|
|
35
|
+
* org whose plan carries the suite and whose `release_crm` flag is
|
|
36
|
+
* on: the plan gate is the digest's own (`crmDigestEntitled`), so a plan
|
|
37
|
+
* that gets no digest gets no reminders either. The recipient's
|
|
38
|
+
* operational-category mute (`notificationPrefs.content`) governs the WHOLE
|
|
39
|
+
* reminder, mail included — unlike the digest, which has a switch of its
|
|
40
|
+
* own for the schedule it keeps; a reminder is one message about one task.
|
|
41
|
+
*
|
|
42
|
+
* ## Once, and resumable
|
|
43
|
+
*
|
|
44
|
+
* `reminderSentAtMs` on the task is the idempotence: stamped in one batch
|
|
45
|
+
* with the member's sends, so a rerun over the same hour reads the mark and
|
|
46
|
+
* sends nothing, and stamped on every reminder the run HANDLED — sent, or
|
|
47
|
+
* found nobody to send it to — so a reminder nobody can be told about does
|
|
48
|
+
* not come back every hour. A reminder is left unstamped only when the run
|
|
49
|
+
* never reached a verdict: the send-rate governor refused, or the org
|
|
50
|
+
* threw. Orgs are swept in id order behind a cursor, as the digest sweeps
|
|
51
|
+
* them, so the workflow's `done:false` loop carries a platform of any
|
|
52
|
+
* size across invocations.
|
|
53
|
+
*
|
|
54
|
+
* The task query reads the NEWEST reminders that have come due, not the
|
|
55
|
+
* oldest — see `CRM_TASK_REMINDER_CEILING` for why that is what makes a
|
|
56
|
+
* bounded page safe against a window that only ever grows.
|
|
57
|
+
*
|
|
58
|
+
* The email is a `bulk` send under the platform send-rate governor
|
|
59
|
+
* (AGL-2409), metered as PLATFORM mail, for the digest's reasons: a cron
|
|
60
|
+
* fan-out on the shared sending domain, and the platform reminding a
|
|
61
|
+
* member is not the org's sending. A refusal STOPS the run and reports
|
|
62
|
+
* `done: true`; the refused member was never stamped, and the next hour's
|
|
63
|
+
* run finds their reminders at the top of the window again.
|
|
64
|
+
*/
|
|
65
|
+
/** The beat `/api/health/crons` reads — the `SCHEDULED_JOBS` row's id. */
|
|
66
|
+
export declare const CRM_TASK_REMINDERS_JOB_ID = "crm-task-reminders";
|
|
67
|
+
/**
|
|
68
|
+
* Orgs per invocation.
|
|
69
|
+
*
|
|
70
|
+
* More than the digest's twenty-five: an org here costs one task query and,
|
|
71
|
+
* only when something fell due, the roster and the sends. The common hour
|
|
72
|
+
* finds nothing due in most orgs, and a chunk that reads a hundred empty
|
|
73
|
+
* queries finishes well inside `maxDuration = 60`.
|
|
74
|
+
*/
|
|
75
|
+
export declare const CRM_TASK_REMINDERS_ORG_CHUNK = 100;
|
|
76
|
+
/**
|
|
77
|
+
* The most members per org whose address is chased through the auth pools
|
|
78
|
+
* when their member document carries none — the digest's bound, for the
|
|
79
|
+
* digest's reason: work, not audience.
|
|
80
|
+
*/
|
|
81
|
+
export declare const CRM_TASK_REMINDERS_MAX_UID_LOOKUPS = 5;
|
|
82
|
+
export declare function crmTaskRemindersRoute(request: Request): Promise<Response>;
|