@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/server/api-v1/openapi.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 type ApiV1ResourceDescription,\n booleanField,\n integerField,\n isoField,\n nullableField,\n objectKindField,\n openObjectField,\n postalAddressField,\n queryParam,\n RECORD_STAMPS,\n stringField,\n stringListField,\n UPDATED_AFTER_PARAM,\n} from '@aglyn/tenant-data-admin/server/api-v1-description'\n\n/**\n * The CRM's resources as the customer API's OpenAPI document describes them\n * (AGL-2733), keyed by the resource each describes — what the console's\n * builder turns into their paths, tags and schemas, and the MCP tools derive\n * from.\n *\n * Transcribed from the response examples in `apps/docs/api/resources/*.md`,\n * the customer-facing contract, as the platform's own resources are; each\n * write body lists its handler's own writable set. `api-v1-openapi.spec.ts`\n * holds the document to every endpoint the documentation promises.\n */\nexport const CRM_API_V1_DESCRIPTIONS: Readonly<Record<string, ApiV1ResourceDescription>> = {\n contacts: {\n tag: 'Contacts',\n description: 'People, organization-wide. Part of the CRM, which is included from Starter.',\n schemaName: 'Contact',\n required: ['id', 'object', 'email'],\n writable: [\n 'email', 'name', 'tags', 'notes', 'marketingConsent', 'consentSiteId', 'consentGroupId', 'custom',\n 'phone', 'jobTitle', 'companyId', 'address', 'ownerUid', 'lifecycleStage', 'mediaIds',\n ],\n writeOnly: {\n consentSiteId: stringField('The site this write is made on behalf of: where the person opted in, and whose profile the profile fields land on.'),\n consentGroupId: stringField('With `marketingConsent: true`: the consent group the person was shown when they opted in. The opt-in then covers every site of that group; without it, or when the site is no longer in that group, it covers `consentSiteId` alone.'),\n custom: openObjectField('Contact custom fields, keyed by field key.'),\n mediaIds: stringListField('Media library files attached by the named site, by id, at most 20. An empty array clears them.'),\n },\n writeNote: '`email` is accepted on create only — a `PATCH` that names it is a `400`, because a contact is identified by its email.',\n fields: {\n id: stringField('Contact id.'),\n object: objectKindField('contact'),\n email: stringField('Primary email. Unique per organization.'),\n name: stringField('Display name.'),\n tags: stringListField('Free-form tags.'),\n notes: stringField('Free-form notes.'),\n marketingConsent: booleanField('Whether the contact accepted marketing email.'),\n consentSites: stringListField('Site ids the consent was given on.'),\n sources: stringListField('Where this contact came from.'),\n phone: stringField('Telephone number.'),\n jobTitle: stringField('Job title.'),\n companyId: nullableField(stringField('Primary company.')),\n address: postalAddressField(),\n ownerUid: nullableField(stringField('Owning user. Checked on the page, not the query.')),\n lifecycleStage: nullableField(stringField('CRM lifecycle stage.')),\n companyIds: stringListField('Every company this contact belongs to.'),\n alternateEmails: stringListField('Other addresses that resolve to this contact.'),\n ...RECORD_STAMPS,\n },\n ops: [\n {\n path: '/v1/contacts', method: 'get', operationId: 'listContacts', summary: 'List contacts', list: true, returns: 'Contact', entitlement: 'crm',\n description: 'Combining `email` with `tag` narrows on `email` and checks `tag` on the page, so pages can come back short. `lifecycleStage` and `ownerUid` live on a per-site profile and are ALWAYS checked on the page.',\n filters: [\n queryParam('email', 'Exact match, normalized the way the write path normalizes it. An unusable value is a 400.'),\n queryParam('tag', 'Checked on the page when combined with `email`.'),\n queryParam('lifecycleStage', 'Always checked on the page.'),\n queryParam('ownerUid', 'Always checked on the page.'),\n ],\n },\n { path: '/v1/contacts', method: 'post', operationId: 'createContact', summary: 'Create a contact', accepts: 'ContactWrite', returns: 'Contact', entitlement: 'crm', creates: true, description: 'A duplicate email is `409 conflict` (`code: \"contact_exists\"`), and the message names the existing id.' },\n { path: '/v1/contacts/{contactId}', method: 'get', operationId: 'getContact', summary: 'Retrieve a contact', returns: 'Contact', entitlement: 'crm', pathParams: [{ name: 'contactId', description: 'Contact id.' }] },\n { path: '/v1/contacts/{contactId}', method: 'patch', operationId: 'updateContact', summary: 'Update a contact', accepts: 'ContactWrite', returns: 'Contact', entitlement: 'crm', pathParams: [{ name: 'contactId', description: 'Contact id.' }] },\n { path: '/v1/contacts/{contactId}', method: 'delete', operationId: 'deleteContact', summary: 'Delete a contact', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'contactId', description: 'Contact id.' }] },\n { path: '/v1/contacts/{contactId}/merge', method: 'post', operationId: 'mergeContact', summary: 'Merge two contacts', accepts: 'ContactMerge', returns: 'Contact', entitlement: 'crm', pathParams: [{ name: 'contactId', description: 'The contact that survives.' }] },\n ],\n components: {\n ContactMerge: {\n type: 'object',\n required: ['sourceContactId'],\n description:\n 'Merge another contact into this one. The source is removed. Any other ' +\n 'member is a `400`.',\n properties: {\n sourceContactId: {\n type: 'string',\n description:\n 'The contact to merge FROM. It does not survive, and it must not be ' +\n 'the contact in the path.',\n },\n },\n additionalProperties: false,\n },\n },\n },\n companies: {\n tag: 'Companies',\n description: 'Organizations in the CRM.',\n schemaName: 'Company',\n required: ['id', 'object', 'name'],\n writable: ['name', 'domain', 'website', 'phone', 'address', 'industry', 'ownerUid', 'notes', 'custom', 'mediaIds', 'consentSiteId'],\n writeOnly: {\n mediaIds: stringListField('Media library files attached to this company, by id, at most 20. An empty array clears them.'),\n consentSiteId: stringField('The site the company is created on behalf of.'),\n },\n writeNote: 'A create needs `name` and `consentSiteId`; a `PATCH` that names `consentSiteId` is a `400`.',\n fields: {\n id: stringField('Company id.'),\n object: objectKindField('company'),\n name: stringField('Company name.'),\n domain: nullableField(stringField('Primary domain. Unique per organization.')),\n website: nullableField(stringField('Website URL.')),\n phone: nullableField(stringField('Telephone number.')),\n address: postalAddressField(),\n industry: nullableField(stringField('Industry label.')),\n ownerUid: nullableField(stringField('Owning user.')),\n notes: nullableField(stringField('Free-form notes.')),\n custom: openObjectField('Customer-defined fields.'),\n nextTaskAt: nullableField(isoField('When the next open task on this company is due.')),\n siteId: nullableField(stringField('Site the record originated on.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/companies', method: 'get', operationId: 'listCompanies', summary: 'List companies', list: true, returns: 'Company', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('domain', 'Exact match.'), queryParam('ownerUid', 'Owning user.')] },\n { path: '/v1/companies', method: 'post', operationId: 'createCompany', summary: 'Create a company', accepts: 'CompanyWrite', returns: 'Company', entitlement: 'crm', creates: true, description: 'A duplicate domain is `409 conflict` (`code: \"company_exists\"`), naming the existing id.' },\n { path: '/v1/companies/{companyId}', method: 'get', operationId: 'getCompany', summary: 'Retrieve a company', returns: 'Company', entitlement: 'crm', pathParams: [{ name: 'companyId', description: 'Company id.' }] },\n { path: '/v1/companies/{companyId}', method: 'patch', operationId: 'updateCompany', summary: 'Update a company', accepts: 'CompanyWrite', returns: 'Company', entitlement: 'crm', pathParams: [{ name: 'companyId', description: 'Company id.' }] },\n { path: '/v1/companies/{companyId}', method: 'delete', operationId: 'deleteCompany', summary: 'Delete a company', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'companyId', description: 'Company id.' }] },\n ],\n },\n pipelines: {\n tag: 'Pipelines',\n description: 'Deal pipelines and their stages. Read-only over the API.',\n schemaName: 'Pipeline',\n required: ['id', 'object', 'name', 'stages'],\n fields: {\n id: stringField('Pipeline id.'),\n object: objectKindField('pipeline'),\n name: stringField('Pipeline name.'),\n isDefault: booleanField('Whether new deals land here when none is named.'),\n archived: booleanField('Whether the pipeline is archived.'),\n archivedAt: nullableField(isoField('When it was archived.')),\n stages: {\n type: 'array',\n description: 'Ordered stages. A deal’s `stageId` names one of these.',\n items: {\n type: 'object',\n properties: { id: { type: 'string' }, name: { type: 'string' }, order: { type: 'integer' } },\n additionalProperties: true,\n },\n },\n siteId: nullableField(stringField('Site the pipeline belongs to, when scoped.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/pipelines', method: 'get', operationId: 'listPipelines', summary: 'List pipelines', list: true, returns: 'Pipeline', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM] },\n { path: '/v1/pipelines/{pipelineId}', method: 'get', operationId: 'getPipeline', summary: 'Retrieve a pipeline', returns: 'Pipeline', entitlement: 'crm', pathParams: [{ name: 'pipelineId', description: 'Pipeline id.' }] },\n ],\n },\n deals: {\n tag: 'Deals',\n description: 'Opportunities moving through a pipeline.',\n schemaName: 'Deal',\n required: ['id', 'object', 'title', 'pipelineId', 'stageId'],\n writable: ['title', 'pipelineId', 'stageId', 'status', 'amountCents', 'currency', 'lineItems', 'expectedCloseAt', 'ownerUid', 'contactId', 'companyId', 'lostReason', 'notes', 'custom', 'mediaIds', 'consentSiteId'],\n writeOnly: {\n mediaIds: stringListField('Media library files attached to this deal, by id, at most 20. An empty array clears them.'),\n consentSiteId: stringField('The site the deal is created on behalf of.'),\n },\n writeNote: '`pipelineId` and `consentSiteId` are accepted on create only; a `PATCH` that names either is a `400`.',\n fields: {\n id: stringField('Deal id.'),\n object: objectKindField('deal'),\n title: stringField('Deal title.'),\n pipelineId: stringField('Pipeline the deal sits in.'),\n stageId: stringField('Stage within that pipeline.'),\n status: stringField('`open`, `won` or `lost`.'),\n amountCents: integerField('Value in the smallest unit of `currency`.'),\n currency: stringField('ISO 4217 code.'),\n lineItems: { type: 'array', description: 'Line items, when the deal carries them.', items: { type: 'object', additionalProperties: true } },\n expectedCloseAt: nullableField(isoField('Forecast close date.')),\n closedAt: nullableField(isoField('When it was actually closed.')),\n stageChangedAt: nullableField(isoField('When the stage last moved.')),\n ownerUid: nullableField(stringField('Owning user.')),\n contactId: nullableField(stringField('Associated contact.')),\n companyId: nullableField(stringField('Associated company.')),\n lostReason: nullableField(stringField('Why it was lost.')),\n notes: nullableField(stringField('Free-form notes.')),\n custom: openObjectField('Customer-defined fields.'),\n nextTaskAt: nullableField(isoField('When the next open task on this deal is due.')),\n siteId: nullableField(stringField('Site the record originated on.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/deals', method: 'get', operationId: 'listDeals', summary: 'List deals', list: true, returns: 'Deal', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('pipelineId', 'Pipeline.'), queryParam('stageId', 'Stage.'), queryParam('status', 'Deal status.'), queryParam('ownerUid', 'Owning user.')] },\n { path: '/v1/deals', method: 'post', operationId: 'createDeal', summary: 'Create a deal', accepts: 'DealWrite', returns: 'Deal', entitlement: 'crm', creates: true },\n { path: '/v1/deals/{dealId}', method: 'get', operationId: 'getDeal', summary: 'Retrieve a deal', returns: 'Deal', entitlement: 'crm', pathParams: [{ name: 'dealId', description: 'Deal id.' }] },\n { path: '/v1/deals/{dealId}', method: 'patch', operationId: 'updateDeal', summary: 'Update a deal', accepts: 'DealWrite', returns: 'Deal', entitlement: 'crm', pathParams: [{ name: 'dealId', description: 'Deal id.' }] },\n { path: '/v1/deals/{dealId}', method: 'delete', operationId: 'deleteDeal', summary: 'Delete a deal', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'dealId', description: 'Deal id.' }] },\n ],\n },\n tasks: {\n tag: 'Tasks',\n description: 'Follow-ups attached to CRM records.',\n schemaName: 'Task',\n required: ['id', 'object', 'title'],\n writable: ['title', 'notes', 'kind', 'priority', 'status', 'dueAt', 'remindAt', 'assigneeUid', 'contactId', 'companyId', 'dealId', 'consentSiteId'],\n writeOnly: {\n consentSiteId: stringField('The site the task is created on behalf of.'),\n },\n writeNote: 'A create needs `title` and `consentSiteId`; a `PATCH` that names `consentSiteId` is a `400`.',\n fields: {\n id: stringField('Task id.'),\n object: objectKindField('task'),\n title: stringField('Task title.'),\n notes: nullableField(stringField('Free-form notes.')),\n kind: stringField('Task kind, e.g. `call` or `email`.'),\n priority: stringField('Priority label.'),\n status: stringField('`open` or `done`.'),\n dueAt: nullableField(isoField('When the task is due.')),\n remindAt: nullableField(isoField('When a reminder is scheduled.')),\n reminderSentAt: nullableField(isoField('When the reminder was sent.')),\n completedAt: nullableField(isoField('When it was completed.')),\n assigneeUid: nullableField(stringField('Assigned user.')),\n contactId: nullableField(stringField('Associated contact.')),\n companyId: nullableField(stringField('Associated company.')),\n dealId: nullableField(stringField('Associated deal.')),\n siteId: nullableField(stringField('Site the record originated on.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/tasks', method: 'get', operationId: 'listTasks', summary: 'List tasks', list: true, returns: 'Task', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('status', 'Task status.'), queryParam('assigneeUid', 'Assigned user.'), queryParam('contactId', 'Associated contact.'), queryParam('dealId', 'Associated deal.')] },\n { path: '/v1/tasks', method: 'post', operationId: 'createTask', summary: 'Create a task', accepts: 'TaskWrite', returns: 'Task', entitlement: 'crm', creates: true },\n { path: '/v1/tasks/{taskId}', method: 'get', operationId: 'getTask', summary: 'Retrieve a task', returns: 'Task', entitlement: 'crm', pathParams: [{ name: 'taskId', description: 'Task id.' }] },\n { path: '/v1/tasks/{taskId}', method: 'patch', operationId: 'updateTask', summary: 'Update a task', accepts: 'TaskWrite', returns: 'Task', entitlement: 'crm', pathParams: [{ name: 'taskId', description: 'Task id.' }] },\n { path: '/v1/tasks/{taskId}', method: 'delete', operationId: 'deleteTask', summary: 'Delete a task', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'taskId', description: 'Task id.' }] },\n ],\n },\n activities: {\n tag: 'Activities',\n description: 'Logged interactions. Append-only: no PATCH.',\n schemaName: 'Activity',\n required: ['id', 'object', 'kind', 'at'],\n writable: ['kind', 'body', 'at', 'byUid', 'contactId', 'companyId', 'dealId', 'outcome', 'durationMinutes', 'consentSiteId'],\n writeOnly: {\n consentSiteId: stringField('The site the activity is logged on behalf of.'),\n },\n writeRequired: ['body', 'consentSiteId'],\n fields: {\n id: stringField('Activity id.'),\n object: objectKindField('activity'),\n kind: stringField('What happened, e.g. `call`, `email`, `note`.'),\n body: stringField('Free-form body.'),\n at: isoField('When the interaction happened — not when it was logged.'),\n byUid: nullableField(stringField('User who logged it.')),\n contactId: nullableField(stringField('Associated contact.')),\n companyId: nullableField(stringField('Associated company.')),\n dealId: nullableField(stringField('Associated deal.')),\n outcome: nullableField(stringField('Outcome label.')),\n durationMinutes: nullableField(integerField('Duration, for calls and meetings.')),\n siteId: nullableField(stringField('Site the record originated on.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/activities', method: 'get', operationId: 'listActivities', summary: 'List activities', list: true, returns: 'Activity', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('kind', 'Activity kind.'), queryParam('contactId', 'Associated contact.'), queryParam('dealId', 'Associated deal.')] },\n { path: '/v1/activities', method: 'post', operationId: 'createActivity', summary: 'Log an activity', accepts: 'ActivityWrite', returns: 'Activity', entitlement: 'crm', creates: true },\n { path: '/v1/activities/{activityId}', method: 'get', operationId: 'getActivity', summary: 'Retrieve an activity', returns: 'Activity', entitlement: 'crm', pathParams: [{ name: 'activityId', description: 'Activity id.' }] },\n { path: '/v1/activities/{activityId}', method: 'delete', operationId: 'deleteActivity', summary: 'Delete an activity', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'activityId', description: 'Activity id.' }] },\n ],\n },\n leads: {\n tag: 'Leads',\n description: 'Unqualified interest, before it becomes a contact. A lead is a record of its own: the person and their company as text, until converting makes the contact and the company.',\n schemaName: 'Lead',\n required: ['id', 'object', 'siteId'],\n writable: ['siteId', 'email', 'name', 'status', 'ownerUid', 'ownerEmail', 'notes', 'unqualifiedReason', 'company', 'jobTitle', 'phone', 'website', 'address', 'tags', 'leadSource'],\n writeNote: '`email` and `name` are taken on a create only; a `PATCH` cannot change the address, which is the lead’s identity within its site.',\n writeRequired: ['email'],\n writeOnly: {\n siteId: stringField('The site the lead belongs to, instead of the `siteId` query parameter.'),\n status: stringField('`new`, `nurturing`, `working` or `unqualified` (`nurturing` and `unqualified` on a `PATCH` only). A lead becomes `qualified` by being converted.'),\n ownerEmail: stringField('A member’s address, resolved against the organization’s roster. Not with `ownerUid` in the same request.'),\n },\n fields: {\n id: stringField('Lead id.'),\n object: objectKindField('lead'),\n siteId: stringField('Site the lead arrived on.'),\n email: nullableField(stringField('Email — the lead’s identity within its site.')),\n name: nullableField(stringField('Name, when supplied.')),\n status: stringField('Lead status.'),\n ownerUid: nullableField(stringField('Owning user.')),\n notes: nullableField(stringField('Free-form notes.')),\n unqualifiedReason: nullableField(stringField('Why the lead was disqualified.')),\n company: nullableField(stringField('The company’s name, as text. Converting the lead is what links or creates the company record.')),\n jobTitle: nullableField(stringField('Job title.')),\n phone: nullableField(stringField('E.164 phone number.')),\n website: nullableField(stringField('An http(s) URL.')),\n address: nullableField(postalAddressField()),\n tags: stringListField('Lower-cased tags.'),\n leadSource: nullableField(\n stringField(\n \"Where the lead came from: one of the organization's active lead source values \" +\n '(CRM › Fields › Leads), matched without regard to case. Any other value is refused ' +\n 'with a 400 naming the values allowed; the value a lead already holds is kept even ' +\n \"after it is deactivated. A create that names none starts from the list's default.\",\n ),\n ),\n sources: stringListField('The surfaces that captured the lead: `signup`, `booking`, `form:{formId}`, `import`, `manual`, `api`.'),\n submissionCount: integerField('How many form submissions this lead has made.'),\n firstSeen: isoField('First interaction.'),\n lastSeen: isoField('Most recent interaction.'),\n marketingConsent: booleanField('Whether marketing consent was given.'),\n marketingConsentAt: nullableField(isoField('When consent was given.')),\n convertedContactId: nullableField(stringField('Contact created by conversion.')),\n convertedAt: nullableField(isoField('When it was converted.')),\n companyId: nullableField(stringField('Company created or matched by conversion.')),\n dealId: nullableField(stringField('Deal created by conversion.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/leads', method: 'get', operationId: 'listLeads', summary: 'List leads', list: true, returns: 'Lead', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('siteId', 'Site the lead arrived on.'), queryParam('status', 'Lead status.'), queryParam('ownerUid', 'Owning user.')] },\n { path: '/v1/leads', method: 'post', operationId: 'createLead', summary: 'Create a lead', accepts: 'LeadWrite', returns: 'Lead', entitlement: 'crm', creates: true, description: 'A lead sourced outside the site, entered before anyone has qualified it. One address is one lead per site: creating one the site already holds updates it and answers 200. No contact and no company are created — converting the lead does that — and no marketing consent is recorded.' },\n { path: '/v1/leads/{leadId}', method: 'get', operationId: 'getLead', summary: 'Retrieve a lead', returns: 'Lead', entitlement: 'crm', pathParams: [{ name: 'leadId', description: 'Lead id.' }] },\n { path: '/v1/leads/{leadId}', method: 'patch', operationId: 'updateLead', summary: 'Update a lead', accepts: 'LeadWrite', returns: 'Lead', entitlement: 'crm', pathParams: [{ name: 'leadId', description: 'Lead id.' }] },\n { path: '/v1/leads/{leadId}/convert', method: 'post', operationId: 'convertLead', summary: 'Convert a lead', returns: 'LeadConversion', entitlement: 'crm', description: 'Creates a contact, and optionally a company and a deal. Idempotent on the lead: converting an already-converted lead returns the existing ids.', pathParams: [{ name: 'leadId', description: 'Lead id.' }] },\n ],\n components: {\n LeadConversion: {\n type: 'object',\n description:\n 'What the conversion produced. Idempotent on the lead: converting an ' +\n 'already-converted lead returns the existing ids rather than creating ' +\n 'a second set.',\n properties: {\n object: { type: 'string', const: 'lead_conversion' },\n contactId: { type: 'string' },\n companyId: { type: ['string', 'null'] },\n dealId: { type: ['string', 'null'] },\n },\n additionalProperties: true,\n },\n },\n },\n 'email-templates': {\n tag: 'Email templates',\n description: 'Reusable email bodies.',\n schemaName: 'EmailTemplate',\n required: ['id', 'object', 'name'],\n writable: ['name', 'kind', 'visibility', 'ownerUid', 'subject', 'body', 'consentSiteId'],\n writeOnly: {\n consentSiteId: stringField('The site the template is created on behalf of.'),\n },\n writeNote: 'A create needs `name`, `body` and `consentSiteId`; a `PATCH` that names `consentSiteId` is a `400`.',\n fields: {\n id: stringField('Template id.'),\n object: objectKindField('email_template'),\n name: stringField('Template name.'),\n kind: stringField('Template kind.'),\n visibility: stringField('`org` or `private`.'),\n ownerUid: nullableField(stringField('Owner, for a private template.')),\n subject: stringField('Subject line.'),\n body: stringField('Body. May contain merge tokens.'),\n siteId: nullableField(stringField('Site the template belongs to, when scoped.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/email-templates', method: 'get', operationId: 'listEmailTemplates', summary: 'List email templates', list: true, returns: 'EmailTemplate', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('kind', 'Template kind.'), queryParam('visibility', 'Visibility.')] },\n { path: '/v1/email-templates', method: 'post', operationId: 'createEmailTemplate', summary: 'Create a template', accepts: 'EmailTemplateWrite', returns: 'EmailTemplate', entitlement: 'crm', creates: true },\n { path: '/v1/email-templates/{templateId}', method: 'get', operationId: 'getEmailTemplate', summary: 'Retrieve a template', returns: 'EmailTemplate', entitlement: 'crm', pathParams: [{ name: 'templateId', description: 'Template id.' }] },\n { path: '/v1/email-templates/{templateId}', method: 'patch', operationId: 'updateEmailTemplate', summary: 'Update a template', accepts: 'EmailTemplateWrite', returns: 'EmailTemplate', entitlement: 'crm', pathParams: [{ name: 'templateId', description: 'Template id.' }] },\n { path: '/v1/email-templates/{templateId}', method: 'delete', operationId: 'deleteEmailTemplate', summary: 'Delete a template', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'templateId', description: 'Template id.' }] },\n ],\n },\n}\n"],"names":["booleanField","integerField","isoField","nullableField","objectKindField","openObjectField","postalAddressField","queryParam","RECORD_STAMPS","stringField","stringListField","UPDATED_AFTER_PARAM","CRM_API_V1_DESCRIPTIONS","contacts","tag","description","schemaName","required","writable","writeOnly","consentSiteId","consentGroupId","custom","mediaIds","writeNote","fields","id","object","email","name","tags","notes","marketingConsent","consentSites","sources","phone","jobTitle","companyId","address","ownerUid","lifecycleStage","companyIds","alternateEmails","ops","path","method","operationId","summary","list","returns","entitlement","filters","accepts","creates","pathParams","components","ContactMerge","type","properties","sourceContactId","additionalProperties","companies","domain","website","industry","nextTaskAt","siteId","pipelines","isDefault","archived","archivedAt","stages","items","order","deals","title","pipelineId","stageId","status","amountCents","currency","lineItems","expectedCloseAt","closedAt","stageChangedAt","contactId","lostReason","tasks","kind","priority","dueAt","remindAt","reminderSentAt","completedAt","assigneeUid","dealId","activities","writeRequired","body","at","byUid","outcome","durationMinutes","leads","ownerEmail","unqualifiedReason","company","leadSource","submissionCount","firstSeen","lastSeen","marketingConsentAt","convertedContactId","convertedAt","LeadConversion","const","visibility","subject"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type ApiV1Context } from '@aglyn/tenant-data-admin/server/api-v1-kit';
|
|
18
|
+
/**
|
|
19
|
+
* The CRM's figures on `GET /v1/usage` (AGL-2277, AGL-2606, AGL-2611): the
|
|
20
|
+
* records band the plan sells, and the size of every collection an
|
|
21
|
+
* integration walks. Reported on every plan, like the platform's own bands —
|
|
22
|
+
* a workspace without the CRM still keeps the people its captures record.
|
|
23
|
+
*
|
|
24
|
+
* Aggregations, so an organization with ten thousand deals pays ten reads.
|
|
25
|
+
*/
|
|
26
|
+
export declare function crmUsageFigures(ctx: ApiV1Context): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
*/ import { checkCrmRecordsQuota, CRM_COLLECTIONS, UNLIMITED } from "@aglyn/aglyn/server";
|
|
17
|
+
import { usageBand } from "@aglyn/tenant-data-admin/server/api-v1-kit";
|
|
18
|
+
/**
|
|
19
|
+
* The CRM's figures on `GET /v1/usage` (AGL-2277, AGL-2606, AGL-2611): the
|
|
20
|
+
* records band the plan sells, and the size of every collection an
|
|
21
|
+
* integration walks. Reported on every plan, like the platform's own bands —
|
|
22
|
+
* a workspace without the CRM still keeps the people its captures record.
|
|
23
|
+
*
|
|
24
|
+
* Aggregations, so an organization with ten thousand deals pays ten reads.
|
|
25
|
+
*/ export async function crmUsageFigures(ctx) {
|
|
26
|
+
var _leadCounts_data_count;
|
|
27
|
+
const orgRef = ctx.firestore.collection('orgs').doc(ctx.orgId);
|
|
28
|
+
const [contactsSnap, companiesSnap, dealsSnap, tasksSnap, activitiesSnap, leadCounts] = await Promise.all([
|
|
29
|
+
orgRef.collection('contacts').count().get(),
|
|
30
|
+
orgRef.collection(CRM_COLLECTIONS.companies).count().get(),
|
|
31
|
+
orgRef.collection(CRM_COLLECTIONS.deals).count().get(),
|
|
32
|
+
orgRef.collection(CRM_COLLECTIONS.tasks).count().get(),
|
|
33
|
+
orgRef.collection(CRM_COLLECTIONS.activities).count().get(),
|
|
34
|
+
/*
|
|
35
|
+
* ONE AGGREGATE (AGL-3275). Leads used to live under each SITE, so this
|
|
36
|
+
* was one count per site the org owns, summed. They share an org
|
|
37
|
+
* collection now, and a sum over per-site counts would report a lead that
|
|
38
|
+
* two brands in a consent group both hold twice — so the org's size is a
|
|
39
|
+
* single unscoped count, the way every other CRM collection above is.
|
|
40
|
+
*/ orgRef.collection('leads').count().get()
|
|
41
|
+
]);
|
|
42
|
+
// The records band is measured on the SUM (AGL-2611); the contacts entry
|
|
43
|
+
// below keeps reporting the people count against it, so a client that
|
|
44
|
+
// only ever read `contacts` still sees the headroom that refuses it.
|
|
45
|
+
const crmRecordsQuota = checkCrmRecordsQuota(ctx.org, contactsSnap.data().count + companiesSnap.data().count + dealsSnap.data().count);
|
|
46
|
+
return {
|
|
47
|
+
contacts: usageBand(contactsSnap.data().count, crmRecordsQuota.included, crmRecordsQuota.remaining, crmRecordsQuota.overageRateUsd),
|
|
48
|
+
/*
|
|
49
|
+
* THE BAND ITSELF (AGL-2611): contacts, companies and deals as one
|
|
50
|
+
* figure against the one band the plan sells. `contacts` above keeps
|
|
51
|
+
* its shape for the client that only reads it — same band, same
|
|
52
|
+
* headroom — and this is the number the invoice and the console
|
|
53
|
+
* meter are computed from. Companies and deals below stay as sizes.
|
|
54
|
+
*/ crmRecords: usageBand(crmRecordsQuota.used, crmRecordsQuota.included, crmRecordsQuota.remaining, crmRecordsQuota.overageRateUsd),
|
|
55
|
+
/*
|
|
56
|
+
* The CRM collections (AGL-2606) — SIZES, in the band shape so a client
|
|
57
|
+
* reads them with the code it already has. No plan bands them: a
|
|
58
|
+
* company or a deal is not metered and is never refused, so `included`
|
|
59
|
+
* and `remaining` are `null` — the unlimited band the docs define —
|
|
60
|
+
* and `metered` is false because there is nothing to bill. What the
|
|
61
|
+
* numbers are for is sizing a sync: how many pages a full walk of
|
|
62
|
+
* `/v1/deals` will take, before taking it.
|
|
63
|
+
*/ crm: {
|
|
64
|
+
companies: usageBand(companiesSnap.data().count, UNLIMITED, UNLIMITED, null),
|
|
65
|
+
deals: usageBand(dealsSnap.data().count, UNLIMITED, UNLIMITED, null),
|
|
66
|
+
tasks: usageBand(tasksSnap.data().count, UNLIMITED, UNLIMITED, null),
|
|
67
|
+
activities: usageBand(activitiesSnap.data().count, UNLIMITED, UNLIMITED, null),
|
|
68
|
+
leads: usageBand(Number((_leadCounts_data_count = leadCounts.data().count) != null ? _leadCounts_data_count : 0), UNLIMITED, UNLIMITED, null)
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//# sourceMappingURL=usage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/server/api-v1/usage.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 { checkCrmRecordsQuota, CRM_COLLECTIONS, UNLIMITED } from '@aglyn/aglyn/server'\nimport { type ApiV1Context, usageBand } from '@aglyn/tenant-data-admin/server/api-v1-kit'\n\n/**\n * The CRM's figures on `GET /v1/usage` (AGL-2277, AGL-2606, AGL-2611): the\n * records band the plan sells, and the size of every collection an\n * integration walks. Reported on every plan, like the platform's own bands —\n * a workspace without the CRM still keeps the people its captures record.\n *\n * Aggregations, so an organization with ten thousand deals pays ten reads.\n */\nexport async function crmUsageFigures(ctx: ApiV1Context): Promise<Record<string, unknown>> {\n const orgRef = ctx.firestore.collection('orgs').doc(ctx.orgId)\n const [contactsSnap, companiesSnap, dealsSnap, tasksSnap, activitiesSnap, leadCounts] =\n await Promise.all([\n orgRef.collection('contacts').count().get(),\n orgRef.collection(CRM_COLLECTIONS.companies).count().get(),\n orgRef.collection(CRM_COLLECTIONS.deals).count().get(),\n orgRef.collection(CRM_COLLECTIONS.tasks).count().get(),\n orgRef.collection(CRM_COLLECTIONS.activities).count().get(),\n /*\n * ONE AGGREGATE (AGL-3275). Leads used to live under each SITE, so this\n * was one count per site the org owns, summed. They share an org\n * collection now, and a sum over per-site counts would report a lead that\n * two brands in a consent group both hold twice — so the org's size is a\n * single unscoped count, the way every other CRM collection above is.\n */\n orgRef.collection('leads').count().get(),\n ])\n\n // The records band is measured on the SUM (AGL-2611); the contacts entry\n // below keeps reporting the people count against it, so a client that\n // only ever read `contacts` still sees the headroom that refuses it.\n const crmRecordsQuota = checkCrmRecordsQuota(\n ctx.org as never,\n contactsSnap.data().count + companiesSnap.data().count + dealsSnap.data().count,\n )\n\n return {\n contacts: usageBand(\n contactsSnap.data().count,\n crmRecordsQuota.included,\n crmRecordsQuota.remaining,\n crmRecordsQuota.overageRateUsd,\n ),\n /*\n * THE BAND ITSELF (AGL-2611): contacts, companies and deals as one\n * figure against the one band the plan sells. `contacts` above keeps\n * its shape for the client that only reads it — same band, same\n * headroom — and this is the number the invoice and the console\n * meter are computed from. Companies and deals below stay as sizes.\n */\n crmRecords: usageBand(\n crmRecordsQuota.used,\n crmRecordsQuota.included,\n crmRecordsQuota.remaining,\n crmRecordsQuota.overageRateUsd,\n ),\n /*\n * The CRM collections (AGL-2606) — SIZES, in the band shape so a client\n * reads them with the code it already has. No plan bands them: a\n * company or a deal is not metered and is never refused, so `included`\n * and `remaining` are `null` — the unlimited band the docs define —\n * and `metered` is false because there is nothing to bill. What the\n * numbers are for is sizing a sync: how many pages a full walk of\n * `/v1/deals` will take, before taking it.\n */\n crm: {\n companies: usageBand(companiesSnap.data().count, UNLIMITED, UNLIMITED, null),\n deals: usageBand(dealsSnap.data().count, UNLIMITED, UNLIMITED, null),\n tasks: usageBand(tasksSnap.data().count, UNLIMITED, UNLIMITED, null),\n activities: usageBand(activitiesSnap.data().count, UNLIMITED, UNLIMITED, null),\n leads: usageBand(Number(leadCounts.data().count ?? 0), UNLIMITED, UNLIMITED, null),\n },\n }\n}\n"],"names":["checkCrmRecordsQuota","CRM_COLLECTIONS","UNLIMITED","usageBand","crmUsageFigures","ctx","leadCounts","orgRef","firestore","collection","doc","orgId","contactsSnap","companiesSnap","dealsSnap","tasksSnap","activitiesSnap","Promise","all","count","get","companies","deals","tasks","activities","crmRecordsQuota","org","data","contacts","included","remaining","overageRateUsd","crmRecords","used","crm","leads","Number"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,EAAEC,eAAe,EAAEC,SAAS,QAAQ,sBAAqB;AACtF,SAA4BC,SAAS,QAAQ,6CAA4C;AAEzF;;;;;;;CAOC,GACD,OAAO,eAAeC,gBAAgBC,GAAiB;QA6DzBC;IA5D5B,MAAMC,SAASF,IAAIG,SAAS,CAACC,UAAU,CAAC,QAAQC,GAAG,CAACL,IAAIM,KAAK;IAC7D,MAAM,CAACC,cAAcC,eAAeC,WAAWC,WAAWC,gBAAgBV,WAAW,GACnF,MAAMW,QAAQC,GAAG,CAAC;QAChBX,OAAOE,UAAU,CAAC,YAAYU,KAAK,GAAGC,GAAG;QACzCb,OAAOE,UAAU,CAACR,gBAAgBoB,SAAS,EAAEF,KAAK,GAAGC,GAAG;QACxDb,OAAOE,UAAU,CAACR,gBAAgBqB,KAAK,EAAEH,KAAK,GAAGC,GAAG;QACpDb,OAAOE,UAAU,CAACR,gBAAgBsB,KAAK,EAAEJ,KAAK,GAAGC,GAAG;QACpDb,OAAOE,UAAU,CAACR,gBAAgBuB,UAAU,EAAEL,KAAK,GAAGC,GAAG;QACzD;;;;;;OAMC,GACDb,OAAOE,UAAU,CAAC,SAASU,KAAK,GAAGC,GAAG;KACvC;IAEH,yEAAyE;IACzE,sEAAsE;IACtE,qEAAqE;IACrE,MAAMK,kBAAkBzB,qBACtBK,IAAIqB,GAAG,EACPd,aAAae,IAAI,GAAGR,KAAK,GAAGN,cAAcc,IAAI,GAAGR,KAAK,GAAGL,UAAUa,IAAI,GAAGR,KAAK;IAGjF,OAAO;QACLS,UAAUzB,UACRS,aAAae,IAAI,GAAGR,KAAK,EACzBM,gBAAgBI,QAAQ,EACxBJ,gBAAgBK,SAAS,EACzBL,gBAAgBM,cAAc;QAEhC;;;;;;KAMC,GACDC,YAAY7B,UACVsB,gBAAgBQ,IAAI,EACpBR,gBAAgBI,QAAQ,EACxBJ,gBAAgBK,SAAS,EACzBL,gBAAgBM,cAAc;QAEhC;;;;;;;;KAQC,GACDG,KAAK;YACHb,WAAWlB,UAAUU,cAAcc,IAAI,GAAGR,KAAK,EAAEjB,WAAWA,WAAW;YACvEoB,OAAOnB,UAAUW,UAAUa,IAAI,GAAGR,KAAK,EAAEjB,WAAWA,WAAW;YAC/DqB,OAAOpB,UAAUY,UAAUY,IAAI,GAAGR,KAAK,EAAEjB,WAAWA,WAAW;YAC/DsB,YAAYrB,UAAUa,eAAeW,IAAI,GAAGR,KAAK,EAAEjB,WAAWA,WAAW;YACzEiC,OAAOhC,UAAUiC,QAAO9B,yBAAAA,WAAWqB,IAAI,GAAGR,KAAK,YAAvBb,yBAA2B,IAAIJ,WAAWA,WAAW;QAC/E;IACF;AACF"}
|
|
@@ -70,7 +70,7 @@ export interface CampaignFilingActivitiesInput {
|
|
|
70
70
|
/** The document id a keyed entry files under: the CRM's namespace, never a raw key. */
|
|
71
71
|
export declare function campaignFilingActivityId(key: string): string;
|
|
72
72
|
/**
|
|
73
|
-
* The names of an org's campaigns
|
|
73
|
+
* The names of an org's campaigns — the `campaign` container kind — by id,
|
|
74
74
|
* for the entries that name them. A container that is gone answers its id,
|
|
75
75
|
* so the entry still says which.
|
|
76
76
|
*/
|
|
@@ -18,6 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
19
19
|
import { CRM_COLLECTIONS, crmActivityLogHasRoom, crmScopeTokens } from "@aglyn/aglyn/app-utils/crm";
|
|
20
20
|
import { countCrmActivitiesForRecord } from "@aglyn/tenant-data-admin/server/crm-records";
|
|
21
|
+
import { readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
|
|
21
22
|
import { createHash } from "crypto";
|
|
22
23
|
import { FieldValue } from "firebase-admin/firestore";
|
|
23
24
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
@@ -27,7 +28,7 @@ import { buildCampaignFilingActivity } from "../model/campaign-filing-activity.j
|
|
|
27
28
|
}
|
|
28
29
|
/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */ const ALREADY_EXISTS = 6;
|
|
29
30
|
/**
|
|
30
|
-
* The names of an org's campaigns
|
|
31
|
+
* The names of an org's campaigns — the `campaign` container kind — by id,
|
|
31
32
|
* for the entries that name them. A container that is gone answers its id,
|
|
32
33
|
* so the entry still says which.
|
|
33
34
|
*/ export async function orgCampaignRefs(firestore, orgId, campaignIds) {
|
|
@@ -36,16 +37,11 @@ import { buildCampaignFilingActivity } from "../model/campaign-filing-activity.j
|
|
|
36
37
|
id,
|
|
37
38
|
name: id
|
|
38
39
|
}));
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
id,
|
|
46
|
-
name: String((_ref = (_found_index = found[index]) == null ? void 0 : _found_index.get('name')) != null ? _ref : '').trim() || id
|
|
47
|
-
};
|
|
48
|
-
});
|
|
40
|
+
const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds);
|
|
41
|
+
return found.map((container)=>({
|
|
42
|
+
id: container.id,
|
|
43
|
+
name: container.name || container.id
|
|
44
|
+
}));
|
|
49
45
|
}
|
|
50
46
|
/** Files one entry per campaign; answers how many were written. */ export async function fileCampaignFilingActivities(firestore, input) {
|
|
51
47
|
if (!input.campaigns.length) return 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/campaign-filing-activity.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 { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport {\n CRM_COLLECTIONS,\n crmActivityLogHasRoom,\n crmScopeTokens,\n type CrmActivityLink,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { countCrmActivitiesForRecord } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { createHash } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n buildCampaignFilingActivity,\n type CampaignFilingAction,\n type CampaignFilingRef,\n} from '../model/campaign-filing-activity'\n\n/**\n * FILES A FILING ON THE RECORD'S TIMELINE (AGL-3274), from the server.\n *\n * The two doors that file a lead under a campaign on the server — the\n * `leads-create` route behind the New lead drawer, and the conversion's\n * carry of a lead's campaigns onto the contact — write the entry through\n * this, with the scope a record made on the site carries and the CRM's\n * own id as the filing plugin, exactly as the console's hook writes one.\n *\n * ## Once, when the caller says so\n *\n * A member's act is an act each time, and the route's entries are named\n * by `createResourceUid()`, the id every console resource carries. The\n * conversion carry runs from a seam every converting\n * door reaches, and a door that runs twice must not file twice — so a\n * caller that hands in a `dedupeKey` gets a keyed `create()`, and the\n * second run finds its own entry and writes nothing. The key is hashed\n * under the CRM's id the way the timeline seam hashes a plugin's, so no\n * caller's raw key becomes a document id.\n *\n * ## Never throws\n *\n * The membership has landed by the time this runs; an entry that could\n * not be filed is logged, and the record's page shows the campaigns from\n * the document either way. A record at its activity ceiling is refused\n * before anything is written, in the same breath.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CampaignFilingActivitiesInput {\n orgId: string\n /**\n * The organization document, when the caller already holds it — the\n * scope tokens read `defaultResourceScope` and the consent groups off\n * it. Read here otherwise.\n */\n org?: Record<string, unknown> | null\n /** The site the entry carries as provenance. */\n hostId: string\n link: CrmActivityLink\n action: CampaignFilingAction\n campaigns: readonly CampaignFilingRef[]\n atMs: number\n /** The member who did it, or `''` for the conversion. */\n byUid: string\n byName?: string | null\n /**\n * A key that files each campaign's entry once across runs — the carry's\n * — or nothing for a member's act, which is new each time.\n */\n dedupeKey?: string | null\n}\n\n/** The document id a keyed entry files under: the CRM's namespace, never a raw key. */\nexport function campaignFilingActivityId(key: string): string {\n return `plg_${createHash('sha256').update(`${BUNDLE_ID}:${key}`).digest('hex').slice(0, 28)}`\n}\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/**\n * The names of an org's campaigns
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/campaign-filing-activity.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 { consentGroupForHost } from '@aglyn/aglyn/app-utils/consent-groups'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport {\n CRM_COLLECTIONS,\n crmActivityLogHasRoom,\n crmScopeTokens,\n type CrmActivityLink,\n} from '@aglyn/aglyn/app-utils/crm'\nimport { countCrmActivitiesForRecord } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { readOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport { createHash } from 'crypto'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n buildCampaignFilingActivity,\n type CampaignFilingAction,\n type CampaignFilingRef,\n} from '../model/campaign-filing-activity'\n\n/**\n * FILES A FILING ON THE RECORD'S TIMELINE (AGL-3274), from the server.\n *\n * The two doors that file a lead under a campaign on the server — the\n * `leads-create` route behind the New lead drawer, and the conversion's\n * carry of a lead's campaigns onto the contact — write the entry through\n * this, with the scope a record made on the site carries and the CRM's\n * own id as the filing plugin, exactly as the console's hook writes one.\n *\n * ## Once, when the caller says so\n *\n * A member's act is an act each time, and the route's entries are named\n * by `createResourceUid()`, the id every console resource carries. The\n * conversion carry runs from a seam every converting\n * door reaches, and a door that runs twice must not file twice — so a\n * caller that hands in a `dedupeKey` gets a keyed `create()`, and the\n * second run finds its own entry and writes nothing. The key is hashed\n * under the CRM's id the way the timeline seam hashes a plugin's, so no\n * caller's raw key becomes a document id.\n *\n * ## Never throws\n *\n * The membership has landed by the time this runs; an entry that could\n * not be filed is logged, and the record's page shows the campaigns from\n * the document either way. A record at its activity ceiling is refused\n * before anything is written, in the same breath.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface CampaignFilingActivitiesInput {\n orgId: string\n /**\n * The organization document, when the caller already holds it — the\n * scope tokens read `defaultResourceScope` and the consent groups off\n * it. Read here otherwise.\n */\n org?: Record<string, unknown> | null\n /** The site the entry carries as provenance. */\n hostId: string\n link: CrmActivityLink\n action: CampaignFilingAction\n campaigns: readonly CampaignFilingRef[]\n atMs: number\n /** The member who did it, or `''` for the conversion. */\n byUid: string\n byName?: string | null\n /**\n * A key that files each campaign's entry once across runs — the carry's\n * — or nothing for a member's act, which is new each time.\n */\n dedupeKey?: string | null\n}\n\n/** The document id a keyed entry files under: the CRM's namespace, never a raw key. */\nexport function campaignFilingActivityId(key: string): string {\n return `plg_${createHash('sha256').update(`${BUNDLE_ID}:${key}`).digest('hex').slice(0, 28)}`\n}\n\n/** gRPC `ALREADY_EXISTS`, which `create()` rejects with when the document is there. */\nconst ALREADY_EXISTS = 6\n\n/**\n * The names of an org's campaigns — the `campaign` container kind — by id,\n * for the entries that name them. A container that is gone answers its id,\n * so the entry still says which.\n */\nexport async function orgCampaignRefs(\n firestore: Firestore,\n orgId: string,\n campaignIds: readonly string[],\n): Promise<CampaignFilingRef[]> {\n if (!campaignIds.length) return []\n if (!orgId) return campaignIds.map((id) => ({ id, name: id }))\n const found = await readOrgContainers(firestore, 'campaign', orgId, campaignIds)\n return found.map((container) => ({ id: container.id, name: container.name || container.id }))\n}\n\n/** Files one entry per campaign; answers how many were written. */\nexport async function fileCampaignFilingActivities(\n firestore: Firestore,\n input: CampaignFilingActivitiesInput,\n): Promise<number> {\n if (!input.campaigns.length) return 0\n try {\n const orgRef = firestore.collection('orgs').doc(input.orgId)\n const org =\n input.org ?? ((await orgRef.get()).data() as Record<string, unknown> | undefined) ?? null\n const visibleTo = crmScopeTokens(org, consentGroupForHost(org, input.hostId))\n if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, input.link))) {\n console.warn('[crm] the filing was not written on the timeline: the record’s log is full', input.link)\n return 0\n }\n const activities = orgRef.collection(CRM_COLLECTIONS.activities)\n let written = 0\n for (const campaign of input.campaigns) {\n const entry = {\n ...buildCampaignFilingActivity({\n action: input.action,\n campaign,\n link: input.link,\n hostId: input.hostId,\n visibleTo,\n atMs: input.atMs,\n byUid: input.byUid,\n byName: input.byName ?? null,\n }),\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n }\n if (input.dedupeKey) {\n try {\n await activities.doc(campaignFilingActivityId(`${input.dedupeKey}:${campaign.id}`)).create(entry)\n written += 1\n } catch (error) {\n if ((error as { code?: unknown })?.code !== ALREADY_EXISTS) throw error\n }\n } else {\n // A member's act, new each time, named by the platform's own id.\n await activities.doc(createResourceUid()).create(entry)\n written += 1\n }\n }\n return written\n } catch (error) {\n console.error('[crm] the filing could not be written on the timeline', input.link, error)\n return 0\n }\n}\n"],"names":["consentGroupForHost","createResourceUid","CRM_COLLECTIONS","crmActivityLogHasRoom","crmScopeTokens","countCrmActivitiesForRecord","readOrgContainers","createHash","FieldValue","BUNDLE_ID","buildCampaignFilingActivity","campaignFilingActivityId","key","update","digest","slice","ALREADY_EXISTS","orgCampaignRefs","firestore","orgId","campaignIds","length","map","id","name","found","container","fileCampaignFilingActivities","input","campaigns","orgRef","collection","doc","org","get","data","visibleTo","hostId","link","console","warn","activities","written","campaign","entry","action","atMs","byUid","byName","createdAt","serverTimestamp","updatedAt","dedupeKey","create","error","code"],"mappings":"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"}
|
|
@@ -189,7 +189,7 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
|
|
|
189
189
|
}
|
|
190
190
|
/** The capture as a CONTACT — what every capture was before the rule above. */ async function captureOnContact(request) {
|
|
191
191
|
try {
|
|
192
|
-
var _request_tags
|
|
192
|
+
var _request_tags;
|
|
193
193
|
const verdict = await captureHostContact(_extends({}, request.actor ? {
|
|
194
194
|
actor: request.actor
|
|
195
195
|
} : {}, {
|
|
@@ -210,9 +210,7 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
|
|
|
210
210
|
marketingConsent: request.marketingConsent
|
|
211
211
|
}, disclosureOf(request), ((_request_tags = request.tags) == null ? void 0 : _request_tags.length) ? {
|
|
212
212
|
tags: request.tags
|
|
213
|
-
} : {}, (
|
|
214
|
-
campaignIds: request.campaignIds
|
|
215
|
-
} : {}, request.purchaseCents === undefined ? {} : {
|
|
213
|
+
} : {}, campaignsFiledUnder(request), request.purchaseCents === undefined ? {} : {
|
|
216
214
|
purchaseCents: request.purchaseCents
|
|
217
215
|
}, request.purchaseCurrency ? {
|
|
218
216
|
purchaseCurrency: request.purchaseCurrency
|
|
@@ -286,6 +284,19 @@ import { convertOpenLeadOntoContact } from "./convert-open-lead.js";
|
|
|
286
284
|
path
|
|
287
285
|
} : {});
|
|
288
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* The campaigns the capture surface is filed under, as the contact writer
|
|
289
|
+
* records them. A capture names what its surface is filed under by container
|
|
290
|
+
* kind; the CRM keeps the `campaign` kind on the person's site facet, and a
|
|
291
|
+
* kind it does not keep is the capture's to name and the CRM's to ignore.
|
|
292
|
+
*/ function campaignsFiledUnder(request) {
|
|
293
|
+
var _ref;
|
|
294
|
+
var _request_containers;
|
|
295
|
+
const ids = (_ref = (_request_containers = request.containers) == null ? void 0 : _request_containers['campaign']) != null ? _ref : [];
|
|
296
|
+
return ids.length ? {
|
|
297
|
+
campaignIds: ids
|
|
298
|
+
} : {};
|
|
299
|
+
}
|
|
289
300
|
/**
|
|
290
301
|
* WHERE THE VISITOR CAME FROM, off the same bag.
|
|
291
302
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/capture-contact.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n PluginContactCaptureRequest,\n PluginContactCaptured,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport {\n CONTACT_SOURCE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { formLeadSource } from '@aglyn/aglyn/app-utils/forms'\nimport {\n type CrmLeadFields,\n isCrmLeadOpen,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { captureHostContact } from '@aglyn/tenant-runtime/capture-host-contact'\nimport { contactCaptureActor } from '@aglyn/tenant-runtime/capture-actor'\nimport { emitHostEvent } from '@aglyn/tenant-runtime/emit-host-event'\nimport {\n addHostLeadOutcome,\n findContactByEmail,\n firebaseAdmin,\n orgDataCollectionForHost,\n readLeadForHost,\n type UpsertHostContactOptions,\n} from '@aglyn/tenant-data-admin'\nimport type { ResolvedCampaignTouch } from '@aglyn/tenant-data-admin/server/campaign-conversion-attribution'\nimport { convertOpenLeadOntoContact } from './convert-open-lead'\n\n/**\n * The CRM answering the platform's contact-capture contract (AGL-3080).\n *\n * Four silos meet the same person — a form submission, a member signing up,\n * an order, a booking — and none of them is the record system. Each holds an\n * address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each imports `captureHostContact`\n * directly, which is the highest fan-in edge in the repo and makes the CRM\n * something a storefront cannot take a payment without.\n *\n * So this is the CRM's side of the seam: the silo reports what it saw, and\n * the plugin that keeps people decides everything a record system decides —\n * keying the address, whether this is somebody new, the audience band, the\n * erasure rows, the company, the owner, and what a new person sets off.\n * `captureHostContact` already does all of that; this translates the\n * contract's vocabulary into its options and its verdict back.\n *\n * ⚠️ IT NEVER THROWS. Every caller has already done the thing it is\n * recording — the submission was accepted, the order was paid — so a refusal\n * is RETURNED and costs the silo nothing. A throw here would lose an order\n * for a contact that could not be filed.\n */\n/**\n * WHICH RECORD A CAPTURE LANDS ON (AGL-3232) — the Salesforce rule, decided\n * here because the CRM is the plugin that models both records.\n *\n * One person is one record: a LEAD until somebody qualifies them, a CONTACT\n * after. Every door used to write both — a lead-routed form filed a lead\n * AND a contact at stage Lead — so one person sat in two lists and the Leads\n * queue was never the whole story. Now a door says what kind of surface it\n * is (`request.surface`) and this decides:\n *\n * - a LEAD surface files a lead, unless the workspace already holds the\n * address as a contact — a customer who books a demo is a customer's\n * interaction, not a new lead — in which case the capture lands on the\n * contact and no lead is filed;\n * - a RELATIONSHIP (a member account, a purchase) makes the contact, and\n * an open lead the site held for the address is stamped converted onto\n * it, so nobody keeps working a lead who already joined or bought;\n * - a TOUCH (an unrouted form, a newsletter opt-in) lands on the open lead\n * when the site holds one — its consent and its history stay on the one\n * record the rep is working — and on the contact otherwise.\n *\n * A lead is filed through `addHostLead`, the one writer of the leads silo,\n * so it is keyed on the person, counted against the ceiling and carries the\n * campaign touch the door resolved. A NEW lead announces itself with the\n * `lead` host event, which is what a \"new lead\" automation listens for; a\n * repeat capture on a lead the site already held announces nothing, the\n * way a repeat visit by a contact is an interaction and not a creation.\n */\nexport async function captureContactForCrm(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n const surface = request.surface ?? 'touch'\n // The one refusal every surface shares, answered before any read: an\n // address nothing can key is a person nothing can record.\n if (!normalizeContactEmail(request.identity.email)) return refusedEmail()\n try {\n if (surface === 'lead') {\n if (!(await heldAsContact(request))) return await fileLead(request)\n } else if (surface === 'touch') {\n if (await openLeadFor(request)) return await fileLead(request)\n }\n const verdict = await captureOnContact(request)\n if (verdict.ok && verdict.record === 'contact' && surface === 'relationship') {\n await convertOpenLeadOntoContact({\n hostId: request.hostId,\n email: request.identity.email,\n contactId: verdict.contactId,\n by: request.interaction.source === 'order' ? 'purchase' : 'signup',\n })\n }\n return verdict\n } catch (error) {\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/** The refusal every door gets for an address nothing can key. */\nfunction refusedEmail(): PluginContactCaptured {\n return { ok: false, reason: 'invalid-email', error: refusalText('invalid-email') }\n}\n\n/**\n * Whether the workspace already holds the address as a contact — the\n * address index first, then the query, the same lookup every dedupe uses.\n * ORG-wide, not scoped to the capturing site: the contact door dedupes\n * across every site in the org, so a person any site holds as a contact\n * is a contact, whichever site met them this time.\n */\nasync function heldAsContact(request: PluginContactCaptureRequest): Promise<boolean> {\n const contacts = await orgDataCollectionForHost(request.hostId, 'contacts')\n return (await findContactByEmail(contacts, request.identity.email)) !== null\n}\n\n/** Whether the site holds an OPEN lead for the address — neither converted nor closed. */\nasync function openLeadFor(request: PluginContactCaptureRequest): Promise<boolean> {\n const key = personKey(request.identity.email) as string\n /*\n * Through the seam, which reads the org row and falls back to a lead the\n * backfill has not reached (AGL-3275). UNSCOPED on purpose: a sibling brand\n * in the same consent group has to find this person, or its door concludes\n * there is no lead and files the duplicate this migration exists to end.\n */\n const snapshot = await readLeadForHost(request.hostId, key)\n if (!snapshot) return false\n const lead = (snapshot.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n return !lead.convertedContactId && isCrmLeadOpen(lead)\n}\n\n/**\n * The capture as a LEAD. The source string is the lead silo's own\n * vocabulary — `form:{formId}` for a form, the door's word otherwise — so\n * a lead's provenance survives the form being renamed, as the submission's\n * does.\n */\nasync function fileLead(request: PluginContactCaptureRequest): Promise<PluginContactCaptured> {\n const email = normalizeContactEmail(request.identity.email) as string\n const key = personKey(email) as string\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(request.hostId)\n // `addHostLead` resolves and writes the org collection itself (AGL-3275);\n // this only needs to know whether the person is new, which decides the event.\n const created = !(await readLeadForHost(request.hostId, key))\n const { formId } = entryPointOf(request.detail)\n const source =\n request.interaction.source === 'form' && formId\n ? formLeadSource(formId)\n : request.interaction.source\n const outcome = await addHostLeadOutcome({\n hostRef,\n hostId: request.hostId,\n lead: {\n email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source,\n ...(request.marketingConsent ? { marketingConsent: true } : {}),\n ...disclosureOf(request),\n },\n ...(campaignTouchOf(request.detail).campaignTouch\n ? { touch: campaignTouchOf(request.detail).campaignTouch }\n : {}),\n })\n if (!outcome.stored) {\n return {\n ok: false,\n reason: 'band',\n error: 'This site is at the number of leads it may hold, so the lead was not recorded.',\n }\n }\n if (created) {\n const cause = contactCaptureActor(\n request.interaction.source,\n email,\n request.actor,\n )\n await emitHostEvent(\n request.hostId,\n 'lead',\n {\n email,\n source,\n leadId: key,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n },\n cause ? { actor: cause } : {},\n )\n }\n return { ok: true, record: 'lead', leadId: key, created, sourceAdded: outcome.sourceAdded }\n}\n\n/** The capture as a CONTACT — what every capture was before the rule above. */\nasync function captureOnContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n try {\n const verdict = await captureHostContact({\n ...(request.actor ? { actor: request.actor } : {}),\n hostId: request.hostId,\n email: request.identity.email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source: contactSourceOf(request.interaction.source),\n interaction: {\n ...(request.interaction.atMs === undefined\n ? {}\n : { atMs: request.interaction.atMs }),\n ...(request.interaction.refId ? { refId: request.interaction.refId } : {}),\n ...(request.interaction.summary\n ? { summary: request.interaction.summary }\n : {}),\n ...entryPointOf(request.detail),\n },\n ...campaignTouchOf(request.detail),\n ...(request.marketingConsent === undefined\n ? {}\n : { marketingConsent: request.marketingConsent }),\n ...disclosureOf(request),\n ...(request.tags?.length ? { tags: request.tags } : {}),\n ...(request.campaignIds?.length ? { campaignIds: request.campaignIds } : {}),\n ...(request.purchaseCents === undefined\n ? {}\n : { purchaseCents: request.purchaseCents }),\n ...(request.purchaseCurrency\n ? { purchaseCurrency: request.purchaseCurrency }\n : {}),\n ...(request.lifecycleFloor\n ? { initialLifecycleStage: request.lifecycleFloor as never }\n : {}),\n ...(request.profile ? { facet: request.profile as never } : {}),\n })\n if ('refused' in verdict) {\n return { ok: false, reason: verdict.refused, error: refusalText(verdict.refused) }\n }\n return {\n ok: true,\n record: 'contact',\n contactId: verdict.contactId,\n created: verdict.created,\n }\n } catch (error) {\n /*\n * The contract's `error` is the last state, not a channel for a stack:\n * a silo may show or log it as it stands, so it says what happened and\n * names nothing internal. The detail goes to the log, where whoever is\n * debugging a missing contact will look.\n */\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/**\n * THE DISCLOSURE the capture surface rendered, handed to whichever record the\n * capture lands on (AGL-3320).\n *\n * Passed through unread: both writers resolve the site's group themselves and\n * pool the opt-in only when this is its current key, so a lead and a contact\n * written by one capture record the same sites. A string or nothing, since\n * the writers compare it exactly and anything else could only fail to match.\n */\nfunction disclosureOf(\n request: PluginContactCaptureRequest,\n): { disclosedConsentGroup?: string } {\n const key = request.disclosedConsentGroup\n return typeof key === 'string' && key ? { disclosedConsentGroup: key } : {}\n}\n\n/**\n * THE ENTRY POINT, off the silo's own `detail` bag (AGL-3080).\n *\n * The contract carries a capture's silo-side facts opaquely, so the two the\n * CRM models are picked out here rather than typed into the platform. Both\n * ride the INTERACTION, which is where `upsertHostContact` already keeps\n * them: which form a person came in through routes the owner-assignment\n * rules and rides the `contactCreated` payload, and the page is what a\n * timeline row says about where they were.\n *\n * Absent, misspelled or the wrong type is the same answer as a door that\n * never had one — a capture without an entry point, which is every capture\n * that did not come through a form. It is never a reason to refuse: the\n * person is the part that matters.\n */\nfunction entryPointOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): { formId?: string; path?: string } {\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' && value.trim() ? value : undefined\n const formId = text(detail?.['formId'])\n const path = text(detail?.['path'])\n return {\n ...(formId ? { formId } : {}),\n ...(path ? { path } : {}),\n }\n}\n\n/**\n * WHERE THE VISITOR CAME FROM, off the same bag.\n *\n * ⚠️ A different fact from `campaignIds` and the two must never be folded\n * together — `upsert-contact.ts` says so at the field itself. A touch is the\n * ad or the link the visitor arrived by, already resolved through the\n * allowlist by the silo; `campaignIds` is which campaigns the merchant filed\n * the capture SURFACE under, which is true of everybody who fills that form\n * in. Folding them would credit a campaign for a visitor who typed the\n * address.\n *\n * Passed through as the silo resolved it, unread: the touch's shape belongs\n * to whatever resolves it, and re-validating it here would be a second copy\n * of a rule that has already run. Only its presence is decided here, because\n * `null` and absent mean the same thing to the writer and a caller should\n * not have to know which one it sends.\n */\nfunction campaignTouchOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): Pick<UpsertHostContactOptions, 'campaignTouch'> {\n const touch = detail?.['campaignTouch']\n return touch ? { campaignTouch: touch as ResolvedCampaignTouch } : {}\n}\n\n/**\n * The silo's word for its door, as a source the CRM stores.\n *\n * ⚠️ `ContactSource` is a CLOSED union and the contract's `source` is an open\n * string, deliberately: a silo declares its door with\n * `registerPluginContactSource` rather than core enumerating every plugin's.\n * The two meet here, and a word outside the union is passed through rather\n * than rejected — refusing it would lose a third-party plugin's capture over\n * a label, and the capture is the part that matters.\n *\n * ⛔ What it costs, until the union is widened: the console's source filter\n * and `SOURCE_LABELS` key on these values, so an undeclared word renders raw\n * and matches no filter. Every first-party silo uses a word in the union, so\n * nothing does that today.\n */\nexport function contactSourceOf(source: string): ContactSource {\n const word = String(source ?? '').trim()\n if (!Object.hasOwn(CONTACT_SOURCE_LABELS, word)) {\n // Said once, where somebody debugging an unlabelled row will find it.\n // Not a refusal: the capture is worth more than the label.\n console.warn(\n `crm contact capture: source \"${word}\" has no label, so it will render ` +\n 'raw and match no filter in the console.',\n )\n }\n return word as ContactSource\n}\n\n/** What a refused caller is told — customer-safe, and never a key. */\nfunction refusalText(reason: 'invalid-email' | 'band' | 'erased' | 'error'): string {\n switch (reason) {\n case 'invalid-email':\n return 'That address could not be read, so no contact was recorded.'\n case 'band':\n return 'This workspace is at the number of contacts its plan holds.'\n case 'erased':\n return 'This person was erased from this workspace and was not recreated.'\n default:\n return 'The contact could not be recorded. Nothing else was affected.'\n }\n}\n"],"names":["CONTACT_SOURCE_LABELS","formLeadSource","isCrmLeadOpen","normalizeContactEmail","personKey","captureHostContact","contactCaptureActor","emitHostEvent","addHostLeadOutcome","findContactByEmail","firebaseAdmin","orgDataCollectionForHost","readLeadForHost","convertOpenLeadOntoContact","captureContactForCrm","request","surface","identity","email","refusedEmail","heldAsContact","fileLead","openLeadFor","verdict","captureOnContact","ok","record","hostId","contactId","by","interaction","source","error","console","reason","refusalText","contacts","snapshot","key","lead","data","convertedContactId","hostRef","app","firestore","collection","doc","created","formId","entryPointOf","detail","outcome","name","marketingConsent","disclosureOf","campaignTouchOf","campaignTouch","touch","stored","cause","actor","leadId","sourceAdded","contactSourceOf","atMs","undefined","refId","summary","tags","length","campaignIds","purchaseCents","purchaseCurrency","lifecycleFloor","initialLifecycleStage","profile","facet","refused","disclosedConsentGroup","text","value","trim","path","word","String","Object","hasOwn","warn"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/capture-contact.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type {\n PluginContactCaptureRequest,\n PluginContactCaptured,\n} from '@aglyn/aglyn/plugin-manager/plugin-contact-capture'\nimport {\n CONTACT_SOURCE_LABELS,\n type ContactSource,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport { formLeadSource } from '@aglyn/aglyn/app-utils/forms'\nimport {\n type CrmLeadFields,\n isCrmLeadOpen,\n normalizeContactEmail,\n personKey,\n} from '@aglyn/aglyn/server'\nimport { captureHostContact } from '@aglyn/tenant-runtime/capture-host-contact'\nimport { contactCaptureActor } from '@aglyn/tenant-runtime/capture-actor'\nimport { emitHostEvent } from '@aglyn/tenant-runtime/emit-host-event'\nimport {\n addHostLeadOutcome,\n findContactByEmail,\n firebaseAdmin,\n orgDataCollectionForHost,\n readLeadForHost,\n type UpsertHostContactOptions,\n} from '@aglyn/tenant-data-admin'\nimport type { ResolvedCampaignTouch } from '@aglyn/tenant-data-admin/server/campaign-conversion-attribution'\nimport { convertOpenLeadOntoContact } from './convert-open-lead'\n\n/**\n * The CRM answering the platform's contact-capture contract (AGL-3080).\n *\n * Four silos meet the same person — a form submission, a member signing up,\n * an order, a booking — and none of them is the record system. Each holds an\n * address, a name and the fact that something happened, and each wants the\n * workspace's person record to know. Today each imports `captureHostContact`\n * directly, which is the highest fan-in edge in the repo and makes the CRM\n * something a storefront cannot take a payment without.\n *\n * So this is the CRM's side of the seam: the silo reports what it saw, and\n * the plugin that keeps people decides everything a record system decides —\n * keying the address, whether this is somebody new, the audience band, the\n * erasure rows, the company, the owner, and what a new person sets off.\n * `captureHostContact` already does all of that; this translates the\n * contract's vocabulary into its options and its verdict back.\n *\n * ⚠️ IT NEVER THROWS. Every caller has already done the thing it is\n * recording — the submission was accepted, the order was paid — so a refusal\n * is RETURNED and costs the silo nothing. A throw here would lose an order\n * for a contact that could not be filed.\n */\n/**\n * WHICH RECORD A CAPTURE LANDS ON (AGL-3232) — the Salesforce rule, decided\n * here because the CRM is the plugin that models both records.\n *\n * One person is one record: a LEAD until somebody qualifies them, a CONTACT\n * after. Every door used to write both — a lead-routed form filed a lead\n * AND a contact at stage Lead — so one person sat in two lists and the Leads\n * queue was never the whole story. Now a door says what kind of surface it\n * is (`request.surface`) and this decides:\n *\n * - a LEAD surface files a lead, unless the workspace already holds the\n * address as a contact — a customer who books a demo is a customer's\n * interaction, not a new lead — in which case the capture lands on the\n * contact and no lead is filed;\n * - a RELATIONSHIP (a member account, a purchase) makes the contact, and\n * an open lead the site held for the address is stamped converted onto\n * it, so nobody keeps working a lead who already joined or bought;\n * - a TOUCH (an unrouted form, a newsletter opt-in) lands on the open lead\n * when the site holds one — its consent and its history stay on the one\n * record the rep is working — and on the contact otherwise.\n *\n * A lead is filed through `addHostLead`, the one writer of the leads silo,\n * so it is keyed on the person, counted against the ceiling and carries the\n * campaign touch the door resolved. A NEW lead announces itself with the\n * `lead` host event, which is what a \"new lead\" automation listens for; a\n * repeat capture on a lead the site already held announces nothing, the\n * way a repeat visit by a contact is an interaction and not a creation.\n */\nexport async function captureContactForCrm(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n const surface = request.surface ?? 'touch'\n // The one refusal every surface shares, answered before any read: an\n // address nothing can key is a person nothing can record.\n if (!normalizeContactEmail(request.identity.email)) return refusedEmail()\n try {\n if (surface === 'lead') {\n if (!(await heldAsContact(request))) return await fileLead(request)\n } else if (surface === 'touch') {\n if (await openLeadFor(request)) return await fileLead(request)\n }\n const verdict = await captureOnContact(request)\n if (verdict.ok && verdict.record === 'contact' && surface === 'relationship') {\n await convertOpenLeadOntoContact({\n hostId: request.hostId,\n email: request.identity.email,\n contactId: verdict.contactId,\n by: request.interaction.source === 'order' ? 'purchase' : 'signup',\n })\n }\n return verdict\n } catch (error) {\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/** The refusal every door gets for an address nothing can key. */\nfunction refusedEmail(): PluginContactCaptured {\n return { ok: false, reason: 'invalid-email', error: refusalText('invalid-email') }\n}\n\n/**\n * Whether the workspace already holds the address as a contact — the\n * address index first, then the query, the same lookup every dedupe uses.\n * ORG-wide, not scoped to the capturing site: the contact door dedupes\n * across every site in the org, so a person any site holds as a contact\n * is a contact, whichever site met them this time.\n */\nasync function heldAsContact(request: PluginContactCaptureRequest): Promise<boolean> {\n const contacts = await orgDataCollectionForHost(request.hostId, 'contacts')\n return (await findContactByEmail(contacts, request.identity.email)) !== null\n}\n\n/** Whether the site holds an OPEN lead for the address — neither converted nor closed. */\nasync function openLeadFor(request: PluginContactCaptureRequest): Promise<boolean> {\n const key = personKey(request.identity.email) as string\n /*\n * Through the seam, which reads the org row and falls back to a lead the\n * backfill has not reached (AGL-3275). UNSCOPED on purpose: a sibling brand\n * in the same consent group has to find this person, or its door concludes\n * there is no lead and files the duplicate this migration exists to end.\n */\n const snapshot = await readLeadForHost(request.hostId, key)\n if (!snapshot) return false\n const lead = (snapshot.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n return !lead.convertedContactId && isCrmLeadOpen(lead)\n}\n\n/**\n * The capture as a LEAD. The source string is the lead silo's own\n * vocabulary — `form:{formId}` for a form, the door's word otherwise — so\n * a lead's provenance survives the form being renamed, as the submission's\n * does.\n */\nasync function fileLead(request: PluginContactCaptureRequest): Promise<PluginContactCaptured> {\n const email = normalizeContactEmail(request.identity.email) as string\n const key = personKey(email) as string\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(request.hostId)\n // `addHostLead` resolves and writes the org collection itself (AGL-3275);\n // this only needs to know whether the person is new, which decides the event.\n const created = !(await readLeadForHost(request.hostId, key))\n const { formId } = entryPointOf(request.detail)\n const source =\n request.interaction.source === 'form' && formId\n ? formLeadSource(formId)\n : request.interaction.source\n const outcome = await addHostLeadOutcome({\n hostRef,\n hostId: request.hostId,\n lead: {\n email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source,\n ...(request.marketingConsent ? { marketingConsent: true } : {}),\n ...disclosureOf(request),\n },\n ...(campaignTouchOf(request.detail).campaignTouch\n ? { touch: campaignTouchOf(request.detail).campaignTouch }\n : {}),\n })\n if (!outcome.stored) {\n return {\n ok: false,\n reason: 'band',\n error: 'This site is at the number of leads it may hold, so the lead was not recorded.',\n }\n }\n if (created) {\n const cause = contactCaptureActor(\n request.interaction.source,\n email,\n request.actor,\n )\n await emitHostEvent(\n request.hostId,\n 'lead',\n {\n email,\n source,\n leadId: key,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n },\n cause ? { actor: cause } : {},\n )\n }\n return { ok: true, record: 'lead', leadId: key, created, sourceAdded: outcome.sourceAdded }\n}\n\n/** The capture as a CONTACT — what every capture was before the rule above. */\nasync function captureOnContact(\n request: PluginContactCaptureRequest,\n): Promise<PluginContactCaptured> {\n try {\n const verdict = await captureHostContact({\n ...(request.actor ? { actor: request.actor } : {}),\n hostId: request.hostId,\n email: request.identity.email,\n ...(request.identity.name ? { name: request.identity.name } : {}),\n source: contactSourceOf(request.interaction.source),\n interaction: {\n ...(request.interaction.atMs === undefined\n ? {}\n : { atMs: request.interaction.atMs }),\n ...(request.interaction.refId ? { refId: request.interaction.refId } : {}),\n ...(request.interaction.summary\n ? { summary: request.interaction.summary }\n : {}),\n ...entryPointOf(request.detail),\n },\n ...campaignTouchOf(request.detail),\n ...(request.marketingConsent === undefined\n ? {}\n : { marketingConsent: request.marketingConsent }),\n ...disclosureOf(request),\n ...(request.tags?.length ? { tags: request.tags } : {}),\n ...campaignsFiledUnder(request),\n ...(request.purchaseCents === undefined\n ? {}\n : { purchaseCents: request.purchaseCents }),\n ...(request.purchaseCurrency\n ? { purchaseCurrency: request.purchaseCurrency }\n : {}),\n ...(request.lifecycleFloor\n ? { initialLifecycleStage: request.lifecycleFloor as never }\n : {}),\n ...(request.profile ? { facet: request.profile as never } : {}),\n })\n if ('refused' in verdict) {\n return { ok: false, reason: verdict.refused, error: refusalText(verdict.refused) }\n }\n return {\n ok: true,\n record: 'contact',\n contactId: verdict.contactId,\n created: verdict.created,\n }\n } catch (error) {\n /*\n * The contract's `error` is the last state, not a channel for a stack:\n * a silo may show or log it as it stands, so it says what happened and\n * names nothing internal. The detail goes to the log, where whoever is\n * debugging a missing contact will look.\n */\n console.error('crm contact capture failed', error)\n return {\n ok: false,\n reason: 'error',\n error: 'The contact could not be recorded. Nothing else was affected.',\n }\n }\n}\n\n/**\n * THE DISCLOSURE the capture surface rendered, handed to whichever record the\n * capture lands on (AGL-3320).\n *\n * Passed through unread: both writers resolve the site's group themselves and\n * pool the opt-in only when this is its current key, so a lead and a contact\n * written by one capture record the same sites. A string or nothing, since\n * the writers compare it exactly and anything else could only fail to match.\n */\nfunction disclosureOf(\n request: PluginContactCaptureRequest,\n): { disclosedConsentGroup?: string } {\n const key = request.disclosedConsentGroup\n return typeof key === 'string' && key ? { disclosedConsentGroup: key } : {}\n}\n\n/**\n * THE ENTRY POINT, off the silo's own `detail` bag (AGL-3080).\n *\n * The contract carries a capture's silo-side facts opaquely, so the two the\n * CRM models are picked out here rather than typed into the platform. Both\n * ride the INTERACTION, which is where `upsertHostContact` already keeps\n * them: which form a person came in through routes the owner-assignment\n * rules and rides the `contactCreated` payload, and the page is what a\n * timeline row says about where they were.\n *\n * Absent, misspelled or the wrong type is the same answer as a door that\n * never had one — a capture without an entry point, which is every capture\n * that did not come through a form. It is never a reason to refuse: the\n * person is the part that matters.\n */\nfunction entryPointOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): { formId?: string; path?: string } {\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' && value.trim() ? value : undefined\n const formId = text(detail?.['formId'])\n const path = text(detail?.['path'])\n return {\n ...(formId ? { formId } : {}),\n ...(path ? { path } : {}),\n }\n}\n\n/**\n * The campaigns the capture surface is filed under, as the contact writer\n * records them. A capture names what its surface is filed under by container\n * kind; the CRM keeps the `campaign` kind on the person's site facet, and a\n * kind it does not keep is the capture's to name and the CRM's to ignore.\n */\nfunction campaignsFiledUnder(\n request: PluginContactCaptureRequest,\n): Pick<UpsertHostContactOptions, 'campaignIds'> {\n const ids = request.containers?.['campaign'] ?? []\n return ids.length ? { campaignIds: ids } : {}\n}\n\n/**\n * WHERE THE VISITOR CAME FROM, off the same bag.\n *\n * ⚠️ A different fact from `campaignIds` and the two must never be folded\n * together — `upsert-contact.ts` says so at the field itself. A touch is the\n * ad or the link the visitor arrived by, already resolved through the\n * allowlist by the silo; `campaignIds` is which campaigns the merchant filed\n * the capture SURFACE under, which is true of everybody who fills that form\n * in. Folding them would credit a campaign for a visitor who typed the\n * address.\n *\n * Passed through as the silo resolved it, unread: the touch's shape belongs\n * to whatever resolves it, and re-validating it here would be a second copy\n * of a rule that has already run. Only its presence is decided here, because\n * `null` and absent mean the same thing to the writer and a caller should\n * not have to know which one it sends.\n */\nfunction campaignTouchOf(\n detail: Readonly<Record<string, unknown>> | undefined,\n): Pick<UpsertHostContactOptions, 'campaignTouch'> {\n const touch = detail?.['campaignTouch']\n return touch ? { campaignTouch: touch as ResolvedCampaignTouch } : {}\n}\n\n/**\n * The silo's word for its door, as a source the CRM stores.\n *\n * ⚠️ `ContactSource` is a CLOSED union and the contract's `source` is an open\n * string, deliberately: a silo declares its door with\n * `registerPluginContactSource` rather than core enumerating every plugin's.\n * The two meet here, and a word outside the union is passed through rather\n * than rejected — refusing it would lose a third-party plugin's capture over\n * a label, and the capture is the part that matters.\n *\n * ⛔ What it costs, until the union is widened: the console's source filter\n * and `SOURCE_LABELS` key on these values, so an undeclared word renders raw\n * and matches no filter. Every first-party silo uses a word in the union, so\n * nothing does that today.\n */\nexport function contactSourceOf(source: string): ContactSource {\n const word = String(source ?? '').trim()\n if (!Object.hasOwn(CONTACT_SOURCE_LABELS, word)) {\n // Said once, where somebody debugging an unlabelled row will find it.\n // Not a refusal: the capture is worth more than the label.\n console.warn(\n `crm contact capture: source \"${word}\" has no label, so it will render ` +\n 'raw and match no filter in the console.',\n )\n }\n return word as ContactSource\n}\n\n/** What a refused caller is told — customer-safe, and never a key. */\nfunction refusalText(reason: 'invalid-email' | 'band' | 'erased' | 'error'): string {\n switch (reason) {\n case 'invalid-email':\n return 'That address could not be read, so no contact was recorded.'\n case 'band':\n return 'This workspace is at the number of contacts its plan holds.'\n case 'erased':\n return 'This person was erased from this workspace and was not recreated.'\n default:\n return 'The contact could not be recorded. Nothing else was affected.'\n }\n}\n"],"names":["CONTACT_SOURCE_LABELS","formLeadSource","isCrmLeadOpen","normalizeContactEmail","personKey","captureHostContact","contactCaptureActor","emitHostEvent","addHostLeadOutcome","findContactByEmail","firebaseAdmin","orgDataCollectionForHost","readLeadForHost","convertOpenLeadOntoContact","captureContactForCrm","request","surface","identity","email","refusedEmail","heldAsContact","fileLead","openLeadFor","verdict","captureOnContact","ok","record","hostId","contactId","by","interaction","source","error","console","reason","refusalText","contacts","snapshot","key","lead","data","convertedContactId","hostRef","app","firestore","collection","doc","created","formId","entryPointOf","detail","outcome","name","marketingConsent","disclosureOf","campaignTouchOf","campaignTouch","touch","stored","cause","actor","leadId","sourceAdded","contactSourceOf","atMs","undefined","refId","summary","tags","length","campaignsFiledUnder","purchaseCents","purchaseCurrency","lifecycleFloor","initialLifecycleStage","profile","facet","refused","disclosedConsentGroup","text","value","trim","path","ids","containers","campaignIds","word","String","Object","hasOwn","warn"],"mappings":"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"}
|
|
@@ -55,7 +55,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
55
55
|
* exactly what the sweep exists to fold in. A SPLIT whose key survives stays
|
|
56
56
|
* a live key, so each copy is stamped with the change that made it
|
|
57
57
|
* (`copiedByChange`), and a pass that finds the stamp does not copy again.
|
|
58
|
-
*/ import { mergeContactFacet } from "
|
|
58
|
+
*/ import { mergeContactFacet } from "../model/contact-merge.js";
|
|
59
59
|
import { CONTACT_FACETS_FIELD, interactionsForGroup } from "@aglyn/aglyn/app-utils/contacts";
|
|
60
60
|
import { COMPANY_CONTACTS_COUNT_FIELD, CONTACT_COMPANY_IDS_FIELD, CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
|
|
61
61
|
import { CAPTURED_BY_HOST_FIELD, CONSENT_GROUP_ID_FIELD, MARKETING_CONSENT_BY_HOST_FIELD, MARKETING_CONSENT_FIELD, MARKETING_CONSENT_SOURCE_FIELD } from "@aglyn/aglyn/app-utils/marketing-consent";
|