@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/crm-leads.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `/v1/leads` (AGL-2627) — the work queue: every person a site captured,\n * with the working state a sales team keeps on them and the door that turns\n * one into a contact.\n *\n * ## A lead belongs to a site, so every request names one\n *\n * The five CRM collections sit under the org and a key reads them whole\n * (`crm-shared.ts`). A lead does not: it lives at\n * `hosts/{siteId}/leads/{id}`, private to the site that captured it, with no\n * `visibleTo` to filter on and an id that is only unique within its site —\n * the person key, so one address is one row per site. There is no org-wide\n * list to read, and `/v1/leads/{id}` alone names a row in every site the org\n * has. So `siteId` is required on every endpoint here, as a query parameter\n * (a `PATCH` or `POST` may carry it in the body instead), and a site the\n * organization does not own is refused the way `consentSiteId` is on a\n * create. That is also what keeps this resource mountable at the org level:\n * the site is a parameter, never an assumption.\n *\n * ## Newest first, and the status filter runs on the page\n *\n * A polling integration asks for what arrived since it last looked, so the\n * list is ordered by `lastSeen` descending — the one field the capture door\n * stamps on every lead, at every capture — with the id as tiebreak, and the\n * cursor carries both. `?status=` is applied to the page rather than sent to\n * Firestore, for a reason a `where` cannot answer: a lead nobody has touched\n * carries no `status` field at all and reads as `new` (`crmLeadStatus`), so\n * `where('status', '==', 'new')` would miss exactly the leads a work queue\n * exists to show. The page can come back short, which the conventions page\n * already documents; no composite index is needed and none is shipped.\n *\n * ## A lead is a record of its own (AGL-3231)\n *\n * `POST /v1/leads` creates one — Salesforce's lead, sourced outside the\n * site and entered before anyone has qualified it: the address, a name,\n * the company as text, a title, a phone, a website, an address, tags and a\n * lead source, with the working state beside them. It goes through the one\n * lead door (`addHostLead`), so a second create for one address updates the\n * lead the site holds and answers `200` rather than minting a second row;\n * it writes no contact and no company, which are the conversion's to make;\n * and it records no marketing basis, because an integration is not a box\n * the person ticked. The same profile fields are writable on a `PATCH`,\n * `null` clearing one, and every one of them is published on the lead.\n *\n * ## What a PATCH may say about a status\n *\n * The same three moves the console offers: `new`, `working`, and\n * `unqualified` with a reason. `qualified` is the converted state — a lead\n * reaches it through `POST /v1/leads/{id}/convert` and nowhere else, because\n * the status is a claim that a contact exists and the conversion is what\n * makes it true. A converted lead's status is fixed for the same reason.\n * Reopening an unqualified lead drops its reason, as the console does: a\n * lead being worked again is not \"unqualified because …\".\n */\nimport {\n checkVisitorRecordCeiling,\n CRM_LEAD_STATUSES,\n type CrmLeadFields,\n type CrmLeadProfilePatch,\n type CrmLeadStatus,\n crmLeadStatus,\n crmMemberOption,\n findOrgMember,\n LEADS_MAX_PER_HOST,\n normalizeCompanyDomain,\n normalizeContactEmail,\n crmPicklistDefaultLabel,\n judgeCrmLeadSource,\n normalizeCrmLeadProfile,\n personKey,\n readMarketingBasis,\n soloConsentGroup,\n} from '@aglyn/aglyn/server'\nimport { scopeTokensForHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport {\n addHostLead,\n apiJson,\n ApiErrors,\n decodeCursor,\n encodeCursor,\n listOrgMembers,\n listResponse,\n parseLimit,\n restampCrmListFieldsAt,\n} from '@aglyn/tenant-data-admin'\nimport {\n type ConvertHostLeadInput,\n type ConvertHostLeadRefusal,\n convertHostLead,\n} from '../convert-host-lead'\nimport { FieldPath, FieldValue, Timestamp } from 'firebase-admin/firestore'\nimport {\n type ApiV1Context,\n claimWrite,\n orgOwnsHost,\n readJsonBody,\n requireScope,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport {\n type Clearable,\n CRM_ID_MAX,\n CRM_LABEL_MAX,\n CRM_TEXT_MAX,\n CRM_TITLE_MAX,\n crmCustomUpdate,\n crmCustomView,\n crmTimes,\n crmValidationFailed,\n isoFromMs,\n memberError,\n readCrmCustomBody,\n readOptionalText,\n readOrgLeadSourcePicklist,\n refuseUnknownKeys,\n updatePayload,\n} from './crm-shared'\n\n/**\n * The statuses a PATCH may set — every status but the converted one.\n *\n * A literal, checked against the model's union, rather than a filter over\n * `CRM_LEAD_STATUSES`: nothing in this module may dereference the model at\n * load time, for the reason `crm-shared.ts` gives — the `/v1` suites mock\n * `@aglyn/aglyn/server` as a closed world, and a module-level read of a\n * constant they never asked for throws at import in every one of them.\n */\nconst LEAD_PATCH_STATUSES = [\n 'new',\n 'nurturing',\n 'working',\n 'unqualified',\n] as const satisfies readonly Exclude<CrmLeadStatus, 'qualified'>[]\n\n// ── The view ────────────────────────────────────────────────────────────────\n\n/** The lead object as published. Every writable field appears here. */\nfunction leadView(siteId: string, doc: FirebaseFirestore.DocumentSnapshot) {\n const data = (doc.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n // The lead's own silo — a group of one, however the org pools its\n // contacts — which is where `addHostLead` recorded the basis.\n const consent = readMarketingBasis(data, soloConsentGroup(siteId))\n return {\n id: doc.id,\n object: 'lead',\n siteId,\n email: typeof data['email'] === 'string' ? data['email'] : null,\n name: typeof data['name'] === 'string' ? data['name'] : null,\n status: crmLeadStatus(data),\n ownerUid: data.ownerUid ?? null,\n notes: data.notes ?? null,\n unqualifiedReason: data.unqualifiedReason ?? null,\n // The lead's own profile (AGL-3231).\n company: data.company ?? null,\n jobTitle: data.jobTitle ?? null,\n phone: data.phone ?? null,\n website: data.website ?? null,\n address: data.address ?? null,\n tags: Array.isArray(data.tags) ? data.tags.map(String) : [],\n leadSource: data.leadSource ?? null,\n sources: Array.isArray(data['sources']) ? data['sources'].map(String) : [],\n submissionCount:\n typeof data['submissionCount'] === 'number' ? data['submissionCount'] : 0,\n firstSeen: isoFromMs(data['firstSeenAtMs']),\n lastSeen: isoFromMs(data['lastSeenAtMs']),\n marketingConsent: consent.basis === 'granted',\n marketingConsentAt: consent.basis === 'granted' ? isoFromMs(consent.basisAtMs) : null,\n convertedContactId: data.convertedContactId ?? null,\n convertedAt: isoFromMs(data.convertedAtMs),\n companyId: data.companyId ?? null,\n dealId: data.dealId ?? null,\n // The org's own lead fields (AGL-3272) — `{}` on a lead that carries\n // none, so a client can index it without a guard.\n custom: crmCustomView(data as FirebaseFirestore.DocumentData),\n ...crmTimes(data as FirebaseFirestore.DocumentData),\n }\n}\n\n// ── The site ────────────────────────────────────────────────────────────────\n\n/**\n * The `siteId` a request names, validated — the query parameter first, the\n * body as the fallback a `POST`/`PATCH` may use. Required, and required to\n * be one of the organization's own sites, for the reason the module header\n * gives.\n */\nfunction readLeadSite(\n ctx: ApiV1Context,\n url: URL,\n body: Record<string, unknown> = {},\n): { siteId: string } | { response: Response } {\n const refuse = (error: string) => ({\n response: crmValidationFailed(ctx, 'lead', { siteId: error }),\n })\n const raw = url.searchParams.get('siteId') ?? body.siteId\n if (raw === undefined || raw === null) {\n return refuse('Required — name the site the lead belongs to')\n }\n const siteId = String(raw).trim()\n if (!siteId) return refuse('Must name a site')\n if (!orgOwnsHost(ctx, siteId)) return refuse('No such site in this organization')\n return { siteId }\n}\n\n/**\n * The org's leads (AGL-3275). The site is still named by every caller — a\n * lead is created and listed in a site's context — but it selects by\n * `visibleTo` now rather than by the collection it lives in, so\n * {@link leadsVisibleTo} is what a LIST must use. This bare ref is for\n * `doc(id)` reads and writes, which the route authorizes for itself.\n */\nfunction leadsCollection(ctx: ApiV1Context, _siteId: string) {\n return ctx.firestore.collection('orgs').doc(ctx.orgId).collection('leads')\n}\n\n/** The same collection, narrowed to the leads one site may see. */\nfunction leadsVisibleTo(ctx: ApiV1Context, siteId: string) {\n return leadsCollection(ctx, siteId).where(\n 'visibleTo',\n 'array-contains-any',\n scopeTokensForHost(siteId),\n )\n}\n\n// ── The owner ───────────────────────────────────────────────────────────────\n\n/**\n * The owner a body names, by uid or by address, resolved to a uid on the\n * roster — or `null` to clear, or `undefined` when the body named nobody.\n *\n * `ownerEmail` is what a spreadsheet or a zap has; a uid is what the record\n * stores. The address resolves the way the CRM's own import and automation\n * steps resolve one (AGL-2614): against the roster alone, so an address the\n * project's auth knows but the organization does not names nobody. One\n * roster read, paid only when an address was given.\n */\nasync function readOwner(\n ctx: ApiV1Context,\n body: Record<string, unknown>,\n errors: Record<string, string>,\n): Promise<Clearable<string>> {\n const ownerUid = readOptionalText(body, 'ownerUid', CRM_TITLE_MAX, errors)\n const ownerEmail = readOptionalText(body, 'ownerEmail', CRM_TITLE_MAX, errors)\n if (ownerUid !== undefined && ownerEmail !== undefined) {\n errors.ownerEmail = 'Name the owner once — by ownerUid or by ownerEmail, not both'\n return undefined\n }\n if (ownerEmail === undefined) return ownerUid\n if (ownerEmail === null) return null\n const email = normalizeContactEmail(ownerEmail)\n if (!email) {\n errors.ownerEmail = 'Must be an email address'\n return undefined\n }\n const members = await listOrgMembers(ctx.orgId)\n const roster = members\n .map((member) => crmMemberOption(member as unknown as Record<string, unknown>))\n .filter((option): option is NonNullable<typeof option> => option !== null)\n const found = findOrgMember(roster, email)\n if (!found) {\n errors.ownerEmail = 'No member of this organization has this address'\n return undefined\n }\n return found.uid\n}\n\n// ── PATCH ───────────────────────────────────────────────────────────────────\n\n/** The lead's own fields, writable on a create and a PATCH alike (AGL-3231). */\nconst LEAD_PROFILE_KEYS = [\n 'company',\n 'jobTitle',\n 'phone',\n 'website',\n 'address',\n 'tags',\n 'leadSource',\n] as const\n\nconst LEAD_WRITABLE = new Set([\n 'siteId',\n 'status',\n 'ownerUid',\n 'ownerEmail',\n 'notes',\n 'unqualifiedReason',\n // The org's own lead fields (AGL-3272), judged against the definitions\n // whose object is `lead` — never read by `readLeadInput`, which knows\n // nothing about the org's definitions, but named here so a body that\n // carries one is not refused as an unknown key.\n 'custom',\n ...LEAD_PROFILE_KEYS,\n])\n\ninterface LeadInput {\n status?: Exclude<CrmLeadStatus, 'qualified'>\n notes?: Clearable<string>\n unqualifiedReason?: Clearable<string>\n /** The profile as the model read it: a key present and `null` clears. */\n profile: CrmLeadProfilePatch\n}\n\n/** The profile as an update: a cleared field is deleted, not blanked. */\nfunction profileUpdate(patch: CrmLeadProfilePatch): Record<string, unknown> {\n const update: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(patch)) {\n if (value === undefined) continue\n update[key] = value === null ? FieldValue.delete() : value\n }\n return update\n}\n\nfunction readLeadInput(\n body: Record<string, unknown>,\n): { values: LeadInput } | { errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n const values: LeadInput = { profile: {} }\n refuseUnknownKeys(body, LEAD_WRITABLE, 'lead', errors)\n\n const profileBody: Record<string, unknown> = {}\n for (const key of LEAD_PROFILE_KEYS) {\n if (key in body) profileBody[key] = body[key]\n }\n const profile = normalizeCrmLeadProfile(profileBody)\n Object.assign(errors, profile.errors)\n values.profile = profile.patch\n\n if (body.status !== undefined) {\n if (body.status === 'qualified') {\n errors.status =\n 'Not settable — a lead is qualified by converting it: POST /v1/leads/{id}/convert'\n } else if (\n typeof body.status === 'string' &&\n (LEAD_PATCH_STATUSES as readonly string[]).includes(body.status)\n ) {\n values.status = body.status as LeadInput['status']\n } else {\n errors.status = `Must be one of: ${LEAD_PATCH_STATUSES.join(', ')}`\n }\n }\n const notes = readOptionalText(body, 'notes', CRM_TEXT_MAX, errors)\n if (notes !== undefined) values.notes = notes\n const reason = readOptionalText(body, 'unqualifiedReason', CRM_TEXT_MAX, errors)\n if (reason !== undefined) values.unqualifiedReason = reason\n\n return Object.keys(errors).length ? { errors } : { values }\n}\n\n/** `PATCH /v1/leads/{id}`. */\nasync function updateLead(\n request: Request,\n ctx: ApiV1Context,\n url: URL,\n leadId: string,\n): Promise<Response> {\n const body = await readJsonBody(request)\n const parsed = readLeadInput(body)\n if ('errors' in parsed) return crmValidationFailed(ctx, 'lead', parsed.errors)\n const site = readLeadSite(ctx, url, body)\n if ('response' in site) return site.response\n const ref = leadsCollection(ctx, site.siteId).doc(leadId)\n const snap = await ref.get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such lead', headers: ctx.headers })\n }\n const stored = (snap.data() ?? {}) as Record<string, unknown> & CrmLeadFields\n\n const { status, unqualifiedReason, profile, ...rest } = parsed.values\n if (status !== undefined && stored.convertedContactId) {\n return ApiErrors.conflict({\n message: 'This lead was converted; its status is fixed',\n code: 'lead_converted',\n headers: ctx.headers,\n })\n }\n const errors: Record<string, string> = {}\n const ownerUid = await readOwner(ctx, body, errors)\n if (ownerUid) Object.assign(errors, await memberError(ctx, 'ownerUid', ownerUid))\n const current = crmLeadStatus(stored)\n const next = status ?? current\n /*\n * A reason travels with the closed state and only with it. Sent beside\n * `status: \"unqualified\"` it is required; sent on its own it changes the\n * reason of a lead that is already unqualified; sent with any other\n * status it describes a state the lead is not in.\n */\n if (next === 'unqualified') {\n const reason =\n unqualifiedReason !== undefined ? unqualifiedReason : stored.unqualifiedReason\n if (!reason) {\n errors.unqualifiedReason = 'A reason is required to mark a lead unqualified'\n }\n } else if (unqualifiedReason) {\n errors.unqualifiedReason = 'Only an unqualified lead carries a reason'\n }\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298): a restricted picklist. An active\n * value is stored as the list spells it; the value the lead already\n * holds is kept even when it has since been deactivated; anything else\n * is a 400 naming the values the list allows.\n */\n if (typeof profile.leadSource === 'string') {\n const judged = judgeCrmLeadSource(\n await readOrgLeadSourcePicklist(ctx),\n profile.leadSource,\n stored.leadSource,\n )\n if (judged.ok === false) errors.leadSource = judged.error\n else profile.leadSource = judged.value\n }\n if (Object.keys(errors).length) return crmValidationFailed(ctx, 'lead', errors)\n // The org's own lead fields (AGL-3272), judged against the definitions\n // whose object is `lead` — a contact field of the same key is a\n // different field with a possibly different type.\n const custom = await readCrmCustomBody(ctx, body, 'lead')\n if (custom && 'errors' in custom) return crmValidationFailed(ctx, 'lead', custom.errors)\n\n const update: Record<string, unknown> = {\n ...updatePayload({ ...rest, ownerUid }),\n ...profileUpdate(profile),\n // One dotted path per key, so the map is merged rather than replaced.\n ...(custom && 'values' in custom ? crmCustomUpdate(custom.values) : {}),\n }\n if (status !== undefined && status !== current) update.status = status\n if (next === 'unqualified') {\n if (unqualifiedReason !== undefined) update.unqualifiedReason = unqualifiedReason\n } else if (stored.unqualifiedReason !== undefined) {\n // Reopening drops the reason with the closed state.\n update.unqualifiedReason = FieldValue.delete()\n }\n if (Object.keys(update).length > 0) {\n await ref.update({ ...update, updatedAt: Timestamp.now() })\n // What the console's Leads list searches and filters by (AGL-3321).\n await restampCrmListFieldsAt(ref, 'leads')\n }\n return apiJson(leadView(site.siteId, await ref.get()), { headers: ctx.headers })\n}\n\n// ── Create ──────────────────────────────────────────────────────────────────\n\nconst LEAD_CREATE_STATUSES = ['new', 'working'] as const\nconst LEAD_NAME_MAX = 120\n\n/** The surface a lead created over the API names, beside `signup`, `booking`, `form:{id}`, `import` and `manual`. */\nconst LEAD_API_SOURCE = 'api'\n\n/**\n * `POST /v1/leads` (AGL-3231). Accepts an `Idempotency-Key`, scoped to the\n * site. Through the one lead door, so one address is one lead per site: a\n * second create updates and answers `200`.\n */\nasync function createLead(request: Request, ctx: ApiV1Context, url: URL): Promise<Response> {\n const body = await readJsonBody(request)\n const errors: Record<string, string> = {}\n refuseUnknownKeys(\n body,\n new Set([...LEAD_WRITABLE, 'email', 'name']),\n 'lead',\n errors,\n )\n const email = normalizeContactEmail(body.email)\n if (!email) errors.email = 'Required — an email address'\n const name = readOptionalText(body, 'name', LEAD_NAME_MAX, errors)\n const notes = readOptionalText(body, 'notes', CRM_TEXT_MAX, errors)\n let status: (typeof LEAD_CREATE_STATUSES)[number] | undefined\n if (body.status !== undefined && body.status !== null) {\n if (\n typeof body.status === 'string' &&\n (LEAD_CREATE_STATUSES as readonly string[]).includes(body.status)\n ) {\n status = body.status as (typeof LEAD_CREATE_STATUSES)[number]\n } else {\n errors.status = `Must be one of: ${LEAD_CREATE_STATUSES.join(', ')}`\n }\n }\n if (body.unqualifiedReason !== undefined) {\n errors.unqualifiedReason = 'Not settable on a create — unqualify with a PATCH'\n }\n const profileBody: Record<string, unknown> = {}\n for (const key of LEAD_PROFILE_KEYS) {\n if (key in body) profileBody[key] = body[key]\n }\n const profile = normalizeCrmLeadProfile(profileBody)\n Object.assign(errors, profile.errors)\n const site = readLeadSite(ctx, url, body)\n if ('response' in site) {\n if (Object.keys(errors).length) {\n return crmValidationFailed(ctx, 'lead', {\n ...errors,\n siteId: 'Required — name the site the lead belongs to',\n })\n }\n return site.response\n }\n const ownerUid = await readOwner(ctx, body, errors)\n if (ownerUid) Object.assign(errors, await memberError(ctx, 'ownerUid', ownerUid))\n if (Object.keys(errors).length) return crmValidationFailed(ctx, 'lead', errors)\n // Judged above the claim (AGL-3272), so a refused field costs no\n // idempotency key — the shape the companies resource writes.\n const custom = await readCrmCustomBody(ctx, body, 'lead')\n if (custom && 'errors' in custom) return crmValidationFailed(ctx, 'lead', custom.errors)\n const customValues = custom && 'values' in custom ? custom.values : {}\n\n const claimed = await claimWrite(\n ctx,\n site.siteId,\n request.headers.get('Idempotency-Key'),\n 'lead-creates',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n try {\n const hostRef = ctx.firestore.collection('hosts').doc(site.siteId)\n const leadsRef = leadsCollection(ctx, site.siteId)\n // Non-null by construction: the normalizer refused every address this\n // derivation could not key.\n const leadId = personKey(email as string) as string\n const existing = await leadsRef.doc(leadId).get()\n const created = !existing.exists\n /*\n * THE ORG'S LEAD SOURCES (AGL-3298), judged once the lead's own value\n * is known: a value outside the list is a 400 naming what it allows, a\n * lead the site already held keeps its current value, and a new lead\n * that named none starts from the list's default.\n */\n const leadSources = await readOrgLeadSourcePicklist(ctx)\n if (typeof profile.patch.leadSource === 'string') {\n const judged = judgeCrmLeadSource(\n leadSources,\n profile.patch.leadSource,\n existing.get('leadSource'),\n )\n if (judged.ok === false) {\n await claim.release()\n return crmValidationFailed(ctx, 'lead', { leadSource: judged.error })\n }\n profile.patch.leadSource = judged.value\n } else if (profile.patch.leadSource === undefined && created) {\n const fallback = crmPicklistDefaultLabel(leadSources)\n if (fallback) profile.patch.leadSource = fallback\n }\n if (created) {\n // What the SITE may see (AGL-3275), matching `addHostLead`'s own count:\n // an unscoped count would charge one site for a sibling's leads.\n const used = (await leadsVisibleTo(ctx, site.siteId).count().get()).data().count\n if (checkVisitorRecordCeiling(used, LEADS_MAX_PER_HOST).exceeded) {\n await claim.release()\n return ApiErrors.conflict({\n message:\n 'This site is at the platform lead limit, so the lead was not ' +\n 'created. Remove some leads, or contact support if this is real traffic.',\n code: 'lead_ceiling',\n headers: ctx.headers,\n })\n }\n }\n const stored = await addHostLead({\n hostRef,\n hostId: site.siteId,\n lead: {\n email: email as string,\n ...(name ? { name } : {}),\n source: LEAD_API_SOURCE,\n },\n })\n if (!stored) {\n await claim.release()\n return ApiErrors.conflict({\n message: 'The lead could not be stored',\n code: 'lead_not_stored',\n headers: ctx.headers,\n })\n }\n const working: Record<string, unknown> = {\n ...profileUpdate(profile.patch),\n ...(status ? { status } : {}),\n ...(ownerUid ? { ownerUid } : {}),\n ...(notes ? { notes } : {}),\n /*\n * The map as a map. The write below is a `set` with `merge`, and a\n * merge is a DEEP one: a second create for an address the site\n * already holds fills in the keys this body named and leaves the\n * rest of the map alone. A dotted path would NOT do it — only\n * `update` reads dots as paths, and `set` would file a field\n * literally called `custom.tier`. An explicit `null` is kept, which\n * is how a create clears a key a first create had set.\n */\n ...(Object.keys(customValues).length ? { custom: customValues } : {}),\n }\n const ref = leadsRef.doc(leadId)\n if (Object.keys(working).length) {\n await ref.set({ ...working, updatedAt: Timestamp.now() }, { merge: true })\n // What the console's Leads list searches and filters by (AGL-3321).\n await restampCrmListFieldsAt(ref, 'leads')\n }\n const view = leadView(site.siteId, await ref.get())\n await claim.record(created ? 201 : 200, view)\n return apiJson(view, { status: created ? 201 : 200, headers: ctx.headers })\n } catch (error) {\n await claim.release()\n throw error\n }\n}\n\n// ── Convert ─────────────────────────────────────────────────────────────────\n\nconst CONVERT_KEYS = new Set(['siteId', 'company', 'deal', 'ownerUid', 'ownerEmail'])\nconst CURRENCY_PATTERN = /^[a-z]{3}$/\n\ntype ConvertPlan = Pick<ConvertHostLeadInput, 'companyId' | 'createCompany' | 'deal'>\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n Boolean(value) && typeof value === 'object' && !Array.isArray(value)\n\n/**\n * The convert body: `company` is `{ link }`, `{ create }` or `null`; `deal`\n * is what to open, or `null`. Each key is named in full — `company.link`,\n * `deal.amountCents` — so a refused nested field is the one the integrator\n * sent.\n */\nfunction readConvertInput(\n body: Record<string, unknown>,\n): { plan: ConvertPlan } | { errors: Record<string, string> } {\n const errors: Record<string, string> = {}\n const plan: ConvertPlan = {}\n refuseUnknownKeys(body, CONVERT_KEYS, 'conversion', errors)\n\n if (body.company !== undefined && body.company !== null) {\n if (!isPlainObject(body.company)) {\n errors.company = 'Must be { \"link\": id }, { \"create\": { … } } or null'\n } else {\n const company = body.company\n const keys = Object.keys(company)\n const shape = keys.length === 1 ? keys[0] : null\n if (shape === 'link') {\n const link = company.link\n if (typeof link !== 'string' || !link.trim() || link.trim().length > CRM_ID_MAX) {\n errors['company.link'] = 'Must be a company id'\n } else {\n plan.companyId = link.trim()\n }\n } else if (shape === 'create' && isPlainObject(company.create)) {\n const create = company.create\n for (const key of Object.keys(create)) {\n if (key !== 'name' && key !== 'domain') {\n errors[`company.create.${key}`] = 'Not writable on a company created here'\n }\n }\n const name = String(create.name ?? '')\n .trim()\n .slice(0, CRM_LABEL_MAX)\n if (!name) errors['company.create.name'] = 'A name is required'\n let domain: string | null = null\n if (create.domain !== undefined && create.domain !== null) {\n const rawDomain = String(create.domain).trim()\n domain = rawDomain ? normalizeCompanyDomain(rawDomain) : null\n if (rawDomain && !domain) {\n errors['company.create.domain'] = 'Must be a domain, like acme.com'\n }\n }\n if (name) plan.createCompany = { name, domain }\n } else {\n errors.company = 'Must be exactly one of { \"link\": id } or { \"create\": { \"name\", \"domain\"? } }'\n }\n }\n }\n\n if (body.deal !== undefined && body.deal !== null) {\n if (!isPlainObject(body.deal)) {\n errors.deal = 'Must be an object, or null'\n } else {\n const deal = body.deal\n for (const key of Object.keys(deal)) {\n if (!['title', 'amountCents', 'currency', 'stageId'].includes(key)) {\n errors[`deal.${key}`] = 'Not writable on a deal opened here'\n }\n }\n const title = String(deal.title ?? '')\n .trim()\n .slice(0, CRM_TITLE_MAX)\n if (!title) errors['deal.title'] = 'A title is required'\n let amountCents: number | null = null\n if (deal.amountCents !== undefined && deal.amountCents !== null) {\n if (\n typeof deal.amountCents !== 'number' ||\n !Number.isInteger(deal.amountCents) ||\n deal.amountCents < 0\n ) {\n errors['deal.amountCents'] = 'Must be a whole number of cents, 0 or more'\n } else {\n amountCents = deal.amountCents\n }\n }\n let currency = 'usd'\n if (deal.currency !== undefined && deal.currency !== null) {\n const value = String(deal.currency).trim().toLowerCase()\n if (CURRENCY_PATTERN.test(value)) currency = value\n else errors['deal.currency'] = 'Must be a three-letter ISO 4217 code, like usd'\n }\n let stageId: string | undefined\n if (deal.stageId !== undefined && deal.stageId !== null) {\n if (typeof deal.stageId !== 'string' || !deal.stageId.trim()) {\n errors['deal.stageId'] = 'Must be a stage id'\n } else {\n stageId = deal.stageId.trim().slice(0, CRM_ID_MAX)\n }\n }\n if (title) plan.deal = { title, amountCents, currency, ...(stageId ? { stageId } : {}) }\n }\n }\n\n return Object.keys(errors).length ? { errors } : { plan }\n}\n\n/** `POST /v1/leads/{id}/convert`. */\nasync function convertLead(\n request: Request,\n ctx: ApiV1Context,\n url: URL,\n leadId: string,\n): Promise<Response> {\n const body = await readJsonBody(request)\n const parsed = readConvertInput(body)\n if ('errors' in parsed) return crmValidationFailed(ctx, 'conversion', parsed.errors)\n const site = readLeadSite(ctx, url, body)\n if ('response' in site) return site.response\n const errors: Record<string, string> = {}\n const ownerUid = await readOwner(ctx, body, errors)\n if (ownerUid) Object.assign(errors, await memberError(ctx, 'ownerUid', ownerUid))\n if (Object.keys(errors).length) return crmValidationFailed(ctx, 'conversion', errors)\n\n const ref = leadsCollection(ctx, site.siteId).doc(leadId)\n if (!(await ref.get()).exists) {\n return ApiErrors.notFound({ message: 'No such lead', headers: ctx.headers })\n }\n\n // Scoped to the SITE: the lead is the site's record, and its id is only\n // unique within the site — see the module header.\n const claimed = await claimWrite(\n ctx,\n site.siteId,\n request.headers.get('Idempotency-Key'),\n 'lead-conversions',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const result = await convertHostLead({\n firestore: ctx.firestore,\n hostId: site.siteId,\n orgId: ctx.orgId,\n org: ctx.org as Record<string, unknown>,\n leadId,\n // A key has no uid; `'api'` is the attribution every REST create\n // stamps, and a key owns nothing — see the runtime's header. Its name\n // is what the site's activity feed shows for the conversion.\n actor: { uid: 'api', email: null, kind: 'api', apiKeyName: ctx.keyName },\n ...(ownerUid ? { ownerUid } : {}),\n ...parsed.plan,\n })\n if (result.ok === false) {\n await claim.release()\n return convertRefusal(ctx, result.reason)\n }\n const receipt = {\n object: 'lead_conversion',\n id: leadId,\n siteId: site.siteId,\n contactId: result.contactId,\n companyId: result.companyId ?? null,\n dealId: result.dealId ?? null,\n alreadyConverted: result.alreadyConverted,\n lead: leadView(site.siteId, await ref.get()),\n }\n await claim.record(200, receipt)\n return apiJson(receipt, {\n status: result.alreadyConverted ? 200 : 201,\n headers: ctx.headers,\n })\n } catch (error) {\n await claim.release()\n throw error\n }\n}\n\n/**\n * Each refusal the writes can answer, in the envelope this API publishes.\n * A band is `plan_required` with the creates' own code, because on the one\n * plan that refuses an upgrade is the whole remedy; the rest are states of\n * the record, which is what `409 conflict` means here.\n */\nfunction convertRefusal(\n ctx: ApiV1Context,\n reason: ConvertHostLeadRefusal,\n): Response {\n switch (reason) {\n case 'unknown-lead':\n return ApiErrors.notFound({ message: 'No such lead', headers: ctx.headers })\n case 'unknown-company':\n return crmValidationFailed(ctx, 'conversion', {\n 'company.link': 'No such company in this organization',\n })\n case 'band-full':\n return ApiErrors.planRequired({\n message:\n 'CRM records limit reached across contacts, companies and deals — ' +\n 'the lead was not converted. Upgrade the plan to add more.',\n code: 'crm_records_quota',\n headers: ctx.headers,\n })\n case 'no-email':\n return ApiErrors.conflict({\n message: 'This lead has no usable email address to convert',\n code: 'lead_not_convertible',\n headers: ctx.headers,\n })\n case 'contact-not-created':\n return ApiErrors.conflict({\n message:\n 'The contact could not be created — the audience band may be full. ' +\n 'Nothing was changed.',\n code: 'contact_not_created',\n headers: ctx.headers,\n })\n case 'erased':\n return ApiErrors.conflict({\n message:\n 'This person was erased from the organization at their request, so ' +\n 'a contact cannot be created for this address. Nothing was changed.',\n code: 'person_erased',\n headers: ctx.headers,\n })\n case 'no-stages':\n return ApiErrors.conflict({\n message: 'The default pipeline has no stages to open a deal in',\n code: 'pipeline_has_no_stages',\n headers: ctx.headers,\n })\n }\n}\n\n// ── List ────────────────────────────────────────────────────────────────────\n\n/** The `lastSeen`-ordered cursor: `ms|id`. */\nfunction encodeLeadCursor(lastSeenAtMs: unknown, id: string): string {\n return `${typeof lastSeenAtMs === 'number' ? lastSeenAtMs : 0}|${id}`\n}\n\nfunction decodeLeadCursor(raw: string | undefined): { lastSeenAtMs: number; id: string } | null {\n if (!raw) return null\n const match = /^(\\d+)\\|(.+)$/.exec(raw)\n return match ? { lastSeenAtMs: Number(match[1]), id: match[2] } : null\n}\n\n/** `GET /v1/leads`. */\nasync function listLeads(ctx: ApiV1Context, url: URL): Promise<Response> {\n const site = readLeadSite(ctx, url)\n if ('response' in site) return site.response\n const rawStatus = (url.searchParams.get('status') ?? '').trim()\n if (rawStatus && !(CRM_LEAD_STATUSES as readonly string[]).includes(rawStatus)) {\n return crmValidationFailed(ctx, 'lead filter', {\n status: `Must be one of: ${CRM_LEAD_STATUSES.join(', ')}`,\n })\n }\n const status = rawStatus as CrmLeadStatus | ''\n const ownerUid = (url.searchParams.get('ownerUid') ?? '').trim().slice(0, CRM_ID_MAX)\n\n const limit = parseLimit(url.searchParams.get('limit'))\n let query: FirebaseFirestore.Query = leadsVisibleTo(ctx, site.siteId)\n .orderBy('lastSeenAtMs', 'desc')\n .orderBy(FieldPath.documentId(), 'desc')\n .limit(limit + 1)\n const cursor = decodeLeadCursor(decodeCursor(url.searchParams.get('cursor')))\n if (cursor) query = query.startAfter(cursor.lastSeenAtMs, cursor.id)\n const snap = await query.get()\n const docs = snap.docs.slice(0, limit)\n const last = docs[docs.length - 1]\n const nextCursor =\n snap.docs.length > limit && last\n ? encodeCursor(encodeLeadCursor(last.get('lastSeenAtMs'), last.id))\n : null\n const page = docs.filter((doc) => {\n const data = (doc.data() ?? {}) as CrmLeadFields\n if (status && crmLeadStatus(data) !== status) return false\n if (ownerUid && data.ownerUid !== ownerUid) return false\n return true\n })\n return listResponse(\n page.map((doc) => leadView(site.siteId, doc)),\n nextCursor,\n ctx.headers,\n )\n}\n\n// ── Dispatch ────────────────────────────────────────────────────────────────\n\nexport async function handleLeads(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const [, leadId, action] = segments\n\n if (!leadId) {\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'crm:read')\n if (denied) return denied\n return listLeads(ctx, url)\n }\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return createLead(request, ctx, url)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, POST' },\n })\n }\n\n if (action === 'convert') {\n if (request.method === 'POST') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return convertLead(request, ctx, url, leadId)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'POST' },\n })\n }\n if (action !== undefined) {\n return ApiErrors.notFound({\n message: `Unknown endpoint: /v1/${segments.join('/')}`,\n headers: ctx.headers,\n })\n }\n\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'crm:read')\n if (denied) return denied\n const site = readLeadSite(ctx, url)\n if ('response' in site) return site.response\n const snap = await leadsCollection(ctx, site.siteId).doc(leadId).get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such lead', headers: ctx.headers })\n }\n return apiJson(leadView(site.siteId, snap), { headers: ctx.headers })\n }\n if (request.method === 'PATCH') {\n const denied = requireScope(ctx, 'crm:write')\n if (denied) return denied\n return updateLead(request, ctx, url, leadId)\n }\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, PATCH' },\n })\n}\n"],"names":["checkVisitorRecordCeiling","CRM_LEAD_STATUSES","crmLeadStatus","crmMemberOption","findOrgMember","LEADS_MAX_PER_HOST","normalizeCompanyDomain","normalizeContactEmail","crmPicklistDefaultLabel","judgeCrmLeadSource","normalizeCrmLeadProfile","personKey","readMarketingBasis","soloConsentGroup","scopeTokensForHost","addHostLead","apiJson","ApiErrors","decodeCursor","encodeCursor","listOrgMembers","listResponse","parseLimit","restampCrmListFieldsAt","convertHostLead","FieldPath","FieldValue","Timestamp","claimWrite","orgOwnsHost","readJsonBody","requireScope","CRM_ID_MAX","CRM_LABEL_MAX","CRM_TEXT_MAX","CRM_TITLE_MAX","crmCustomUpdate","crmCustomView","crmTimes","crmValidationFailed","isoFromMs","memberError","readCrmCustomBody","readOptionalText","readOrgLeadSourcePicklist","refuseUnknownKeys","updatePayload","LEAD_PATCH_STATUSES","leadView","siteId","doc","data","consent","id","object","email","name","status","ownerUid","notes","unqualifiedReason","company","jobTitle","phone","website","address","tags","Array","isArray","map","String","leadSource","sources","submissionCount","firstSeen","lastSeen","marketingConsent","basis","marketingConsentAt","basisAtMs","convertedContactId","convertedAt","convertedAtMs","companyId","dealId","custom","readLeadSite","ctx","url","body","refuse","error","response","raw","searchParams","get","undefined","trim","leadsCollection","_siteId","firestore","collection","orgId","leadsVisibleTo","where","readOwner","errors","ownerEmail","members","roster","member","filter","option","found","uid","LEAD_PROFILE_KEYS","LEAD_WRITABLE","Set","profileUpdate","patch","update","key","value","Object","entries","delete","readLeadInput","values","profile","profileBody","assign","includes","join","reason","keys","length","updateLead","request","leadId","snap","parsed","site","ref","exists","notFound","message","headers","stored","rest","conflict","code","current","next","judged","ok","updatedAt","now","LEAD_CREATE_STATUSES","LEAD_NAME_MAX","LEAD_API_SOURCE","createLead","customValues","claimed","replay","claim","hostRef","leadsRef","existing","created","leadSources","release","fallback","used","count","exceeded","hostId","lead","source","working","set","merge","view","record","CONVERT_KEYS","CURRENCY_PATTERN","isPlainObject","Boolean","readConvertInput","plan","shape","link","create","slice","domain","rawDomain","createCompany","deal","title","amountCents","Number","isInteger","currency","toLowerCase","test","stageId","convertLead","result","org","actor","kind","apiKeyName","keyName","convertRefusal","receipt","contactId","alreadyConverted","planRequired","encodeLeadCursor","lastSeenAtMs","decodeLeadCursor","match","exec","listLeads","rawStatus","limit","query","orderBy","documentId","cursor","startAfter","docs","last","nextCursor","page","handleLeads","segments","action","method","denied","methodNotAllowed","Allow"],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* `/v1/pipelines` (AGL-2606) — the stages a deal moves through, read-only.
|
|
19
|
+
*
|
|
20
|
+
* A pipeline is how one business sells, and its stages are named by the
|
|
21
|
+
* people who sell that way; the API reads them so a deal can be created in
|
|
22
|
+
* the right one and never edits them. What it does do is SEED one: an
|
|
23
|
+
* organization that has never opened the CRM has no pipeline, and a deal
|
|
24
|
+
* cannot exist without a stage, so the first deal created without naming a
|
|
25
|
+
* pipeline gets a default one built from `DEFAULT_DEAL_STAGES` — the same
|
|
26
|
+
* set a console would seed — stamped for the site that deal names.
|
|
27
|
+
*
|
|
28
|
+
* An ARCHIVED pipeline (AGL-2620) is still read — the deals it closed name
|
|
29
|
+
* it — but it takes no new deal: a create that names one is refused, and
|
|
30
|
+
* the default is picked among the active ones.
|
|
31
|
+
*/
|
|
32
|
+
import { type CrmDealStage, type CrmPipeline } from '@aglyn/aglyn/server';
|
|
33
|
+
import { type ApiV1Context } from '@aglyn/tenant-data-admin/server/api-v1-kit';
|
|
34
|
+
/** Stages in pipeline order, whatever order the document holds them in. */
|
|
35
|
+
export declare function orderedStages(pipeline: Pick<CrmPipeline, 'stages'>): CrmDealStage[];
|
|
36
|
+
/** The pipeline object as published. */
|
|
37
|
+
export declare function pipelineView(doc: FirebaseFirestore.DocumentSnapshot): {
|
|
38
|
+
created: string;
|
|
39
|
+
updated: string;
|
|
40
|
+
id: string;
|
|
41
|
+
object: string;
|
|
42
|
+
name: string;
|
|
43
|
+
isDefault: boolean;
|
|
44
|
+
archived: boolean;
|
|
45
|
+
archivedAt: string;
|
|
46
|
+
stages: {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
order: number;
|
|
50
|
+
probability: number;
|
|
51
|
+
kind: "open" | "won" | "lost";
|
|
52
|
+
}[];
|
|
53
|
+
siteId: string;
|
|
54
|
+
};
|
|
55
|
+
/** A pipeline as the deal handlers use it: its id beside its stages. */
|
|
56
|
+
export interface ResolvedPipeline {
|
|
57
|
+
id: string;
|
|
58
|
+
pipeline: CrmPipeline;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The pipeline a deal lands in.
|
|
62
|
+
*
|
|
63
|
+
* Named → that one, or a refusal naming `pipelineId`. Unnamed → the default
|
|
64
|
+
* among the pipelines VISIBLE FROM the deal's site, because a pipeline is
|
|
65
|
+
* scoped like every CRM row and a deal in a pipeline its own site cannot
|
|
66
|
+
* read is a deal the console renders with no stage. The visibility query is
|
|
67
|
+
* one `array-contains-any` on the site's own tokens, and the default is
|
|
68
|
+
* picked in memory: `isDefault` first, then the lowest id, so two consoles
|
|
69
|
+
* seeding at once still agree. None at all → one is created here.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolvePipeline(ctx: ApiV1Context, siteId: string, pipelineId: string | undefined, options?: {
|
|
72
|
+
allowArchived?: boolean;
|
|
73
|
+
}): Promise<ResolvedPipeline | {
|
|
74
|
+
error: string;
|
|
75
|
+
}>;
|
|
76
|
+
export declare function handlePipelines(request: Request, ctx: ApiV1Context, segments: string[], url: URL): Promise<Response>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ /**
|
|
18
|
+
* `/v1/pipelines` (AGL-2606) — the stages a deal moves through, read-only.
|
|
19
|
+
*
|
|
20
|
+
* A pipeline is how one business sells, and its stages are named by the
|
|
21
|
+
* people who sell that way; the API reads them so a deal can be created in
|
|
22
|
+
* the right one and never edits them. What it does do is SEED one: an
|
|
23
|
+
* organization that has never opened the CRM has no pipeline, and a deal
|
|
24
|
+
* cannot exist without a stage, so the first deal created without naming a
|
|
25
|
+
* pipeline gets a default one built from `DEFAULT_DEAL_STAGES` — the same
|
|
26
|
+
* set a console would seed — stamped for the site that deal names.
|
|
27
|
+
*
|
|
28
|
+
* An ARCHIVED pipeline (AGL-2620) is still read — the deals it closed name
|
|
29
|
+
* it — but it takes no new deal: a create that names one is refused, and
|
|
30
|
+
* the default is picked among the active ones.
|
|
31
|
+
*/ import { CRM_COLLECTIONS, createResourceUid, consentGroupForHost, crmScopeTokens, DEFAULT_DEAL_STAGES, isPipelineArchived } from "@aglyn/aglyn/server";
|
|
32
|
+
import { apiJson, ApiErrors } from "@aglyn/tenant-data-admin";
|
|
33
|
+
import { FieldPath } from "firebase-admin/firestore";
|
|
34
|
+
import { requireScope } from "@aglyn/tenant-data-admin/server/api-v1-kit";
|
|
35
|
+
import { crmCollection, crmCreateStamp, crmTimes, isoFromMs, listCrm } from "./crm-shared.js";
|
|
36
|
+
/** Stages in pipeline order, whatever order the document holds them in. */ export function orderedStages(pipeline) {
|
|
37
|
+
var _pipeline_stages;
|
|
38
|
+
return [
|
|
39
|
+
...(_pipeline_stages = pipeline.stages) != null ? _pipeline_stages : []
|
|
40
|
+
].sort((a, b)=>a.order - b.order);
|
|
41
|
+
}
|
|
42
|
+
/** The pipeline object as published. */ export function pipelineView(doc) {
|
|
43
|
+
var _doc_data, _data_name, _data_stages, _data_hostId;
|
|
44
|
+
const data = (_doc_data = doc.data()) != null ? _doc_data : {};
|
|
45
|
+
return _extends({
|
|
46
|
+
id: doc.id,
|
|
47
|
+
object: 'pipeline',
|
|
48
|
+
name: (_data_name = data.name) != null ? _data_name : null,
|
|
49
|
+
isDefault: data.isDefault === true,
|
|
50
|
+
archived: isPipelineArchived(data),
|
|
51
|
+
archivedAt: isPipelineArchived(data) ? isoFromMs(data.archivedAt) : null,
|
|
52
|
+
stages: orderedStages({
|
|
53
|
+
stages: (_data_stages = data.stages) != null ? _data_stages : []
|
|
54
|
+
}).map((stage)=>({
|
|
55
|
+
id: stage.id,
|
|
56
|
+
name: stage.name,
|
|
57
|
+
order: stage.order,
|
|
58
|
+
probability: stage.probability,
|
|
59
|
+
kind: stage.kind
|
|
60
|
+
})),
|
|
61
|
+
siteId: (_data_hostId = data.hostId) != null ? _data_hostId : null
|
|
62
|
+
}, crmTimes(data));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The pipeline a deal lands in.
|
|
66
|
+
*
|
|
67
|
+
* Named → that one, or a refusal naming `pipelineId`. Unnamed → the default
|
|
68
|
+
* among the pipelines VISIBLE FROM the deal's site, because a pipeline is
|
|
69
|
+
* scoped like every CRM row and a deal in a pipeline its own site cannot
|
|
70
|
+
* read is a deal the console renders with no stage. The visibility query is
|
|
71
|
+
* one `array-contains-any` on the site's own tokens, and the default is
|
|
72
|
+
* picked in memory: `isDefault` first, then the lowest id, so two consoles
|
|
73
|
+
* seeding at once still agree. None at all → one is created here.
|
|
74
|
+
*/ export async function resolvePipeline(ctx, siteId, pipelineId, options = {}) {
|
|
75
|
+
var _candidates_find;
|
|
76
|
+
const collection = crmCollection(ctx, CRM_COLLECTIONS.pipelines);
|
|
77
|
+
if (pipelineId) {
|
|
78
|
+
const snap = await collection.doc(pipelineId).get();
|
|
79
|
+
if (!snap.exists) return {
|
|
80
|
+
error: 'No such pipeline in this organization'
|
|
81
|
+
};
|
|
82
|
+
const pipeline = snap.data();
|
|
83
|
+
if (!options.allowArchived && isPipelineArchived(pipeline)) {
|
|
84
|
+
return {
|
|
85
|
+
error: 'This pipeline is archived — create the deal in an active one'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
id: snap.id,
|
|
90
|
+
pipeline
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const org = ctx.org;
|
|
94
|
+
const tokens = crmScopeTokens(org, consentGroupForHost(org, siteId));
|
|
95
|
+
// Ordered, because a `limit()` with no `orderBy` is a random sample: fifty
|
|
96
|
+
// is more pipelines than any org has, but the bound has to be a bound.
|
|
97
|
+
const visible = await collection.where('visibleTo', 'array-contains-any', tokens).orderBy(FieldPath.documentId()).limit(50).get();
|
|
98
|
+
const candidates = [
|
|
99
|
+
...visible.docs
|
|
100
|
+
].filter((doc)=>!isPipelineArchived(doc.data())).sort((a, b)=>a.id.localeCompare(b.id));
|
|
101
|
+
const chosen = (_candidates_find = candidates.find((doc)=>doc.get('isDefault') === true)) != null ? _candidates_find : candidates[0];
|
|
102
|
+
if (chosen) return {
|
|
103
|
+
id: chosen.id,
|
|
104
|
+
pipeline: chosen.data()
|
|
105
|
+
};
|
|
106
|
+
const id = createResourceUid();
|
|
107
|
+
const seeded = _extends({
|
|
108
|
+
name: 'Sales',
|
|
109
|
+
// A COPY: the module's default set must stay what the next seed starts
|
|
110
|
+
// from once a merchant edits this pipeline's stages.
|
|
111
|
+
stages: DEFAULT_DEAL_STAGES.map((stage)=>_extends({}, stage)),
|
|
112
|
+
isDefault: true,
|
|
113
|
+
archivedAt: null
|
|
114
|
+
}, crmCreateStamp(ctx, siteId));
|
|
115
|
+
await collection.doc(id).create(seeded);
|
|
116
|
+
return {
|
|
117
|
+
id,
|
|
118
|
+
pipeline: seeded
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export async function handlePipelines(request, ctx, segments, url) {
|
|
122
|
+
const denied = requireScope(ctx, 'crm:read');
|
|
123
|
+
if (denied) return denied;
|
|
124
|
+
if (request.method !== 'GET') {
|
|
125
|
+
return ApiErrors.methodNotAllowed({
|
|
126
|
+
headers: _extends({}, ctx.headers, {
|
|
127
|
+
Allow: 'GET'
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const collection = crmCollection(ctx, CRM_COLLECTIONS.pipelines);
|
|
132
|
+
const [, pipelineId] = segments;
|
|
133
|
+
if (!pipelineId) return listCrm(ctx, collection, url, [], pipelineView);
|
|
134
|
+
const snap = await collection.doc(pipelineId).get();
|
|
135
|
+
if (!snap.exists) {
|
|
136
|
+
return ApiErrors.notFound({
|
|
137
|
+
message: 'No such pipeline',
|
|
138
|
+
headers: ctx.headers
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return apiJson(pipelineView(snap), {
|
|
142
|
+
headers: ctx.headers
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
//# sourceMappingURL=crm-pipelines.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/server/api-v1/crm-pipelines.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * `/v1/pipelines` (AGL-2606) — the stages a deal moves through, read-only.\n *\n * A pipeline is how one business sells, and its stages are named by the\n * people who sell that way; the API reads them so a deal can be created in\n * the right one and never edits them. What it does do is SEED one: an\n * organization that has never opened the CRM has no pipeline, and a deal\n * cannot exist without a stage, so the first deal created without naming a\n * pipeline gets a default one built from `DEFAULT_DEAL_STAGES` — the same\n * set a console would seed — stamped for the site that deal names.\n *\n * An ARCHIVED pipeline (AGL-2620) is still read — the deals it closed name\n * it — but it takes no new deal: a create that names one is refused, and\n * the default is picked among the active ones.\n */\nimport {\n CRM_COLLECTIONS,\n type CrmDealStage,\n type CrmPipeline,\n createResourceUid,\n consentGroupForHost,\n crmScopeTokens,\n DEFAULT_DEAL_STAGES,\n isPipelineArchived,\n} from '@aglyn/aglyn/server'\nimport { apiJson, ApiErrors } from '@aglyn/tenant-data-admin'\nimport { FieldPath } from 'firebase-admin/firestore'\nimport {\n type ApiV1Context,\n requireScope,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport { crmCollection, crmCreateStamp, crmTimes, isoFromMs, listCrm } from './crm-shared'\n\n/** Stages in pipeline order, whatever order the document holds them in. */\nexport function orderedStages(pipeline: Pick<CrmPipeline, 'stages'>): CrmDealStage[] {\n return [...(pipeline.stages ?? [])].sort((a, b) => a.order - b.order)\n}\n\n/** The pipeline object as published. */\nexport function pipelineView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = (doc.data() ?? {}) as Partial<CrmPipeline>\n return {\n id: doc.id,\n object: 'pipeline',\n name: data.name ?? null,\n isDefault: data.isDefault === true,\n archived: isPipelineArchived(data),\n archivedAt: isPipelineArchived(data) ? isoFromMs(data.archivedAt) : null,\n stages: orderedStages({ stages: data.stages ?? [] }).map((stage) => ({\n id: stage.id,\n name: stage.name,\n order: stage.order,\n probability: stage.probability,\n kind: stage.kind,\n })),\n siteId: data.hostId ?? null,\n ...crmTimes(data as FirebaseFirestore.DocumentData),\n }\n}\n\n/** A pipeline as the deal handlers use it: its id beside its stages. */\nexport interface ResolvedPipeline {\n id: string\n pipeline: CrmPipeline\n}\n\n/**\n * The pipeline a deal lands in.\n *\n * Named → that one, or a refusal naming `pipelineId`. Unnamed → the default\n * among the pipelines VISIBLE FROM the deal's site, because a pipeline is\n * scoped like every CRM row and a deal in a pipeline its own site cannot\n * read is a deal the console renders with no stage. The visibility query is\n * one `array-contains-any` on the site's own tokens, and the default is\n * picked in memory: `isDefault` first, then the lowest id, so two consoles\n * seeding at once still agree. None at all → one is created here.\n */\nexport async function resolvePipeline(\n ctx: ApiV1Context,\n siteId: string,\n pipelineId: string | undefined,\n options: { allowArchived?: boolean } = {},\n): Promise<ResolvedPipeline | { error: string }> {\n const collection = crmCollection(ctx, CRM_COLLECTIONS.pipelines)\n if (pipelineId) {\n const snap = await collection.doc(pipelineId).get()\n if (!snap.exists) return { error: 'No such pipeline in this organization' }\n const pipeline = snap.data() as CrmPipeline\n if (!options.allowArchived && isPipelineArchived(pipeline)) {\n return { error: 'This pipeline is archived — create the deal in an active one' }\n }\n return { id: snap.id, pipeline }\n }\n const org = ctx.org as Record<string, unknown>\n const tokens = crmScopeTokens(org, consentGroupForHost(org, siteId))\n // Ordered, because a `limit()` with no `orderBy` is a random sample: fifty\n // is more pipelines than any org has, but the bound has to be a bound.\n const visible = await collection\n .where('visibleTo', 'array-contains-any', tokens)\n .orderBy(FieldPath.documentId())\n .limit(50)\n .get()\n const candidates = [...visible.docs]\n .filter((doc) => !isPipelineArchived(doc.data() as CrmPipeline))\n .sort((a, b) => a.id.localeCompare(b.id))\n const chosen =\n candidates.find((doc) => doc.get('isDefault') === true) ?? candidates[0]\n if (chosen) return { id: chosen.id, pipeline: chosen.data() as CrmPipeline }\n\n const id = createResourceUid()\n const seeded: CrmPipeline = {\n name: 'Sales',\n // A COPY: the module's default set must stay what the next seed starts\n // from once a merchant edits this pipeline's stages.\n stages: DEFAULT_DEAL_STAGES.map((stage) => ({ ...stage })),\n isDefault: true,\n archivedAt: null,\n ...crmCreateStamp(ctx, siteId),\n }\n await collection.doc(id).create(seeded)\n return { id, pipeline: seeded }\n}\n\nexport async function handlePipelines(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const denied = requireScope(ctx, 'crm:read')\n if (denied) return denied\n if (request.method !== 'GET') {\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET' },\n })\n }\n const collection = crmCollection(ctx, CRM_COLLECTIONS.pipelines)\n const [, pipelineId] = segments\n if (!pipelineId) return listCrm(ctx, collection, url, [], pipelineView)\n const snap = await collection.doc(pipelineId).get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such pipeline', headers: ctx.headers })\n }\n return apiJson(pipelineView(snap), { headers: ctx.headers })\n}\n"],"names":["CRM_COLLECTIONS","createResourceUid","consentGroupForHost","crmScopeTokens","DEFAULT_DEAL_STAGES","isPipelineArchived","apiJson","ApiErrors","FieldPath","requireScope","crmCollection","crmCreateStamp","crmTimes","isoFromMs","listCrm","orderedStages","pipeline","stages","sort","a","b","order","pipelineView","doc","data","id","object","name","isDefault","archived","archivedAt","map","stage","probability","kind","siteId","hostId","resolvePipeline","ctx","pipelineId","options","candidates","collection","pipelines","snap","get","exists","error","allowArchived","org","tokens","visible","where","orderBy","documentId","limit","docs","filter","localeCompare","chosen","find","seeded","create","handlePipelines","request","segments","url","denied","method","methodNotAllowed","headers","Allow","notFound","message"],"mappings":"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"}
|