@aglyn/plugins-crm 1.0.0-beta.217 → 1.0.0-beta.219

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.
Files changed (249) hide show
  1. package/package.json +14 -13
  2. package/src/index.d.ts +1 -0
  3. package/src/index.js.map +1 -1
  4. package/src/lib/components/companies-bulk-bar.js +12 -4
  5. package/src/lib/components/companies-bulk-bar.js.map +1 -1
  6. package/src/lib/components/company-detail-page.js +11 -0
  7. package/src/lib/components/company-detail-page.js.map +1 -1
  8. package/src/lib/components/company-edit-drawer.js +12 -2
  9. package/src/lib/components/company-edit-drawer.js.map +1 -1
  10. package/src/lib/components/company-import-drawer.d.ts +1 -1
  11. package/src/lib/components/company-picker.js +10 -3
  12. package/src/lib/components/company-picker.js.map +1 -1
  13. package/src/lib/components/contact-detail-page.d.ts +4 -4
  14. package/src/lib/components/contact-detail-page.js +50 -53
  15. package/src/lib/components/contact-detail-page.js.map +1 -1
  16. package/src/lib/components/contact-list-columns.d.ts +3 -0
  17. package/src/lib/components/contact-list-columns.js +21 -5
  18. package/src/lib/components/contact-list-columns.js.map +1 -1
  19. package/src/lib/components/contact-merge-dialog.js +2 -1
  20. package/src/lib/components/contact-merge-dialog.js.map +1 -1
  21. package/src/lib/components/contact-timeline-card.js +4 -3
  22. package/src/lib/components/contact-timeline-card.js.map +1 -1
  23. package/src/lib/components/contacts-bulk-bar.d.ts +3 -3
  24. package/src/lib/components/contacts-bulk-bar.js +24 -51
  25. package/src/lib/components/contacts-bulk-bar.js.map +1 -1
  26. package/src/lib/components/contacts-section.js +9 -3
  27. package/src/lib/components/contacts-section.js.map +1 -1
  28. package/src/lib/components/crm-export-all-button.d.ts +1 -1
  29. package/src/lib/components/crm-export-all-button.js.map +1 -1
  30. package/src/lib/components/crm-send-email-dialog.js +9 -6
  31. package/src/lib/components/crm-send-email-dialog.js.map +1 -1
  32. package/src/lib/components/csv-import-drawer.js +2 -2
  33. package/src/lib/components/csv-import-drawer.js.map +1 -1
  34. package/src/lib/components/deal-detail-page.js +18 -2
  35. package/src/lib/components/deal-detail-page.js.map +1 -1
  36. package/src/lib/components/deal-edit-drawer.js +10 -0
  37. package/src/lib/components/deal-edit-drawer.js.map +1 -1
  38. package/src/lib/components/deals-bulk-bar.js +13 -5
  39. package/src/lib/components/deals-bulk-bar.js.map +1 -1
  40. package/src/lib/components/lead-campaigns-card.js +6 -0
  41. package/src/lib/components/lead-campaigns-card.js.map +1 -1
  42. package/src/lib/components/lead-detail-page.js +25 -5
  43. package/src/lib/components/lead-detail-page.js.map +1 -1
  44. package/src/lib/components/lead-properties-card.js +6 -0
  45. package/src/lib/components/lead-properties-card.js.map +1 -1
  46. package/src/lib/components/lead-surfaces-note.js +4 -5
  47. package/src/lib/components/lead-surfaces-note.js.map +1 -1
  48. package/src/lib/components/lead-unqualify-dialog.js +6 -0
  49. package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
  50. package/src/lib/components/leads-bulk-bar.js +28 -3
  51. package/src/lib/components/leads-bulk-bar.js.map +1 -1
  52. package/src/lib/components/leads-section.js +36 -7
  53. package/src/lib/components/leads-section.js.map +1 -1
  54. package/src/lib/components/org-lead-surfaces-note.js +6 -2
  55. package/src/lib/components/org-lead-surfaces-note.js.map +1 -1
  56. package/src/lib/components/recipes-card.js +9 -4
  57. package/src/lib/components/recipes-card.js.map +1 -1
  58. package/src/lib/components/record-sharing-card.d.ts +57 -0
  59. package/src/lib/components/record-sharing-card.js +244 -0
  60. package/src/lib/components/record-sharing-card.js.map +1 -0
  61. package/src/lib/components/settings-section.js +7 -0
  62. package/src/lib/components/settings-section.js.map +1 -1
  63. package/src/lib/components/share-records-dialog.d.ts +28 -0
  64. package/src/lib/components/share-records-dialog.js +184 -0
  65. package/src/lib/components/share-records-dialog.js.map +1 -0
  66. package/src/lib/components/sharing-rule-drawer.d.ts +37 -0
  67. package/src/lib/components/sharing-rule-drawer.js +374 -0
  68. package/src/lib/components/sharing-rule-drawer.js.map +1 -0
  69. package/src/lib/components/sharing-rules-card.d.ts +24 -0
  70. package/src/lib/components/sharing-rules-card.js +287 -0
  71. package/src/lib/components/sharing-rules-card.js.map +1 -0
  72. package/src/lib/components/use-crm-api.d.ts +1 -1
  73. package/src/lib/components/use-crm-api.js.map +1 -1
  74. package/src/lib/components/use-site-route-context.d.ts +25 -0
  75. package/src/lib/components/use-site-route-context.js +58 -0
  76. package/src/lib/components/use-site-route-context.js.map +1 -0
  77. package/src/lib/constants/api-routes.d.ts +31 -0
  78. package/src/lib/constants/api-routes.js +27 -1
  79. package/src/lib/constants/api-routes.js.map +1 -1
  80. package/src/lib/declarations.console-server.d.ts +37 -0
  81. package/src/lib/declarations.console-server.js +103 -0
  82. package/src/lib/declarations.console-server.js.map +1 -0
  83. package/src/lib/declarations.server.d.ts +6 -0
  84. package/src/lib/declarations.server.js +33 -0
  85. package/src/lib/declarations.server.js.map +1 -1
  86. package/src/lib/hooks/use-contact-remove.d.ts +35 -0
  87. package/src/lib/hooks/use-contact-remove.js +77 -0
  88. package/src/lib/hooks/use-contact-remove.js.map +1 -0
  89. package/src/lib/hooks/use-crm-sharing.d.ts +30 -0
  90. package/src/lib/hooks/use-crm-sharing.js +109 -0
  91. package/src/lib/hooks/use-crm-sharing.js.map +1 -0
  92. package/src/lib/model/companies-csv.d.ts +1 -1
  93. package/src/lib/model/companies-csv.js +2 -2
  94. package/src/lib/model/companies-csv.js.map +1 -1
  95. package/src/lib/model/contact-holder.d.ts +68 -0
  96. package/src/lib/model/contact-holder.js +129 -0
  97. package/src/lib/model/contact-holder.js.map +1 -0
  98. package/src/lib/model/contact-interaction-links.d.ts +46 -0
  99. package/src/lib/model/contact-interaction-links.js +65 -0
  100. package/src/lib/model/contact-interaction-links.js.map +1 -0
  101. package/src/lib/model/contact-merge.d.ts +136 -0
  102. package/src/lib/model/contact-merge.js +412 -0
  103. package/src/lib/model/contact-merge.js.map +1 -0
  104. package/src/lib/model/contact-record.d.ts +7 -1
  105. package/src/lib/model/contact-record.js +6 -5
  106. package/src/lib/model/contact-record.js.map +1 -1
  107. package/src/lib/model/contact-remove.d.ts +48 -0
  108. package/src/lib/model/contact-remove.js +22 -0
  109. package/src/lib/model/contact-remove.js.map +1 -0
  110. package/src/lib/model/contacts-bulk-writes.d.ts +2 -71
  111. package/src/lib/model/contacts-bulk-writes.js +2 -82
  112. package/src/lib/model/contacts-bulk-writes.js.map +1 -1
  113. package/src/lib/model/contacts-csv.d.ts +2 -2
  114. package/src/lib/model/contacts-csv.js +2 -2
  115. package/src/lib/model/contacts-csv.js.map +1 -1
  116. package/src/lib/model/crm-csv.d.ts +239 -0
  117. package/src/lib/model/crm-csv.js +467 -0
  118. package/src/lib/model/crm-csv.js.map +1 -0
  119. package/src/lib/model/crm-digest.d.ts +185 -0
  120. package/src/lib/model/crm-digest.js +272 -0
  121. package/src/lib/model/crm-digest.js.map +1 -0
  122. package/src/lib/model/crm-import.d.ts +7 -1
  123. package/src/lib/model/crm-import.js +2 -1
  124. package/src/lib/model/crm-import.js.map +1 -1
  125. package/src/lib/model/crm-record-routes.d.ts +5 -0
  126. package/src/lib/model/crm-record-routes.js +7 -0
  127. package/src/lib/model/crm-record-routes.js.map +1 -1
  128. package/src/lib/model/crm-routes.d.ts +6 -5
  129. package/src/lib/model/crm-routes.js +7 -7
  130. package/src/lib/model/crm-routes.js.map +1 -1
  131. package/src/lib/model/crm-search-sources.d.ts +15 -0
  132. package/src/lib/model/crm-search-sources.js +162 -0
  133. package/src/lib/model/crm-search-sources.js.map +1 -0
  134. package/src/lib/model/crm-sharing-api.d.ts +32 -0
  135. package/src/lib/model/crm-sharing-api.js +59 -0
  136. package/src/lib/model/crm-sharing-api.js.map +1 -0
  137. package/src/lib/model/crm-sharing.d.ts +333 -0
  138. package/src/lib/model/crm-sharing.js +603 -0
  139. package/src/lib/model/crm-sharing.js.map +1 -0
  140. package/src/lib/model/deals-csv.d.ts +1 -1
  141. package/src/lib/model/deals-csv.js +4 -5
  142. package/src/lib/model/deals-csv.js.map +1 -1
  143. package/src/lib/model/lead-filters.d.ts +1 -1
  144. package/src/lib/model/leads-csv.d.ts +1 -1
  145. package/src/lib/model/leads-csv.js +1 -1
  146. package/src/lib/model/leads-csv.js.map +1 -1
  147. package/src/lib/model/tasks-csv.d.ts +1 -1
  148. package/src/lib/model/tasks-csv.js +1 -1
  149. package/src/lib/model/tasks-csv.js.map +1 -1
  150. package/src/lib/plan-entitlements.d.ts +75 -0
  151. package/src/lib/plan-entitlements.js +56 -0
  152. package/src/lib/plan-entitlements.js.map +1 -0
  153. package/src/lib/plugin.js +3 -0
  154. package/src/lib/plugin.js.map +1 -1
  155. package/src/lib/server/api-v1/contacts-merge.d.ts +20 -0
  156. package/src/lib/server/api-v1/contacts-merge.js +120 -0
  157. package/src/lib/server/api-v1/contacts-merge.js.map +1 -0
  158. package/src/lib/server/api-v1/contacts.d.ts +18 -0
  159. package/src/lib/server/api-v1/contacts.js +1076 -0
  160. package/src/lib/server/api-v1/contacts.js.map +1 -0
  161. package/src/lib/server/api-v1/crm-activities.d.ts +18 -0
  162. package/src/lib/server/api-v1/crm-activities.js +269 -0
  163. package/src/lib/server/api-v1/crm-activities.js.map +1 -0
  164. package/src/lib/server/api-v1/crm-companies.d.ts +18 -0
  165. package/src/lib/server/api-v1/crm-companies.js +391 -0
  166. package/src/lib/server/api-v1/crm-companies.js.map +1 -0
  167. package/src/lib/server/api-v1/crm-deals.d.ts +18 -0
  168. package/src/lib/server/api-v1/crm-deals.js +631 -0
  169. package/src/lib/server/api-v1/crm-deals.js.map +1 -0
  170. package/src/lib/server/api-v1/crm-email-templates.d.ts +18 -0
  171. package/src/lib/server/api-v1/crm-email-templates.js +336 -0
  172. package/src/lib/server/api-v1/crm-email-templates.js.map +1 -0
  173. package/src/lib/server/api-v1/crm-leads.d.ts +18 -0
  174. package/src/lib/server/api-v1/crm-leads.js +832 -0
  175. package/src/lib/server/api-v1/crm-leads.js.map +1 -0
  176. package/src/lib/server/api-v1/crm-pipelines.d.ts +76 -0
  177. package/src/lib/server/api-v1/crm-pipelines.js +146 -0
  178. package/src/lib/server/api-v1/crm-pipelines.js.map +1 -0
  179. package/src/lib/server/api-v1/crm-shared.d.ts +271 -0
  180. package/src/lib/server/api-v1/crm-shared.js +436 -0
  181. package/src/lib/server/api-v1/crm-shared.js.map +1 -0
  182. package/src/lib/server/api-v1/crm-tasks.d.ts +18 -0
  183. package/src/lib/server/api-v1/crm-tasks.js +398 -0
  184. package/src/lib/server/api-v1/crm-tasks.js.map +1 -0
  185. package/src/lib/server/api-v1/openapi.d.ts +29 -0
  186. package/src/lib/server/api-v1/openapi.js +985 -0
  187. package/src/lib/server/api-v1/openapi.js.map +1 -0
  188. package/src/lib/server/api-v1/usage.d.ts +26 -0
  189. package/src/lib/server/api-v1/usage.js +73 -0
  190. package/src/lib/server/api-v1/usage.js.map +1 -0
  191. package/src/lib/server/consent-group-participant.js +1 -1
  192. package/src/lib/server/consent-group-participant.js.map +1 -1
  193. package/src/lib/server/contact-lifecycle-floor.d.ts +89 -0
  194. package/src/lib/server/contact-lifecycle-floor.js +91 -0
  195. package/src/lib/server/contact-lifecycle-floor.js.map +1 -0
  196. package/src/lib/server/contact-remove.d.ts +75 -0
  197. package/src/lib/server/contact-remove.js +244 -0
  198. package/src/lib/server/contact-remove.js.map +1 -0
  199. package/src/lib/server/contact-update.js +2 -1
  200. package/src/lib/server/contact-update.js.map +1 -1
  201. package/src/lib/server/contacts-import.d.ts +47 -11
  202. package/src/lib/server/contacts-import.js +171 -71
  203. package/src/lib/server/contacts-import.js.map +1 -1
  204. package/src/lib/server/contacts-merge.d.ts +1 -1
  205. package/src/lib/server/contacts-merge.js +3 -2
  206. package/src/lib/server/contacts-merge.js.map +1 -1
  207. package/src/lib/server/convert-host-lead.d.ts +189 -0
  208. package/src/lib/server/convert-host-lead.js +518 -0
  209. package/src/lib/server/convert-host-lead.js.map +1 -0
  210. package/src/lib/server/crm-sharing.d.ts +108 -0
  211. package/src/lib/server/crm-sharing.js +790 -0
  212. package/src/lib/server/crm-sharing.js.map +1 -0
  213. package/src/lib/server/daily-digest-route.d.ts +107 -0
  214. package/src/lib/server/daily-digest-route.js +588 -0
  215. package/src/lib/server/daily-digest-route.js.map +1 -0
  216. package/src/lib/server/export-route.d.ts +60 -0
  217. package/src/lib/server/export-route.js +447 -0
  218. package/src/lib/server/export-route.js.map +1 -0
  219. package/src/lib/server/inbound-route.d.ts +74 -0
  220. package/src/lib/server/inbound-route.js +257 -0
  221. package/src/lib/server/inbound-route.js.map +1 -0
  222. package/src/lib/server/lead-convert.d.ts +1 -1
  223. package/src/lib/server/lead-convert.js +2 -2
  224. package/src/lib/server/lead-convert.js.map +1 -1
  225. package/src/lib/server/merge-contacts.d.ts +55 -0
  226. package/src/lib/server/merge-contacts.js +274 -0
  227. package/src/lib/server/merge-contacts.js.map +1 -0
  228. package/src/lib/server/pipeline-record-index.d.ts +36 -0
  229. package/src/lib/server/pipeline-record-index.js +87 -0
  230. package/src/lib/server/pipeline-record-index.js.map +1 -0
  231. package/src/lib/server/record-facts.js +2 -1
  232. package/src/lib/server/record-facts.js.map +1 -1
  233. package/src/lib/server/task-reminders-route.d.ts +82 -0
  234. package/src/lib/server/task-reminders-route.js +497 -0
  235. package/src/lib/server/task-reminders-route.js.map +1 -0
  236. package/src/lib/server-deal-stage.js +2 -1
  237. package/src/lib/server-deal-stage.js.map +1 -1
  238. package/src/lib/server.d.ts +5 -2
  239. package/src/lib/server.js +47 -2
  240. package/src/lib/server.js.map +1 -1
  241. package/src/lib/testing/stand-in-site-record-routes.d.ts +26 -0
  242. package/src/lib/testing/stand-in-site-record-routes.js +55 -0
  243. package/src/lib/testing/stand-in-site-record-routes.js.map +1 -0
  244. package/src/lib/usage-axes.d.ts +35 -0
  245. package/src/lib/usage-axes.js +68 -0
  246. package/src/lib/usage-axes.js.map +1 -0
  247. package/src/lib/components/use-emails-hub-path.d.ts +0 -24
  248. package/src/lib/components/use-emails-hub-path.js +0 -59
  249. package/src/lib/components/use-emails-hub-path.js.map +0 -1
