@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 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/server/api-v1/openapi.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n type ApiV1ResourceDescription,\n booleanField,\n integerField,\n isoField,\n nullableField,\n objectKindField,\n openObjectField,\n postalAddressField,\n queryParam,\n RECORD_STAMPS,\n stringField,\n stringListField,\n UPDATED_AFTER_PARAM,\n} from '@aglyn/tenant-data-admin/server/api-v1-description'\n\n/**\n * The CRM's resources as the customer API's OpenAPI document describes them\n * (AGL-2733), keyed by the resource each describes — what the console's\n * builder turns into their paths, tags and schemas, and the MCP tools derive\n * from.\n *\n * Transcribed from the response examples in `apps/docs/api/resources/*.md`,\n * the customer-facing contract, as the platform's own resources are; each\n * write body lists its handler's own writable set. `api-v1-openapi.spec.ts`\n * holds the document to every endpoint the documentation promises.\n */\nexport const CRM_API_V1_DESCRIPTIONS: Readonly<Record<string, ApiV1ResourceDescription>> = {\n contacts: {\n tag: 'Contacts',\n description: 'People, organization-wide. Part of the CRM, which is included from Starter.',\n schemaName: 'Contact',\n required: ['id', 'object', 'email'],\n writable: [\n 'email', 'name', 'tags', 'notes', 'marketingConsent', 'consentSiteId', 'consentGroupId', 'custom',\n 'phone', 'jobTitle', 'companyId', 'address', 'ownerUid', 'lifecycleStage', 'mediaIds',\n ],\n writeOnly: {\n consentSiteId: stringField('The site this write is made on behalf of: where the person opted in, and whose profile the profile fields land on.'),\n consentGroupId: stringField('With `marketingConsent: true`: the consent group the person was shown when they opted in. The opt-in then covers every site of that group; without it, or when the site is no longer in that group, it covers `consentSiteId` alone.'),\n custom: openObjectField('Contact custom fields, keyed by field key.'),\n mediaIds: stringListField('Media library files attached by the named site, by id, at most 20. An empty array clears them.'),\n },\n writeNote: '`email` is accepted on create only — a `PATCH` that names it is a `400`, because a contact is identified by its email.',\n fields: {\n id: stringField('Contact id.'),\n object: objectKindField('contact'),\n email: stringField('Primary email. Unique per organization.'),\n name: stringField('Display name.'),\n tags: stringListField('Free-form tags.'),\n notes: stringField('Free-form notes.'),\n marketingConsent: booleanField('Whether the contact accepted marketing email.'),\n consentSites: stringListField('Site ids the consent was given on.'),\n sources: stringListField('Where this contact came from.'),\n phone: stringField('Telephone number.'),\n jobTitle: stringField('Job title.'),\n companyId: nullableField(stringField('Primary company.')),\n address: postalAddressField(),\n ownerUid: nullableField(stringField('Owning user. Checked on the page, not the query.')),\n lifecycleStage: nullableField(stringField('CRM lifecycle stage.')),\n companyIds: stringListField('Every company this contact belongs to.'),\n alternateEmails: stringListField('Other addresses that resolve to this contact.'),\n ...RECORD_STAMPS,\n },\n ops: [\n {\n path: '/v1/contacts', method: 'get', operationId: 'listContacts', summary: 'List contacts', list: true, returns: 'Contact', entitlement: 'crm',\n description: 'Combining `email` with `tag` narrows on `email` and checks `tag` on the page, so pages can come back short. `lifecycleStage` and `ownerUid` live on a per-site profile and are ALWAYS checked on the page.',\n filters: [\n queryParam('email', 'Exact match, normalized the way the write path normalizes it. An unusable value is a 400.'),\n queryParam('tag', 'Checked on the page when combined with `email`.'),\n queryParam('lifecycleStage', 'Always checked on the page.'),\n queryParam('ownerUid', 'Always checked on the page.'),\n ],\n },\n { path: '/v1/contacts', method: 'post', operationId: 'createContact', summary: 'Create a contact', accepts: 'ContactWrite', returns: 'Contact', entitlement: 'crm', creates: true, description: 'A duplicate email is `409 conflict` (`code: \"contact_exists\"`), and the message names the existing id.' },\n { path: '/v1/contacts/{contactId}', method: 'get', operationId: 'getContact', summary: 'Retrieve a contact', returns: 'Contact', entitlement: 'crm', pathParams: [{ name: 'contactId', description: 'Contact id.' }] },\n { path: '/v1/contacts/{contactId}', method: 'patch', operationId: 'updateContact', summary: 'Update a contact', accepts: 'ContactWrite', returns: 'Contact', entitlement: 'crm', pathParams: [{ name: 'contactId', description: 'Contact id.' }] },\n { path: '/v1/contacts/{contactId}', method: 'delete', operationId: 'deleteContact', summary: 'Delete a contact', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'contactId', description: 'Contact id.' }] },\n { path: '/v1/contacts/{contactId}/merge', method: 'post', operationId: 'mergeContact', summary: 'Merge two contacts', accepts: 'ContactMerge', returns: 'Contact', entitlement: 'crm', pathParams: [{ name: 'contactId', description: 'The contact that survives.' }] },\n ],\n components: {\n ContactMerge: {\n type: 'object',\n required: ['sourceContactId'],\n description:\n 'Merge another contact into this one. The source is removed. Any other ' +\n 'member is a `400`.',\n properties: {\n sourceContactId: {\n type: 'string',\n description:\n 'The contact to merge FROM. It does not survive, and it must not be ' +\n 'the contact in the path.',\n },\n },\n additionalProperties: false,\n },\n },\n },\n companies: {\n tag: 'Companies',\n description: 'Organizations in the CRM.',\n schemaName: 'Company',\n required: ['id', 'object', 'name'],\n writable: ['name', 'domain', 'website', 'phone', 'address', 'industry', 'ownerUid', 'notes', 'custom', 'mediaIds', 'consentSiteId'],\n writeOnly: {\n mediaIds: stringListField('Media library files attached to this company, by id, at most 20. An empty array clears them.'),\n consentSiteId: stringField('The site the company is created on behalf of.'),\n },\n writeNote: 'A create needs `name` and `consentSiteId`; a `PATCH` that names `consentSiteId` is a `400`.',\n fields: {\n id: stringField('Company id.'),\n object: objectKindField('company'),\n name: stringField('Company name.'),\n domain: nullableField(stringField('Primary domain. Unique per organization.')),\n website: nullableField(stringField('Website URL.')),\n phone: nullableField(stringField('Telephone number.')),\n address: postalAddressField(),\n industry: nullableField(stringField('Industry label.')),\n ownerUid: nullableField(stringField('Owning user.')),\n notes: nullableField(stringField('Free-form notes.')),\n custom: openObjectField('Customer-defined fields.'),\n nextTaskAt: nullableField(isoField('When the next open task on this company is due.')),\n siteId: nullableField(stringField('Site the record originated on.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/companies', method: 'get', operationId: 'listCompanies', summary: 'List companies', list: true, returns: 'Company', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('domain', 'Exact match.'), queryParam('ownerUid', 'Owning user.')] },\n { path: '/v1/companies', method: 'post', operationId: 'createCompany', summary: 'Create a company', accepts: 'CompanyWrite', returns: 'Company', entitlement: 'crm', creates: true, description: 'A duplicate domain is `409 conflict` (`code: \"company_exists\"`), naming the existing id.' },\n { path: '/v1/companies/{companyId}', method: 'get', operationId: 'getCompany', summary: 'Retrieve a company', returns: 'Company', entitlement: 'crm', pathParams: [{ name: 'companyId', description: 'Company id.' }] },\n { path: '/v1/companies/{companyId}', method: 'patch', operationId: 'updateCompany', summary: 'Update a company', accepts: 'CompanyWrite', returns: 'Company', entitlement: 'crm', pathParams: [{ name: 'companyId', description: 'Company id.' }] },\n { path: '/v1/companies/{companyId}', method: 'delete', operationId: 'deleteCompany', summary: 'Delete a company', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'companyId', description: 'Company id.' }] },\n ],\n },\n pipelines: {\n tag: 'Pipelines',\n description: 'Deal pipelines and their stages. Read-only over the API.',\n schemaName: 'Pipeline',\n required: ['id', 'object', 'name', 'stages'],\n fields: {\n id: stringField('Pipeline id.'),\n object: objectKindField('pipeline'),\n name: stringField('Pipeline name.'),\n isDefault: booleanField('Whether new deals land here when none is named.'),\n archived: booleanField('Whether the pipeline is archived.'),\n archivedAt: nullableField(isoField('When it was archived.')),\n stages: {\n type: 'array',\n description: 'Ordered stages. A deal’s `stageId` names one of these.',\n items: {\n type: 'object',\n properties: { id: { type: 'string' }, name: { type: 'string' }, order: { type: 'integer' } },\n additionalProperties: true,\n },\n },\n siteId: nullableField(stringField('Site the pipeline belongs to, when scoped.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/pipelines', method: 'get', operationId: 'listPipelines', summary: 'List pipelines', list: true, returns: 'Pipeline', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM] },\n { path: '/v1/pipelines/{pipelineId}', method: 'get', operationId: 'getPipeline', summary: 'Retrieve a pipeline', returns: 'Pipeline', entitlement: 'crm', pathParams: [{ name: 'pipelineId', description: 'Pipeline id.' }] },\n ],\n },\n deals: {\n tag: 'Deals',\n description: 'Opportunities moving through a pipeline.',\n schemaName: 'Deal',\n required: ['id', 'object', 'title', 'pipelineId', 'stageId'],\n writable: ['title', 'pipelineId', 'stageId', 'status', 'amountCents', 'currency', 'lineItems', 'expectedCloseAt', 'ownerUid', 'contactId', 'companyId', 'lostReason', 'notes', 'custom', 'mediaIds', 'consentSiteId'],\n writeOnly: {\n mediaIds: stringListField('Media library files attached to this deal, by id, at most 20. An empty array clears them.'),\n consentSiteId: stringField('The site the deal is created on behalf of.'),\n },\n writeNote: '`pipelineId` and `consentSiteId` are accepted on create only; a `PATCH` that names either is a `400`.',\n fields: {\n id: stringField('Deal id.'),\n object: objectKindField('deal'),\n title: stringField('Deal title.'),\n pipelineId: stringField('Pipeline the deal sits in.'),\n stageId: stringField('Stage within that pipeline.'),\n status: stringField('`open`, `won` or `lost`.'),\n amountCents: integerField('Value in the smallest unit of `currency`.'),\n currency: stringField('ISO 4217 code.'),\n lineItems: { type: 'array', description: 'Line items, when the deal carries them.', items: { type: 'object', additionalProperties: true } },\n expectedCloseAt: nullableField(isoField('Forecast close date.')),\n closedAt: nullableField(isoField('When it was actually closed.')),\n stageChangedAt: nullableField(isoField('When the stage last moved.')),\n ownerUid: nullableField(stringField('Owning user.')),\n contactId: nullableField(stringField('Associated contact.')),\n companyId: nullableField(stringField('Associated company.')),\n lostReason: nullableField(stringField('Why it was lost.')),\n notes: nullableField(stringField('Free-form notes.')),\n custom: openObjectField('Customer-defined fields.'),\n nextTaskAt: nullableField(isoField('When the next open task on this deal is due.')),\n siteId: nullableField(stringField('Site the record originated on.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/deals', method: 'get', operationId: 'listDeals', summary: 'List deals', list: true, returns: 'Deal', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('pipelineId', 'Pipeline.'), queryParam('stageId', 'Stage.'), queryParam('status', 'Deal status.'), queryParam('ownerUid', 'Owning user.')] },\n { path: '/v1/deals', method: 'post', operationId: 'createDeal', summary: 'Create a deal', accepts: 'DealWrite', returns: 'Deal', entitlement: 'crm', creates: true },\n { path: '/v1/deals/{dealId}', method: 'get', operationId: 'getDeal', summary: 'Retrieve a deal', returns: 'Deal', entitlement: 'crm', pathParams: [{ name: 'dealId', description: 'Deal id.' }] },\n { path: '/v1/deals/{dealId}', method: 'patch', operationId: 'updateDeal', summary: 'Update a deal', accepts: 'DealWrite', returns: 'Deal', entitlement: 'crm', pathParams: [{ name: 'dealId', description: 'Deal id.' }] },\n { path: '/v1/deals/{dealId}', method: 'delete', operationId: 'deleteDeal', summary: 'Delete a deal', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'dealId', description: 'Deal id.' }] },\n ],\n },\n tasks: {\n tag: 'Tasks',\n description: 'Follow-ups attached to CRM records.',\n schemaName: 'Task',\n required: ['id', 'object', 'title'],\n writable: ['title', 'notes', 'kind', 'priority', 'status', 'dueAt', 'remindAt', 'assigneeUid', 'contactId', 'companyId', 'dealId', 'consentSiteId'],\n writeOnly: {\n consentSiteId: stringField('The site the task is created on behalf of.'),\n },\n writeNote: 'A create needs `title` and `consentSiteId`; a `PATCH` that names `consentSiteId` is a `400`.',\n fields: {\n id: stringField('Task id.'),\n object: objectKindField('task'),\n title: stringField('Task title.'),\n notes: nullableField(stringField('Free-form notes.')),\n kind: stringField('Task kind, e.g. `call` or `email`.'),\n priority: stringField('Priority label.'),\n status: stringField('`open` or `done`.'),\n dueAt: nullableField(isoField('When the task is due.')),\n remindAt: nullableField(isoField('When a reminder is scheduled.')),\n reminderSentAt: nullableField(isoField('When the reminder was sent.')),\n completedAt: nullableField(isoField('When it was completed.')),\n assigneeUid: nullableField(stringField('Assigned user.')),\n contactId: nullableField(stringField('Associated contact.')),\n companyId: nullableField(stringField('Associated company.')),\n dealId: nullableField(stringField('Associated deal.')),\n siteId: nullableField(stringField('Site the record originated on.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/tasks', method: 'get', operationId: 'listTasks', summary: 'List tasks', list: true, returns: 'Task', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('status', 'Task status.'), queryParam('assigneeUid', 'Assigned user.'), queryParam('contactId', 'Associated contact.'), queryParam('dealId', 'Associated deal.')] },\n { path: '/v1/tasks', method: 'post', operationId: 'createTask', summary: 'Create a task', accepts: 'TaskWrite', returns: 'Task', entitlement: 'crm', creates: true },\n { path: '/v1/tasks/{taskId}', method: 'get', operationId: 'getTask', summary: 'Retrieve a task', returns: 'Task', entitlement: 'crm', pathParams: [{ name: 'taskId', description: 'Task id.' }] },\n { path: '/v1/tasks/{taskId}', method: 'patch', operationId: 'updateTask', summary: 'Update a task', accepts: 'TaskWrite', returns: 'Task', entitlement: 'crm', pathParams: [{ name: 'taskId', description: 'Task id.' }] },\n { path: '/v1/tasks/{taskId}', method: 'delete', operationId: 'deleteTask', summary: 'Delete a task', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'taskId', description: 'Task id.' }] },\n ],\n },\n activities: {\n tag: 'Activities',\n description: 'Logged interactions. Append-only: no PATCH.',\n schemaName: 'Activity',\n required: ['id', 'object', 'kind', 'at'],\n writable: ['kind', 'body', 'at', 'byUid', 'contactId', 'companyId', 'dealId', 'outcome', 'durationMinutes', 'consentSiteId'],\n writeOnly: {\n consentSiteId: stringField('The site the activity is logged on behalf of.'),\n },\n writeRequired: ['body', 'consentSiteId'],\n fields: {\n id: stringField('Activity id.'),\n object: objectKindField('activity'),\n kind: stringField('What happened, e.g. `call`, `email`, `note`.'),\n body: stringField('Free-form body.'),\n at: isoField('When the interaction happened — not when it was logged.'),\n byUid: nullableField(stringField('User who logged it.')),\n contactId: nullableField(stringField('Associated contact.')),\n companyId: nullableField(stringField('Associated company.')),\n dealId: nullableField(stringField('Associated deal.')),\n outcome: nullableField(stringField('Outcome label.')),\n durationMinutes: nullableField(integerField('Duration, for calls and meetings.')),\n siteId: nullableField(stringField('Site the record originated on.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/activities', method: 'get', operationId: 'listActivities', summary: 'List activities', list: true, returns: 'Activity', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('kind', 'Activity kind.'), queryParam('contactId', 'Associated contact.'), queryParam('dealId', 'Associated deal.')] },\n { path: '/v1/activities', method: 'post', operationId: 'createActivity', summary: 'Log an activity', accepts: 'ActivityWrite', returns: 'Activity', entitlement: 'crm', creates: true },\n { path: '/v1/activities/{activityId}', method: 'get', operationId: 'getActivity', summary: 'Retrieve an activity', returns: 'Activity', entitlement: 'crm', pathParams: [{ name: 'activityId', description: 'Activity id.' }] },\n { path: '/v1/activities/{activityId}', method: 'delete', operationId: 'deleteActivity', summary: 'Delete an activity', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'activityId', description: 'Activity id.' }] },\n ],\n },\n leads: {\n tag: 'Leads',\n description: 'Unqualified interest, before it becomes a contact. A lead is a record of its own: the person and their company as text, until converting makes the contact and the company.',\n schemaName: 'Lead',\n required: ['id', 'object', 'siteId'],\n writable: ['siteId', 'email', 'name', 'status', 'ownerUid', 'ownerEmail', 'notes', 'unqualifiedReason', 'company', 'jobTitle', 'phone', 'website', 'address', 'tags', 'leadSource'],\n writeNote: '`email` and `name` are taken on a create only; a `PATCH` cannot change the address, which is the lead’s identity within its site.',\n writeRequired: ['email'],\n writeOnly: {\n siteId: stringField('The site the lead belongs to, instead of the `siteId` query parameter.'),\n status: stringField('`new`, `working` or `unqualified` (`unqualified` on a `PATCH` only). A lead becomes `qualified` by being converted.'),\n ownerEmail: stringField('A member’s address, resolved against the organization’s roster. Not with `ownerUid` in the same request.'),\n },\n fields: {\n id: stringField('Lead id.'),\n object: objectKindField('lead'),\n siteId: stringField('Site the lead arrived on.'),\n email: nullableField(stringField('Email — the lead’s identity within its site.')),\n name: nullableField(stringField('Name, when supplied.')),\n status: stringField('Lead status.'),\n ownerUid: nullableField(stringField('Owning user.')),\n notes: nullableField(stringField('Free-form notes.')),\n unqualifiedReason: nullableField(stringField('Why the lead was disqualified.')),\n company: nullableField(stringField('The company’s name, as text. Converting the lead is what links or creates the company record.')),\n jobTitle: nullableField(stringField('Job title.')),\n phone: nullableField(stringField('E.164 phone number.')),\n website: nullableField(stringField('An http(s) URL.')),\n address: nullableField(postalAddressField()),\n tags: stringListField('Lower-cased tags.'),\n leadSource: nullableField(\n stringField(\n \"Where the lead came from: one of the organization's active lead source values \" +\n '(CRM › Fields › Leads), matched without regard to case. Any other value is refused ' +\n 'with a 400 naming the values allowed; the value a lead already holds is kept even ' +\n \"after it is deactivated. A create that names none starts from the list's default.\",\n ),\n ),\n sources: stringListField('The surfaces that captured the lead: `signup`, `booking`, `form:{formId}`, `import`, `manual`, `api`.'),\n submissionCount: integerField('How many form submissions this lead has made.'),\n firstSeen: isoField('First interaction.'),\n lastSeen: isoField('Most recent interaction.'),\n marketingConsent: booleanField('Whether marketing consent was given.'),\n marketingConsentAt: nullableField(isoField('When consent was given.')),\n convertedContactId: nullableField(stringField('Contact created by conversion.')),\n convertedAt: nullableField(isoField('When it was converted.')),\n companyId: nullableField(stringField('Company created or matched by conversion.')),\n dealId: nullableField(stringField('Deal created by conversion.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/leads', method: 'get', operationId: 'listLeads', summary: 'List leads', list: true, returns: 'Lead', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('siteId', 'Site the lead arrived on.'), queryParam('status', 'Lead status.'), queryParam('ownerUid', 'Owning user.')] },\n { path: '/v1/leads', method: 'post', operationId: 'createLead', summary: 'Create a lead', accepts: 'LeadWrite', returns: 'Lead', entitlement: 'crm', creates: true, description: 'A lead sourced outside the site, entered before anyone has qualified it. One address is one lead per site: creating one the site already holds updates it and answers 200. No contact and no company are created — converting the lead does that — and no marketing consent is recorded.' },\n { path: '/v1/leads/{leadId}', method: 'get', operationId: 'getLead', summary: 'Retrieve a lead', returns: 'Lead', entitlement: 'crm', pathParams: [{ name: 'leadId', description: 'Lead id.' }] },\n { path: '/v1/leads/{leadId}', method: 'patch', operationId: 'updateLead', summary: 'Update a lead', accepts: 'LeadWrite', returns: 'Lead', entitlement: 'crm', pathParams: [{ name: 'leadId', description: 'Lead id.' }] },\n { path: '/v1/leads/{leadId}/convert', method: 'post', operationId: 'convertLead', summary: 'Convert a lead', returns: 'LeadConversion', entitlement: 'crm', description: 'Creates a contact, and optionally a company and a deal. Idempotent on the lead: converting an already-converted lead returns the existing ids.', pathParams: [{ name: 'leadId', description: 'Lead id.' }] },\n ],\n components: {\n LeadConversion: {\n type: 'object',\n description:\n 'What the conversion produced. Idempotent on the lead: converting an ' +\n 'already-converted lead returns the existing ids rather than creating ' +\n 'a second set.',\n properties: {\n object: { type: 'string', const: 'lead_conversion' },\n contactId: { type: 'string' },\n companyId: { type: ['string', 'null'] },\n dealId: { type: ['string', 'null'] },\n },\n additionalProperties: true,\n },\n },\n },\n 'email-templates': {\n tag: 'Email templates',\n description: 'Reusable email bodies.',\n schemaName: 'EmailTemplate',\n required: ['id', 'object', 'name'],\n writable: ['name', 'kind', 'visibility', 'ownerUid', 'subject', 'body', 'consentSiteId'],\n writeOnly: {\n consentSiteId: stringField('The site the template is created on behalf of.'),\n },\n writeNote: 'A create needs `name`, `body` and `consentSiteId`; a `PATCH` that names `consentSiteId` is a `400`.',\n fields: {\n id: stringField('Template id.'),\n object: objectKindField('email_template'),\n name: stringField('Template name.'),\n kind: stringField('Template kind.'),\n visibility: stringField('`org` or `private`.'),\n ownerUid: nullableField(stringField('Owner, for a private template.')),\n subject: stringField('Subject line.'),\n body: stringField('Body. May contain merge tokens.'),\n siteId: nullableField(stringField('Site the template belongs to, when scoped.')),\n ...RECORD_STAMPS,\n },\n ops: [\n { path: '/v1/email-templates', method: 'get', operationId: 'listEmailTemplates', summary: 'List email templates', list: true, returns: 'EmailTemplate', entitlement: 'crm', filters: [UPDATED_AFTER_PARAM, queryParam('kind', 'Template kind.'), queryParam('visibility', 'Visibility.')] },\n { path: '/v1/email-templates', method: 'post', operationId: 'createEmailTemplate', summary: 'Create a template', accepts: 'EmailTemplateWrite', returns: 'EmailTemplate', entitlement: 'crm', creates: true },\n { path: '/v1/email-templates/{templateId}', method: 'get', operationId: 'getEmailTemplate', summary: 'Retrieve a template', returns: 'EmailTemplate', entitlement: 'crm', pathParams: [{ name: 'templateId', description: 'Template id.' }] },\n { path: '/v1/email-templates/{templateId}', method: 'patch', operationId: 'updateEmailTemplate', summary: 'Update a template', accepts: 'EmailTemplateWrite', returns: 'EmailTemplate', entitlement: 'crm', pathParams: [{ name: 'templateId', description: 'Template id.' }] },\n { path: '/v1/email-templates/{templateId}', method: 'delete', operationId: 'deleteEmailTemplate', summary: 'Delete a template', returns: 'Deleted', entitlement: 'crm', pathParams: [{ name: 'templateId', description: 'Template id.' }] },\n ],\n },\n}\n"],"names":["booleanField","integerField","isoField","nullableField","objectKindField","openObjectField","postalAddressField","queryParam","RECORD_STAMPS","stringField","stringListField","UPDATED_AFTER_PARAM","CRM_API_V1_DESCRIPTIONS","contacts","tag","description","schemaName","required","writable","writeOnly","consentSiteId","consentGroupId","custom","mediaIds","writeNote","fields","id","object","email","name","tags","notes","marketingConsent","consentSites","sources","phone","jobTitle","companyId","address","ownerUid","lifecycleStage","companyIds","alternateEmails","ops","path","method","operationId","summary","list","returns","entitlement","filters","accepts","creates","pathParams","components","ContactMerge","type","properties","sourceContactId","additionalProperties","companies","domain","website","industry","nextTaskAt","siteId","pipelines","isDefault","archived","archivedAt","stages","items","order","deals","title","pipelineId","stageId","status","amountCents","currency","lineItems","expectedCloseAt","closedAt","stageChangedAt","contactId","lostReason","tasks","kind","priority","dueAt","remindAt","reminderSentAt","completedAt","assigneeUid","dealId","activities","writeRequired","body","at","byUid","outcome","durationMinutes","leads","ownerEmail","unqualifiedReason","company","leadSource","submissionCount","firstSeen","lastSeen","marketingConsentAt","convertedContactId","convertedAt","LeadConversion","const","visibility","subject"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type ApiV1Context } from '@aglyn/tenant-data-admin/server/api-v1-kit';
18
+ /**
19
+ * The CRM's figures on `GET /v1/usage` (AGL-2277, AGL-2606, AGL-2611): the
20
+ * records band the plan sells, and the size of every collection an
21
+ * integration walks. Reported on every plan, like the platform's own bands —
22
+ * a workspace without the CRM still keeps the people its captures record.
23
+ *
24
+ * Aggregations, so an organization with ten thousand deals pays ten reads.
25
+ */
26
+ export declare function crmUsageFigures(ctx: ApiV1Context): Promise<Record<string, unknown>>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { checkCrmRecordsQuota, CRM_COLLECTIONS, UNLIMITED } from "@aglyn/aglyn/server";
17
+ import { usageBand } from "@aglyn/tenant-data-admin/server/api-v1-kit";
18
+ /**
19
+ * The CRM's figures on `GET /v1/usage` (AGL-2277, AGL-2606, AGL-2611): the
20
+ * records band the plan sells, and the size of every collection an
21
+ * integration walks. Reported on every plan, like the platform's own bands —
22
+ * a workspace without the CRM still keeps the people its captures record.
23
+ *
24
+ * Aggregations, so an organization with ten thousand deals pays ten reads.
25
+ */ export async function crmUsageFigures(ctx) {
26
+ var _leadCounts_data_count;
27
+ const orgRef = ctx.firestore.collection('orgs').doc(ctx.orgId);
28
+ const [contactsSnap, companiesSnap, dealsSnap, tasksSnap, activitiesSnap, leadCounts] = await Promise.all([
29
+ orgRef.collection('contacts').count().get(),
30
+ orgRef.collection(CRM_COLLECTIONS.companies).count().get(),
31
+ orgRef.collection(CRM_COLLECTIONS.deals).count().get(),
32
+ orgRef.collection(CRM_COLLECTIONS.tasks).count().get(),
33
+ orgRef.collection(CRM_COLLECTIONS.activities).count().get(),
34
+ /*
35
+ * ONE AGGREGATE (AGL-3275). Leads used to live under each SITE, so this
36
+ * was one count per site the org owns, summed. They share an org
37
+ * collection now, and a sum over per-site counts would report a lead that
38
+ * two brands in a consent group both hold twice — so the org's size is a
39
+ * single unscoped count, the way every other CRM collection above is.
40
+ */ orgRef.collection('leads').count().get()
41
+ ]);
42
+ // The records band is measured on the SUM (AGL-2611); the contacts entry
43
+ // below keeps reporting the people count against it, so a client that
44
+ // only ever read `contacts` still sees the headroom that refuses it.
45
+ const crmRecordsQuota = checkCrmRecordsQuota(ctx.org, contactsSnap.data().count + companiesSnap.data().count + dealsSnap.data().count);
46
+ return {
47
+ contacts: usageBand(contactsSnap.data().count, crmRecordsQuota.included, crmRecordsQuota.remaining, crmRecordsQuota.overageRateUsd),
48
+ /*
49
+ * THE BAND ITSELF (AGL-2611): contacts, companies and deals as one
50
+ * figure against the one band the plan sells. `contacts` above keeps
51
+ * its shape for the client that only reads it — same band, same
52
+ * headroom — and this is the number the invoice and the console
53
+ * meter are computed from. Companies and deals below stay as sizes.
54
+ */ crmRecords: usageBand(crmRecordsQuota.used, crmRecordsQuota.included, crmRecordsQuota.remaining, crmRecordsQuota.overageRateUsd),
55
+ /*
56
+ * The CRM collections (AGL-2606) — SIZES, in the band shape so a client
57
+ * reads them with the code it already has. No plan bands them: a
58
+ * company or a deal is not metered and is never refused, so `included`
59
+ * and `remaining` are `null` — the unlimited band the docs define —
60
+ * and `metered` is false because there is nothing to bill. What the
61
+ * numbers are for is sizing a sync: how many pages a full walk of
62
+ * `/v1/deals` will take, before taking it.
63
+ */ crm: {
64
+ companies: usageBand(companiesSnap.data().count, UNLIMITED, UNLIMITED, null),
65
+ deals: usageBand(dealsSnap.data().count, UNLIMITED, UNLIMITED, null),
66
+ tasks: usageBand(tasksSnap.data().count, UNLIMITED, UNLIMITED, null),
67
+ activities: usageBand(activitiesSnap.data().count, UNLIMITED, UNLIMITED, null),
68
+ leads: usageBand(Number((_leadCounts_data_count = leadCounts.data().count) != null ? _leadCounts_data_count : 0), UNLIMITED, UNLIMITED, null)
69
+ }
70
+ };
71
+ }
72
+
73
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/crm/src/lib/server/api-v1/usage.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkCrmRecordsQuota, CRM_COLLECTIONS, UNLIMITED } from '@aglyn/aglyn/server'\nimport { type ApiV1Context, usageBand } from '@aglyn/tenant-data-admin/server/api-v1-kit'\n\n/**\n * The CRM's figures on `GET /v1/usage` (AGL-2277, AGL-2606, AGL-2611): the\n * records band the plan sells, and the size of every collection an\n * integration walks. Reported on every plan, like the platform's own bands —\n * a workspace without the CRM still keeps the people its captures record.\n *\n * Aggregations, so an organization with ten thousand deals pays ten reads.\n */\nexport async function crmUsageFigures(ctx: ApiV1Context): Promise<Record<string, unknown>> {\n const orgRef = ctx.firestore.collection('orgs').doc(ctx.orgId)\n const [contactsSnap, companiesSnap, dealsSnap, tasksSnap, activitiesSnap, leadCounts] =\n await Promise.all([\n orgRef.collection('contacts').count().get(),\n orgRef.collection(CRM_COLLECTIONS.companies).count().get(),\n orgRef.collection(CRM_COLLECTIONS.deals).count().get(),\n orgRef.collection(CRM_COLLECTIONS.tasks).count().get(),\n orgRef.collection(CRM_COLLECTIONS.activities).count().get(),\n /*\n * ONE AGGREGATE (AGL-3275). Leads used to live under each SITE, so this\n * was one count per site the org owns, summed. They share an org\n * collection now, and a sum over per-site counts would report a lead that\n * two brands in a consent group both hold twice — so the org's size is a\n * single unscoped count, the way every other CRM collection above is.\n */\n orgRef.collection('leads').count().get(),\n ])\n\n // The records band is measured on the SUM (AGL-2611); the contacts entry\n // below keeps reporting the people count against it, so a client that\n // only ever read `contacts` still sees the headroom that refuses it.\n const crmRecordsQuota = checkCrmRecordsQuota(\n ctx.org as never,\n contactsSnap.data().count + companiesSnap.data().count + dealsSnap.data().count,\n )\n\n return {\n contacts: usageBand(\n contactsSnap.data().count,\n crmRecordsQuota.included,\n crmRecordsQuota.remaining,\n crmRecordsQuota.overageRateUsd,\n ),\n /*\n * THE BAND ITSELF (AGL-2611): contacts, companies and deals as one\n * figure against the one band the plan sells. `contacts` above keeps\n * its shape for the client that only reads it — same band, same\n * headroom — and this is the number the invoice and the console\n * meter are computed from. Companies and deals below stay as sizes.\n */\n crmRecords: usageBand(\n crmRecordsQuota.used,\n crmRecordsQuota.included,\n crmRecordsQuota.remaining,\n crmRecordsQuota.overageRateUsd,\n ),\n /*\n * The CRM collections (AGL-2606) — SIZES, in the band shape so a client\n * reads them with the code it already has. No plan bands them: a\n * company or a deal is not metered and is never refused, so `included`\n * and `remaining` are `null` — the unlimited band the docs define —\n * and `metered` is false because there is nothing to bill. What the\n * numbers are for is sizing a sync: how many pages a full walk of\n * `/v1/deals` will take, before taking it.\n */\n crm: {\n companies: usageBand(companiesSnap.data().count, UNLIMITED, UNLIMITED, null),\n deals: usageBand(dealsSnap.data().count, UNLIMITED, UNLIMITED, null),\n tasks: usageBand(tasksSnap.data().count, UNLIMITED, UNLIMITED, null),\n activities: usageBand(activitiesSnap.data().count, UNLIMITED, UNLIMITED, null),\n leads: usageBand(Number(leadCounts.data().count ?? 0), UNLIMITED, UNLIMITED, null),\n },\n }\n}\n"],"names":["checkCrmRecordsQuota","CRM_COLLECTIONS","UNLIMITED","usageBand","crmUsageFigures","ctx","leadCounts","orgRef","firestore","collection","doc","orgId","contactsSnap","companiesSnap","dealsSnap","tasksSnap","activitiesSnap","Promise","all","count","get","companies","deals","tasks","activities","crmRecordsQuota","org","data","contacts","included","remaining","overageRateUsd","crmRecords","used","crm","leads","Number"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,oBAAoB,EAAEC,eAAe,EAAEC,SAAS,QAAQ,sBAAqB;AACtF,SAA4BC,SAAS,QAAQ,6CAA4C;AAEzF;;;;;;;CAOC,GACD,OAAO,eAAeC,gBAAgBC,GAAiB;QA6DzBC;IA5D5B,MAAMC,SAASF,IAAIG,SAAS,CAACC,UAAU,CAAC,QAAQC,GAAG,CAACL,IAAIM,KAAK;IAC7D,MAAM,CAACC,cAAcC,eAAeC,WAAWC,WAAWC,gBAAgBV,WAAW,GACnF,MAAMW,QAAQC,GAAG,CAAC;QAChBX,OAAOE,UAAU,CAAC,YAAYU,KAAK,GAAGC,GAAG;QACzCb,OAAOE,UAAU,CAACR,gBAAgBoB,SAAS,EAAEF,KAAK,GAAGC,GAAG;QACxDb,OAAOE,UAAU,CAACR,gBAAgBqB,KAAK,EAAEH,KAAK,GAAGC,GAAG;QACpDb,OAAOE,UAAU,CAACR,gBAAgBsB,KAAK,EAAEJ,KAAK,GAAGC,GAAG;QACpDb,OAAOE,UAAU,CAACR,gBAAgBuB,UAAU,EAAEL,KAAK,GAAGC,GAAG;QACzD;;;;;;OAMC,GACDb,OAAOE,UAAU,CAAC,SAASU,KAAK,GAAGC,GAAG;KACvC;IAEH,yEAAyE;IACzE,sEAAsE;IACtE,qEAAqE;IACrE,MAAMK,kBAAkBzB,qBACtBK,IAAIqB,GAAG,EACPd,aAAae,IAAI,GAAGR,KAAK,GAAGN,cAAcc,IAAI,GAAGR,KAAK,GAAGL,UAAUa,IAAI,GAAGR,KAAK;IAGjF,OAAO;QACLS,UAAUzB,UACRS,aAAae,IAAI,GAAGR,KAAK,EACzBM,gBAAgBI,QAAQ,EACxBJ,gBAAgBK,SAAS,EACzBL,gBAAgBM,cAAc;QAEhC;;;;;;KAMC,GACDC,YAAY7B,UACVsB,gBAAgBQ,IAAI,EACpBR,gBAAgBI,QAAQ,EACxBJ,gBAAgBK,SAAS,EACzBL,gBAAgBM,cAAc;QAEhC;;;;;;;;KAQC,GACDG,KAAK;YACHb,WAAWlB,UAAUU,cAAcc,IAAI,GAAGR,KAAK,EAAEjB,WAAWA,WAAW;YACvEoB,OAAOnB,UAAUW,UAAUa,IAAI,GAAGR,KAAK,EAAEjB,WAAWA,WAAW;YAC/DqB,OAAOpB,UAAUY,UAAUY,IAAI,GAAGR,KAAK,EAAEjB,WAAWA,WAAW;YAC/DsB,YAAYrB,UAAUa,eAAeW,IAAI,GAAGR,KAAK,EAAEjB,WAAWA,WAAW;YACzEiC,OAAOhC,UAAUiC,QAAO9B,yBAAAA,WAAWqB,IAAI,GAAGR,KAAK,YAAvBb,yBAA2B,IAAIJ,WAAWA,WAAW;QAC/E;IACF;AACF"}
@@ -55,7 +55,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
55
55
  * exactly what the sweep exists to fold in. A SPLIT whose key survives stays
56
56
  * a live key, so each copy is stamped with the change that made it
57
57
  * (`copiedByChange`), and a pass that finds the stamp does not copy again.
58
- */ import { mergeContactFacet } from "@aglyn/aglyn/app-utils/contact-merge";
58
+ */ import { mergeContactFacet } from "../model/contact-merge.js";
59
59
  import { CONTACT_FACETS_FIELD, interactionsForGroup } from "@aglyn/aglyn/app-utils/contacts";
60
60
  import { COMPANY_CONTACTS_COUNT_FIELD, CONTACT_COMPANY_IDS_FIELD, CRM_COLLECTIONS } from "@aglyn/aglyn/app-utils/crm";
61
61
  import { CAPTURED_BY_HOST_FIELD, CONSENT_GROUP_ID_FIELD, MARKETING_CONSENT_BY_HOST_FIELD, MARKETING_CONSENT_FIELD, MARKETING_CONSENT_SOURCE_FIELD } from "@aglyn/aglyn/app-utils/marketing-consent";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/consent-group-participant.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CRM'S SHARE OF A CONSENT GROUP CHANGE (AGL-3320).\n *\n * Two things the CRM keeps are read through the CURRENT consent group, so a\n * change to the declaration moves under them unless they move too:\n *\n * - **C4 — a contact's per-site refusal.** `marketingConsentByHost.{site}`\n * holding `marketingConsent: false` refuses mail from every site of that\n * site's group (`readMarketingBasis`). When two sites stop being one\n * sender, each keeps the refusals the other held: the refusal is written\n * onto the receiving site's entry by dotted path, with the one it\n * replaced kept as `supersededEntry`. A refusal is carried whenever it was\n * given — over-carrying is the safe direction — and one the receiving site\n * already holds is left alone. Only contacts carry per-site refusals;\n * leads and list memberships carry grants alone.\n * - **The holder records**, `facets.{groupId}` — a profile, an owner, a\n * stage, notes, order figures and a timeline, keyed by the group that\n * keeps them. Every writer keys by the declaration in force, so after the\n * flip a site's records must be under its new key:\n * - a MOVE folds every old key's facet into the new key's, with the\n * existing target first and then the sources in the order the person\n * met their sites, through `mergeContactFacet` — the survivor's\n * values win, lists union, figures sum — and deletes each source in\n * the same write, so a figure is summed exactly once;\n * - a SPLIT copies the facet, without its figures and with only the\n * timeline entries of the successor's own sites, to each successor\n * that met the person itself, merged under whatever the successor\n * already holds. The figures follow the capture only when every site\n * that met the person lands under one key; otherwise they stay with the\n * surviving key, or — when the key dissolves — are dropped and counted.\n *\n * ## Run twice, write once\n *\n * The executor runs `carry` before the flip and again as the catch-up, and\n * the sweep runs everything once more. A carried refusal is skipped once the\n * receiving entry refuses; a MOVE or a dissolving SPLIT deletes its source in\n * the write that folds it, so a second pass finds nothing — except a\n * straggler a long job wrote under the old key after the first pass, which is\n * exactly what the sweep exists to fold in. A SPLIT whose key survives stays\n * a live key, so each copy is stamped with the change that made it\n * (`copiedByChange`), and a pass that finds the stamp does not copy again.\n */\n\nimport { mergeContactFacet } from '@aglyn/aglyn/app-utils/contact-merge'\nimport type {\n ConsentGroupChangePlan,\n ConsentGroupChangePreviewLine,\n ConsentGroupHolderMove,\n ConsentGroupHolderSplit,\n} from '@aglyn/aglyn/app-utils/consent-group-change'\nimport {\n CONTACT_FACETS_FIELD,\n type ContactInteraction,\n interactionsForGroup,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport {\n COMPANY_CONTACTS_COUNT_FIELD,\n CONTACT_COMPANY_IDS_FIELD,\n CRM_COLLECTIONS,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n CAPTURED_BY_HOST_FIELD,\n CONSENT_GROUP_ID_FIELD,\n MARKETING_CONSENT_BY_HOST_FIELD,\n MARKETING_CONSENT_FIELD,\n MARKETING_CONSENT_SOURCE_FIELD,\n} from '@aglyn/aglyn/app-utils/marketing-consent'\nimport type {\n ConsentGroupChangeParticipant,\n ConsentGroupChangeRunRequest,\n ConsentGroupChangeRunResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { restampCrmListFieldsAt } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport { summarizeConsentGroupChange } from '../model/consent-group-summary'\n\n/** Contacts one page of the refusal carry reads. */\nexport const REFUSAL_PAGE = 300\n\n/** Contacts one page of the holder pass reads. */\nexport const HOLDER_PAGE = 200\n\n/**\n * The most records a preview reads to answer a count no aggregation can:\n * past it, the line says so with a `null` count rather than guess.\n */\nexport const PREVIEW_SCAN_LIMIT = 5_000\n\n/**\n * The figures a facet sums, and the instants that go with them — what\n * `mergeContactFacet` adds up or takes the earliest or latest of. A split\n * cannot divide one person's purchases between two holders, so a copy\n * leaves them behind.\n */\nexport const HOLDER_FIGURE_FIELDS = [\n 'ltvCents',\n 'ordersCount',\n 'refundedCents',\n 'refundedOrdersCount',\n 'firstPurchaseAtMs',\n 'lastPurchaseAtMs',\n 'lastRefundAtMs',\n 'lastEmailEngagementAtMs',\n] as const\n\n/** The stamp a split's copy carries: `{ [sourceKey]: changeId }`. */\nexport const COPIED_BY_CHANGE_FIELD = 'copiedByChange'\n\ntype Doc = Record<string, unknown>\n\n/** The error codes that mean the contact was written since it was read. */\nconst RACED = new Set([5, 9])\nconst codeOf = (error: unknown): number | undefined => (error as { code?: number } | null)?.code\n\nfunction mapOf(value: unknown): Doc {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Doc) : {}\n}\n\n/** The sites that met this person, in the order they did — then the legacy `hostId`. */\nfunction captureOrder(contact: Doc): string[] {\n const raw = contact[CAPTURED_BY_HOST_FIELD]\n const ordered = Array.isArray(raw)\n ? raw.map((id) => String(id ?? '').trim()).filter(Boolean)\n : []\n const legacy = typeof contact['hostId'] === 'string' ? (contact['hostId'] as string) : ''\n return legacy && !ordered.includes(legacy) ? [...ordered, legacy] : ordered\n}\n\nfunction withoutFigures(facet: Doc): Doc {\n const out: Doc = { ...facet }\n for (const field of HOLDER_FIGURE_FIELDS) delete out[field]\n return out\n}\n\nfunction figuresOf(facet: Doc): Doc {\n const out: Doc = {}\n for (const field of HOLDER_FIGURE_FIELDS) {\n if (field in facet) out[field] = facet[field]\n }\n return out\n}\n\n/** What one contact's holder records become, as a dotted `update()` patch. */\nexport interface HolderPatch {\n patch: Doc\n combined: number\n copied: number\n figuresDropped: number\n /** Each company whose contacts count moves, by how much. */\n companyCounts: Array<{ companyId: string; delta: 1 | -1 }>\n}\n\n/**\n * The holder records of one contact after the plan's flows — or `null` when\n * nothing moves.\n *\n * `stragglersOnly` is the sweep: only the flows that delete their source\n * (MOVE, and a SPLIT whose key dissolves) run, because only those can have\n * left anything behind that a later writer recreated. A key that survived a\n * split is a live key after the flip, and its new writes belong to it.\n */\nexport function planHolderRecords(\n contact: Doc,\n plan: Pick<ConsentGroupChangePlan, 'flows'>,\n changeId: string,\n options: { stragglersOnly?: boolean } = {},\n): HolderPatch | null {\n const facets = mapOf(contact[CONTACT_FACETS_FIELD]) as Record<string, Doc>\n const captured = captureOrder(contact)\n const next: Record<string, Doc | null> = {}\n const current = (key: string): Doc | undefined =>\n key in next ? (next[key] ?? undefined) : facets[key]\n let combined = 0\n let copied = 0\n let figuresDropped = 0\n\n const splits = plan.flows.filter(\n (flow): flow is ConsentGroupHolderSplit =>\n flow.kind === 'split' && !(options.stragglersOnly && flow.survives),\n )\n const copies: Array<{ key: string; facet: Doc }> = []\n for (const split of splits) {\n const source = facets[split.from]\n if (!source || typeof source !== 'object') continue\n if (\n split.survives &&\n split.successors.some(\n (successor) => mapOf(facets[successor.key]?.[COPIED_BY_CHANGE_FIELD])[split.from] === changeId,\n )\n ) {\n continue\n }\n const keyOf = (hostId: string) =>\n split.stayHostIds.includes(hostId)\n ? split.from\n : (split.successors.find((successor) => successor.hostIds.includes(hostId))?.key ?? null)\n let eligible = split.successors.filter((successor) =>\n successor.hostIds.some((hostId) => captured.includes(hostId)),\n )\n if (!split.survives && !eligible.length) eligible = split.successors\n const landing = [\n ...new Set(\n captured\n .filter((hostId) => split.fromHostIds.includes(hostId))\n .map(keyOf)\n .filter((key): key is string => key !== null),\n ),\n ]\n const figuresTo =\n landing.length === 1 && landing[0] !== split.from && eligible.some((s) => s.key === landing[0])\n ? landing[0]\n : null\n for (const successor of eligible) {\n const copy: Doc = {\n ...withoutFigures(source),\n interactions: interactionsForGroup(\n (source['interactions'] ?? []) as ContactInteraction[],\n successor.hostIds,\n ),\n ...(successor.key === figuresTo ? figuresOf(source) : {}),\n }\n if (split.survives) copy[COPIED_BY_CHANGE_FIELD] = { [split.from]: changeId }\n copies.push({ key: successor.key, facet: copy })\n }\n if (split.survives) {\n if (figuresTo) next[split.from] = withoutFigures(current(split.from) ?? source)\n } else {\n next[split.from] = null\n const figures = figuresOf(source)\n if (!figuresTo && Object.keys(figures).some((field) => field !== 'lastEmailEngagementAtMs')) {\n figuresDropped += 1\n }\n }\n }\n\n const moves = new Map<string, ConsentGroupHolderMove[]>()\n for (const flow of plan.flows) {\n if (flow.kind !== 'move' || !facets[flow.from]) continue\n moves.set(flow.to, [...(moves.get(flow.to) ?? []), flow])\n }\n const metAt = (hostIds: readonly string[]) => {\n const indexes = hostIds.map((hostId) => captured.indexOf(hostId)).filter((index) => index >= 0)\n return indexes.length ? Math.min(...indexes) : Number.POSITIVE_INFINITY\n }\n for (const [target, sources] of moves) {\n const ordered = [...sources].sort(\n (a, b) => metAt(a.fromHostIds) - metAt(b.fromHostIds) || (a.from < b.from ? -1 : a.from > b.from ? 1 : 0),\n )\n let folded = current(target)\n // A person is COMBINED when two or more records become one; a lone\n // record under a new key is only moved.\n if (ordered.length + (folded ? 1 : 0) >= 2) combined += 1\n for (const source of ordered) {\n const facet = facets[source.from]\n folded = folded ? mergeContactFacet(folded, facet) : { ...facet }\n next[source.from] = null\n }\n if (folded) next[target] = folded\n }\n\n for (const { key, facet } of copies) {\n const existing = current(key)\n const merged = existing ? mergeContactFacet(existing, facet) : facet\n // The stamps are a union, never the survivor's alone: the merge keeps a\n // map the survivor already has, and a lost stamp would copy again.\n const stamps = {\n ...mapOf(existing?.[COPIED_BY_CHANGE_FIELD]),\n ...mapOf(facet[COPIED_BY_CHANGE_FIELD]),\n }\n if (Object.keys(stamps).length) merged[COPIED_BY_CHANGE_FIELD] = stamps\n next[key] = merged\n copied += 1\n }\n\n const keys = Object.keys(next)\n if (!keys.length) return null\n\n /*\n * The company mirror follows the facets: an id that no facet names any\n * more leaves it, and one a facet newly names joins it. Ids the mirror held\n * without a facet naming them — links older than the facets — are left.\n */\n const named = (source: Record<string, Doc | null | undefined>) =>\n new Set(\n Object.values(source)\n .map((facet) => (facet ? facet['companyId'] : null))\n .filter((id): id is string => typeof id === 'string' && id !== ''),\n )\n const after: Record<string, Doc | null> = { ...facets, ...next }\n const before = named(facets)\n const now = named(after)\n const mirror = Array.isArray(contact[CONTACT_COMPANY_IDS_FIELD])\n ? (contact[CONTACT_COMPANY_IDS_FIELD] as unknown[]).filter(\n (id): id is string => typeof id === 'string' && id !== '',\n )\n : []\n const companyCounts: HolderPatch['companyCounts'] = []\n const leaving = [...before].filter((id) => !now.has(id))\n const joining = [...now].filter((id) => !before.has(id))\n for (const id of leaving) if (mirror.includes(id)) companyCounts.push({ companyId: id, delta: -1 })\n for (const id of joining) if (!mirror.includes(id)) companyCounts.push({ companyId: id, delta: 1 })\n\n const patch: Doc = {}\n for (const key of keys) {\n patch[`${CONTACT_FACETS_FIELD}.${key}`] = next[key] === null ? FieldValue.delete() : next[key]\n }\n if (companyCounts.length) {\n patch[CONTACT_COMPANY_IDS_FIELD] = [\n ...mirror.filter((id) => !leaving.includes(id)),\n ...joining.filter((id) => !mirror.includes(id)),\n ]\n }\n patch['updatedAt'] = FieldValue.serverTimestamp()\n return { patch, combined, copied, figuresDropped, companyCounts }\n}\n\n/**\n * The refusal `from` holds, carried onto `to` — or `null` when `to` already\n * refuses or `from` does not.\n */\nexport function planRefusalCarry(\n contact: Doc,\n carry: { toHostId: string; fromHostId: string },\n changeId: string,\n): Doc | null {\n const byHost = mapOf(contact[MARKETING_CONSENT_BY_HOST_FIELD])\n const source = mapOf(byHost[carry.fromHostId])\n if (source[MARKETING_CONSENT_FIELD] !== false) return null\n const target = byHost[carry.toHostId]\n if (mapOf(target)[MARKETING_CONSENT_FIELD] === false) return null\n const entry: Doc = {\n [MARKETING_CONSENT_FIELD]: false,\n ...(source['marketingConsentAtMs'] !== undefined\n ? { marketingConsentAtMs: source['marketingConsentAtMs'] }\n : {}),\n ...(source[MARKETING_CONSENT_SOURCE_FIELD] !== undefined\n ? { [MARKETING_CONSENT_SOURCE_FIELD]: source[MARKETING_CONSENT_SOURCE_FIELD] }\n : {}),\n carriedFromHostId: carry.fromHostId,\n carriedByChangeId: changeId,\n ...(target && typeof target === 'object' ? { supersededEntry: target } : {}),\n }\n return {\n [`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.toHostId}`]: entry,\n updatedAt: FieldValue.serverTimestamp(),\n }\n}\n\n/*==========================================\n * THE PASSES\n *=========================================*/\n\ninterface Cursor {\n /** `refusals` walks the carries; `holders` walks every contact. */\n stage: 'refusals' | 'holders'\n /** The carry being walked, in `refusals`. */\n carry: number\n after: string | null\n}\n\nfunction readCursor(raw: string | null, first: Cursor['stage']): Cursor {\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Cursor\n if (parsed && (parsed.stage === 'refusals' || parsed.stage === 'holders')) return parsed\n } catch {\n // A cursor this participant did not write starts the phase over, which\n // every pass is safe to do.\n }\n }\n return { stage: first, carry: 0, after: null }\n}\n\nexport interface ConsentGroupParticipantOptions {\n firestore?: () => FirebaseFirestore.Firestore\n now?: () => number\n}\n\n/** One contact write, guarded by the read it was computed from. */\ninterface Pending {\n snapshot: FirebaseFirestore.QueryDocumentSnapshot | FirebaseFirestore.DocumentSnapshot\n patch: Doc\n after?: () => Promise<void>\n}\n\n/**\n * Writes a page's patches, each with a `lastUpdateTime` precondition, and\n * recomputes the ones that raced once from a fresh read. Answers how many\n * were written.\n */\nasync function writeGuarded(\n firestore: FirebaseFirestore.Firestore,\n pending: Pending[],\n recompute: (snapshot: FirebaseFirestore.DocumentSnapshot) => Pending | null,\n): Promise<number> {\n let written = 0\n let due = pending\n for (let round = 0; round < 2 && due.length; round += 1) {\n const writer = firestore.bulkWriter()\n const outcomes = due.map((entry) =>\n writer\n .update(entry.snapshot.ref, entry.patch, {\n lastUpdateTime: entry.snapshot.updateTime as FirebaseFirestore.Timestamp,\n })\n .then(\n () => ({ entry, error: null as unknown }),\n (error: unknown) => ({ entry, error: error ?? new Error('The write failed') }),\n ),\n )\n await writer.close()\n const raced: Pending[] = []\n for (const { entry, error } of await Promise.all(outcomes)) {\n if (error === null) {\n written += 1\n await entry.after?.()\n continue\n }\n const code = codeOf(error)\n if (code === undefined || !RACED.has(code)) throw error\n raced.push(entry)\n }\n if (!raced.length) return written\n const fresh = await firestore.getAll(...raced.map((entry) => entry.snapshot.ref))\n due = fresh\n .filter((snapshot) => snapshot.exists)\n .map(recompute)\n .filter((entry): entry is Pending => entry !== null)\n }\n if (due.length) {\n throw new Error(`[crm] ${due.length} contact(s) kept changing under the consent group change`)\n }\n return written\n}\n\n/**\n * The CRM's participant. `firestore` and `now` are injectable for the specs;\n * the defaults are the Admin SDK's and the wall clock.\n */\nexport function createConsentGroupParticipant(\n options: ConsentGroupParticipantOptions = {},\n): ConsentGroupChangeParticipant {\n const db = () => (options.firestore ?? (() => firebaseAdmin.app().firestore()))()\n const now = options.now ?? Date.now\n const contactsOf = (orgId: string) => db().collection('orgs').doc(orgId).collection('contacts')\n\n async function countOf(query: FirebaseFirestore.Query): Promise<number> {\n return Number((await query.count().get()).data().count) || 0\n }\n\n /** One page of one carry's refusals. */\n async function refusalPage(\n request: ConsentGroupChangeRunRequest,\n cursor: Cursor,\n counts: Record<string, number>,\n ): Promise<Cursor | null> {\n const carries = request.plan.carries\n if (cursor.carry >= carries.length) return null\n const carry = carries[cursor.carry]\n let query = contactsOf(request.orgId)\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.fromHostId}.${MARKETING_CONSENT_FIELD}`, '==', false)\n .orderBy(FieldPath.documentId())\n .limit(REFUSAL_PAGE)\n if (cursor.after) query = query.startAfter(cursor.after)\n const page = await query.get()\n const plan = (snapshot: FirebaseFirestore.DocumentSnapshot): Pending | null => {\n const patch = planRefusalCarry((snapshot.data() ?? {}) as Doc, carry, request.changeId)\n return patch ? { snapshot, patch } : null\n }\n const pending = page.docs.map(plan).filter((entry): entry is Pending => entry !== null)\n counts['refusals'] = (counts['refusals'] ?? 0) +\n (request.dryRun ? pending.length : await writeGuarded(db(), pending, plan))\n if (page.size < REFUSAL_PAGE) return { stage: 'refusals', carry: cursor.carry + 1, after: null }\n return { stage: 'refusals', carry: cursor.carry, after: page.docs[page.docs.length - 1].id }\n }\n\n /** One page of the holder pass over every contact. */\n async function holderPage(\n request: ConsentGroupChangeRunRequest,\n cursor: Cursor,\n counts: Record<string, number>,\n ): Promise<Cursor | null> {\n let query = contactsOf(request.orgId).orderBy(FieldPath.documentId()).limit(HOLDER_PAGE)\n if (cursor.after) query = query.startAfter(cursor.after)\n const page = await query.get()\n const companies = db().collection('orgs').doc(request.orgId).collection(CRM_COLLECTIONS.companies)\n const stragglersOnly = request.phase === 'sweep'\n const tally = { combined: 0, copied: 0, figuresDropped: 0 }\n const count = (planned: HolderPatch) => {\n tally.combined += planned.combined\n tally.copied += planned.copied\n tally.figuresDropped += planned.figuresDropped\n }\n const plan = (snapshot: FirebaseFirestore.DocumentSnapshot): (Pending & { planned: HolderPatch }) | null => {\n const planned = planHolderRecords((snapshot.data() ?? {}) as Doc, request.plan, request.changeId, {\n stragglersOnly,\n })\n if (!planned) return null\n return {\n snapshot,\n planned,\n patch: planned.patch,\n after: async () => {\n count(planned)\n // The holders moved, and with them the facet keys the Contacts\n // list filters by (AGL-3321).\n await restampCrmListFieldsAt(snapshot.ref, 'contacts')\n // A count that could not move is logged and left: the company's own\n // page takes the live aggregate, which corrects it.\n await Promise.all(\n planned.companyCounts.map((change) =>\n companies\n .doc(change.companyId)\n .update({ [COMPANY_CONTACTS_COUNT_FIELD]: FieldValue.increment(change.delta) })\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', change.companyId, error)\n }),\n ),\n )\n },\n }\n }\n const pending = page.docs\n .map(plan)\n .filter((entry): entry is Pending & { planned: HolderPatch } => entry !== null)\n if (request.dryRun) for (const entry of pending) count(entry.planned)\n else await writeGuarded(db(), pending, plan)\n counts['combined'] = (counts['combined'] ?? 0) + tally.combined\n counts['copied'] = (counts['copied'] ?? 0) + tally.copied\n counts['figuresDropped'] = (counts['figuresDropped'] ?? 0) + tally.figuresDropped\n if (page.size < HOLDER_PAGE) return null\n return { stage: 'holders', carry: 0, after: page.docs[page.docs.length - 1].id }\n }\n\n return {\n async run(request): Promise<ConsentGroupChangeRunResult> {\n const counts: Record<string, number> = {}\n const hasHolderFlows = request.plan.flows.length > 0\n // `carry` walks the refusals; `rehome` the holders; `sweep` both.\n let cursor: Cursor | null = readCursor(\n request.cursor,\n request.phase === 'rehome' ? 'holders' : 'refusals',\n )\n while (cursor && now() < request.deadlineMs) {\n if (cursor.stage === 'refusals') {\n const next: Cursor | null = await refusalPage(request, cursor, counts)\n cursor =\n next ??\n (request.phase === 'sweep' && hasHolderFlows\n ? { stage: 'holders', carry: 0, after: null }\n : null)\n continue\n }\n cursor = hasHolderFlows ? await holderPage(request, cursor, counts) : null\n }\n return { done: cursor === null, cursor: cursor ? JSON.stringify(cursor) : null, counts }\n },\n\n async preview({ orgId, plan }): Promise<ConsentGroupChangePreviewLine[]> {\n const contacts = contactsOf(orgId)\n const lines: ConsentGroupChangePreviewLine[] = []\n const moves = plan.flows.filter((flow): flow is ConsentGroupHolderMove => flow.kind === 'move')\n const splits = plan.flows.filter((flow): flow is ConsentGroupHolderSplit => flow.kind === 'split')\n const people = (count: number | null) => (count === 1 ? 'person' : 'people')\n const figure = (count: number | null) => (count === null ? 'some' : String(count))\n\n /*\n * `crm.combine`: the people whose records under two or more of the\n * folding keys become one. No aggregation can ask \"two or more\", so the\n * records under each source key are read — bounded, and `null` past\n * the bound — and put through the same fold the re-home runs.\n */\n if (moves.length) {\n const seen = new Map<string, Doc>()\n let bounded = true\n for (const move of moves) {\n const page = await contacts\n .where(`${CONTACT_FACETS_FIELD}.${move.from}`, '!=', null)\n .limit(PREVIEW_SCAN_LIMIT + 1)\n .get()\n if (page.size > PREVIEW_SCAN_LIMIT) bounded = false\n for (const doc of page.docs) seen.set(doc.id, (doc.data() ?? {}) as Doc)\n }\n const combine = bounded\n ? [...seen.values()].reduce(\n (total, contact) =>\n total + (planHolderRecords(contact, { flows: moves }, '')?.combined ?? 0),\n 0,\n )\n : null\n lines.push({\n id: 'crm.combine',\n text: `The CRM records these sites keep about the same person will be combined for ${figure(combine)} ${people(combine)}.`,\n count: combine,\n severity: 'info',\n })\n }\n\n if (splits.length) {\n /*\n * `crm.copy`: the people a separating site takes a copy for. When the\n * key survives, those are the people the separating sites met\n * themselves; when it dissolves, everybody it holds, since a record\n * nobody met goes to every successor.\n */\n let copy = 0\n for (const split of splits) {\n if (!split.survives) {\n copy += await countOf(contacts.where(`${CONTACT_FACETS_FIELD}.${split.from}`, '!=', null))\n continue\n }\n const leavers = split.successors.flatMap((successor) => successor.hostIds)\n for (let index = 0; index < leavers.length; index += 30) {\n copy += await countOf(\n contacts.where(CAPTURED_BY_HOST_FIELD, 'array-contains-any', leavers.slice(index, index + 30)),\n )\n }\n }\n lines.push({\n id: 'crm.copy',\n text: `The separating sites keep a copy of the CRM record for the ${copy} ${people(copy)} they met themselves.`,\n count: copy,\n severity: 'info',\n })\n\n /*\n * `crm.grants` and `crm.visibility`, per separating site: the people\n * who signed up to the group through it and still hold that grant,\n * and the people it can see without having met them.\n */\n let grants = 0\n let visible = 0\n for (const split of splits) {\n for (const hostId of split.successors.flatMap((successor) => successor.hostIds)) {\n grants += await countOf(\n contacts\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${CONSENT_GROUP_ID_FIELD}`, '==', split.from)\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${MARKETING_CONSENT_FIELD}`, '==', true),\n )\n const seen = await countOf(contacts.where('visibleTo', 'array-contains', `host:${hostId}`))\n const met = await countOf(contacts.where(CAPTURED_BY_HOST_FIELD, 'array-contains', hostId))\n visible += Math.max(0, seen - met)\n }\n }\n lines.push({\n id: 'crm.grants',\n text: `The separating sites can still email the ${grants} ${people(grants)} who signed up to the group while they were part of it.`,\n count: grants,\n severity: 'info',\n })\n lines.push({\n id: 'crm.visibility',\n text: `The separating sites will still see ${visible} ${people(visible)} they met only through the group's other sites, without their CRM details.`,\n count: visible,\n severity: 'info',\n })\n\n /*\n * `crm.figures`: on a dissolve, the people whose order totals cannot\n * follow one site and are dropped. Read like the combine — the people\n * with orders under the key, bounded — and decided by the same split.\n */\n const dissolving = splits.filter((split) => !split.survives)\n if (dissolving.length) {\n let figures: number | null = 0\n for (const split of dissolving) {\n const page = await contacts\n .where(`${CONTACT_FACETS_FIELD}.${split.from}.ordersCount`, '>', 0)\n .limit(PREVIEW_SCAN_LIMIT + 1)\n .get()\n if (page.size > PREVIEW_SCAN_LIMIT || figures === null) {\n figures = null\n continue\n }\n for (const doc of page.docs) {\n figures +=\n planHolderRecords((doc.data() ?? {}) as Doc, { flows: [split] }, '')?.figuresDropped ?? 0\n }\n }\n lines.push({\n id: 'crm.figures',\n text: `Order totals for ${figure(figures)} ${people(figures)} who bought on more than one of these sites can't be split and will be removed.`,\n count: figures,\n severity: 'warning',\n })\n }\n }\n\n /*\n * `crm.refusals`: the refusals C4 would write — each separating pair's\n * declined consents, less the ones the receiving site already holds.\n */\n if (plan.carries.length) {\n let refusals = 0\n for (const carry of plan.carries) {\n const declined = (hostId: string) =>\n `${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${MARKETING_CONSENT_FIELD}`\n const from = contacts.where(declined(carry.fromHostId), '==', false)\n refusals += Math.max(\n 0,\n (await countOf(from)) - (await countOf(from.where(declined(carry.toHostId), '==', false))),\n )\n }\n lines.push({\n id: 'crm.refusals',\n text: `${refusals} declined ${refusals === 1 ? 'consent' : 'consents'} will be copied to the sites that separate.`,\n count: refusals,\n severity: 'info',\n })\n }\n return lines\n },\n\n summarize: summarizeConsentGroupChange,\n }\n}\n\n/** The participant the CRM registers, on the Admin SDK and the wall clock. */\nexport const consentGroupParticipant = createConsentGroupParticipant()\n"],"names":["mergeContactFacet","CONTACT_FACETS_FIELD","interactionsForGroup","COMPANY_CONTACTS_COUNT_FIELD","CONTACT_COMPANY_IDS_FIELD","CRM_COLLECTIONS","CAPTURED_BY_HOST_FIELD","CONSENT_GROUP_ID_FIELD","MARKETING_CONSENT_BY_HOST_FIELD","MARKETING_CONSENT_FIELD","MARKETING_CONSENT_SOURCE_FIELD","firebaseAdmin","restampCrmListFieldsAt","FieldPath","FieldValue","summarizeConsentGroupChange","REFUSAL_PAGE","HOLDER_PAGE","PREVIEW_SCAN_LIMIT","HOLDER_FIGURE_FIELDS","COPIED_BY_CHANGE_FIELD","RACED","Set","codeOf","error","code","mapOf","value","Array","isArray","captureOrder","contact","raw","ordered","map","id","String","trim","filter","Boolean","legacy","includes","withoutFigures","facet","out","field","figuresOf","planHolderRecords","plan","changeId","options","facets","captured","next","current","key","undefined","combined","copied","figuresDropped","splits","flows","flow","kind","stragglersOnly","survives","copies","split","source","from","successors","some","successor","keyOf","hostId","stayHostIds","find","hostIds","eligible","length","landing","fromHostIds","figuresTo","s","copy","interactions","push","figures","Object","keys","moves","Map","set","to","get","metAt","indexes","indexOf","index","Math","min","Number","POSITIVE_INFINITY","target","sources","sort","a","b","folded","existing","merged","stamps","named","values","after","before","now","mirror","companyCounts","leaving","has","joining","companyId","delta","patch","delete","serverTimestamp","planRefusalCarry","carry","byHost","fromHostId","toHostId","entry","marketingConsentAtMs","carriedFromHostId","carriedByChangeId","supersededEntry","updatedAt","readCursor","first","parsed","JSON","parse","stage","writeGuarded","firestore","pending","recompute","written","due","round","writer","bulkWriter","outcomes","update","snapshot","ref","lastUpdateTime","updateTime","then","Error","close","raced","Promise","all","fresh","getAll","exists","createConsentGroupParticipant","db","app","Date","contactsOf","orgId","collection","doc","countOf","query","count","data","refusalPage","request","cursor","counts","carries","where","orderBy","documentId","limit","startAfter","page","docs","dryRun","size","holderPage","companies","phase","tally","planned","change","increment","catch","console","run","hasHolderFlows","deadlineMs","done","stringify","preview","contacts","lines","people","figure","seen","bounded","move","combine","reduce","total","text","severity","leavers","flatMap","slice","grants","visible","met","max","dissolving","refusals","declined","summarize","consentGroupParticipant"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/consent-group-participant.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * THE CRM'S SHARE OF A CONSENT GROUP CHANGE (AGL-3320).\n *\n * Two things the CRM keeps are read through the CURRENT consent group, so a\n * change to the declaration moves under them unless they move too:\n *\n * - **C4 — a contact's per-site refusal.** `marketingConsentByHost.{site}`\n * holding `marketingConsent: false` refuses mail from every site of that\n * site's group (`readMarketingBasis`). When two sites stop being one\n * sender, each keeps the refusals the other held: the refusal is written\n * onto the receiving site's entry by dotted path, with the one it\n * replaced kept as `supersededEntry`. A refusal is carried whenever it was\n * given — over-carrying is the safe direction — and one the receiving site\n * already holds is left alone. Only contacts carry per-site refusals;\n * leads and list memberships carry grants alone.\n * - **The holder records**, `facets.{groupId}` — a profile, an owner, a\n * stage, notes, order figures and a timeline, keyed by the group that\n * keeps them. Every writer keys by the declaration in force, so after the\n * flip a site's records must be under its new key:\n * - a MOVE folds every old key's facet into the new key's, with the\n * existing target first and then the sources in the order the person\n * met their sites, through `mergeContactFacet` — the survivor's\n * values win, lists union, figures sum — and deletes each source in\n * the same write, so a figure is summed exactly once;\n * - a SPLIT copies the facet, without its figures and with only the\n * timeline entries of the successor's own sites, to each successor\n * that met the person itself, merged under whatever the successor\n * already holds. The figures follow the capture only when every site\n * that met the person lands under one key; otherwise they stay with the\n * surviving key, or — when the key dissolves — are dropped and counted.\n *\n * ## Run twice, write once\n *\n * The executor runs `carry` before the flip and again as the catch-up, and\n * the sweep runs everything once more. A carried refusal is skipped once the\n * receiving entry refuses; a MOVE or a dissolving SPLIT deletes its source in\n * the write that folds it, so a second pass finds nothing — except a\n * straggler a long job wrote under the old key after the first pass, which is\n * exactly what the sweep exists to fold in. A SPLIT whose key survives stays\n * a live key, so each copy is stamped with the change that made it\n * (`copiedByChange`), and a pass that finds the stamp does not copy again.\n */\n\nimport { mergeContactFacet } from '../model/contact-merge'\nimport type {\n ConsentGroupChangePlan,\n ConsentGroupChangePreviewLine,\n ConsentGroupHolderMove,\n ConsentGroupHolderSplit,\n} from '@aglyn/aglyn/app-utils/consent-group-change'\nimport {\n CONTACT_FACETS_FIELD,\n type ContactInteraction,\n interactionsForGroup,\n} from '@aglyn/aglyn/app-utils/contacts'\nimport {\n COMPANY_CONTACTS_COUNT_FIELD,\n CONTACT_COMPANY_IDS_FIELD,\n CRM_COLLECTIONS,\n} from '@aglyn/aglyn/app-utils/crm'\nimport {\n CAPTURED_BY_HOST_FIELD,\n CONSENT_GROUP_ID_FIELD,\n MARKETING_CONSENT_BY_HOST_FIELD,\n MARKETING_CONSENT_FIELD,\n MARKETING_CONSENT_SOURCE_FIELD,\n} from '@aglyn/aglyn/app-utils/marketing-consent'\nimport type {\n ConsentGroupChangeParticipant,\n ConsentGroupChangeRunRequest,\n ConsentGroupChangeRunResult,\n} from '@aglyn/aglyn/plugin-manager/plugin-consent-group-change'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { restampCrmListFieldsAt } from '@aglyn/tenant-data-admin/server/crm-records'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport { summarizeConsentGroupChange } from '../model/consent-group-summary'\n\n/** Contacts one page of the refusal carry reads. */\nexport const REFUSAL_PAGE = 300\n\n/** Contacts one page of the holder pass reads. */\nexport const HOLDER_PAGE = 200\n\n/**\n * The most records a preview reads to answer a count no aggregation can:\n * past it, the line says so with a `null` count rather than guess.\n */\nexport const PREVIEW_SCAN_LIMIT = 5_000\n\n/**\n * The figures a facet sums, and the instants that go with them — what\n * `mergeContactFacet` adds up or takes the earliest or latest of. A split\n * cannot divide one person's purchases between two holders, so a copy\n * leaves them behind.\n */\nexport const HOLDER_FIGURE_FIELDS = [\n 'ltvCents',\n 'ordersCount',\n 'refundedCents',\n 'refundedOrdersCount',\n 'firstPurchaseAtMs',\n 'lastPurchaseAtMs',\n 'lastRefundAtMs',\n 'lastEmailEngagementAtMs',\n] as const\n\n/** The stamp a split's copy carries: `{ [sourceKey]: changeId }`. */\nexport const COPIED_BY_CHANGE_FIELD = 'copiedByChange'\n\ntype Doc = Record<string, unknown>\n\n/** The error codes that mean the contact was written since it was read. */\nconst RACED = new Set([5, 9])\nconst codeOf = (error: unknown): number | undefined => (error as { code?: number } | null)?.code\n\nfunction mapOf(value: unknown): Doc {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Doc) : {}\n}\n\n/** The sites that met this person, in the order they did — then the legacy `hostId`. */\nfunction captureOrder(contact: Doc): string[] {\n const raw = contact[CAPTURED_BY_HOST_FIELD]\n const ordered = Array.isArray(raw)\n ? raw.map((id) => String(id ?? '').trim()).filter(Boolean)\n : []\n const legacy = typeof contact['hostId'] === 'string' ? (contact['hostId'] as string) : ''\n return legacy && !ordered.includes(legacy) ? [...ordered, legacy] : ordered\n}\n\nfunction withoutFigures(facet: Doc): Doc {\n const out: Doc = { ...facet }\n for (const field of HOLDER_FIGURE_FIELDS) delete out[field]\n return out\n}\n\nfunction figuresOf(facet: Doc): Doc {\n const out: Doc = {}\n for (const field of HOLDER_FIGURE_FIELDS) {\n if (field in facet) out[field] = facet[field]\n }\n return out\n}\n\n/** What one contact's holder records become, as a dotted `update()` patch. */\nexport interface HolderPatch {\n patch: Doc\n combined: number\n copied: number\n figuresDropped: number\n /** Each company whose contacts count moves, by how much. */\n companyCounts: Array<{ companyId: string; delta: 1 | -1 }>\n}\n\n/**\n * The holder records of one contact after the plan's flows — or `null` when\n * nothing moves.\n *\n * `stragglersOnly` is the sweep: only the flows that delete their source\n * (MOVE, and a SPLIT whose key dissolves) run, because only those can have\n * left anything behind that a later writer recreated. A key that survived a\n * split is a live key after the flip, and its new writes belong to it.\n */\nexport function planHolderRecords(\n contact: Doc,\n plan: Pick<ConsentGroupChangePlan, 'flows'>,\n changeId: string,\n options: { stragglersOnly?: boolean } = {},\n): HolderPatch | null {\n const facets = mapOf(contact[CONTACT_FACETS_FIELD]) as Record<string, Doc>\n const captured = captureOrder(contact)\n const next: Record<string, Doc | null> = {}\n const current = (key: string): Doc | undefined =>\n key in next ? (next[key] ?? undefined) : facets[key]\n let combined = 0\n let copied = 0\n let figuresDropped = 0\n\n const splits = plan.flows.filter(\n (flow): flow is ConsentGroupHolderSplit =>\n flow.kind === 'split' && !(options.stragglersOnly && flow.survives),\n )\n const copies: Array<{ key: string; facet: Doc }> = []\n for (const split of splits) {\n const source = facets[split.from]\n if (!source || typeof source !== 'object') continue\n if (\n split.survives &&\n split.successors.some(\n (successor) => mapOf(facets[successor.key]?.[COPIED_BY_CHANGE_FIELD])[split.from] === changeId,\n )\n ) {\n continue\n }\n const keyOf = (hostId: string) =>\n split.stayHostIds.includes(hostId)\n ? split.from\n : (split.successors.find((successor) => successor.hostIds.includes(hostId))?.key ?? null)\n let eligible = split.successors.filter((successor) =>\n successor.hostIds.some((hostId) => captured.includes(hostId)),\n )\n if (!split.survives && !eligible.length) eligible = split.successors\n const landing = [\n ...new Set(\n captured\n .filter((hostId) => split.fromHostIds.includes(hostId))\n .map(keyOf)\n .filter((key): key is string => key !== null),\n ),\n ]\n const figuresTo =\n landing.length === 1 && landing[0] !== split.from && eligible.some((s) => s.key === landing[0])\n ? landing[0]\n : null\n for (const successor of eligible) {\n const copy: Doc = {\n ...withoutFigures(source),\n interactions: interactionsForGroup(\n (source['interactions'] ?? []) as ContactInteraction[],\n successor.hostIds,\n ),\n ...(successor.key === figuresTo ? figuresOf(source) : {}),\n }\n if (split.survives) copy[COPIED_BY_CHANGE_FIELD] = { [split.from]: changeId }\n copies.push({ key: successor.key, facet: copy })\n }\n if (split.survives) {\n if (figuresTo) next[split.from] = withoutFigures(current(split.from) ?? source)\n } else {\n next[split.from] = null\n const figures = figuresOf(source)\n if (!figuresTo && Object.keys(figures).some((field) => field !== 'lastEmailEngagementAtMs')) {\n figuresDropped += 1\n }\n }\n }\n\n const moves = new Map<string, ConsentGroupHolderMove[]>()\n for (const flow of plan.flows) {\n if (flow.kind !== 'move' || !facets[flow.from]) continue\n moves.set(flow.to, [...(moves.get(flow.to) ?? []), flow])\n }\n const metAt = (hostIds: readonly string[]) => {\n const indexes = hostIds.map((hostId) => captured.indexOf(hostId)).filter((index) => index >= 0)\n return indexes.length ? Math.min(...indexes) : Number.POSITIVE_INFINITY\n }\n for (const [target, sources] of moves) {\n const ordered = [...sources].sort(\n (a, b) => metAt(a.fromHostIds) - metAt(b.fromHostIds) || (a.from < b.from ? -1 : a.from > b.from ? 1 : 0),\n )\n let folded = current(target)\n // A person is COMBINED when two or more records become one; a lone\n // record under a new key is only moved.\n if (ordered.length + (folded ? 1 : 0) >= 2) combined += 1\n for (const source of ordered) {\n const facet = facets[source.from]\n folded = folded ? mergeContactFacet(folded, facet) : { ...facet }\n next[source.from] = null\n }\n if (folded) next[target] = folded\n }\n\n for (const { key, facet } of copies) {\n const existing = current(key)\n const merged = existing ? mergeContactFacet(existing, facet) : facet\n // The stamps are a union, never the survivor's alone: the merge keeps a\n // map the survivor already has, and a lost stamp would copy again.\n const stamps = {\n ...mapOf(existing?.[COPIED_BY_CHANGE_FIELD]),\n ...mapOf(facet[COPIED_BY_CHANGE_FIELD]),\n }\n if (Object.keys(stamps).length) merged[COPIED_BY_CHANGE_FIELD] = stamps\n next[key] = merged\n copied += 1\n }\n\n const keys = Object.keys(next)\n if (!keys.length) return null\n\n /*\n * The company mirror follows the facets: an id that no facet names any\n * more leaves it, and one a facet newly names joins it. Ids the mirror held\n * without a facet naming them — links older than the facets — are left.\n */\n const named = (source: Record<string, Doc | null | undefined>) =>\n new Set(\n Object.values(source)\n .map((facet) => (facet ? facet['companyId'] : null))\n .filter((id): id is string => typeof id === 'string' && id !== ''),\n )\n const after: Record<string, Doc | null> = { ...facets, ...next }\n const before = named(facets)\n const now = named(after)\n const mirror = Array.isArray(contact[CONTACT_COMPANY_IDS_FIELD])\n ? (contact[CONTACT_COMPANY_IDS_FIELD] as unknown[]).filter(\n (id): id is string => typeof id === 'string' && id !== '',\n )\n : []\n const companyCounts: HolderPatch['companyCounts'] = []\n const leaving = [...before].filter((id) => !now.has(id))\n const joining = [...now].filter((id) => !before.has(id))\n for (const id of leaving) if (mirror.includes(id)) companyCounts.push({ companyId: id, delta: -1 })\n for (const id of joining) if (!mirror.includes(id)) companyCounts.push({ companyId: id, delta: 1 })\n\n const patch: Doc = {}\n for (const key of keys) {\n patch[`${CONTACT_FACETS_FIELD}.${key}`] = next[key] === null ? FieldValue.delete() : next[key]\n }\n if (companyCounts.length) {\n patch[CONTACT_COMPANY_IDS_FIELD] = [\n ...mirror.filter((id) => !leaving.includes(id)),\n ...joining.filter((id) => !mirror.includes(id)),\n ]\n }\n patch['updatedAt'] = FieldValue.serverTimestamp()\n return { patch, combined, copied, figuresDropped, companyCounts }\n}\n\n/**\n * The refusal `from` holds, carried onto `to` — or `null` when `to` already\n * refuses or `from` does not.\n */\nexport function planRefusalCarry(\n contact: Doc,\n carry: { toHostId: string; fromHostId: string },\n changeId: string,\n): Doc | null {\n const byHost = mapOf(contact[MARKETING_CONSENT_BY_HOST_FIELD])\n const source = mapOf(byHost[carry.fromHostId])\n if (source[MARKETING_CONSENT_FIELD] !== false) return null\n const target = byHost[carry.toHostId]\n if (mapOf(target)[MARKETING_CONSENT_FIELD] === false) return null\n const entry: Doc = {\n [MARKETING_CONSENT_FIELD]: false,\n ...(source['marketingConsentAtMs'] !== undefined\n ? { marketingConsentAtMs: source['marketingConsentAtMs'] }\n : {}),\n ...(source[MARKETING_CONSENT_SOURCE_FIELD] !== undefined\n ? { [MARKETING_CONSENT_SOURCE_FIELD]: source[MARKETING_CONSENT_SOURCE_FIELD] }\n : {}),\n carriedFromHostId: carry.fromHostId,\n carriedByChangeId: changeId,\n ...(target && typeof target === 'object' ? { supersededEntry: target } : {}),\n }\n return {\n [`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.toHostId}`]: entry,\n updatedAt: FieldValue.serverTimestamp(),\n }\n}\n\n/*==========================================\n * THE PASSES\n *=========================================*/\n\ninterface Cursor {\n /** `refusals` walks the carries; `holders` walks every contact. */\n stage: 'refusals' | 'holders'\n /** The carry being walked, in `refusals`. */\n carry: number\n after: string | null\n}\n\nfunction readCursor(raw: string | null, first: Cursor['stage']): Cursor {\n if (raw) {\n try {\n const parsed = JSON.parse(raw) as Cursor\n if (parsed && (parsed.stage === 'refusals' || parsed.stage === 'holders')) return parsed\n } catch {\n // A cursor this participant did not write starts the phase over, which\n // every pass is safe to do.\n }\n }\n return { stage: first, carry: 0, after: null }\n}\n\nexport interface ConsentGroupParticipantOptions {\n firestore?: () => FirebaseFirestore.Firestore\n now?: () => number\n}\n\n/** One contact write, guarded by the read it was computed from. */\ninterface Pending {\n snapshot: FirebaseFirestore.QueryDocumentSnapshot | FirebaseFirestore.DocumentSnapshot\n patch: Doc\n after?: () => Promise<void>\n}\n\n/**\n * Writes a page's patches, each with a `lastUpdateTime` precondition, and\n * recomputes the ones that raced once from a fresh read. Answers how many\n * were written.\n */\nasync function writeGuarded(\n firestore: FirebaseFirestore.Firestore,\n pending: Pending[],\n recompute: (snapshot: FirebaseFirestore.DocumentSnapshot) => Pending | null,\n): Promise<number> {\n let written = 0\n let due = pending\n for (let round = 0; round < 2 && due.length; round += 1) {\n const writer = firestore.bulkWriter()\n const outcomes = due.map((entry) =>\n writer\n .update(entry.snapshot.ref, entry.patch, {\n lastUpdateTime: entry.snapshot.updateTime as FirebaseFirestore.Timestamp,\n })\n .then(\n () => ({ entry, error: null as unknown }),\n (error: unknown) => ({ entry, error: error ?? new Error('The write failed') }),\n ),\n )\n await writer.close()\n const raced: Pending[] = []\n for (const { entry, error } of await Promise.all(outcomes)) {\n if (error === null) {\n written += 1\n await entry.after?.()\n continue\n }\n const code = codeOf(error)\n if (code === undefined || !RACED.has(code)) throw error\n raced.push(entry)\n }\n if (!raced.length) return written\n const fresh = await firestore.getAll(...raced.map((entry) => entry.snapshot.ref))\n due = fresh\n .filter((snapshot) => snapshot.exists)\n .map(recompute)\n .filter((entry): entry is Pending => entry !== null)\n }\n if (due.length) {\n throw new Error(`[crm] ${due.length} contact(s) kept changing under the consent group change`)\n }\n return written\n}\n\n/**\n * The CRM's participant. `firestore` and `now` are injectable for the specs;\n * the defaults are the Admin SDK's and the wall clock.\n */\nexport function createConsentGroupParticipant(\n options: ConsentGroupParticipantOptions = {},\n): ConsentGroupChangeParticipant {\n const db = () => (options.firestore ?? (() => firebaseAdmin.app().firestore()))()\n const now = options.now ?? Date.now\n const contactsOf = (orgId: string) => db().collection('orgs').doc(orgId).collection('contacts')\n\n async function countOf(query: FirebaseFirestore.Query): Promise<number> {\n return Number((await query.count().get()).data().count) || 0\n }\n\n /** One page of one carry's refusals. */\n async function refusalPage(\n request: ConsentGroupChangeRunRequest,\n cursor: Cursor,\n counts: Record<string, number>,\n ): Promise<Cursor | null> {\n const carries = request.plan.carries\n if (cursor.carry >= carries.length) return null\n const carry = carries[cursor.carry]\n let query = contactsOf(request.orgId)\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${carry.fromHostId}.${MARKETING_CONSENT_FIELD}`, '==', false)\n .orderBy(FieldPath.documentId())\n .limit(REFUSAL_PAGE)\n if (cursor.after) query = query.startAfter(cursor.after)\n const page = await query.get()\n const plan = (snapshot: FirebaseFirestore.DocumentSnapshot): Pending | null => {\n const patch = planRefusalCarry((snapshot.data() ?? {}) as Doc, carry, request.changeId)\n return patch ? { snapshot, patch } : null\n }\n const pending = page.docs.map(plan).filter((entry): entry is Pending => entry !== null)\n counts['refusals'] = (counts['refusals'] ?? 0) +\n (request.dryRun ? pending.length : await writeGuarded(db(), pending, plan))\n if (page.size < REFUSAL_PAGE) return { stage: 'refusals', carry: cursor.carry + 1, after: null }\n return { stage: 'refusals', carry: cursor.carry, after: page.docs[page.docs.length - 1].id }\n }\n\n /** One page of the holder pass over every contact. */\n async function holderPage(\n request: ConsentGroupChangeRunRequest,\n cursor: Cursor,\n counts: Record<string, number>,\n ): Promise<Cursor | null> {\n let query = contactsOf(request.orgId).orderBy(FieldPath.documentId()).limit(HOLDER_PAGE)\n if (cursor.after) query = query.startAfter(cursor.after)\n const page = await query.get()\n const companies = db().collection('orgs').doc(request.orgId).collection(CRM_COLLECTIONS.companies)\n const stragglersOnly = request.phase === 'sweep'\n const tally = { combined: 0, copied: 0, figuresDropped: 0 }\n const count = (planned: HolderPatch) => {\n tally.combined += planned.combined\n tally.copied += planned.copied\n tally.figuresDropped += planned.figuresDropped\n }\n const plan = (snapshot: FirebaseFirestore.DocumentSnapshot): (Pending & { planned: HolderPatch }) | null => {\n const planned = planHolderRecords((snapshot.data() ?? {}) as Doc, request.plan, request.changeId, {\n stragglersOnly,\n })\n if (!planned) return null\n return {\n snapshot,\n planned,\n patch: planned.patch,\n after: async () => {\n count(planned)\n // The holders moved, and with them the facet keys the Contacts\n // list filters by (AGL-3321).\n await restampCrmListFieldsAt(snapshot.ref, 'contacts')\n // A count that could not move is logged and left: the company's own\n // page takes the live aggregate, which corrects it.\n await Promise.all(\n planned.companyCounts.map((change) =>\n companies\n .doc(change.companyId)\n .update({ [COMPANY_CONTACTS_COUNT_FIELD]: FieldValue.increment(change.delta) })\n .catch((error: unknown) => {\n console.error('[crm] company contacts count could not move', change.companyId, error)\n }),\n ),\n )\n },\n }\n }\n const pending = page.docs\n .map(plan)\n .filter((entry): entry is Pending & { planned: HolderPatch } => entry !== null)\n if (request.dryRun) for (const entry of pending) count(entry.planned)\n else await writeGuarded(db(), pending, plan)\n counts['combined'] = (counts['combined'] ?? 0) + tally.combined\n counts['copied'] = (counts['copied'] ?? 0) + tally.copied\n counts['figuresDropped'] = (counts['figuresDropped'] ?? 0) + tally.figuresDropped\n if (page.size < HOLDER_PAGE) return null\n return { stage: 'holders', carry: 0, after: page.docs[page.docs.length - 1].id }\n }\n\n return {\n async run(request): Promise<ConsentGroupChangeRunResult> {\n const counts: Record<string, number> = {}\n const hasHolderFlows = request.plan.flows.length > 0\n // `carry` walks the refusals; `rehome` the holders; `sweep` both.\n let cursor: Cursor | null = readCursor(\n request.cursor,\n request.phase === 'rehome' ? 'holders' : 'refusals',\n )\n while (cursor && now() < request.deadlineMs) {\n if (cursor.stage === 'refusals') {\n const next: Cursor | null = await refusalPage(request, cursor, counts)\n cursor =\n next ??\n (request.phase === 'sweep' && hasHolderFlows\n ? { stage: 'holders', carry: 0, after: null }\n : null)\n continue\n }\n cursor = hasHolderFlows ? await holderPage(request, cursor, counts) : null\n }\n return { done: cursor === null, cursor: cursor ? JSON.stringify(cursor) : null, counts }\n },\n\n async preview({ orgId, plan }): Promise<ConsentGroupChangePreviewLine[]> {\n const contacts = contactsOf(orgId)\n const lines: ConsentGroupChangePreviewLine[] = []\n const moves = plan.flows.filter((flow): flow is ConsentGroupHolderMove => flow.kind === 'move')\n const splits = plan.flows.filter((flow): flow is ConsentGroupHolderSplit => flow.kind === 'split')\n const people = (count: number | null) => (count === 1 ? 'person' : 'people')\n const figure = (count: number | null) => (count === null ? 'some' : String(count))\n\n /*\n * `crm.combine`: the people whose records under two or more of the\n * folding keys become one. No aggregation can ask \"two or more\", so the\n * records under each source key are read — bounded, and `null` past\n * the bound — and put through the same fold the re-home runs.\n */\n if (moves.length) {\n const seen = new Map<string, Doc>()\n let bounded = true\n for (const move of moves) {\n const page = await contacts\n .where(`${CONTACT_FACETS_FIELD}.${move.from}`, '!=', null)\n .limit(PREVIEW_SCAN_LIMIT + 1)\n .get()\n if (page.size > PREVIEW_SCAN_LIMIT) bounded = false\n for (const doc of page.docs) seen.set(doc.id, (doc.data() ?? {}) as Doc)\n }\n const combine = bounded\n ? [...seen.values()].reduce(\n (total, contact) =>\n total + (planHolderRecords(contact, { flows: moves }, '')?.combined ?? 0),\n 0,\n )\n : null\n lines.push({\n id: 'crm.combine',\n text: `The CRM records these sites keep about the same person will be combined for ${figure(combine)} ${people(combine)}.`,\n count: combine,\n severity: 'info',\n })\n }\n\n if (splits.length) {\n /*\n * `crm.copy`: the people a separating site takes a copy for. When the\n * key survives, those are the people the separating sites met\n * themselves; when it dissolves, everybody it holds, since a record\n * nobody met goes to every successor.\n */\n let copy = 0\n for (const split of splits) {\n if (!split.survives) {\n copy += await countOf(contacts.where(`${CONTACT_FACETS_FIELD}.${split.from}`, '!=', null))\n continue\n }\n const leavers = split.successors.flatMap((successor) => successor.hostIds)\n for (let index = 0; index < leavers.length; index += 30) {\n copy += await countOf(\n contacts.where(CAPTURED_BY_HOST_FIELD, 'array-contains-any', leavers.slice(index, index + 30)),\n )\n }\n }\n lines.push({\n id: 'crm.copy',\n text: `The separating sites keep a copy of the CRM record for the ${copy} ${people(copy)} they met themselves.`,\n count: copy,\n severity: 'info',\n })\n\n /*\n * `crm.grants` and `crm.visibility`, per separating site: the people\n * who signed up to the group through it and still hold that grant,\n * and the people it can see without having met them.\n */\n let grants = 0\n let visible = 0\n for (const split of splits) {\n for (const hostId of split.successors.flatMap((successor) => successor.hostIds)) {\n grants += await countOf(\n contacts\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${CONSENT_GROUP_ID_FIELD}`, '==', split.from)\n .where(`${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${MARKETING_CONSENT_FIELD}`, '==', true),\n )\n const seen = await countOf(contacts.where('visibleTo', 'array-contains', `host:${hostId}`))\n const met = await countOf(contacts.where(CAPTURED_BY_HOST_FIELD, 'array-contains', hostId))\n visible += Math.max(0, seen - met)\n }\n }\n lines.push({\n id: 'crm.grants',\n text: `The separating sites can still email the ${grants} ${people(grants)} who signed up to the group while they were part of it.`,\n count: grants,\n severity: 'info',\n })\n lines.push({\n id: 'crm.visibility',\n text: `The separating sites will still see ${visible} ${people(visible)} they met only through the group's other sites, without their CRM details.`,\n count: visible,\n severity: 'info',\n })\n\n /*\n * `crm.figures`: on a dissolve, the people whose order totals cannot\n * follow one site and are dropped. Read like the combine — the people\n * with orders under the key, bounded — and decided by the same split.\n */\n const dissolving = splits.filter((split) => !split.survives)\n if (dissolving.length) {\n let figures: number | null = 0\n for (const split of dissolving) {\n const page = await contacts\n .where(`${CONTACT_FACETS_FIELD}.${split.from}.ordersCount`, '>', 0)\n .limit(PREVIEW_SCAN_LIMIT + 1)\n .get()\n if (page.size > PREVIEW_SCAN_LIMIT || figures === null) {\n figures = null\n continue\n }\n for (const doc of page.docs) {\n figures +=\n planHolderRecords((doc.data() ?? {}) as Doc, { flows: [split] }, '')?.figuresDropped ?? 0\n }\n }\n lines.push({\n id: 'crm.figures',\n text: `Order totals for ${figure(figures)} ${people(figures)} who bought on more than one of these sites can't be split and will be removed.`,\n count: figures,\n severity: 'warning',\n })\n }\n }\n\n /*\n * `crm.refusals`: the refusals C4 would write — each separating pair's\n * declined consents, less the ones the receiving site already holds.\n */\n if (plan.carries.length) {\n let refusals = 0\n for (const carry of plan.carries) {\n const declined = (hostId: string) =>\n `${MARKETING_CONSENT_BY_HOST_FIELD}.${hostId}.${MARKETING_CONSENT_FIELD}`\n const from = contacts.where(declined(carry.fromHostId), '==', false)\n refusals += Math.max(\n 0,\n (await countOf(from)) - (await countOf(from.where(declined(carry.toHostId), '==', false))),\n )\n }\n lines.push({\n id: 'crm.refusals',\n text: `${refusals} declined ${refusals === 1 ? 'consent' : 'consents'} will be copied to the sites that separate.`,\n count: refusals,\n severity: 'info',\n })\n }\n return lines\n },\n\n summarize: summarizeConsentGroupChange,\n }\n}\n\n/** The participant the CRM registers, on the Admin SDK and the wall clock. */\nexport const consentGroupParticipant = createConsentGroupParticipant()\n"],"names":["mergeContactFacet","CONTACT_FACETS_FIELD","interactionsForGroup","COMPANY_CONTACTS_COUNT_FIELD","CONTACT_COMPANY_IDS_FIELD","CRM_COLLECTIONS","CAPTURED_BY_HOST_FIELD","CONSENT_GROUP_ID_FIELD","MARKETING_CONSENT_BY_HOST_FIELD","MARKETING_CONSENT_FIELD","MARKETING_CONSENT_SOURCE_FIELD","firebaseAdmin","restampCrmListFieldsAt","FieldPath","FieldValue","summarizeConsentGroupChange","REFUSAL_PAGE","HOLDER_PAGE","PREVIEW_SCAN_LIMIT","HOLDER_FIGURE_FIELDS","COPIED_BY_CHANGE_FIELD","RACED","Set","codeOf","error","code","mapOf","value","Array","isArray","captureOrder","contact","raw","ordered","map","id","String","trim","filter","Boolean","legacy","includes","withoutFigures","facet","out","field","figuresOf","planHolderRecords","plan","changeId","options","facets","captured","next","current","key","undefined","combined","copied","figuresDropped","splits","flows","flow","kind","stragglersOnly","survives","copies","split","source","from","successors","some","successor","keyOf","hostId","stayHostIds","find","hostIds","eligible","length","landing","fromHostIds","figuresTo","s","copy","interactions","push","figures","Object","keys","moves","Map","set","to","get","metAt","indexes","indexOf","index","Math","min","Number","POSITIVE_INFINITY","target","sources","sort","a","b","folded","existing","merged","stamps","named","values","after","before","now","mirror","companyCounts","leaving","has","joining","companyId","delta","patch","delete","serverTimestamp","planRefusalCarry","carry","byHost","fromHostId","toHostId","entry","marketingConsentAtMs","carriedFromHostId","carriedByChangeId","supersededEntry","updatedAt","readCursor","first","parsed","JSON","parse","stage","writeGuarded","firestore","pending","recompute","written","due","round","writer","bulkWriter","outcomes","update","snapshot","ref","lastUpdateTime","updateTime","then","Error","close","raced","Promise","all","fresh","getAll","exists","createConsentGroupParticipant","db","app","Date","contactsOf","orgId","collection","doc","countOf","query","count","data","refusalPage","request","cursor","counts","carries","where","orderBy","documentId","limit","startAfter","page","docs","dryRun","size","holderPage","companies","phase","tally","planned","change","increment","catch","console","run","hasHolderFlows","deadlineMs","done","stringify","preview","contacts","lines","people","figure","seen","bounded","move","combine","reduce","total","text","severity","leavers","flatMap","slice","grants","visible","met","max","dissolving","refusals","declined","summarize","consentGroupParticipant"],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * A person's lifecycle stage, FLOORED at what just happened to them
19
+ * (AGL-2641).
20
+ *
21
+ * The capture doors floor a stage inside `upsertHostContact`: an order
22
+ * names `customer` as its `initialLifecycleStage`, and the door fills an
23
+ * empty stage or advances an earlier one and never moves anybody back. A
24
+ * won deal is the same fact as a purchase — the business has decided this
25
+ * person bought — but it arrives at a record that already exists, through a
26
+ * writer that is not a capture door: the deal-stage route and the REST
27
+ * deals resource. This is the door's rule for them, over a contact document
28
+ * they already hold a reference to.
29
+ *
30
+ * The write goes into ONE holder's facet, because a stage is one business's
31
+ * reading of a person and the row is shared by every site in the org. The
32
+ * holder is the site the deal was made on, resolved to its consent group
33
+ * the way every other facet writer resolves it; a deal that names no site
34
+ * falls back to the site that captured the person, which is the one
35
+ * holder a contact always has. A contact that names none either is left
36
+ * alone and the caller told, rather than written into a facet nobody reads.
37
+ *
38
+ * The write is skipped when it would change nothing — a customer stays a
39
+ * customer, an evangelist stays an evangelist, `other` is never overwritten
40
+ * — so a caller can announce `contactStageChanged` exactly when `advanced`
41
+ * says the stage moved, and an automation listening for the change never
42
+ * hears one that did not happen.
43
+ */
44
+ import { type ContactLifecycleStage } from '@aglyn/aglyn/server';
45
+ export type ContactLifecycleFloor = {
46
+ /** The stage was raised to the floor. */
47
+ outcome: 'advanced';
48
+ contactId: string;
49
+ /** The address on the row, for the event a caller announces. */
50
+ email: string;
51
+ /** The site whose facet was written. */
52
+ hostId: string;
53
+ groupId: string;
54
+ /** The stage before the write, or `''` for a person who had none. */
55
+ previousStage: ContactLifecycleStage | '';
56
+ lifecycleStage: ContactLifecycleStage;
57
+ } | {
58
+ /** The person already held the floor or a later stage; nothing was written. */
59
+ outcome: 'held';
60
+ contactId: string;
61
+ email: string;
62
+ hostId: string;
63
+ groupId: string;
64
+ lifecycleStage: ContactLifecycleStage;
65
+ } | {
66
+ /** No such contact. */
67
+ outcome: 'missing';
68
+ contactId: string;
69
+ } | {
70
+ /** Neither the caller nor the contact names a site, so there is no facet to write. */
71
+ outcome: 'unheld';
72
+ contactId: string;
73
+ };
74
+ export interface ContactLifecycleFloorOptions {
75
+ contactRef: FirebaseFirestore.DocumentReference;
76
+ /**
77
+ * The org the contact belongs to, for the consent groups — the same
78
+ * document every facet writer resolves the holder through.
79
+ */
80
+ org: Record<string, unknown> | null | undefined;
81
+ /**
82
+ * The site the act happened on, whose facet takes the stage. `null` for a
83
+ * record no site captured; the contact's own capturing site is used then.
84
+ */
85
+ hostId: string | null | undefined;
86
+ /** The EARLIEST stage that describes what happened — see `advanceContactLifecycleStage`. */
87
+ floor: ContactLifecycleStage;
88
+ }
89
+ export declare function floorContactLifecycleStage(options: ContactLifecycleFloorOptions): Promise<ContactLifecycleFloor>;