@aglyn/plugins-crm 1.0.0-beta.221 → 1.0.0-beta.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +13 -13
- package/src/lib/components/activity-list.js +12 -5
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.d.ts +1 -4
- package/src/lib/components/companies-bulk-bar.js +7 -20
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/companies-section.d.ts +3 -3
- package/src/lib/components/companies-section.js +149 -46
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +281 -20
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-properties-card.js +99 -8
- package/src/lib/components/company-properties-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +3 -1
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +33 -10
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +0 -69
- package/src/lib/components/contact-properties-card.js +350 -60
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/contact-reports-to-field.d.ts +46 -0
- package/src/lib/components/contact-reports-to-field.js +67 -0
- package/src/lib/components/contact-reports-to-field.js.map +1 -0
- package/src/lib/components/contacts-bulk-bar.d.ts +1 -8
- package/src/lib/components/contacts-bulk-bar.js +7 -19
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +46 -33
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-call-actions.d.ts +10 -0
- package/src/lib/components/crm-call-actions.js +26 -9
- package/src/lib/components/crm-call-actions.js.map +1 -1
- package/src/lib/components/crm-next-activity-column.js +1 -1
- package/src/lib/components/crm-next-activity-column.js.map +1 -1
- package/src/lib/components/crm-record-picker.d.ts +15 -0
- package/src/lib/components/crm-record-picker.js +16 -10
- package/src/lib/components/crm-record-picker.js.map +1 -1
- package/src/lib/components/crm-transfer-buttons.d.ts +54 -0
- package/src/lib/components/crm-transfer-buttons.js +167 -0
- package/src/lib/components/crm-transfer-buttons.js.map +1 -0
- package/src/lib/components/deal-contact-roles-card.d.ts +46 -0
- package/src/lib/components/deal-contact-roles-card.js +368 -0
- package/src/lib/components/deal-contact-roles-card.js.map +1 -0
- package/src/lib/components/deal-detail-page.d.ts +3 -2
- package/src/lib/components/deal-detail-page.js +25 -6
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.d.ts +10 -0
- package/src/lib/components/deal-edit-drawer.js +219 -10
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deal-properties-card.d.ts +7 -0
- package/src/lib/components/deal-properties-card.js +34 -3
- package/src/lib/components/deal-properties-card.js.map +1 -1
- package/src/lib/components/deal-stage-card.d.ts +4 -0
- package/src/lib/components/deal-stage-card.js +10 -2
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.d.ts +0 -3
- package/src/lib/components/deals-bulk-bar.js +7 -20
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +3 -3
- package/src/lib/components/deals-section.js +123 -63
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.js +31 -17
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-convert-dialog.d.ts +4 -0
- package/src/lib/components/lead-convert-dialog.js +23 -1
- package/src/lib/components/lead-convert-dialog.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +8 -2
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-profile-fields.d.ts +71 -0
- package/src/lib/components/lead-profile-fields.js +386 -0
- package/src/lib/components/lead-profile-fields.js.map +1 -0
- package/src/lib/components/lead-properties-card.d.ts +2 -1
- package/src/lib/components/lead-properties-card.js +85 -176
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-source-select.d.ts +4 -23
- package/src/lib/components/lead-source-select.js +9 -73
- package/src/lib/components/lead-source-select.js.map +1 -1
- package/src/lib/components/lead-status-chip.d.ts +9 -3
- package/src/lib/components/lead-status-chip.js +8 -4
- package/src/lib/components/lead-status-chip.js.map +1 -1
- package/src/lib/components/lead-status-options.d.ts +20 -0
- package/src/lib/components/lead-status-options.js +51 -0
- package/src/lib/components/lead-status-options.js.map +1 -0
- package/src/lib/components/lead-unqualify-dialog.d.ts +5 -0
- package/src/lib/components/lead-unqualify-dialog.js +50 -18
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.d.ts +0 -3
- package/src/lib/components/leads-bulk-bar.js +29 -29
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +120 -68
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/linked-deals-card.d.ts +4 -0
- package/src/lib/components/linked-deals-card.js +47 -3
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/log-activity-dialog.js +43 -3
- package/src/lib/components/log-activity-dialog.js.map +1 -1
- package/src/lib/components/new-contact-drawer.d.ts +8 -0
- package/src/lib/components/new-contact-drawer.js +108 -6
- package/src/lib/components/new-contact-drawer.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +9 -2
- package/src/lib/components/new-lead-drawer.js +88 -121
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/components/picklist-select.d.ts +36 -0
- package/src/lib/components/picklist-select.js +114 -0
- package/src/lib/components/picklist-select.js.map +1 -0
- package/src/lib/components/picklist-values-card.d.ts +34 -0
- package/src/lib/components/{lead-source-values-card.js → picklist-values-card.js} +241 -58
- package/src/lib/components/picklist-values-card.js.map +1 -0
- package/src/lib/components/pipeline-stages-dialog.d.ts +7 -0
- package/src/lib/components/pipeline-stages-dialog.js +26 -4
- package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
- package/src/lib/components/pipelines-dialog.js +15 -6
- package/src/lib/components/pipelines-dialog.js.map +1 -1
- package/src/lib/components/recent-activity-feed.js +20 -5
- package/src/lib/components/recent-activity-feed.js.map +1 -1
- package/src/lib/components/recipes-card.js +4 -2
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-tasks-card.js +15 -8
- package/src/lib/components/record-tasks-card.js.map +1 -1
- package/src/lib/components/reports/forecast-card.d.ts +6 -0
- package/src/lib/components/reports/forecast-card.js +101 -3
- package/src/lib/components/reports/forecast-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.d.ts +12 -0
- package/src/lib/components/reports/lead-funnel-card.js +9 -5
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/lead-source-card.d.ts +23 -0
- package/src/lib/components/reports/lead-source-card.js +318 -0
- package/src/lib/components/reports/lead-source-card.js.map +1 -0
- package/src/lib/components/reports/pipeline-card.js +2 -1
- package/src/lib/components/reports/pipeline-card.js.map +1 -1
- package/src/lib/components/reports/report-export.js +1 -1
- package/src/lib/components/reports/report-export.js.map +1 -1
- package/src/lib/components/reports-section.js +8 -0
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/sharing-rule-drawer.d.ts +5 -0
- package/src/lib/components/sharing-rule-drawer.js +108 -19
- package/src/lib/components/sharing-rule-drawer.js.map +1 -1
- package/src/lib/components/sharing-rules-card.js +11 -3
- package/src/lib/components/sharing-rules-card.js.map +1 -1
- package/src/lib/components/task-cells.d.ts +9 -1
- package/src/lib/components/task-cells.js +9 -4
- package/src/lib/components/task-cells.js.map +1 -1
- package/src/lib/components/task-edit-drawer.js +111 -43
- package/src/lib/components/task-edit-drawer.js.map +1 -1
- package/src/lib/components/tasks-bulk-bar.d.ts +0 -3
- package/src/lib/components/tasks-bulk-bar.js +7 -20
- package/src/lib/components/tasks-bulk-bar.js.map +1 -1
- package/src/lib/components/tasks-calendar.d.ts +3 -0
- package/src/lib/components/tasks-calendar.js +8 -1
- package/src/lib/components/tasks-calendar.js.map +1 -1
- package/src/lib/components/tasks-section.d.ts +3 -3
- package/src/lib/components/tasks-section.js +58 -43
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +2 -9
- package/src/lib/constants/api-routes.js +1 -7
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +6 -0
- package/src/lib/constants/bundle-common.js +11 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +14 -0
- package/src/lib/constants/company-filters.js +51 -1
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +2 -0
- package/src/lib/constants/contact-filters.js +18 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +22 -1
- package/src/lib/constants/deal-filters.js +67 -4
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +3 -0
- package/src/lib/declarations.console-server.js +35 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +35 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/declarations.server.d.ts +11 -0
- package/src/lib/declarations.server.js +97 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-company-picklists.d.ts +15 -0
- package/src/lib/hooks/use-company-picklists.js +55 -0
- package/src/lib/hooks/use-company-picklists.js.map +1 -0
- package/src/lib/hooks/use-crm-picklist.d.ts +25 -0
- package/src/lib/hooks/use-crm-picklist.js +53 -0
- package/src/lib/hooks/use-crm-picklist.js.map +1 -0
- package/src/lib/hooks/use-crm-record-names.d.ts +16 -0
- package/src/lib/hooks/use-crm-record-names.js +45 -19
- package/src/lib/hooks/use-crm-record-names.js.map +1 -1
- package/src/lib/hooks/use-crm-task-picklists.d.ts +11 -0
- package/src/lib/hooks/use-crm-task-picklists.js +45 -0
- package/src/lib/hooks/use-crm-task-picklists.js.map +1 -0
- package/src/lib/hooks/use-deals.d.ts +14 -0
- package/src/lib/hooks/use-deals.js +29 -0
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/hooks/use-lead-picklists.d.ts +14 -0
- package/src/lib/hooks/use-lead-picklists.js +46 -0
- package/src/lib/hooks/use-lead-picklists.js.map +1 -0
- package/src/lib/hooks/use-lead-source-picklist.d.ts +5 -19
- package/src/lib/hooks/use-lead-source-picklist.js +12 -22
- package/src/lib/hooks/use-lead-source-picklist.js.map +1 -1
- package/src/lib/hooks/use-lead-status-picklist.d.ts +9 -0
- package/src/lib/hooks/use-lead-status-picklist.js +40 -0
- package/src/lib/hooks/use-lead-status-picklist.js.map +1 -0
- package/src/lib/model/companies.d.ts +32 -3
- package/src/lib/model/companies.js +133 -16
- package/src/lib/model/companies.js.map +1 -1
- package/src/lib/model/contact-merge.d.ts +27 -3
- package/src/lib/model/contact-merge.js +81 -7
- package/src/lib/model/contact-merge.js.map +1 -1
- package/src/lib/model/contact-record.d.ts +17 -0
- package/src/lib/model/contact-record.js +17 -2
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-update.d.ts +31 -2
- package/src/lib/model/contact-update.js +14 -0
- package/src/lib/model/contact-update.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +5 -0
- package/src/lib/model/crm-list-query.js +8 -0
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/crm-next-activity.d.ts +107 -0
- package/src/lib/model/crm-next-activity.js +140 -0
- package/src/lib/model/crm-next-activity.js.map +1 -0
- package/src/lib/model/crm-recipes.d.ts +40 -0
- package/src/lib/model/crm-recipes.js +259 -0
- package/src/lib/model/crm-recipes.js.map +1 -0
- package/src/lib/model/crm-record-counts.d.ts +36 -0
- package/src/lib/model/crm-record-counts.js +54 -0
- package/src/lib/model/crm-record-counts.js.map +1 -0
- package/src/lib/model/crm-record-lists.d.ts +23 -0
- package/src/lib/model/crm-record-lists.js +161 -0
- package/src/lib/model/crm-record-lists.js.map +1 -0
- package/src/lib/model/crm-reports.d.ts +73 -3
- package/src/lib/model/crm-reports.js +120 -1
- package/src/lib/model/crm-reports.js.map +1 -1
- package/src/lib/model/crm-sharing.d.ts +21 -3
- package/src/lib/model/crm-sharing.js +27 -6
- package/src/lib/model/crm-sharing.js.map +1 -1
- package/src/lib/model/crm-task-reminders.d.ts +152 -0
- package/src/lib/model/crm-task-reminders.js +161 -0
- package/src/lib/model/crm-task-reminders.js.map +1 -0
- package/src/lib/model/deal-board-model.d.ts +8 -2
- package/src/lib/model/deal-board-model.js +15 -2
- package/src/lib/model/deal-board-model.js.map +1 -1
- package/src/lib/model/deal-contact-roles.d.ts +45 -0
- package/src/lib/model/deal-contact-roles.js +73 -0
- package/src/lib/model/deal-contact-roles.js.map +1 -0
- package/src/lib/model/deal-form-model.d.ts +56 -0
- package/src/lib/model/deal-form-model.js +79 -7
- package/src/lib/model/deal-form-model.js.map +1 -1
- package/src/lib/model/download-text-file.d.ts +23 -0
- package/src/lib/model/download-text-file.js +34 -0
- package/src/lib/model/download-text-file.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +36 -2
- package/src/lib/model/lead-filters.js +123 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-source-direction.d.ts +38 -0
- package/src/lib/model/lead-source-direction.js +52 -0
- package/src/lib/model/lead-source-direction.js.map +1 -0
- package/src/lib/model/next-activity.d.ts +1 -1
- package/src/lib/model/next-activity.js.map +1 -1
- package/src/lib/model/picklist-values.d.ts +119 -0
- package/src/lib/model/picklist-values.js +256 -0
- package/src/lib/model/picklist-values.js.map +1 -0
- package/src/lib/model/record-facts-disclosed.d.ts +81 -0
- package/src/lib/model/record-facts-disclosed.js +281 -0
- package/src/lib/model/record-facts-disclosed.js.map +1 -0
- package/src/lib/model/record-facts.d.ts +215 -34
- package/src/lib/model/record-facts.js +307 -128
- package/src/lib/model/record-facts.js.map +1 -1
- package/src/lib/model/task-picklists.d.ts +52 -0
- package/src/lib/model/task-picklists.js +83 -0
- package/src/lib/model/task-picklists.js.map +1 -0
- package/src/lib/model/task-routes.d.ts +13 -0
- package/src/lib/model/task-routes.js +23 -4
- package/src/lib/model/task-routes.js.map +1 -1
- package/src/lib/plan-entitlements.js +6 -4
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +17 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts.js +220 -14
- package/src/lib/server/api-v1/contacts.js.map +1 -1
- package/src/lib/server/api-v1/crm-activities.js +14 -1
- package/src/lib/server/api-v1/crm-activities.js.map +1 -1
- package/src/lib/server/api-v1/crm-companies.js +123 -14
- package/src/lib/server/api-v1/crm-companies.js.map +1 -1
- package/src/lib/server/api-v1/crm-deals.js +291 -26
- package/src/lib/server/api-v1/crm-deals.js.map +1 -1
- package/src/lib/server/api-v1/crm-leads.js +144 -38
- package/src/lib/server/api-v1/crm-leads.js.map +1 -1
- package/src/lib/server/api-v1/crm-pipelines.d.ts +6 -0
- package/src/lib/server/api-v1/crm-pipelines.js +8 -2
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
- package/src/lib/server/api-v1/crm-tasks.js +78 -34
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
- package/src/lib/server/api-v1/openapi.d.ts +0 -11
- package/src/lib/server/api-v1/openapi.js +171 -16
- package/src/lib/server/api-v1/openapi.js.map +1 -1
- package/src/lib/server/assign-contact-owner.d.ts +188 -0
- package/src/lib/server/assign-contact-owner.js +446 -0
- package/src/lib/server/assign-contact-owner.js.map +1 -0
- package/src/lib/server/associate-company-by-domain.d.ts +70 -0
- package/src/lib/server/associate-company-by-domain.js +153 -0
- package/src/lib/server/associate-company-by-domain.js.map +1 -0
- package/src/lib/server/automation-steps.d.ts +140 -0
- package/src/lib/server/automation-steps.js +669 -0
- package/src/lib/server/automation-steps.js.map +1 -0
- package/src/lib/server/capture-contact.d.ts +17 -0
- package/src/lib/server/capture-contact.js +95 -26
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/capture-host-contact.d.ts +104 -0
- package/src/lib/server/capture-host-contact.js +151 -0
- package/src/lib/server/capture-host-contact.js.map +1 -0
- package/src/lib/server/company-children.d.ts +25 -0
- package/src/lib/server/company-children.js +42 -0
- package/src/lib/server/company-children.js.map +1 -0
- package/src/lib/server/company-delete.d.ts +18 -0
- package/src/lib/server/company-delete.js +44 -21
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/contact-email-history.js +21 -19
- package/src/lib/server/contact-email-history.js.map +1 -1
- package/src/lib/server/contact-remove.js +27 -2
- package/src/lib/server/contact-remove.js.map +1 -1
- package/src/lib/server/contact-reports-to.d.ts +61 -0
- package/src/lib/server/contact-reports-to.js +114 -0
- package/src/lib/server/contact-reports-to.js.map +1 -0
- package/src/lib/server/contact-update.d.ts +42 -1
- package/src/lib/server/contact-update.js +204 -23
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/convert-host-lead.d.ts +46 -1
- package/src/lib/server/convert-host-lead.js +173 -15
- package/src/lib/server/convert-host-lead.js.map +1 -1
- package/src/lib/server/convert-open-lead.js +12 -5
- package/src/lib/server/convert-open-lead.js.map +1 -1
- package/src/lib/server/crm-email-activity.d.ts +118 -0
- package/src/lib/server/crm-email-activity.js +148 -0
- package/src/lib/server/crm-email-activity.js.map +1 -0
- package/src/lib/server/crm-inbound-email.js +1 -1
- package/src/lib/server/crm-inbound-email.js.map +1 -1
- package/src/lib/server/crm-next-activity.d.ts +74 -0
- package/src/lib/server/crm-next-activity.js +194 -0
- package/src/lib/server/crm-next-activity.js.map +1 -0
- package/src/lib/server/crm-record-indexes.d.ts +45 -0
- package/src/lib/server/crm-record-indexes.js +78 -0
- package/src/lib/server/crm-record-indexes.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +9 -1
- package/src/lib/server/crm-sharing.js +23 -3
- package/src/lib/server/crm-sharing.js.map +1 -1
- package/src/lib/server/deal-contact-roles.d.ts +27 -0
- package/src/lib/server/deal-contact-roles.js +89 -0
- package/src/lib/server/deal-contact-roles.js.map +1 -0
- package/src/lib/server/email-send.js +2 -1
- package/src/lib/server/email-send.js.map +1 -1
- package/src/lib/server/lead-convert.d.ts +5 -0
- package/src/lib/server/lead-convert.js +19 -3
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +17 -2
- package/src/lib/server/lead-create.js +46 -14
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-nurturing.d.ts +6 -0
- package/src/lib/server/lead-nurturing.js +57 -20
- package/src/lib/server/lead-nurturing.js.map +1 -1
- package/src/lib/server/lead-picklists.d.ts +35 -0
- package/src/lib/server/lead-picklists.js +52 -0
- package/src/lib/server/lead-picklists.js.map +1 -0
- package/src/lib/server/lead-source-picklist.d.ts +1 -13
- package/src/lib/server/lead-source-picklist.js +6 -33
- package/src/lib/server/lead-source-picklist.js.map +1 -1
- package/src/lib/server/merge-contacts.js +23 -5
- package/src/lib/server/merge-contacts.js.map +1 -1
- package/src/lib/server/next-activity-routes.js +2 -1
- package/src/lib/server/next-activity-routes.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +53 -0
- package/src/lib/server/person-eraser.js +149 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server/person-records.d.ts +69 -0
- package/src/lib/server/person-records.js +317 -0
- package/src/lib/server/person-records.js.map +1 -0
- package/src/lib/server/person-refund.d.ts +97 -0
- package/src/lib/server/person-refund.js +221 -0
- package/src/lib/server/person-refund.js.map +1 -0
- package/src/lib/server/picklist-values.d.ts +21 -0
- package/src/lib/server/picklist-values.js +308 -0
- package/src/lib/server/picklist-values.js.map +1 -0
- package/src/lib/server/read-picklist.d.ts +44 -0
- package/src/lib/server/read-picklist.js +65 -0
- package/src/lib/server/read-picklist.js.map +1 -0
- package/src/lib/server/recipe-routes.js +22 -8
- package/src/lib/server/recipe-routes.js.map +1 -1
- package/src/lib/server/record-email-state.js +7 -1
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-facts.d.ts +3 -0
- package/src/lib/server/record-facts.js +233 -31
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/record-origin.d.ts +27 -0
- package/src/lib/server/record-origin.js +104 -0
- package/src/lib/server/record-origin.js.map +1 -0
- package/src/lib/server/record-timeline.js +40 -3
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/suite-gate.js +4 -3
- package/src/lib/server/suite-gate.js.map +1 -1
- package/src/lib/server/task-reminders-route.js +2 -1
- package/src/lib/server/task-reminders-route.js.map +1 -1
- package/src/lib/server/task-routes.js +33 -17
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server-deal-stage.d.ts +5 -1
- package/src/lib/server-deal-stage.js +10 -5
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +8 -3
- package/src/lib/server.js +47 -43
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +55 -0
- package/src/lib/testing/memory-firestore.js +335 -0
- package/src/lib/testing/memory-firestore.js.map +1 -0
- package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
- package/src/lib/testing/stand-in-email-send-index.js +49 -0
- package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
- package/src/lib/transfer/activities.d.ts +30 -0
- package/src/lib/transfer/activities.js +132 -0
- package/src/lib/transfer/activities.js.map +1 -0
- package/src/lib/transfer/aliases.d.ts +22 -0
- package/src/lib/transfer/aliases.js +1342 -0
- package/src/lib/transfer/aliases.js.map +1 -0
- package/src/lib/transfer/common.d.ts +222 -0
- package/src/lib/transfer/common.js +832 -0
- package/src/lib/transfer/common.js.map +1 -0
- package/src/lib/transfer/companies.d.ts +43 -0
- package/src/lib/transfer/companies.js +549 -0
- package/src/lib/transfer/companies.js.map +1 -0
- package/src/lib/transfer/contacts.d.ts +31 -0
- package/src/lib/transfer/contacts.js +727 -0
- package/src/lib/transfer/contacts.js.map +1 -0
- package/src/lib/transfer/deals.d.ts +66 -0
- package/src/lib/transfer/deals.js +698 -0
- package/src/lib/transfer/deals.js.map +1 -0
- package/src/lib/transfer/email-templates-package.d.ts +40 -0
- package/src/lib/transfer/email-templates-package.js +76 -0
- package/src/lib/transfer/email-templates-package.js.map +1 -0
- package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
- package/src/lib/transfer/email-templates-package.server.js +176 -0
- package/src/lib/transfer/email-templates-package.server.js.map +1 -0
- package/src/lib/transfer/fields.d.ts +102 -0
- package/src/lib/transfer/fields.js +1755 -0
- package/src/lib/transfer/fields.js.map +1 -0
- package/src/lib/transfer/leads.d.ts +30 -0
- package/src/lib/transfer/leads.js +582 -0
- package/src/lib/transfer/leads.js.map +1 -0
- package/src/lib/transfer/pipelines.d.ts +21 -0
- package/src/lib/transfer/pipelines.js +172 -0
- package/src/lib/transfer/pipelines.js.map +1 -0
- package/src/lib/transfer/register.d.ts +47 -0
- package/src/lib/transfer/register.js +277 -0
- package/src/lib/transfer/register.js.map +1 -0
- package/src/lib/transfer/tasks.d.ts +30 -0
- package/src/lib/transfer/tasks.js +418 -0
- package/src/lib/transfer/tasks.js.map +1 -0
- package/src/lib/transfer/ui.d.ts +20 -0
- package/src/lib/transfer/ui.js +42 -0
- package/src/lib/transfer/ui.js.map +1 -0
- package/src/lib/components/company-import-drawer.d.ts +0 -42
- package/src/lib/components/company-import-drawer.js +0 -99
- package/src/lib/components/company-import-drawer.js.map +0 -1
- package/src/lib/components/contact-import-drawer.d.ts +0 -46
- package/src/lib/components/contact-import-drawer.js +0 -149
- package/src/lib/components/contact-import-drawer.js.map +0 -1
- package/src/lib/components/crm-export-all-button.d.ts +0 -39
- package/src/lib/components/crm-export-all-button.js +0 -98
- package/src/lib/components/crm-export-all-button.js.map +0 -1
- package/src/lib/components/csv-import-drawer.d.ts +0 -139
- package/src/lib/components/csv-import-drawer.js +0 -757
- package/src/lib/components/csv-import-drawer.js.map +0 -1
- package/src/lib/components/deal-import-drawer.d.ts +0 -24
- package/src/lib/components/deal-import-drawer.js +0 -83
- package/src/lib/components/deal-import-drawer.js.map +0 -1
- package/src/lib/components/lead-import-drawer.d.ts +0 -27
- package/src/lib/components/lead-import-drawer.js +0 -118
- package/src/lib/components/lead-import-drawer.js.map +0 -1
- package/src/lib/components/lead-source-values-card.d.ts +0 -27
- package/src/lib/components/lead-source-values-card.js.map +0 -1
- package/src/lib/components/task-import-drawer.d.ts +0 -24
- package/src/lib/components/task-import-drawer.js +0 -82
- package/src/lib/components/task-import-drawer.js.map +0 -1
- package/src/lib/model/companies-csv.d.ts +0 -36
- package/src/lib/model/companies-csv.js +0 -38
- package/src/lib/model/companies-csv.js.map +0 -1
- package/src/lib/model/contacts-csv.d.ts +0 -54
- package/src/lib/model/contacts-csv.js +0 -67
- package/src/lib/model/contacts-csv.js.map +0 -1
- package/src/lib/model/crm-company-import.d.ts +0 -154
- package/src/lib/model/crm-company-import.js +0 -331
- package/src/lib/model/crm-company-import.js.map +0 -1
- package/src/lib/model/crm-csv.d.ts +0 -239
- package/src/lib/model/crm-csv.js +0 -467
- package/src/lib/model/crm-csv.js.map +0 -1
- package/src/lib/model/crm-deal-import.d.ts +0 -112
- package/src/lib/model/crm-deal-import.js +0 -263
- package/src/lib/model/crm-deal-import.js.map +0 -1
- package/src/lib/model/crm-import.d.ts +0 -250
- package/src/lib/model/crm-import.js +0 -467
- package/src/lib/model/crm-import.js.map +0 -1
- package/src/lib/model/crm-lead-import.d.ts +0 -138
- package/src/lib/model/crm-lead-import.js +0 -446
- package/src/lib/model/crm-lead-import.js.map +0 -1
- package/src/lib/model/crm-task-import.d.ts +0 -94
- package/src/lib/model/crm-task-import.js +0 -275
- package/src/lib/model/crm-task-import.js.map +0 -1
- package/src/lib/model/deals-csv.d.ts +0 -32
- package/src/lib/model/deals-csv.js +0 -36
- package/src/lib/model/deals-csv.js.map +0 -1
- package/src/lib/model/import-column-shapes.d.ts +0 -36
- package/src/lib/model/import-column-shapes.js +0 -76
- package/src/lib/model/import-column-shapes.js.map +0 -1
- package/src/lib/model/lead-source-values.d.ts +0 -82
- package/src/lib/model/lead-source-values.js +0 -149
- package/src/lib/model/lead-source-values.js.map +0 -1
- package/src/lib/model/leads-csv.d.ts +0 -40
- package/src/lib/model/leads-csv.js +0 -40
- package/src/lib/model/leads-csv.js.map +0 -1
- package/src/lib/model/tasks-csv.d.ts +0 -28
- package/src/lib/model/tasks-csv.js +0 -28
- package/src/lib/model/tasks-csv.js.map +0 -1
- package/src/lib/server/companies-import.d.ts +0 -57
- package/src/lib/server/companies-import.js +0 -242
- package/src/lib/server/companies-import.js.map +0 -1
- package/src/lib/server/contacts-import.d.ts +0 -109
- package/src/lib/server/contacts-import.js +0 -393
- package/src/lib/server/contacts-import.js.map +0 -1
- package/src/lib/server/deals-import.d.ts +0 -76
- package/src/lib/server/deals-import.js +0 -248
- package/src/lib/server/deals-import.js.map +0 -1
- package/src/lib/server/export-route.d.ts +0 -60
- package/src/lib/server/export-route.js +0 -447
- package/src/lib/server/export-route.js.map +0 -1
- package/src/lib/server/import-context.d.ts +0 -87
- package/src/lib/server/import-context.js +0 -182
- package/src/lib/server/import-context.js.map +0 -1
- package/src/lib/server/lead-source-values.d.ts +0 -50
- package/src/lib/server/lead-source-values.js +0 -191
- package/src/lib/server/lead-source-values.js.map +0 -1
- package/src/lib/server/leads-import.d.ts +0 -84
- package/src/lib/server/leads-import.js +0 -367
- package/src/lib/server/leads-import.js.map +0 -1
- package/src/lib/server/tasks-import.d.ts +0 -54
- package/src/lib/server/tasks-import.js +0 -160
- package/src/lib/server/tasks-import.js.map +0 -1
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
*/ import { mergeContactInteraction, normalizeContactEmail } from "@aglyn/aglyn/server";
|
|
18
|
+
import { firebaseAdmin, orgDataCollectionForHost } from "@aglyn/tenant-data-admin";
|
|
19
|
+
// Leaf imports, not the barrel: most specs in this library mock
|
|
20
|
+
// `@aglyn/tenant-data-admin` wholesale, and a permissive stub of either
|
|
21
|
+
// function would turn every case below green without a document ever having
|
|
22
|
+
// to exist.
|
|
23
|
+
import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
|
|
24
|
+
import { updateExisting } from "@aglyn/tenant-data-admin/server/update-existing";
|
|
25
|
+
/**
|
|
26
|
+
* Money going BACK to a customer, recorded on their contact (AGL-1754).
|
|
27
|
+
*
|
|
28
|
+
* The CRM's answer to `plugin-person-records`' `recordRefund` (AGL-3080):
|
|
29
|
+
* commerce reports a refund or a lost dispute through the seam, and what it
|
|
30
|
+
* does to the person is decided here, by the plugin that keeps them. It used
|
|
31
|
+
* to be commerce's own module, writing these fields onto the CRM's contacts.
|
|
32
|
+
*
|
|
33
|
+
* `upsertHostContact` accumulates `ltvCents` and nothing anywhere decremented
|
|
34
|
+
* it, so a customer who bought $500 and returned all of it read identically to
|
|
35
|
+
* one who bought $500 and kept it — and the first surface built on the field
|
|
36
|
+
* (RFM segmentation, a lifetime-value column, a best-customers list) would have
|
|
37
|
+
* ranked a serial returner among the merchant's best customers.
|
|
38
|
+
*
|
|
39
|
+
* WHAT THIS WRITES, AND WHY IT IS NOT A DECREMENT
|
|
40
|
+
*
|
|
41
|
+
* `ltvCents` is left exactly as it is and the reversal is recorded BESIDE it:
|
|
42
|
+
*
|
|
43
|
+
* | field | meaning |
|
|
44
|
+
* | -- | -- |
|
|
45
|
+
* | `ltvCents` | gross money handed over, unchanged (AGL-1748) |
|
|
46
|
+
* | `refundedCents` | gross money handed back |
|
|
47
|
+
* | `ordersCount` | orders placed, unchanged |
|
|
48
|
+
* | `refundedOrdersCount` | of those, how many ended fully reversed |
|
|
49
|
+
* | `lastRefundAtMs` | when money last went back |
|
|
50
|
+
*
|
|
51
|
+
* This is the shape AGL-1747 chose two days earlier for the same question on
|
|
52
|
+
* the orders CSV — `amountUsd` stayed gross under its existing name and
|
|
53
|
+
* `refundedUsd`/`netUsd` were appended — and it is the shape AGL-1748 named
|
|
54
|
+
* when it recorded the gross semantics in `upsert-contact.ts`. Netting in
|
|
55
|
+
* place would make a stored number mean one thing for rows written before this
|
|
56
|
+
* commit and another for rows written after, with nothing on the document to
|
|
57
|
+
* tell the two apart. Two fields cost one number's worth of storage and keep
|
|
58
|
+
* both facts, and a reader that wants the net has `ltvCents - refundedCents`.
|
|
59
|
+
*
|
|
60
|
+
* It also answers "can it go negative?" by moving the question off storage.
|
|
61
|
+
* Both figures are monotonic counters of money that actually moved in one
|
|
62
|
+
* direction, so neither can go negative. The DERIVED net can, for exactly one
|
|
63
|
+
* reason: an order placed before AGL-1748 taught POS, drafts, reservations and
|
|
64
|
+
* bookings to pass `purchaseCents` at all, whose sale was never added and whose
|
|
65
|
+
* refund is subtracted here. That is a true statement about the data, and
|
|
66
|
+
* clamping it away at write time would erase the only evidence that a purchase
|
|
67
|
+
* is missing. AGL-1753 is the backfill that reconciles it, by SETTING both
|
|
68
|
+
* fields from the orders — which already carry `refundedCents`, so whatever a
|
|
69
|
+
* reader decides here costs the rebuild nothing. A merchant-facing reader
|
|
70
|
+
* should show gross and refunded, and clamp only the net it ranks on.
|
|
71
|
+
*
|
|
72
|
+
* `ordersCount` is deliberately NOT decremented. A fully refunded order still
|
|
73
|
+
* happened, and a decrement cannot be undone by a reader that would rather
|
|
74
|
+
* count it — while `refundedOrdersCount` beside it lets RFM's frequency term
|
|
75
|
+
* net the two, or not, at the point of display. Same argument as the amount,
|
|
76
|
+
* one field along.
|
|
77
|
+
*
|
|
78
|
+
* WHEN THE CONTACT IS MISSING, THIS REFUSES — and records the refusal.
|
|
79
|
+
*
|
|
80
|
+
* Refusing is right on AGL-1732's test, "does refusing discard money or work
|
|
81
|
+
* that already happened?", because it does not: the refund is durable on the
|
|
82
|
+
* order (`refundedCents`, the timeline entry, Stripe), and the contact is a
|
|
83
|
+
* derived aggregate that AGL-1753's rebuild reads back out of those same
|
|
84
|
+
* orders. Creating one here would be worse three ways — creation is audience
|
|
85
|
+
* BAND gated, so it hard-bands a Free org (raising the AGL-891 dropped-contacts
|
|
86
|
+
* alert for a record the merchant never had) or meters onto a paid invoice; the
|
|
87
|
+
* contact it created would hold a refund and no purchase, which is the negative
|
|
88
|
+
* lifetime value above with nothing to reconcile it against; and it is the
|
|
89
|
+
* phantom-document shape the `updateExisting` sweep spent yesterday closing.
|
|
90
|
+
*
|
|
91
|
+
* So the write is an `update()`, never `set(…, { merge: true })` — the contact
|
|
92
|
+
* may have been deleted between the query and the write, and a merge-set would
|
|
93
|
+
* resurrect it as a document holding nothing but a refund. Every refusal
|
|
94
|
+
* increments `hosts/{hostId}/counters/contactRefundsUnmatched`, the counter
|
|
95
|
+
* shape `contactsDropped` already uses, so a refund that reached no contact is
|
|
96
|
+
* countable rather than silent.
|
|
97
|
+
*
|
|
98
|
+
* CHARGEBACKS (AGL-1554) are a separate decision and are NOT handled here.
|
|
99
|
+
* This takes `kind` so the handler that eventually answers
|
|
100
|
+
* `charge.dispute.closed` can record a lost dispute through the same writer
|
|
101
|
+
* with the same fields: money reversed is money reversed, whichever door it
|
|
102
|
+
* left by, and only the wording of the timeline entry differs.
|
|
103
|
+
*/ export async function recordPersonRefund(request) {
|
|
104
|
+
var _snapshot_get, _snapshot_get1;
|
|
105
|
+
/*
|
|
106
|
+
* The request's fields, as the seam names them. `refId` is the order the
|
|
107
|
+
* money went back on — the contact timeline's `refId`. `email` is the buyer
|
|
108
|
+
* as the ORDER recorded them, raw, normalized here because that is the key
|
|
109
|
+
* the contacts are stored under and orders are not consistent about it
|
|
110
|
+
* (AGL-1753 item 3); no name rides along on purpose — a refund is not a
|
|
111
|
+
* capture point and must not rewrite who the contact is. `amountCents` is
|
|
112
|
+
* what THIS attempt reversed, never the order total: the seller caps each
|
|
113
|
+
* attempt against what is left, so several partials sum to at most the
|
|
114
|
+
* total. `closedTheSale` is true only for the write that moved the order
|
|
115
|
+
* INTO `refunded` — two partials settling at once can both observe a fully
|
|
116
|
+
* refunded total, so the caller decides it from the transition it made.
|
|
117
|
+
*/ const options = {
|
|
118
|
+
hostId: request.hostId,
|
|
119
|
+
orderId: request.refId,
|
|
120
|
+
email: request.email,
|
|
121
|
+
amountCents: request.amountCents,
|
|
122
|
+
closedTheOrder: request.closedTheSale,
|
|
123
|
+
kind: request.reason
|
|
124
|
+
};
|
|
125
|
+
// The money has already moved and the order already records it, so nothing
|
|
126
|
+
// here may fail the refund: a throw reaches `recordPluginPersonRefund`,
|
|
127
|
+
// which logs it and answers the seller `null`.
|
|
128
|
+
const amountCents = Math.round(Number(options.amountCents));
|
|
129
|
+
// Nothing went back, so there is nothing to record against anybody.
|
|
130
|
+
if (!(amountCents > 0)) return 'recorded';
|
|
131
|
+
const email = normalizeContactEmail(options.email);
|
|
132
|
+
if (!email) {
|
|
133
|
+
await recordUnmatchedRefund(options.hostId, options.orderId, 'no-email');
|
|
134
|
+
return 'no-email';
|
|
135
|
+
}
|
|
136
|
+
// Contacts are ORG-scoped (AGL-237), not `hosts/{h}/contacts`, and reads
|
|
137
|
+
// narrow to what this host may see (AGL-1039) — the same lookup
|
|
138
|
+
// `upsertHostContact` makes, through the org's address index
|
|
139
|
+
// (AGL-2633), so both writers resolve the same document even when the
|
|
140
|
+
// order's address is one a merge folded into the buyer's record.
|
|
141
|
+
const contactsRef = await orgDataCollectionForHost(options.hostId, 'contacts');
|
|
142
|
+
const snapshot = await findContactByEmail(contactsRef, email, {
|
|
143
|
+
hostId: options.hostId
|
|
144
|
+
});
|
|
145
|
+
if (!snapshot) {
|
|
146
|
+
await recordUnmatchedRefund(options.hostId, options.orderId, 'no-contact');
|
|
147
|
+
return 'no-person';
|
|
148
|
+
}
|
|
149
|
+
const atMs = Date.now();
|
|
150
|
+
const interaction = {
|
|
151
|
+
// `refId` names an order, so the interaction is an order interaction.
|
|
152
|
+
// There is no `'refund'` ContactSource and this deliberately does not
|
|
153
|
+
// add one: `sources` records which capture silo produced the contact,
|
|
154
|
+
// and a refund captures nobody.
|
|
155
|
+
type: 'order',
|
|
156
|
+
atMs,
|
|
157
|
+
refId: options.orderId,
|
|
158
|
+
summary: `$${(amountCents / 100).toFixed(2)} ` + (options.kind === 'chargeback' ? 'charged back' : 'refunded') + (options.closedTheOrder ? ' (full)' : '')
|
|
159
|
+
};
|
|
160
|
+
// Reused for the newest-first ordering and the interactions cap; only the
|
|
161
|
+
// timeline is taken. The `sources` half of the merge is discarded on
|
|
162
|
+
// purpose, per the note above.
|
|
163
|
+
const { interactions } = mergeContactInteraction({
|
|
164
|
+
sources: (_snapshot_get = snapshot.get('sources')) != null ? _snapshot_get : {},
|
|
165
|
+
interactions: (_snapshot_get1 = snapshot.get('interactions')) != null ? _snapshot_get1 : []
|
|
166
|
+
}, {
|
|
167
|
+
source: 'order',
|
|
168
|
+
interaction
|
|
169
|
+
});
|
|
170
|
+
const FieldValue = firebaseAdmin.firestore.FieldValue;
|
|
171
|
+
// Every field is top-level. `update()` replaces a nested map wholesale
|
|
172
|
+
// rather than merging into it, so anything under a map would have to be
|
|
173
|
+
// written as a dotted path; there is nothing here that is.
|
|
174
|
+
//
|
|
175
|
+
// The amounts are increments, so two refunds settling at once both land.
|
|
176
|
+
// `interactions` is a read-modify-write and therefore CAN lose one of two
|
|
177
|
+
// concurrent entries — the same exposure `upsertHostContact` has on the
|
|
178
|
+
// same array, and it costs a timeline line rather than a number.
|
|
179
|
+
const landed = await updateExisting(snapshot.ref, _extends({
|
|
180
|
+
refundedCents: FieldValue.increment(amountCents)
|
|
181
|
+
}, options.closedTheOrder ? {
|
|
182
|
+
refundedOrdersCount: FieldValue.increment(1)
|
|
183
|
+
} : {}, {
|
|
184
|
+
lastRefundAtMs: atMs,
|
|
185
|
+
interactions,
|
|
186
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
187
|
+
}));
|
|
188
|
+
if (!landed) {
|
|
189
|
+
await recordUnmatchedRefund(options.hostId, options.orderId, 'contact-deleted');
|
|
190
|
+
return 'gone';
|
|
191
|
+
}
|
|
192
|
+
return 'recorded';
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* A refund that reached no contact, recorded rather than dropped (AGL-1754).
|
|
196
|
+
*
|
|
197
|
+
* The `set(…, { merge: true })` here IS meant to conjure the document — it is
|
|
198
|
+
* a counter, and the shape is `counters/contactsDropped`'s, so an operator
|
|
199
|
+
* reading a host's counters finds both kinds of missed contact record in one
|
|
200
|
+
* place. The reason is stamped because the three cases want different answers:
|
|
201
|
+
* `no-email` is an order that never identified its buyer, `no-contact` is the
|
|
202
|
+
* AGL-1753 backfill's work, and `contact-deleted` is a contact removed between
|
|
203
|
+
* the sale and the refund, which nothing should recreate.
|
|
204
|
+
*/ async function recordUnmatchedRefund(hostId, orderId, reason) {
|
|
205
|
+
console.warn('refund reached no contact', {
|
|
206
|
+
hostId,
|
|
207
|
+
orderId,
|
|
208
|
+
reason
|
|
209
|
+
});
|
|
210
|
+
const FieldValue = firebaseAdmin.firestore.FieldValue;
|
|
211
|
+
await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection('counters').doc('contactRefundsUnmatched').set({
|
|
212
|
+
total: FieldValue.increment(1),
|
|
213
|
+
lastOrderId: orderId,
|
|
214
|
+
lastReason: reason,
|
|
215
|
+
lastAtMs: Date.now()
|
|
216
|
+
}, {
|
|
217
|
+
merge: true
|
|
218
|
+
}).catch(()=>undefined);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
//# sourceMappingURL=person-refund.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/person-refund.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 mergeContactInteraction,\n normalizeContactEmail,\n type ContactInteraction,\n} from '@aglyn/aglyn/server'\nimport type {\n PluginPersonRefundOutcome,\n PluginPersonRefundRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\nimport {\n firebaseAdmin,\n orgDataCollectionForHost,\n} from '@aglyn/tenant-data-admin'\n// Leaf imports, not the barrel: most specs in this library mock\n// `@aglyn/tenant-data-admin` wholesale, and a permissive stub of either\n// function would turn every case below green without a document ever having\n// to exist.\nimport { findContactByEmail } from '@aglyn/tenant-data-admin/server/contact-email-index'\nimport { updateExisting } from '@aglyn/tenant-data-admin/server/update-existing'\n\n/**\n * Money going BACK to a customer, recorded on their contact (AGL-1754).\n *\n * The CRM's answer to `plugin-person-records`' `recordRefund` (AGL-3080):\n * commerce reports a refund or a lost dispute through the seam, and what it\n * does to the person is decided here, by the plugin that keeps them. It used\n * to be commerce's own module, writing these fields onto the CRM's contacts.\n *\n * `upsertHostContact` accumulates `ltvCents` and nothing anywhere decremented\n * it, so a customer who bought $500 and returned all of it read identically to\n * one who bought $500 and kept it — and the first surface built on the field\n * (RFM segmentation, a lifetime-value column, a best-customers list) would have\n * ranked a serial returner among the merchant's best customers.\n *\n * WHAT THIS WRITES, AND WHY IT IS NOT A DECREMENT\n *\n * `ltvCents` is left exactly as it is and the reversal is recorded BESIDE it:\n *\n * | field | meaning |\n * | -- | -- |\n * | `ltvCents` | gross money handed over, unchanged (AGL-1748) |\n * | `refundedCents` | gross money handed back |\n * | `ordersCount` | orders placed, unchanged |\n * | `refundedOrdersCount` | of those, how many ended fully reversed |\n * | `lastRefundAtMs` | when money last went back |\n *\n * This is the shape AGL-1747 chose two days earlier for the same question on\n * the orders CSV — `amountUsd` stayed gross under its existing name and\n * `refundedUsd`/`netUsd` were appended — and it is the shape AGL-1748 named\n * when it recorded the gross semantics in `upsert-contact.ts`. Netting in\n * place would make a stored number mean one thing for rows written before this\n * commit and another for rows written after, with nothing on the document to\n * tell the two apart. Two fields cost one number's worth of storage and keep\n * both facts, and a reader that wants the net has `ltvCents - refundedCents`.\n *\n * It also answers \"can it go negative?\" by moving the question off storage.\n * Both figures are monotonic counters of money that actually moved in one\n * direction, so neither can go negative. The DERIVED net can, for exactly one\n * reason: an order placed before AGL-1748 taught POS, drafts, reservations and\n * bookings to pass `purchaseCents` at all, whose sale was never added and whose\n * refund is subtracted here. That is a true statement about the data, and\n * clamping it away at write time would erase the only evidence that a purchase\n * is missing. AGL-1753 is the backfill that reconciles it, by SETTING both\n * fields from the orders — which already carry `refundedCents`, so whatever a\n * reader decides here costs the rebuild nothing. A merchant-facing reader\n * should show gross and refunded, and clamp only the net it ranks on.\n *\n * `ordersCount` is deliberately NOT decremented. A fully refunded order still\n * happened, and a decrement cannot be undone by a reader that would rather\n * count it — while `refundedOrdersCount` beside it lets RFM's frequency term\n * net the two, or not, at the point of display. Same argument as the amount,\n * one field along.\n *\n * WHEN THE CONTACT IS MISSING, THIS REFUSES — and records the refusal.\n *\n * Refusing is right on AGL-1732's test, \"does refusing discard money or work\n * that already happened?\", because it does not: the refund is durable on the\n * order (`refundedCents`, the timeline entry, Stripe), and the contact is a\n * derived aggregate that AGL-1753's rebuild reads back out of those same\n * orders. Creating one here would be worse three ways — creation is audience\n * BAND gated, so it hard-bands a Free org (raising the AGL-891 dropped-contacts\n * alert for a record the merchant never had) or meters onto a paid invoice; the\n * contact it created would hold a refund and no purchase, which is the negative\n * lifetime value above with nothing to reconcile it against; and it is the\n * phantom-document shape the `updateExisting` sweep spent yesterday closing.\n *\n * So the write is an `update()`, never `set(…, { merge: true })` — the contact\n * may have been deleted between the query and the write, and a merge-set would\n * resurrect it as a document holding nothing but a refund. Every refusal\n * increments `hosts/{hostId}/counters/contactRefundsUnmatched`, the counter\n * shape `contactsDropped` already uses, so a refund that reached no contact is\n * countable rather than silent.\n *\n * CHARGEBACKS (AGL-1554) are a separate decision and are NOT handled here.\n * This takes `kind` so the handler that eventually answers\n * `charge.dispute.closed` can record a lost dispute through the same writer\n * with the same fields: money reversed is money reversed, whichever door it\n * left by, and only the wording of the timeline entry differs.\n */\nexport async function recordPersonRefund(\n request: PluginPersonRefundRequest,\n): Promise<PluginPersonRefundOutcome> {\n /*\n * The request's fields, as the seam names them. `refId` is the order the\n * money went back on — the contact timeline's `refId`. `email` is the buyer\n * as the ORDER recorded them, raw, normalized here because that is the key\n * the contacts are stored under and orders are not consistent about it\n * (AGL-1753 item 3); no name rides along on purpose — a refund is not a\n * capture point and must not rewrite who the contact is. `amountCents` is\n * what THIS attempt reversed, never the order total: the seller caps each\n * attempt against what is left, so several partials sum to at most the\n * total. `closedTheSale` is true only for the write that moved the order\n * INTO `refunded` — two partials settling at once can both observe a fully\n * refunded total, so the caller decides it from the transition it made.\n */\n const options = {\n hostId: request.hostId,\n orderId: request.refId,\n email: request.email,\n amountCents: request.amountCents,\n closedTheOrder: request.closedTheSale,\n kind: request.reason,\n }\n // The money has already moved and the order already records it, so nothing\n // here may fail the refund: a throw reaches `recordPluginPersonRefund`,\n // which logs it and answers the seller `null`.\n const amountCents = Math.round(Number(options.amountCents))\n // Nothing went back, so there is nothing to record against anybody.\n if (!(amountCents > 0)) return 'recorded'\n const email = normalizeContactEmail(options.email)\n if (!email) {\n await recordUnmatchedRefund(options.hostId, options.orderId, 'no-email')\n return 'no-email'\n }\n // Contacts are ORG-scoped (AGL-237), not `hosts/{h}/contacts`, and reads\n // narrow to what this host may see (AGL-1039) — the same lookup\n // `upsertHostContact` makes, through the org's address index\n // (AGL-2633), so both writers resolve the same document even when the\n // order's address is one a merge folded into the buyer's record.\n const contactsRef = await orgDataCollectionForHost(\n options.hostId,\n 'contacts',\n )\n const snapshot = await findContactByEmail(contactsRef, email, {\n hostId: options.hostId,\n })\n if (!snapshot) {\n await recordUnmatchedRefund(options.hostId, options.orderId, 'no-contact')\n return 'no-person'\n }\n const atMs = Date.now()\n const interaction: ContactInteraction = {\n // `refId` names an order, so the interaction is an order interaction.\n // There is no `'refund'` ContactSource and this deliberately does not\n // add one: `sources` records which capture silo produced the contact,\n // and a refund captures nobody.\n type: 'order',\n atMs,\n refId: options.orderId,\n summary:\n `$${(amountCents / 100).toFixed(2)} ` +\n (options.kind === 'chargeback' ? 'charged back' : 'refunded') +\n (options.closedTheOrder ? ' (full)' : ''),\n }\n // Reused for the newest-first ordering and the interactions cap; only the\n // timeline is taken. The `sources` half of the merge is discarded on\n // purpose, per the note above.\n const { interactions } = mergeContactInteraction(\n {\n sources: snapshot.get('sources') ?? {},\n interactions: snapshot.get('interactions') ?? [],\n },\n { source: 'order', interaction },\n )\n\n const FieldValue = firebaseAdmin.firestore.FieldValue\n // Every field is top-level. `update()` replaces a nested map wholesale\n // rather than merging into it, so anything under a map would have to be\n // written as a dotted path; there is nothing here that is.\n //\n // The amounts are increments, so two refunds settling at once both land.\n // `interactions` is a read-modify-write and therefore CAN lose one of two\n // concurrent entries — the same exposure `upsertHostContact` has on the\n // same array, and it costs a timeline line rather than a number.\n const landed = await updateExisting(snapshot.ref, {\n refundedCents: FieldValue.increment(amountCents),\n ...(options.closedTheOrder\n ? { refundedOrdersCount: FieldValue.increment(1) }\n : {}),\n lastRefundAtMs: atMs,\n interactions,\n updatedAt: FieldValue.serverTimestamp(),\n })\n if (!landed) {\n await recordUnmatchedRefund(\n options.hostId,\n options.orderId,\n 'contact-deleted',\n )\n return 'gone'\n }\n return 'recorded'\n}\n\n/**\n * A refund that reached no contact, recorded rather than dropped (AGL-1754).\n *\n * The `set(…, { merge: true })` here IS meant to conjure the document — it is\n * a counter, and the shape is `counters/contactsDropped`'s, so an operator\n * reading a host's counters finds both kinds of missed contact record in one\n * place. The reason is stamped because the three cases want different answers:\n * `no-email` is an order that never identified its buyer, `no-contact` is the\n * AGL-1753 backfill's work, and `contact-deleted` is a contact removed between\n * the sale and the refund, which nothing should recreate.\n */\nasync function recordUnmatchedRefund(\n hostId: string,\n orderId: string,\n reason: 'no-email' | 'no-contact' | 'contact-deleted',\n): Promise<void> {\n console.warn('refund reached no contact', { hostId, orderId, reason })\n const FieldValue = firebaseAdmin.firestore.FieldValue\n await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('counters')\n .doc('contactRefundsUnmatched')\n .set(\n {\n total: FieldValue.increment(1),\n lastOrderId: orderId,\n lastReason: reason,\n lastAtMs: Date.now(),\n },\n { merge: true },\n )\n .catch(() => undefined)\n}\n"],"names":["mergeContactInteraction","normalizeContactEmail","firebaseAdmin","orgDataCollectionForHost","findContactByEmail","updateExisting","recordPersonRefund","request","snapshot","options","hostId","orderId","refId","email","amountCents","closedTheOrder","closedTheSale","kind","reason","Math","round","Number","recordUnmatchedRefund","contactsRef","atMs","Date","now","interaction","type","summary","toFixed","interactions","sources","get","source","FieldValue","firestore","landed","ref","refundedCents","increment","refundedOrdersCount","lastRefundAtMs","updatedAt","serverTimestamp","console","warn","app","collection","doc","set","total","lastOrderId","lastReason","lastAtMs","merge","catch","undefined"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
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 PluginApiHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
/** `POST crm/picklist-values` — any registered picklist, named by `picklistId`. */
|
|
19
|
+
export declare const crmPicklistValuesHandler: PluginApiHandler;
|
|
20
|
+
/** `POST crm/lead-source-values` — the same route with the picklist fixed to the lead source. */
|
|
21
|
+
export declare const crmLeadSourceValuesHandler: PluginApiHandler;
|
|
@@ -0,0 +1,308 @@
|
|
|
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
|
+
*/ import { consentGroupForHost, CONTACT_FACETS_FIELD, CRM_COLLECTIONS, CRM_CONTACT_FACET_KEYS_FIELD, CRM_LEAD_SOURCE_PICKLIST, crmContactFacetKeys, crmPicklistDefinition, crmPicklistKey, effectiveCrmPicklist, newResourceScopeFields, normalizeCrmPicklist, ORG_SCOPE_TOKEN } from "@aglyn/aglyn/server";
|
|
18
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { FieldPath, FieldValue } from "firebase-admin/firestore";
|
|
20
|
+
import { deletePicklistValue, renamePicklistValue } from "../model/picklist-values.js";
|
|
21
|
+
import { orgHostIds, readCrmRouteScope } from "./org-caller.js";
|
|
22
|
+
import { authorizeCrmWriter } from "./task-routes.js";
|
|
23
|
+
/** What the suite gate names when a plan without the CRM asks. */ const suiteAct = (definition)=>`Editing the organization's ${definition.plural}`;
|
|
24
|
+
/** The org subcollection each target object's records live in. */ const TARGET_COLLECTIONS = {
|
|
25
|
+
contact: 'contacts',
|
|
26
|
+
lead: 'leads',
|
|
27
|
+
company: CRM_COLLECTIONS.companies,
|
|
28
|
+
deal: CRM_COLLECTIONS.deals,
|
|
29
|
+
task: CRM_COLLECTIONS.tasks
|
|
30
|
+
};
|
|
31
|
+
/** Writes per batch — under Firestore's 500, with room to spare. */ const BATCH_SIZE = 400;
|
|
32
|
+
/**
|
|
33
|
+
* Set `field` to `to` (or remove it, for `null`) on every document `query`
|
|
34
|
+
* finds holding `from`, a batch at a time, with `beside` written alongside.
|
|
35
|
+
* Answers how many were written.
|
|
36
|
+
*
|
|
37
|
+
* The query is re-run after each batch rather than paged, because each
|
|
38
|
+
* batch moves its documents OUT of the query's answer — the next run finds
|
|
39
|
+
* only what is left, and a run that finds nothing is the end.
|
|
40
|
+
*/ async function replaceEverywhere(firestore, query, field, to, beside = {}) {
|
|
41
|
+
let written = 0;
|
|
42
|
+
for(;;){
|
|
43
|
+
const page = await query.limit(BATCH_SIZE).get();
|
|
44
|
+
if (page.empty) return written;
|
|
45
|
+
const batch = firestore.batch();
|
|
46
|
+
for (const snapshot of page.docs){
|
|
47
|
+
const alongside = typeof beside === 'function' ? beside(snapshot) : beside;
|
|
48
|
+
batch.update(snapshot.ref, field, to != null ? to : FieldValue.delete(), 'updatedAt', FieldValue.serverTimestamp(), ...Object.entries(alongside).flat());
|
|
49
|
+
}
|
|
50
|
+
await batch.commit();
|
|
51
|
+
written += page.size;
|
|
52
|
+
if (page.size < BATCH_SIZE) return written;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Rewrite an ARRAY target (AGL-3521): every record whose `keyField` lists
|
|
57
|
+
* the old label's key has each entry of `field` holding `from` at
|
|
58
|
+
* `arrayKey` moved to `to` — or the entry's label removed, for `null` —
|
|
59
|
+
* and its `keyField` restamped from the entries as moved. Answers how many
|
|
60
|
+
* records were written.
|
|
61
|
+
*
|
|
62
|
+
* Paged by document id rather than re-run, because a rename that changes
|
|
63
|
+
* only the label's case keeps its key, so a written record can stay in the
|
|
64
|
+
* query's answer.
|
|
65
|
+
*/ async function replaceInArrayEverywhere(firestore, records, target, from, to) {
|
|
66
|
+
const fromKey = crmPicklistKey(from);
|
|
67
|
+
if (!fromKey) return 0;
|
|
68
|
+
const query = records.where(target.keyField, 'array-contains', fromKey).orderBy(FieldPath.documentId()).limit(BATCH_SIZE);
|
|
69
|
+
let written = 0;
|
|
70
|
+
let after = null;
|
|
71
|
+
for(;;){
|
|
72
|
+
var _page_docs_;
|
|
73
|
+
const page = await (after ? query.startAfter(after) : query).get();
|
|
74
|
+
if (page.empty) return written;
|
|
75
|
+
const batch = firestore.batch();
|
|
76
|
+
let changed = 0;
|
|
77
|
+
for (const snapshot of page.docs){
|
|
78
|
+
const entries = snapshot.get(target.field);
|
|
79
|
+
if (!Array.isArray(entries)) continue;
|
|
80
|
+
let moved = false;
|
|
81
|
+
const next = entries.map((entry)=>{
|
|
82
|
+
if (!entry || typeof entry !== 'object') return entry;
|
|
83
|
+
const row = entry;
|
|
84
|
+
if (row[target.arrayKey] !== from) return entry;
|
|
85
|
+
moved = true;
|
|
86
|
+
const rest = _extends({}, row);
|
|
87
|
+
delete rest[target.arrayKey];
|
|
88
|
+
return to === null ? rest : _extends({}, rest, {
|
|
89
|
+
[target.arrayKey]: to
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
if (!moved) continue;
|
|
93
|
+
const keys = new Set();
|
|
94
|
+
for (const entry of next){
|
|
95
|
+
const key = entry && typeof entry === 'object' ? crmPicklistKey(entry[target.arrayKey]) : null;
|
|
96
|
+
if (key) keys.add(key);
|
|
97
|
+
}
|
|
98
|
+
batch.update(snapshot.ref, {
|
|
99
|
+
[target.field]: next,
|
|
100
|
+
[target.keyField]: [
|
|
101
|
+
...keys
|
|
102
|
+
],
|
|
103
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
104
|
+
});
|
|
105
|
+
changed += 1;
|
|
106
|
+
}
|
|
107
|
+
if (changed) await batch.commit();
|
|
108
|
+
written += changed;
|
|
109
|
+
if (page.size < BATCH_SIZE) return written;
|
|
110
|
+
after = (_page_docs_ = page.docs[page.docs.length - 1]) != null ? _page_docs_ : null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** A contact as it reads once one holder's facet field holds `to` (or nothing, for `null`). */ function withFacetValue(data, groupId, field, to) {
|
|
114
|
+
var _data_CONTACT_FACETS_FIELD, _facets_groupId;
|
|
115
|
+
const facets = _extends({}, (_data_CONTACT_FACETS_FIELD = data[CONTACT_FACETS_FIELD]) != null ? _data_CONTACT_FACETS_FIELD : {});
|
|
116
|
+
const facet = _extends({}, (_facets_groupId = facets[groupId]) != null ? _facets_groupId : {});
|
|
117
|
+
if (to === null) delete facet[field];
|
|
118
|
+
else facet[field] = to;
|
|
119
|
+
facets[groupId] = facet;
|
|
120
|
+
return _extends({}, data, {
|
|
121
|
+
[CONTACT_FACETS_FIELD]: facets
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* `POST crm/picklist-values` — rename a value of one of the CRM's standard
|
|
126
|
+
* picklists, or delete an added one and move its records to another
|
|
127
|
+
* (AGL-3298, AGL-3510). `crm/lead-source-values` is the same route with the
|
|
128
|
+
* picklist fixed to the lead source.
|
|
129
|
+
*
|
|
130
|
+
* Body: `{ hostId | orgId, picklistId, action: 'rename', valueId, label }` or
|
|
131
|
+
* `{ hostId | orgId, picklistId, action: 'delete', valueId, replaceWith: label | null }`.
|
|
132
|
+
* `picklistId` is one of `CRM_PICKLIST_DEFINITIONS`; anything else is refused.
|
|
133
|
+
*
|
|
134
|
+
* Records store the value's LABEL (see the picklist block in `crm.ts`), so
|
|
135
|
+
* both moves change records as well as the list: every record the
|
|
136
|
+
* definition's `targets` name that holds the old label is rewritten to the
|
|
137
|
+
* new one — or, for a delete with no replacement, cleared. That is
|
|
138
|
+
* Salesforce's Replace, done as part of the move, so a report grouped by
|
|
139
|
+
* the field follows a rename instead of splitting into the old name and
|
|
140
|
+
* the new. A standard value cannot be deleted, and on a definition with
|
|
141
|
+
* meanings a delete's replacement must mean the same (see
|
|
142
|
+
* `deletePicklistValue`).
|
|
143
|
+
*
|
|
144
|
+
* ## The list first, then the records
|
|
145
|
+
*
|
|
146
|
+
* The list is written in a transaction before any record: from that moment
|
|
147
|
+
* no picker offers the old label and no import or API write accepts it, so
|
|
148
|
+
* the sweep that follows cannot be outrun by a new record taking the name
|
|
149
|
+
* it is removing. A sweep interrupted part-way leaves records holding a
|
|
150
|
+
* label the list no longer has, which they show as "(not in the list)"; the
|
|
151
|
+
* same request sent again finishes it, because the rename of a value to its
|
|
152
|
+
* own current label is a no-op on the list and still sweeps.
|
|
153
|
+
*
|
|
154
|
+
* ## Who may
|
|
155
|
+
*
|
|
156
|
+
* Whoever may write the CRM — `authorizeCrmWriter`, the question every CRM
|
|
157
|
+
* route asks — which is who the rules let edit the list itself. The sweep
|
|
158
|
+
* reaches every record in the org whatever the caller's scope: it changes
|
|
159
|
+
* one label to another that the same caller could already have picked, and
|
|
160
|
+
* reveals nothing about the records but their count.
|
|
161
|
+
*/ function picklistValuesHandler(fixed) {
|
|
162
|
+
return async (req, res)=>{
|
|
163
|
+
var _req_body, _body_valueId;
|
|
164
|
+
if (req.method !== 'POST') {
|
|
165
|
+
res.setHeader('Allow', 'POST');
|
|
166
|
+
res.status(405).json({
|
|
167
|
+
error: 'Method not allowed'
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const body = (_req_body = req.body) != null ? _req_body : {};
|
|
172
|
+
const scope = readCrmRouteScope(body);
|
|
173
|
+
if (!scope) {
|
|
174
|
+
res.status(400).json({
|
|
175
|
+
error: 'Missing hostId'
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const definition = crmPicklistDefinition(fixed != null ? fixed : body['picklistId']);
|
|
180
|
+
if (!definition) {
|
|
181
|
+
res.status(400).json({
|
|
182
|
+
error: 'Name one of the CRM’s picklists.'
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const picklistId = definition.id;
|
|
187
|
+
const action = body['action'];
|
|
188
|
+
const valueId = String((_body_valueId = body['valueId']) != null ? _body_valueId : '').trim().slice(0, 64);
|
|
189
|
+
if (action !== 'rename' && action !== 'delete' || !valueId) {
|
|
190
|
+
res.status(400).json({
|
|
191
|
+
error: 'Name a value, and whether to rename or delete it.'
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const replaceWith = body['replaceWith'] === null || body['replaceWith'] === undefined ? null : String(body['replaceWith']);
|
|
196
|
+
try {
|
|
197
|
+
const writer = await authorizeCrmWriter(req, scope, {
|
|
198
|
+
suiteAct: suiteAct(definition)
|
|
199
|
+
});
|
|
200
|
+
if (writer.ok === false) {
|
|
201
|
+
res.status(writer.status).json(writer.body);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
205
|
+
const orgRef = firestore.collection('orgs').doc(writer.orgId);
|
|
206
|
+
const listRef = orgRef.collection(CRM_COLLECTIONS.picklists).doc(picklistId);
|
|
207
|
+
/*
|
|
208
|
+
* THE LIST. Read and written in one transaction, so two admins editing
|
|
209
|
+
* at once cannot each write a list missing the other's change.
|
|
210
|
+
*/ const moved = await firestore.runTransaction(async (transaction)=>{
|
|
211
|
+
var _body_label, _ref, _ref1;
|
|
212
|
+
var _move_picklist_values_find, _move_picklist_values_find1;
|
|
213
|
+
const snapshot = await transaction.get(listRef);
|
|
214
|
+
const before = effectiveCrmPicklist(picklistId, snapshot.data());
|
|
215
|
+
const value = before.values.find((entry)=>entry.id === valueId);
|
|
216
|
+
if (!value) return {
|
|
217
|
+
ok: false,
|
|
218
|
+
error: 'That value is no longer in the list.'
|
|
219
|
+
};
|
|
220
|
+
const move = action === 'rename' ? renamePicklistValue(before, valueId, String((_body_label = body['label']) != null ? _body_label : '')) : deletePicklistValue(definition, before, valueId, replaceWith);
|
|
221
|
+
if (move.ok === false) return move;
|
|
222
|
+
const created = normalizeCrmPicklist(snapshot.data()) === null;
|
|
223
|
+
transaction.set(listRef, _extends({
|
|
224
|
+
values: move.picklist.values,
|
|
225
|
+
defaultValueId: move.picklist.defaultValueId,
|
|
226
|
+
updatedAt: FieldValue.serverTimestamp()
|
|
227
|
+
}, created ? _extends({
|
|
228
|
+
hostId: scope.hostId || null,
|
|
229
|
+
createdAt: FieldValue.serverTimestamp()
|
|
230
|
+
}, newResourceScopeFields([
|
|
231
|
+
ORG_SCOPE_TOKEN
|
|
232
|
+
])) : {}), {
|
|
233
|
+
merge: true
|
|
234
|
+
});
|
|
235
|
+
const to = action === 'rename' ? (_ref = (_move_picklist_values_find = move.picklist.values.find((entry)=>entry.id === valueId)) == null ? void 0 : _move_picklist_values_find.label) != null ? _ref : null : replaceWith === null ? null : (_ref1 = (_move_picklist_values_find1 = move.picklist.values.find((entry)=>entry.label.toLowerCase() === replaceWith.trim().toLowerCase())) == null ? void 0 : _move_picklist_values_find1.label) != null ? _ref1 : null;
|
|
236
|
+
return {
|
|
237
|
+
ok: true,
|
|
238
|
+
from: value.label,
|
|
239
|
+
to
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
if (moved.ok === false) {
|
|
243
|
+
res.status(400).json({
|
|
244
|
+
error: moved.error
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
/*
|
|
249
|
+
* THE RECORDS, target by target: a record's own field, with its query
|
|
250
|
+
* key moved beside it; for an array target, each entry of a record's
|
|
251
|
+
* list (a deal's contact roles); or, for a facet target, each holder's
|
|
252
|
+
* facet on a contact — one query per consent group the org's sites
|
|
253
|
+
* belong to.
|
|
254
|
+
* A rename to the same label moves nothing, and says so.
|
|
255
|
+
*/ const updated = {};
|
|
256
|
+
let groupIds = null;
|
|
257
|
+
for (const target of definition.targets){
|
|
258
|
+
var _updated, _target_object, _, _updated_target_object;
|
|
259
|
+
(_ = (_updated = updated)[_target_object = target.object]) != null ? _ : _updated[_target_object] = 0;
|
|
260
|
+
if (moved.from === moved.to) continue;
|
|
261
|
+
const records = orgRef.collection(TARGET_COLLECTIONS[target.object]);
|
|
262
|
+
if (target.arrayKey && target.keyField) {
|
|
263
|
+
var _updated_target_object1;
|
|
264
|
+
updated[target.object] = ((_updated_target_object1 = updated[target.object]) != null ? _updated_target_object1 : 0) + await replaceInArrayEverywhere(firestore, records, _extends({}, target, {
|
|
265
|
+
arrayKey: target.arrayKey,
|
|
266
|
+
keyField: target.keyField
|
|
267
|
+
}), moved.from, moved.to);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (target.facet) {
|
|
271
|
+
if (!groupIds) {
|
|
272
|
+
const hostIds = await orgHostIds(firestore, writer.orgId);
|
|
273
|
+
groupIds = [
|
|
274
|
+
...new Set(hostIds.map((hostId)=>consentGroupForHost(writer.org, hostId).groupId))
|
|
275
|
+
].filter(Boolean);
|
|
276
|
+
}
|
|
277
|
+
for (const groupId of groupIds){
|
|
278
|
+
var _updated_target_object2;
|
|
279
|
+
const path = new FieldPath(CONTACT_FACETS_FIELD, groupId, target.field);
|
|
280
|
+
updated[target.object] = ((_updated_target_object2 = updated[target.object]) != null ? _updated_target_object2 : 0) + await replaceEverywhere(firestore, records.where(path, '==', moved.from), path, moved.to, // A facet value a contact list filters by is keyed in
|
|
281
|
+
// `facetKeys` (AGL-3511), restamped from the record as moved.
|
|
282
|
+
(snapshot)=>({
|
|
283
|
+
[CRM_CONTACT_FACET_KEYS_FIELD]: crmContactFacetKeys(withFacetValue(snapshot.data(), groupId, target.field, moved.to))
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
updated[target.object] = ((_updated_target_object = updated[target.object]) != null ? _updated_target_object : 0) + await replaceEverywhere(firestore, records.where(target.field, '==', moved.from), target.field, moved.to, target.keyField ? {
|
|
289
|
+
[target.keyField]: crmPicklistKey(moved.to)
|
|
290
|
+
} : {});
|
|
291
|
+
}
|
|
292
|
+
const answer = {
|
|
293
|
+
ok: true,
|
|
294
|
+
updated
|
|
295
|
+
};
|
|
296
|
+
res.status(200).json(answer);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error('[crm] picklist-values failed', error);
|
|
299
|
+
res.status(500).json({
|
|
300
|
+
error: `The ${definition.plural} could not be saved.`
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** `POST crm/picklist-values` — any registered picklist, named by `picklistId`. */ export const crmPicklistValuesHandler = picklistValuesHandler();
|
|
306
|
+
/** `POST crm/lead-source-values` — the same route with the picklist fixed to the lead source. */ export const crmLeadSourceValuesHandler = picklistValuesHandler(CRM_LEAD_SOURCE_PICKLIST);
|
|
307
|
+
|
|
308
|
+
//# sourceMappingURL=picklist-values.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/picklist-values.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 consentGroupForHost,\n CONTACT_FACETS_FIELD,\n CRM_COLLECTIONS,\n CRM_CONTACT_FACET_KEYS_FIELD,\n CRM_LEAD_SOURCE_PICKLIST,\n type CrmPicklist,\n type CrmPicklistDefinition,\n type CrmPicklistId,\n type CrmPicklistObject,\n type CrmPicklistTarget,\n crmContactFacetKeys,\n crmPicklistDefinition,\n crmPicklistKey,\n effectiveCrmPicklist,\n newResourceScopeFields,\n normalizeCrmPicklist,\n ORG_SCOPE_TOKEN,\n type PluginApiHandler,\n} from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport {\n deletePicklistValue,\n type PicklistMove,\n type PicklistValuesResponse,\n renamePicklistValue,\n} from '../model/picklist-values'\nimport { orgHostIds, readCrmRouteScope } from './org-caller'\nimport { authorizeCrmWriter } from './task-routes'\n\n/** What the suite gate names when a plan without the CRM asks. */\nconst suiteAct = (definition: CrmPicklistDefinition) =>\n `Editing the organization's ${definition.plural}`\n\n/** The org subcollection each target object's records live in. */\nconst TARGET_COLLECTIONS: Record<CrmPicklistObject, string> = {\n contact: 'contacts',\n lead: 'leads',\n company: CRM_COLLECTIONS.companies,\n deal: CRM_COLLECTIONS.deals,\n task: CRM_COLLECTIONS.tasks,\n}\n\n/** Writes per batch — under Firestore's 500, with room to spare. */\nconst BATCH_SIZE = 400\n\n/**\n * Set `field` to `to` (or remove it, for `null`) on every document `query`\n * finds holding `from`, a batch at a time, with `beside` written alongside.\n * Answers how many were written.\n *\n * The query is re-run after each batch rather than paged, because each\n * batch moves its documents OUT of the query's answer — the next run finds\n * only what is left, and a run that finds nothing is the end.\n */\nasync function replaceEverywhere(\n firestore: FirebaseFirestore.Firestore,\n query: FirebaseFirestore.Query,\n field: string | FirebaseFirestore.FieldPath,\n to: string | null,\n beside:\n | Record<string, unknown>\n | ((snapshot: FirebaseFirestore.QueryDocumentSnapshot) => Record<string, unknown>) = {},\n): Promise<number> {\n let written = 0\n for (;;) {\n const page = await query.limit(BATCH_SIZE).get()\n if (page.empty) return written\n const batch = firestore.batch()\n for (const snapshot of page.docs) {\n const alongside = typeof beside === 'function' ? beside(snapshot) : beside\n batch.update(\n snapshot.ref,\n field,\n to ?? FieldValue.delete(),\n 'updatedAt',\n FieldValue.serverTimestamp(),\n ...Object.entries(alongside).flat(),\n )\n }\n await batch.commit()\n written += page.size\n if (page.size < BATCH_SIZE) return written\n }\n}\n\n/**\n * Rewrite an ARRAY target (AGL-3521): every record whose `keyField` lists\n * the old label's key has each entry of `field` holding `from` at\n * `arrayKey` moved to `to` — or the entry's label removed, for `null` —\n * and its `keyField` restamped from the entries as moved. Answers how many\n * records were written.\n *\n * Paged by document id rather than re-run, because a rename that changes\n * only the label's case keeps its key, so a written record can stay in the\n * query's answer.\n */\nasync function replaceInArrayEverywhere(\n firestore: FirebaseFirestore.Firestore,\n records: FirebaseFirestore.CollectionReference,\n target: CrmPicklistTarget & { arrayKey: string; keyField: string },\n from: string,\n to: string | null,\n): Promise<number> {\n const fromKey = crmPicklistKey(from)\n if (!fromKey) return 0\n const query = records\n .where(target.keyField, 'array-contains', fromKey)\n .orderBy(FieldPath.documentId())\n .limit(BATCH_SIZE)\n let written = 0\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (;;) {\n const page: FirebaseFirestore.QuerySnapshot = await (after ? query.startAfter(after) : query).get()\n if (page.empty) return written\n const batch = firestore.batch()\n let changed = 0\n for (const snapshot of page.docs) {\n const entries = snapshot.get(target.field)\n if (!Array.isArray(entries)) continue\n let moved = false\n const next = entries.map((entry: unknown) => {\n if (!entry || typeof entry !== 'object') return entry\n const row = entry as Record<string, unknown>\n if (row[target.arrayKey] !== from) return entry\n moved = true\n const rest = { ...row }\n delete rest[target.arrayKey]\n return to === null ? rest : { ...rest, [target.arrayKey]: to }\n })\n if (!moved) continue\n const keys = new Set<string>()\n for (const entry of next) {\n const key =\n entry && typeof entry === 'object'\n ? crmPicklistKey((entry as Record<string, unknown>)[target.arrayKey])\n : null\n if (key) keys.add(key)\n }\n batch.update(snapshot.ref, {\n [target.field]: next,\n [target.keyField]: [...keys],\n updatedAt: FieldValue.serverTimestamp(),\n })\n changed += 1\n }\n if (changed) await batch.commit()\n written += changed\n if (page.size < BATCH_SIZE) return written\n after = page.docs[page.docs.length - 1] ?? null\n }\n}\n\n/** A contact as it reads once one holder's facet field holds `to` (or nothing, for `null`). */\nfunction withFacetValue(\n data: Record<string, unknown>,\n groupId: string,\n field: string,\n to: string | null,\n): Record<string, unknown> {\n const facets = { ...((data[CONTACT_FACETS_FIELD] as Record<string, unknown> | undefined) ?? {}) }\n const facet = { ...((facets[groupId] as Record<string, unknown> | undefined) ?? {}) }\n if (to === null) delete facet[field]\n else facet[field] = to\n facets[groupId] = facet\n return { ...data, [CONTACT_FACETS_FIELD]: facets }\n}\n\n/**\n * `POST crm/picklist-values` — rename a value of one of the CRM's standard\n * picklists, or delete an added one and move its records to another\n * (AGL-3298, AGL-3510). `crm/lead-source-values` is the same route with the\n * picklist fixed to the lead source.\n *\n * Body: `{ hostId | orgId, picklistId, action: 'rename', valueId, label }` or\n * `{ hostId | orgId, picklistId, action: 'delete', valueId, replaceWith: label | null }`.\n * `picklistId` is one of `CRM_PICKLIST_DEFINITIONS`; anything else is refused.\n *\n * Records store the value's LABEL (see the picklist block in `crm.ts`), so\n * both moves change records as well as the list: every record the\n * definition's `targets` name that holds the old label is rewritten to the\n * new one — or, for a delete with no replacement, cleared. That is\n * Salesforce's Replace, done as part of the move, so a report grouped by\n * the field follows a rename instead of splitting into the old name and\n * the new. A standard value cannot be deleted, and on a definition with\n * meanings a delete's replacement must mean the same (see\n * `deletePicklistValue`).\n *\n * ## The list first, then the records\n *\n * The list is written in a transaction before any record: from that moment\n * no picker offers the old label and no import or API write accepts it, so\n * the sweep that follows cannot be outrun by a new record taking the name\n * it is removing. A sweep interrupted part-way leaves records holding a\n * label the list no longer has, which they show as \"(not in the list)\"; the\n * same request sent again finishes it, because the rename of a value to its\n * own current label is a no-op on the list and still sweeps.\n *\n * ## Who may\n *\n * Whoever may write the CRM — `authorizeCrmWriter`, the question every CRM\n * route asks — which is who the rules let edit the list itself. The sweep\n * reaches every record in the org whatever the caller's scope: it changes\n * one label to another that the same caller could already have picked, and\n * reveals nothing about the records but their count.\n */\nfunction picklistValuesHandler(fixed?: CrmPicklistId): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') {\n res.setHeader('Allow', 'POST')\n res.status(405).json({ error: 'Method not allowed' })\n return\n }\n const body = (req.body ?? {}) as Record<string, unknown>\n const scope = readCrmRouteScope(body)\n if (!scope) {\n res.status(400).json({ error: 'Missing hostId' })\n return\n }\n const definition = crmPicklistDefinition(fixed ?? body['picklistId'])\n if (!definition) {\n res.status(400).json({ error: 'Name one of the CRM’s picklists.' })\n return\n }\n const picklistId = definition.id as CrmPicklistId\n const action = body['action']\n const valueId = String(body['valueId'] ?? '').trim().slice(0, 64)\n if ((action !== 'rename' && action !== 'delete') || !valueId) {\n res.status(400).json({ error: 'Name a value, and whether to rename or delete it.' })\n return\n }\n const replaceWith =\n body['replaceWith'] === null || body['replaceWith'] === undefined\n ? null\n : String(body['replaceWith'])\n\n try {\n const writer = await authorizeCrmWriter(req, scope, { suiteAct: suiteAct(definition) })\n if (writer.ok === false) {\n res.status(writer.status).json(writer.body)\n return\n }\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(writer.orgId)\n const listRef = orgRef.collection(CRM_COLLECTIONS.picklists).doc(picklistId)\n\n /*\n * THE LIST. Read and written in one transaction, so two admins editing\n * at once cannot each write a list missing the other's change.\n */\n const moved = await firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(listRef)\n const before: CrmPicklist = effectiveCrmPicklist(picklistId, snapshot.data())\n const value = before.values.find((entry) => entry.id === valueId)\n if (!value) return { ok: false as const, error: 'That value is no longer in the list.' }\n const move: PicklistMove =\n action === 'rename'\n ? renamePicklistValue(before, valueId, String(body['label'] ?? ''))\n : deletePicklistValue(definition, before, valueId, replaceWith)\n if (move.ok === false) return move\n const created = normalizeCrmPicklist(snapshot.data()) === null\n transaction.set(\n listRef,\n {\n values: move.picklist.values,\n defaultValueId: move.picklist.defaultValueId,\n updatedAt: FieldValue.serverTimestamp(),\n // The stamp every org-wide CRM document carries (see the Fields\n // page), written when this move is what creates the document.\n ...(created\n ? {\n hostId: scope.hostId || null,\n createdAt: FieldValue.serverTimestamp(),\n ...newResourceScopeFields([ORG_SCOPE_TOKEN]),\n }\n : {}),\n },\n { merge: true },\n )\n const to =\n action === 'rename'\n ? (move.picklist.values.find((entry) => entry.id === valueId)?.label ?? null)\n : replaceWith === null\n ? null\n : (move.picklist.values.find(\n (entry) => entry.label.toLowerCase() === replaceWith.trim().toLowerCase(),\n )?.label ?? null)\n return { ok: true as const, from: value.label, to }\n })\n if (moved.ok === false) {\n res.status(400).json({ error: moved.error })\n return\n }\n\n /*\n * THE RECORDS, target by target: a record's own field, with its query\n * key moved beside it; for an array target, each entry of a record's\n * list (a deal's contact roles); or, for a facet target, each holder's\n * facet on a contact — one query per consent group the org's sites\n * belong to.\n * A rename to the same label moves nothing, and says so.\n */\n const updated: PicklistValuesResponse['updated'] = {}\n let groupIds: string[] | null = null\n for (const target of definition.targets) {\n updated[target.object] ??= 0\n if (moved.from === moved.to) continue\n const records = orgRef.collection(TARGET_COLLECTIONS[target.object])\n if (target.arrayKey && target.keyField) {\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceInArrayEverywhere(\n firestore,\n records,\n { ...target, arrayKey: target.arrayKey, keyField: target.keyField },\n moved.from,\n moved.to,\n ))\n continue\n }\n if (target.facet) {\n if (!groupIds) {\n const hostIds = await orgHostIds(firestore, writer.orgId)\n groupIds = [\n ...new Set(hostIds.map((hostId) => consentGroupForHost(writer.org, hostId).groupId)),\n ].filter(Boolean)\n }\n for (const groupId of groupIds) {\n const path = new FieldPath(CONTACT_FACETS_FIELD, groupId, target.field)\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(path, '==', moved.from),\n path,\n moved.to,\n // A facet value a contact list filters by is keyed in\n // `facetKeys` (AGL-3511), restamped from the record as moved.\n (snapshot) => ({\n [CRM_CONTACT_FACET_KEYS_FIELD]: crmContactFacetKeys(\n withFacetValue(snapshot.data(), groupId, target.field, moved.to),\n ),\n }),\n ))\n }\n continue\n }\n updated[target.object] =\n (updated[target.object] ?? 0) +\n (await replaceEverywhere(\n firestore,\n records.where(target.field, '==', moved.from),\n target.field,\n moved.to,\n target.keyField ? { [target.keyField]: crmPicklistKey(moved.to) } : {},\n ))\n }\n const answer: PicklistValuesResponse = { ok: true, updated }\n res.status(200).json(answer)\n } catch (error) {\n console.error('[crm] picklist-values failed', error)\n res.status(500).json({ error: `The ${definition.plural} could not be saved.` })\n }\n }\n}\n\n/** `POST crm/picklist-values` — any registered picklist, named by `picklistId`. */\nexport const crmPicklistValuesHandler: PluginApiHandler = picklistValuesHandler()\n\n/** `POST crm/lead-source-values` — the same route with the picklist fixed to the lead source. */\nexport const crmLeadSourceValuesHandler: PluginApiHandler =\n picklistValuesHandler(CRM_LEAD_SOURCE_PICKLIST)\n"],"names":["consentGroupForHost","CONTACT_FACETS_FIELD","CRM_COLLECTIONS","CRM_CONTACT_FACET_KEYS_FIELD","CRM_LEAD_SOURCE_PICKLIST","crmContactFacetKeys","crmPicklistDefinition","crmPicklistKey","effectiveCrmPicklist","newResourceScopeFields","normalizeCrmPicklist","ORG_SCOPE_TOKEN","firebaseAdmin","FieldPath","FieldValue","deletePicklistValue","renamePicklistValue","orgHostIds","readCrmRouteScope","authorizeCrmWriter","suiteAct","definition","plural","TARGET_COLLECTIONS","contact","lead","company","companies","deal","deals","task","tasks","BATCH_SIZE","replaceEverywhere","firestore","query","field","to","beside","written","page","limit","get","empty","batch","snapshot","docs","alongside","update","ref","delete","serverTimestamp","Object","entries","flat","commit","size","replaceInArrayEverywhere","records","target","from","fromKey","where","keyField","orderBy","documentId","after","startAfter","changed","Array","isArray","moved","next","map","entry","row","arrayKey","rest","keys","Set","key","add","updatedAt","length","withFacetValue","data","groupId","facets","facet","picklistValuesHandler","fixed","req","res","body","method","setHeader","status","json","error","scope","picklistId","id","action","valueId","String","trim","slice","replaceWith","undefined","writer","ok","app","orgRef","collection","doc","orgId","listRef","picklists","runTransaction","transaction","move","before","value","values","find","created","set","picklist","defaultValueId","hostId","createdAt","merge","label","toLowerCase","updated","groupIds","targets","object","hostIds","org","filter","Boolean","path","answer","console","crmPicklistValuesHandler","crmLeadSourceValuesHandler"],"mappings":"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"}
|