@@ -0,0 +1,790 @@
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
+ * The server half of CRM sharing (AGL-3336) — see `model/crm-sharing.ts`
19
+ * for the model.
20
+ *
21
+ * Three writers, one transaction shape. A record's sharing is RECOMPUTED,
22
+ * never patched: the transaction reads the record, keeps its hand shares
23
+ * (changed by the caller when the act is a share or an unshare), matches
24
+ * every active rule against it as it stands, and writes `visibleTo`,
25
+ * `writeTo`, `sharing` and the list fields derived from `visibleTo` only
26
+ * when any of them moves. So the same function serves
27
+ *
28
+ * - a manager's share or unshare (`crm/sharing`, `share`/`unshare`);
29
+ * - a write to the record anywhere on the server, through the core's
30
+ * record-written seam (`crmSharingRecordWritten`), and a client-direct
31
+ * write's follow-up (`evaluate`);
32
+ * - a rule's recompute over existing records (`rule-save`, `rule-continue`),
33
+ * and its removal (`rule-delete`), in budgeted passes with the progress on
34
+ * the rule.
35
+ */ import { CRM_COLLECTIONS, crmActivityLogHasRoom, crmListFieldsPatch } from "@aglyn/aglyn/app-utils/crm";
36
+ import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
37
+ import { hostIdsFromScope } from "@aglyn/aglyn/app-utils/scope-tokens";
38
+ import { firebaseAdmin, getOrgForHost, logOrgActivity } from "@aglyn/tenant-data-admin";
39
+ import { countCrmActivitiesForRecord } from "@aglyn/tenant-data-admin/server/crm-records";
40
+ import { FieldPath, FieldValue } from "firebase-admin/firestore";
41
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
42
+ import { activeSharingRules, CRM_SHARING_CRITERIA_FOR, CRM_SHARING_CRITERIA_KEYS, CRM_SHARING_FIELD, CRM_SHARING_IDS_MAX, CRM_SHARING_OBJECT_LABELS, CRM_SHARING_RULE_IDS_PATH, CRM_SHARING_RULES_MAX, CRM_WRITE_SCOPE_FIELD, describeShareTargets, evaluateRecordGrants, isCrmShareAccess, isCrmSharingObject, planRecordSharing, readCrmSharingRule, readCrmSharingRules, recordHeldScope, sharingPlanChanges, shareTargetsFrom, withManualShares, withoutManualShares } from "../model/crm-sharing.js";
43
+ import { authorizeOrgCaller, orgHostIds, readCrmRouteScope } from "./org-caller.js";
44
+ import { crmSuiteRefusal } from "./suite-gate.js";
45
+ import { authorizeCrmWriter } from "./task-routes.js";
46
+ /*==========================================
47
+ * THE RULES, READ OFF THE ORG DOCUMENT.
48
+ *=========================================*/ /**
49
+ * A few seconds of memory per process: every CRM write asks, and an org
50
+ * with no rules should cost it nothing past the first read. A cached list
51
+ * is never the reason a grant is TAKEN away — see `recomputeRecordSharing`.
52
+ */ const RULES_TTL_MS = 10000;
53
+ const rulesCache = new Map();
54
+ /** Test seam, and what a rule write calls so its own process reads it fresh. */ export function forgetCrmSharingRules(orgId) {
55
+ if (orgId) rulesCache.delete(orgId);
56
+ else rulesCache.clear();
57
+ }
58
+ export async function crmSharingRulesOf(firestore, orgId, options = {}) {
59
+ var _options_nowMs, _snapshot_data;
60
+ const nowMs = (_options_nowMs = options.nowMs) != null ? _options_nowMs : Date.now();
61
+ const cached = rulesCache.get(orgId);
62
+ if (!options.fresh && cached && nowMs - cached.atMs < RULES_TTL_MS) {
63
+ return {
64
+ rules: cached.rules,
65
+ fresh: false
66
+ };
67
+ }
68
+ const snapshot = await firestore.collection('orgs').doc(orgId).get();
69
+ const rules = readCrmSharingRules((_snapshot_data = snapshot.data()) != null ? _snapshot_data : null);
70
+ rulesCache.set(orgId, {
71
+ atMs: nowMs,
72
+ rules
73
+ });
74
+ return {
75
+ rules,
76
+ fresh: true
77
+ };
78
+ }
79
+ export async function recomputeRecordSharing(input) {
80
+ const { firestore, orgId, object, id, atMs } = input;
81
+ const ref = firestore.collection('orgs').doc(orgId).collection(object).doc(id);
82
+ return await firestore.runTransaction(async (tx)=>{
83
+ var _snapshot_data, _plan_writeTo, _plan_sharing, _plan_writeTo1, _plan_sharing1;
84
+ const snapshot = await tx.get(ref);
85
+ if (!snapshot.exists) return 'missing';
86
+ const record = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
87
+ let rules = input.rules;
88
+ /*
89
+ * A GRANT IS NEVER TAKEN AWAY ON A STALE ANSWER. A rule grant whose rule
90
+ * the cached list does not name is dropped only once a fresh read of the
91
+ * org agrees: another process may have saved that rule seconds ago and
92
+ * applied it to this very record.
93
+ */ if (!input.fresh) {
94
+ const known = new Set(rules.map((rule)=>rule.id));
95
+ const stored = record[CRM_SHARING_FIELD];
96
+ const ruleIds = Array.isArray(stored == null ? void 0 : stored.ruleIds) ? stored.ruleIds : [];
97
+ if (ruleIds.some((ruleId)=>!known.has(String(ruleId)))) {
98
+ var _org_data;
99
+ const org = await tx.get(firestore.collection('orgs').doc(orgId));
100
+ rules = readCrmSharingRules((_org_data = org.data()) != null ? _org_data : null);
101
+ }
102
+ }
103
+ let grants = evaluateRecordGrants(object, record, rules, atMs);
104
+ if (input.manual) grants = input.manual(grants);
105
+ const plan = planRecordSharing(record, grants);
106
+ if (!sharingPlanChanges(record, plan)) return 'unchanged';
107
+ const next = _extends({}, record, {
108
+ visibleTo: plan.visibleTo,
109
+ [CRM_WRITE_SCOPE_FIELD]: (_plan_writeTo = plan.writeTo) != null ? _plan_writeTo : undefined,
110
+ [CRM_SHARING_FIELD]: (_plan_sharing = plan.sharing) != null ? _plan_sharing : undefined
111
+ });
112
+ tx.update(ref, _extends({
113
+ visibleTo: plan.visibleTo,
114
+ [CRM_WRITE_SCOPE_FIELD]: (_plan_writeTo1 = plan.writeTo) != null ? _plan_writeTo1 : FieldValue.delete(),
115
+ [CRM_SHARING_FIELD]: (_plan_sharing1 = plan.sharing) != null ? _plan_sharing1 : FieldValue.delete()
116
+ }, crmListFieldsPatch(object, next)));
117
+ return 'changed';
118
+ });
119
+ }
120
+ /** Several records at once, a few in flight. */ async function recomputeMany(input, ids) {
121
+ const queue = [
122
+ ...new Set(ids)
123
+ ];
124
+ const changed = [];
125
+ let missing = 0;
126
+ const worker = async ()=>{
127
+ for(let id = queue.shift(); id !== undefined; id = queue.shift()){
128
+ const outcome = await recomputeRecordSharing(_extends({}, input, {
129
+ id
130
+ }));
131
+ if (outcome === 'changed') changed.push(id);
132
+ if (outcome === 'missing') missing += 1;
133
+ }
134
+ };
135
+ await Promise.all(Array.from({
136
+ length: Math.min(8, queue.length)
137
+ }, worker));
138
+ return {
139
+ changed,
140
+ missing
141
+ };
142
+ }
143
+ /**
144
+ * Re-evaluates the org's rules for these records — the one thing a write
145
+ * anywhere owes the sharing. An org with no active rule for the object
146
+ * costs one cached read and nothing else; grants of a rule switched off
147
+ * are taken away by that rule's own removal pass, not here.
148
+ */ export async function evaluateCrmSharing(firestore, orgId, object, ids) {
149
+ const { rules, fresh } = await crmSharingRulesOf(firestore, orgId);
150
+ if (!activeSharingRules(rules, object).length) return {
151
+ changed: 0,
152
+ missing: 0
153
+ };
154
+ const outcome = await recomputeMany({
155
+ firestore,
156
+ orgId,
157
+ object,
158
+ rules,
159
+ fresh,
160
+ atMs: Date.now()
161
+ }, ids);
162
+ return {
163
+ changed: outcome.changed.length,
164
+ missing: outcome.missing
165
+ };
166
+ }
167
+ /**
168
+ * The core's record-written seam (AGL-3336): every server writer of a lead,
169
+ * a contact, a company or a deal restamps it, and the restamp tells this.
170
+ * Never throws — the write it is told about already landed.
171
+ */ export async function crmSharingRecordWritten(event) {
172
+ if (!isCrmSharingObject(event.collection)) return;
173
+ const segments = event.path.split('/');
174
+ if (segments.length !== 4 || segments[0] !== 'orgs' || segments[2] !== event.collection) return;
175
+ try {
176
+ await evaluateCrmSharing(firebaseAdmin.app().firestore(), segments[1], event.collection, [
177
+ segments[3]
178
+ ]);
179
+ } catch (error) {
180
+ console.error(`[crm] sharing rules could not be evaluated for ${event.path}`, error);
181
+ }
182
+ }
183
+ /*==========================================
184
+ * THE AUDIT: a line on the record, a line in the org's feed.
185
+ *=========================================*/ const LINK_FIELD = {
186
+ leads: 'leadId',
187
+ contacts: 'contactId',
188
+ companies: 'companyId',
189
+ deals: 'dealId'
190
+ };
191
+ const ORG_TARGET = {
192
+ leads: 'lead',
193
+ contacts: 'contact',
194
+ companies: 'company',
195
+ deals: 'deal'
196
+ };
197
+ /**
198
+ * The entry on the record's Activity: who shared it with which sites, or
199
+ * took a share away. Visible to the sites that hold the record and to the
200
+ * sites it names, so both sides of a share read how it came about.
201
+ */ async function fileSharingActivity(firestore, input) {
202
+ try {
203
+ var _data, _hostIdsFromScope_;
204
+ const orgRef = firestore.collection('orgs').doc(input.orgId);
205
+ const record = (_data = (await orgRef.collection(input.object).doc(input.id).get()).data()) != null ? _data : {};
206
+ const held = recordHeldScope(record);
207
+ const visibleTo = [
208
+ ...new Set([
209
+ ...held,
210
+ ...input.targets
211
+ ])
212
+ ];
213
+ if (!visibleTo.length) return;
214
+ const link = {
215
+ [LINK_FIELD[input.object]]: input.id
216
+ };
217
+ if (!crmActivityLogHasRoom(await countCrmActivitiesForRecord(orgRef, link))) return;
218
+ const hostId = typeof record['hostId'] === 'string' && record['hostId'] ? record['hostId'] : (_hostIdsFromScope_ = hostIdsFromScope(held)[0]) != null ? _hostIdsFromScope_ : '';
219
+ await orgRef.collection(CRM_COLLECTIONS.activities).doc(createResourceUid()).create(_extends({
220
+ kind: 'note',
221
+ body: input.body,
222
+ atMs: input.atMs,
223
+ byUid: input.by.uid
224
+ }, input.by.name ? {
225
+ byName: input.by.name
226
+ } : {}, link, {
227
+ hostId,
228
+ visibleTo,
229
+ sourcePluginId: BUNDLE_ID,
230
+ createdAt: FieldValue.serverTimestamp(),
231
+ updatedAt: FieldValue.serverTimestamp()
232
+ }));
233
+ } catch (error) {
234
+ console.error('[crm] the sharing entry could not be written on the record', input.id, error);
235
+ }
236
+ }
237
+ /*==========================================
238
+ * THE ORG'S SITES.
239
+ *=========================================*/ export async function crmSharingSites(firestore, orgId) {
240
+ const hosts = await firestore.collection('hosts').where('orgId', '==', orgId).get();
241
+ return hosts.docs.map((doc)=>{
242
+ var _doc_get, _doc_get1;
243
+ const displayName = String((_doc_get = doc.get('displayName')) != null ? _doc_get : '').trim();
244
+ const subdomain = String((_doc_get1 = doc.get('subdomain')) != null ? _doc_get1 : '').trim();
245
+ return {
246
+ id: String(doc.id),
247
+ name: displayName || subdomain || String(doc.id)
248
+ };
249
+ }).sort((a, b)=>a.name.localeCompare(b.name));
250
+ }
251
+ /*==========================================
252
+ * A RULE, SAVED FROM A REQUEST.
253
+ *=========================================*/ function listFrom(raw, max) {
254
+ if (!Array.isArray(raw)) return [];
255
+ return [
256
+ ...new Set(raw.map((entry)=>String(entry != null ? entry : '').trim().slice(0, 200)).filter(Boolean))
257
+ ].slice(0, max);
258
+ }
259
+ /**
260
+ * The rule a request describes, judged: an object, a name, sites that are
261
+ * the org's own, targets that are the org's own or all sites. `null` with
262
+ * the reason when it cannot be saved.
263
+ */ export function ruleFromRequest(raw, context) {
264
+ var _value_name, _value_criteria, _ref, _ref1, _ref2;
265
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
266
+ return {
267
+ rule: null,
268
+ error: 'Describe the rule.'
269
+ };
270
+ }
271
+ const value = raw;
272
+ const object = value['object'];
273
+ if (!isCrmSharingObject(object)) {
274
+ return {
275
+ rule: null,
276
+ error: 'Choose which records the rule shares.'
277
+ };
278
+ }
279
+ const name = String((_value_name = value['name']) != null ? _value_name : '').trim().slice(0, 120);
280
+ if (!name) return {
281
+ rule: null,
282
+ error: 'Name the rule.'
283
+ };
284
+ const own = new Set(context.hostIds);
285
+ const sourceHostIds = listFrom(value['sourceHostIds'], 30);
286
+ if (sourceHostIds.some((id)=>!own.has(id))) {
287
+ return {
288
+ rule: null,
289
+ error: 'A site the rule names is not one of this organization’s.'
290
+ };
291
+ }
292
+ const targets = shareTargetsFrom(value['targets']);
293
+ if (!targets) return {
294
+ rule: null,
295
+ error: 'Choose the sites to share with, or All sites.'
296
+ };
297
+ if (hostIdsFromScope(targets).some((id)=>!own.has(id))) {
298
+ return {
299
+ rule: null,
300
+ error: 'A site the rule shares with is not one of this organization’s.'
301
+ };
302
+ }
303
+ const access = value['access'];
304
+ if (access !== undefined && !isCrmShareAccess(access)) {
305
+ return {
306
+ rule: null,
307
+ error: 'Access is read-only, or read and edit.'
308
+ };
309
+ }
310
+ const rawCriteria = (_value_criteria = value['criteria']) != null ? _value_criteria : {};
311
+ const criteria = {};
312
+ for (const key of CRM_SHARING_CRITERIA_KEYS){
313
+ if (!CRM_SHARING_CRITERIA_FOR[object].includes(key)) continue;
314
+ const list = listFrom(rawCriteria[key], 20);
315
+ if (list.length) criteria[key] = list;
316
+ }
317
+ const existing = context.existing;
318
+ const rule = readCrmSharingRule({
319
+ id: (_ref = existing == null ? void 0 : existing.id) != null ? _ref : createResourceUid(),
320
+ name,
321
+ object,
322
+ enabled: value['enabled'] !== false,
323
+ sourceHostIds,
324
+ criteria,
325
+ targets,
326
+ access: access != null ? access : 'read',
327
+ createdByUid: (_ref1 = existing == null ? void 0 : existing.createdByUid) != null ? _ref1 : context.uid,
328
+ createdAtMs: (_ref2 = existing == null ? void 0 : existing.createdAtMs) != null ? _ref2 : context.nowMs,
329
+ updatedAtMs: context.nowMs
330
+ });
331
+ if (!rule) return {
332
+ rule: null,
333
+ error: 'The rule could not be read.'
334
+ };
335
+ return {
336
+ rule,
337
+ error: null
338
+ };
339
+ }
340
+ /** A rule as the org document stores it — no `undefined` anywhere. */ function storedRule(rule) {
341
+ return JSON.parse(JSON.stringify(rule));
342
+ }
343
+ /**
344
+ * Writes the org's rule list through `change`, in a transaction over the org
345
+ * document, and answers the list as written.
346
+ */ async function writeRules(firestore, orgId, change) {
347
+ const orgRef = firestore.collection('orgs').doc(orgId);
348
+ const result = await firestore.runTransaction(async (tx)=>{
349
+ var _snapshot_data;
350
+ const snapshot = await tx.get(orgRef);
351
+ const next = change(readCrmSharingRules((_snapshot_data = snapshot.data()) != null ? _snapshot_data : null));
352
+ if ('error' in next) return {
353
+ rules: null,
354
+ error: next.error
355
+ };
356
+ tx.update(orgRef, new FieldPath('crm', 'sharingRules'), next.map(storedRule));
357
+ return {
358
+ rules: next,
359
+ error: null
360
+ };
361
+ });
362
+ forgetCrmSharingRules(orgId);
363
+ return result;
364
+ }
365
+ /** Replaces one rule's run, leaving every other rule as it is. */ async function writeRun(firestore, orgId, ruleId, run, options = {}) {
366
+ var _ref;
367
+ var _written_rules;
368
+ const written = await writeRules(firestore, orgId, (rules)=>options.dropRule ? rules.filter((rule)=>rule.id !== ruleId) : rules.map((rule)=>rule.id === ruleId ? _extends({}, rule, {
369
+ run
370
+ }) : rule));
371
+ return (_ref = (_written_rules = written.rules) == null ? void 0 : _written_rules.find((rule)=>rule.id === ruleId)) != null ? _ref : null;
372
+ }
373
+ /*==========================================
374
+ * A RULE'S RUN: apply it everywhere, or take it away.
375
+ *=========================================*/ const RUN_PAGE = 200;
376
+ /**
377
+ * One budgeted pass of a rule's run, with the progress written on the rule
378
+ * after every page so a reopened settings page shows where it stands and
379
+ * `rule-continue` picks up from the cursor.
380
+ *
381
+ * `apply` walks the object's whole collection by document id — a rule
382
+ * matches on fields no index narrows to (a facet's tags, a site that
383
+ * captured the person) — and writes only the records whose sharing moves.
384
+ * `remove` asks for exactly the records the rule holds a grant on
385
+ * (`sharing.ruleIds`), each of which the recompute then takes it off; a
386
+ * page that makes no progress fails the run rather than looping.
387
+ */ export async function advanceSharingRuleRun(firestore, orgId, ruleId, deadlineMs) {
388
+ var _read_rules_find;
389
+ const read = await crmSharingRulesOf(firestore, orgId, {
390
+ fresh: true
391
+ });
392
+ let rule = (_read_rules_find = read.rules.find((entry)=>entry.id === ruleId)) != null ? _read_rules_find : null;
393
+ if (!(rule == null ? void 0 : rule.run) || rule.run.status !== 'running') return {
394
+ rule,
395
+ more: false
396
+ };
397
+ const records = firestore.collection('orgs').doc(orgId).collection(rule.object);
398
+ let run = _extends({}, rule.run);
399
+ const seen = new Set();
400
+ try {
401
+ while(Date.now() < deadlineMs){
402
+ var _ref, _ref1;
403
+ var _page_docs_;
404
+ const { rules } = await crmSharingRulesOf(firestore, orgId, {
405
+ fresh: true
406
+ });
407
+ let page;
408
+ if (run.mode === 'apply') {
409
+ let query = records.orderBy(FieldPath.documentId()).limit(RUN_PAGE);
410
+ if (run.cursor) query = query.startAfter(run.cursor);
411
+ page = await query.get();
412
+ } else {
413
+ page = await records.where(CRM_SHARING_RULE_IDS_PATH, 'array-contains', ruleId).limit(RUN_PAGE).get();
414
+ }
415
+ const atMs = Date.now();
416
+ // Only the records whose sharing would move are written.
417
+ const moving = page.docs.filter((doc)=>{
418
+ var _doc_data;
419
+ const record = (_doc_data = doc.data()) != null ? _doc_data : {};
420
+ const plan = planRecordSharing(record, evaluateRecordGrants(rule.object, record, rules, atMs));
421
+ return sharingPlanChanges(record, plan);
422
+ }).map((doc)=>doc.id);
423
+ if (run.mode === 'remove' && page.docs.length && page.docs.every((doc)=>seen.has(doc.id))) {
424
+ throw new Error('the rule’s grants did not come off these records');
425
+ }
426
+ for (const doc of page.docs)seen.add(doc.id);
427
+ const outcome = await recomputeMany({
428
+ firestore,
429
+ orgId,
430
+ object: rule.object,
431
+ rules,
432
+ fresh: true,
433
+ atMs
434
+ }, moving);
435
+ const done = run.mode === 'apply' ? page.size < RUN_PAGE : page.empty;
436
+ run = _extends({}, run, {
437
+ processed: run.processed + page.size,
438
+ changed: run.changed + outcome.changed.length,
439
+ cursor: run.mode === 'apply' ? (_ref = (_ref1 = (_page_docs_ = page.docs[page.docs.length - 1]) == null ? void 0 : _page_docs_.id) != null ? _ref1 : run.cursor) != null ? _ref : null : null
440
+ }, done ? {
441
+ status: 'done',
442
+ finishedAtMs: Date.now()
443
+ } : {});
444
+ rule = await writeRun(firestore, orgId, ruleId, run, {
445
+ dropRule: done && rule.deleting === true
446
+ });
447
+ if (done || !rule) return {
448
+ rule,
449
+ more: false
450
+ };
451
+ }
452
+ return {
453
+ rule,
454
+ more: true
455
+ };
456
+ } catch (error) {
457
+ console.error('[crm] a sharing rule run failed', orgId, ruleId, error);
458
+ rule = await writeRun(firestore, orgId, ruleId, _extends({}, run, {
459
+ status: 'failed',
460
+ finishedAtMs: Date.now(),
461
+ error: 'The recompute stopped. Resume it to carry on from where it stopped.'
462
+ }));
463
+ return {
464
+ rule,
465
+ more: false
466
+ };
467
+ }
468
+ }
469
+ /*==========================================
470
+ * THE ROUTE.
471
+ *=========================================*/ /** How long one request works on a rule before it answers and the page asks again. */ const RUN_BUDGET_MS = 20000;
472
+ const MANAGERS_ONLY = 'Sharing records across sites is for the workspace’s owners and admins.';
473
+ function readIds(raw) {
474
+ if (!Array.isArray(raw)) return null;
475
+ const ids = [
476
+ ...new Set(raw.map((entry)=>String(entry != null ? entry : '').trim().slice(0, 256)).filter((id)=>id && !id.includes('/')))
477
+ ].slice(0, CRM_SHARING_IDS_MAX);
478
+ return ids.length ? ids : null;
479
+ }
480
+ /**
481
+ * `POST /api/crm/sharing` — `{ action, hostId | orgId, … }`.
482
+ *
483
+ * Every action but `evaluate` is an org manager's: an OWNER or ADMIN of the
484
+ * whole workspace (`authorizeOrgCaller(…, 'manage-org')`), under a site's
485
+ * hub or the org's. `evaluate` is what a client-direct write owes the rules
486
+ * and is open to whoever may write the CRM. The plan gate is the CRM's own
487
+ * (`crmSuiteRefusal`): sharing is only meaningful across several sites, and
488
+ * the number of sites a workspace may have is already the plan's.
489
+ */ export const crmSharingHandler = async (req, res)=>{
490
+ var _req_body, _body_action;
491
+ if (req.method !== 'POST') {
492
+ res.setHeader('Allow', 'POST');
493
+ res.status(405).json({
494
+ error: 'Method not allowed'
495
+ });
496
+ return;
497
+ }
498
+ const body = (_req_body = req.body) != null ? _req_body : {};
499
+ const scope = readCrmRouteScope(body);
500
+ if (!scope) {
501
+ res.status(400).json({
502
+ error: 'Missing hostId'
503
+ });
504
+ return;
505
+ }
506
+ const action = String((_body_action = body['action']) != null ? _body_action : '');
507
+ const firestore = firebaseAdmin.app().firestore();
508
+ try {
509
+ var _ref;
510
+ var _this;
511
+ if (action === 'evaluate') {
512
+ const object = body['object'];
513
+ const ids = readIds(body['ids']);
514
+ if (!isCrmSharingObject(object) || !ids) {
515
+ res.status(400).json({
516
+ error: 'Name the records.'
517
+ });
518
+ return;
519
+ }
520
+ const writer = await authorizeCrmWriter(req, scope, {
521
+ suiteAct: null
522
+ });
523
+ if (writer.ok === false) {
524
+ res.status(writer.status).json(writer.body);
525
+ return;
526
+ }
527
+ const outcome = await evaluateCrmSharing(firestore, writer.orgId, object, ids);
528
+ const answer = _extends({
529
+ ok: true
530
+ }, outcome);
531
+ res.status(200).json(answer);
532
+ return;
533
+ }
534
+ const orgId = scope.level === 'org' ? scope.orgId : (_ref = (_this = await getOrgForHost(scope.hostId).catch(()=>null)) == null ? void 0 : _this.orgId) != null ? _ref : '';
535
+ if (!orgId) {
536
+ res.status(404).json({
537
+ error: 'This site has no organization, so it has no CRM.'
538
+ });
539
+ return;
540
+ }
541
+ const caller = await authorizeOrgCaller(req, orgId, {
542
+ needs: 'manage-org',
543
+ refusal: MANAGERS_ONLY
544
+ });
545
+ if (caller.ok === false) {
546
+ res.status(caller.status).json({
547
+ error: caller.error
548
+ });
549
+ return;
550
+ }
551
+ const suite = crmSuiteRefusal(caller.org, 'Sharing records across sites');
552
+ if (suite) {
553
+ res.status(suite.status).json(suite.body);
554
+ return;
555
+ }
556
+ const actor = {
557
+ uid: caller.uid,
558
+ email: caller.email
559
+ };
560
+ const byName = caller.name || caller.email || '';
561
+ const nowMs = Date.now();
562
+ if (action === 'sites') {
563
+ const answer = {
564
+ ok: true,
565
+ sites: await crmSharingSites(firestore, orgId)
566
+ };
567
+ res.status(200).json(answer);
568
+ return;
569
+ }
570
+ if (action === 'share' || action === 'unshare') {
571
+ const object = body['object'];
572
+ const ids = readIds(body['ids']);
573
+ if (!isCrmSharingObject(object) || !ids) {
574
+ res.status(400).json({
575
+ error: 'Name the records to share.'
576
+ });
577
+ return;
578
+ }
579
+ const targets = shareTargetsFrom(body['targets']);
580
+ if (!targets) {
581
+ res.status(400).json({
582
+ error: 'Choose the sites, or All sites.'
583
+ });
584
+ return;
585
+ }
586
+ const sites = await crmSharingSites(firestore, orgId);
587
+ const own = new Set(sites.map((site)=>site.id));
588
+ if (hostIdsFromScope(targets).some((id)=>!own.has(id))) {
589
+ res.status(400).json({
590
+ error: 'A site named is not one of this organization’s.'
591
+ });
592
+ return;
593
+ }
594
+ const access = body['access'] === 'edit' ? 'edit' : 'read';
595
+ const { rules, fresh } = await crmSharingRulesOf(firestore, orgId);
596
+ const outcome = await recomputeMany({
597
+ firestore,
598
+ orgId,
599
+ object,
600
+ rules,
601
+ fresh,
602
+ atMs: nowMs,
603
+ manual: (grants)=>action === 'share' ? withManualShares(grants, targets, access, {
604
+ uid: caller.uid,
605
+ name: byName
606
+ }, nowMs) : withoutManualShares(grants, targets)
607
+ }, ids);
608
+ const siteName = (id)=>{
609
+ var _ref;
610
+ var _sites_find;
611
+ return (_ref = (_sites_find = sites.find((site)=>site.id === id)) == null ? void 0 : _sites_find.name) != null ? _ref : 'a site';
612
+ };
613
+ const named = describeShareTargets(targets, siteName);
614
+ const sentence = action === 'share' ? `Shared with ${named}${access === 'edit' ? ' (read and edit)' : ' (read-only)'}` : `Stopped sharing with ${named}`;
615
+ await Promise.all(outcome.changed.map((id)=>fileSharingActivity(firestore, {
616
+ orgId,
617
+ object,
618
+ id,
619
+ body: sentence,
620
+ targets,
621
+ by: {
622
+ uid: caller.uid,
623
+ name: byName
624
+ },
625
+ atMs: nowMs
626
+ })));
627
+ if (outcome.changed.length) {
628
+ const label = CRM_SHARING_OBJECT_LABELS[object];
629
+ await logOrgActivity(orgId, actor, outcome.changed.length === 1 ? `${sentence}: one ${label.one}` : `${sentence}: ${outcome.changed.length} ${label.many.toLowerCase()}`, _extends({
630
+ type: ORG_TARGET[object]
631
+ }, outcome.changed.length === 1 ? {
632
+ id: outcome.changed[0]
633
+ } : {})).catch(()=>undefined);
634
+ }
635
+ const answer = {
636
+ ok: true,
637
+ changed: outcome.changed.length,
638
+ missing: outcome.missing
639
+ };
640
+ res.status(200).json(answer);
641
+ return;
642
+ }
643
+ if (action === 'rule-save') {
644
+ var _raw_id;
645
+ const hostIds = await orgHostIds(firestore, orgId);
646
+ const raw = body['rule'];
647
+ const ruleId = raw && typeof raw === 'object' ? String((_raw_id = raw['id']) != null ? _raw_id : '') : '';
648
+ let saved = null;
649
+ let error = null;
650
+ const written = await writeRules(firestore, orgId, (rules)=>{
651
+ var _rules_find;
652
+ const existing = ruleId ? (_rules_find = rules.find((rule)=>rule.id === ruleId)) != null ? _rules_find : null : null;
653
+ if (ruleId && !existing) return {
654
+ error: 'That rule no longer exists.'
655
+ };
656
+ if (existing == null ? void 0 : existing.deleting) return {
657
+ error: 'That rule is being deleted.'
658
+ };
659
+ if (!existing && rules.length >= CRM_SHARING_RULES_MAX) {
660
+ return {
661
+ error: `A workspace keeps at most ${CRM_SHARING_RULES_MAX} sharing rules.`
662
+ };
663
+ }
664
+ const judged = ruleFromRequest(raw, {
665
+ hostIds,
666
+ existing,
667
+ uid: caller.uid,
668
+ nowMs
669
+ });
670
+ if (!judged.rule) {
671
+ error = judged.error;
672
+ return {
673
+ error: judged.error
674
+ };
675
+ }
676
+ /*
677
+ * An enabled rule walks every record — it may now match some and not
678
+ * others. A disabled one only has grants to take away, and asks for
679
+ * exactly the records holding them.
680
+ */ saved = _extends({}, judged.rule, {
681
+ run: {
682
+ status: 'running',
683
+ mode: judged.rule.enabled ? 'apply' : 'remove',
684
+ processed: 0,
685
+ changed: 0,
686
+ cursor: null,
687
+ startedAtMs: nowMs
688
+ }
689
+ });
690
+ return existing ? rules.map((rule)=>rule.id === existing.id ? saved : rule) : [
691
+ ...rules,
692
+ saved
693
+ ];
694
+ });
695
+ if (!written.rules || !saved) {
696
+ var _ref1;
697
+ res.status(400).json({
698
+ error: (_ref1 = error != null ? error : written.error) != null ? _ref1 : 'The rule could not be saved.'
699
+ });
700
+ return;
701
+ }
702
+ const rule = saved;
703
+ await logOrgActivity(orgId, actor, `${ruleId ? 'Changed' : 'Added'} the sharing rule “${rule.name}”`, {
704
+ type: `${BUNDLE_ID}:sharingRule`,
705
+ id: rule.id,
706
+ name: rule.name
707
+ }).catch(()=>undefined);
708
+ const advanced = await advanceSharingRuleRun(firestore, orgId, rule.id, Date.now() + RUN_BUDGET_MS);
709
+ const answer = {
710
+ ok: true,
711
+ rule: advanced.rule,
712
+ more: advanced.more
713
+ };
714
+ res.status(200).json(answer);
715
+ return;
716
+ }
717
+ if (action === 'rule-delete' || action === 'rule-continue') {
718
+ var _body_ruleId;
719
+ const ruleId = String((_body_ruleId = body['ruleId']) != null ? _body_ruleId : '').trim();
720
+ if (!ruleId) {
721
+ res.status(400).json({
722
+ error: 'Name the rule.'
723
+ });
724
+ return;
725
+ }
726
+ if (action === 'rule-delete') {
727
+ let name = '';
728
+ const written = await writeRules(firestore, orgId, (rules)=>{
729
+ const existing = rules.find((rule)=>rule.id === ruleId);
730
+ if (!existing) return {
731
+ error: 'That rule no longer exists.'
732
+ };
733
+ name = existing.name;
734
+ return rules.map((rule)=>rule.id === ruleId ? _extends({}, rule, {
735
+ enabled: false,
736
+ deleting: true,
737
+ updatedAtMs: nowMs,
738
+ run: {
739
+ status: 'running',
740
+ mode: 'remove',
741
+ processed: 0,
742
+ changed: 0,
743
+ cursor: null,
744
+ startedAtMs: nowMs
745
+ }
746
+ }) : rule);
747
+ });
748
+ if (!written.rules) {
749
+ res.status(404).json({
750
+ error: written.error
751
+ });
752
+ return;
753
+ }
754
+ await logOrgActivity(orgId, actor, `Deleted the sharing rule “${name}”`, {
755
+ type: `${BUNDLE_ID}:sharingRule`,
756
+ id: ruleId,
757
+ name
758
+ }).catch(()=>undefined);
759
+ } else {
760
+ // A failed run resumes from its cursor.
761
+ await writeRules(firestore, orgId, (rules)=>rules.map((rule)=>{
762
+ var _rule_run;
763
+ return rule.id === ruleId && ((_rule_run = rule.run) == null ? void 0 : _rule_run.status) === 'failed' ? _extends({}, rule, {
764
+ run: _extends({}, rule.run, {
765
+ status: 'running'
766
+ })
767
+ }) : rule;
768
+ }));
769
+ }
770
+ const advanced = await advanceSharingRuleRun(firestore, orgId, ruleId, Date.now() + RUN_BUDGET_MS);
771
+ const answer = {
772
+ ok: true,
773
+ rule: advanced.rule,
774
+ more: advanced.more
775
+ };
776
+ res.status(200).json(answer);
777
+ return;
778
+ }
779
+ res.status(400).json({
780
+ error: 'Unknown sharing action.'
781
+ });
782
+ } catch (error) {
783
+ console.error('[crm] sharing failed', action, error);
784
+ res.status(500).json({
785
+ error: 'The sharing could not be saved. Try again.'
786
+ });
787
+ }
788
+ };
789
+
790
+ //# sourceMappingURL=crm-sharing.js.map