@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,631 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
3
+ /**
4
+ * @license
5
+ * Copyright 2026 Aglyn LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */ /**
19
+ * `/v1/deals` (AGL-2606) — the most commercially sensitive row in the CRM.
20
+ *
21
+ * ## The stage owns the status
22
+ *
23
+ * `status` is denormalized from the stage's `kind` (`crm.ts`), so the two
24
+ * can never disagree on a stored deal. A client may move a deal by either
25
+ * name: `stageId` picks a stage and the status follows; `status` alone picks
26
+ * the pipeline's one `won` or `lost` stage, or its FIRST open stage for
27
+ * `open` — a reopen has to land somewhere, and the top of the pipeline is
28
+ * the only somewhere that needs no second field. Both together must agree,
29
+ * and a pair that does not is refused rather than resolved in favor of
30
+ * either, because whichever one we picked would be the one the client did
31
+ * not mean.
32
+ *
33
+ * Every stage move stamps `stageChangedAtMs`, which is what "stuck in
34
+ * stage" reports read, and a move into or out of a closed stage sets or
35
+ * clears `closedAtMs`.
36
+ *
37
+ * ## A won deal makes its contact a customer
38
+ *
39
+ * The console's stage route floors the linked contact's lifecycle stage at
40
+ * `customer` when a deal is won (AGL-2641), and this resource is the same
41
+ * writer with a different door: a `PATCH` that moves a deal into its `won`
42
+ * stage, or a `POST` that creates one there, applies the same floor to the
43
+ * contact the deal names, in the facet of the deal's site — filling an
44
+ * empty stage, advancing an earlier one, never moving anybody back. The
45
+ * deal's own write is not undone by a contact write that fails; the deal
46
+ * is what the integrator asked for, and the floor is what the win implies.
47
+ *
48
+ * ## The line items own the amount
49
+ *
50
+ * A deal may carry `lineItems` (AGL-2620) — the products behind its value,
51
+ * each in the deal's currency. With one or more lines the stored
52
+ * `amountCents` IS their sum, written beside them, so a typed `amountCents`
53
+ * in the same body, or alone on a deal that has lines, is refused rather
54
+ * than reconciled: whichever number we kept would be the one the client did
55
+ * not mean. `lineItems: []` clears the lines and hands the amount back to
56
+ * the client; the last sum stays until it is retyped. Changing the currency
57
+ * of a deal with lines needs the lines resent in that currency, because
58
+ * their sum is one number in one unit.
59
+ */ import { CRM_COLLECTIONS, CRM_MEDIA_IDS_MAX, createResourceUid, crmNewRecordListFields, dealStageById, lineItemsTotalCents, normalizeCrmMediaIds, readDealLineItems } from "@aglyn/aglyn/server";
60
+ import { apiJson, ApiErrors, restampCrmListFieldsAt } from "@aglyn/tenant-data-admin";
61
+ import { Timestamp } from "firebase-admin/firestore";
62
+ import { claimWrite, readJsonBody, requireScope } from "@aglyn/tenant-data-admin/server/api-v1-kit";
63
+ import { floorContactLifecycleStage } from "../contact-lifecycle-floor.js";
64
+ import { orderedStages, resolvePipeline } from "./crm-pipelines.js";
65
+ import { CRM_TEXT_MAX, CRM_TITLE_MAX, createPayload, crmCollection, crmCreateStamp, crmCustomUpdate, crmCustomView, crmRecordsBandRefusal, crmRefErrors, crmTimes, crmValidationFailed, isoFromMs, listCrm, memberError, parseIsoInstant, readChoice, readCrmCustomBody, readCrmSite, readEqualityFilters, readOptionalText, readRefId, refuseUnknownKeys, updatePayload } from "./crm-shared.js";
66
+ const DEAL_STATUSES = [
67
+ 'open',
68
+ 'won',
69
+ 'lost'
70
+ ];
71
+ /** One line as published: the stored shape, with `productId` always present. */ function lineItemView(item) {
72
+ var _item_productId;
73
+ return {
74
+ productId: (_item_productId = item.productId) != null ? _item_productId : null,
75
+ name: item.name,
76
+ quantity: item.quantity,
77
+ unitAmountCents: item.unitAmountCents,
78
+ currency: item.currency
79
+ };
80
+ }
81
+ /** The deal object as published. Every writable field appears here. */ function dealView(doc) {
82
+ var _doc_data, _data_title, _data_pipelineId, _data_stageId, _data_status, _data_currency, _data_ownerUid, _data_contactId, _data_companyId, _data_lostReason, _data_notes, _data_hostId;
83
+ const data = (_doc_data = doc.data()) != null ? _doc_data : {};
84
+ return _extends({
85
+ id: doc.id,
86
+ object: 'deal',
87
+ title: (_data_title = data.title) != null ? _data_title : null,
88
+ pipelineId: (_data_pipelineId = data.pipelineId) != null ? _data_pipelineId : null,
89
+ stageId: (_data_stageId = data.stageId) != null ? _data_stageId : null,
90
+ status: (_data_status = data.status) != null ? _data_status : null,
91
+ amountCents: typeof data.amountCents === 'number' ? data.amountCents : null,
92
+ // `'usd'` when absent, as the model says — a deal with no currency is a
93
+ // dollar deal, not a deal with an unknown one.
94
+ currency: (_data_currency = data.currency) != null ? _data_currency : 'usd',
95
+ lineItems: Array.isArray(data.lineItems) ? data.lineItems.map(lineItemView) : [],
96
+ expectedCloseAt: isoFromMs(data.expectedCloseAtMs),
97
+ closedAt: isoFromMs(data.closedAtMs),
98
+ stageChangedAt: isoFromMs(data.stageChangedAtMs),
99
+ ownerUid: (_data_ownerUid = data.ownerUid) != null ? _data_ownerUid : null,
100
+ contactId: (_data_contactId = data.contactId) != null ? _data_contactId : null,
101
+ companyId: (_data_companyId = data.companyId) != null ? _data_companyId : null,
102
+ lostReason: (_data_lostReason = data.lostReason) != null ? _data_lostReason : null,
103
+ notes: (_data_notes = data.notes) != null ? _data_notes : null,
104
+ // The org's deal custom fields, keyed by field key (AGL-2661).
105
+ custom: crmCustomView(data),
106
+ // When the earliest open task against the deal is due — see
107
+ // `CrmDeal.nextTaskAtMs`. Read-only: the tasks resource maintains it.
108
+ nextTaskAt: isoFromMs(data.nextTaskAtMs),
109
+ mediaIds: normalizeCrmMediaIds(data.mediaIds),
110
+ siteId: (_data_hostId = data.hostId) != null ? _data_hostId : null
111
+ }, crmTimes(data));
112
+ }
113
+ const DEAL_WRITABLE = new Set([
114
+ 'title',
115
+ 'stageId',
116
+ 'status',
117
+ 'amountCents',
118
+ 'currency',
119
+ 'lineItems',
120
+ 'expectedCloseAt',
121
+ 'ownerUid',
122
+ 'contactId',
123
+ 'companyId',
124
+ 'lostReason',
125
+ 'notes',
126
+ 'custom',
127
+ 'mediaIds'
128
+ ]);
129
+ /**
130
+ * The writable half of a deal, validated. `pipelineId` is accepted on a
131
+ * create only: a deal's stages are its pipeline's, and moving one between
132
+ * pipelines is a new deal in the other pipeline, not an edit.
133
+ */ function readDealInput(body, { partial }) {
134
+ const errors = {};
135
+ const values = {};
136
+ const allowed = new Set(DEAL_WRITABLE);
137
+ if (!partial) {
138
+ allowed.add('consentSiteId');
139
+ allowed.add('pipelineId');
140
+ }
141
+ refuseUnknownKeys(body, allowed, 'deal', errors);
142
+ if (partial && body.pipelineId !== undefined) {
143
+ errors.pipelineId = 'Not writable — create the deal in the other pipeline';
144
+ }
145
+ if (body.mediaIds !== undefined) {
146
+ if (!Array.isArray(body.mediaIds)) {
147
+ errors.mediaIds = 'Must be an array of media ids';
148
+ } else if (body.mediaIds.length > CRM_MEDIA_IDS_MAX) {
149
+ errors.mediaIds = `At most ${CRM_MEDIA_IDS_MAX} files may be attached`;
150
+ } else {
151
+ // The SAME normalizer the console card writes through, so the API
152
+ // cannot store a list the console would refuse. An empty array is
153
+ // legal and clears the attachments.
154
+ values.mediaIds = normalizeCrmMediaIds(body.mediaIds);
155
+ }
156
+ }
157
+ if (body.title !== undefined || !partial) {
158
+ var _body_title;
159
+ const title = String((_body_title = body.title) != null ? _body_title : '').trim().slice(0, CRM_TITLE_MAX);
160
+ if (title) values.title = title;
161
+ else errors.title = partial ? 'Must not be empty' : 'A title is required';
162
+ }
163
+ if (!partial && body.pipelineId !== undefined) {
164
+ const pipelineId = readRefId(body, 'pipelineId', errors);
165
+ if (pipelineId) values.pipelineId = pipelineId;
166
+ else if (pipelineId === null) errors.pipelineId = 'Must be an id';
167
+ }
168
+ const stageId = readRefId(body, 'stageId', errors);
169
+ if (stageId) values.stageId = stageId;
170
+ else if (stageId === null) errors.stageId = 'Must be a stage id';
171
+ const status = readChoice(body, 'status', DEAL_STATUSES, errors);
172
+ if (status) values.status = status;
173
+ if (body.amountCents !== undefined) {
174
+ if (body.amountCents === null) {
175
+ values.amountCents = null;
176
+ } else if (typeof body.amountCents !== 'number' || !Number.isInteger(body.amountCents) || body.amountCents < 0) {
177
+ errors.amountCents = 'Must be a whole number of cents, 0 or more';
178
+ } else {
179
+ values.amountCents = body.amountCents;
180
+ }
181
+ }
182
+ if (body.currency !== undefined) {
183
+ var _body_currency;
184
+ const currency = String((_body_currency = body.currency) != null ? _body_currency : '').trim().toLowerCase();
185
+ if (/^[a-z]{3}$/.test(currency)) values.currency = currency;
186
+ else errors.currency = 'Must be a three-letter ISO 4217 code, like usd';
187
+ }
188
+ if (body.lineItems !== undefined) {
189
+ if (body.lineItems === null || Array.isArray(body.lineItems) && body.lineItems.length === 0) {
190
+ values.lineItems = [];
191
+ } else if (!Array.isArray(body.lineItems)) {
192
+ errors.lineItems = 'Must be a list of line items';
193
+ } else {
194
+ values.lineItems = body.lineItems;
195
+ if (body.amountCents !== undefined) {
196
+ errors.amountCents = 'Derived from lineItems — omit it, or send lineItems: [] to type an amount';
197
+ }
198
+ }
199
+ }
200
+ if (body.expectedCloseAt !== undefined) {
201
+ if (body.expectedCloseAt === null) {
202
+ values.expectedCloseAtMs = null;
203
+ } else {
204
+ const ms = parseIsoInstant(body.expectedCloseAt);
205
+ if (ms === null) {
206
+ errors.expectedCloseAt = 'Must be an ISO 8601 instant, like 2026-12-31T00:00:00Z';
207
+ } else {
208
+ values.expectedCloseAtMs = ms;
209
+ }
210
+ }
211
+ }
212
+ const ownerUid = readOptionalText(body, 'ownerUid', CRM_TITLE_MAX, errors);
213
+ if (ownerUid !== undefined) values.ownerUid = ownerUid;
214
+ const contactId = readRefId(body, 'contactId', errors);
215
+ if (contactId !== undefined) values.contactId = contactId;
216
+ const companyId = readRefId(body, 'companyId', errors);
217
+ if (companyId !== undefined) values.companyId = companyId;
218
+ const lostReason = readOptionalText(body, 'lostReason', CRM_TEXT_MAX, errors);
219
+ if (lostReason !== undefined) values.lostReason = lostReason;
220
+ const notes = readOptionalText(body, 'notes', CRM_TEXT_MAX, errors);
221
+ if (notes !== undefined) values.notes = notes;
222
+ // `custom` is shape-checked here and judged against the org's
223
+ // definitions by the writers, which is where the read is paid.
224
+ if (body.custom !== undefined) {
225
+ if (!body.custom || typeof body.custom !== 'object' || Array.isArray(body.custom)) {
226
+ errors.custom = 'Must be an object of field values keyed by field key';
227
+ }
228
+ }
229
+ return Object.keys(errors).length ? {
230
+ errors
231
+ } : {
232
+ values
233
+ };
234
+ }
235
+ /**
236
+ * The stage a write lands the deal in, from `stageId` and/or `status` — see
237
+ * the module header for the rule. `null` when neither was sent, which on a
238
+ * PATCH means "leave the stage alone" and on a create means the first open
239
+ * stage.
240
+ */ function resolveStage(pipeline, values) {
241
+ const stages = orderedStages(pipeline);
242
+ if (values.stageId) {
243
+ const stage = dealStageById(pipeline, values.stageId);
244
+ if (!stage) return {
245
+ errors: {
246
+ stageId: 'No such stage in this pipeline'
247
+ }
248
+ };
249
+ if (values.status && values.status !== stage.kind) {
250
+ return {
251
+ errors: {
252
+ status: `Must match the stage, which is ${stage.kind}`
253
+ }
254
+ };
255
+ }
256
+ return {
257
+ stage
258
+ };
259
+ }
260
+ if (values.status) {
261
+ const stage = values.status === 'open' ? stages.find((candidate)=>candidate.kind === 'open') : stages.find((candidate)=>candidate.kind === values.status);
262
+ if (!stage) {
263
+ return {
264
+ errors: {
265
+ status: `This pipeline has no ${values.status} stage`
266
+ }
267
+ };
268
+ }
269
+ return {
270
+ stage
271
+ };
272
+ }
273
+ return {
274
+ stage: null
275
+ };
276
+ }
277
+ /**
278
+ * The stored lines and the amount they derive, for a write that sent
279
+ * `lineItems`, validated against the currency the deal WILL have — the
280
+ * body's, else the stored one, else `usd`. Returns nothing to write when
281
+ * the body carried no lines.
282
+ */ function placeLineItems(values, stored) {
283
+ var _ref, _values_currency;
284
+ if (values.lineItems === undefined) return {
285
+ fields: {}
286
+ };
287
+ const currency = (_ref = (_values_currency = values.currency) != null ? _values_currency : stored == null ? void 0 : stored.currency) != null ? _ref : 'usd';
288
+ const read = readDealLineItems(values.lineItems, currency);
289
+ if ('error' in read) return {
290
+ errors: {
291
+ lineItems: read.error
292
+ }
293
+ };
294
+ return {
295
+ fields: _extends({
296
+ lineItems: read.items
297
+ }, read.items.length ? {
298
+ amountCents: lineItemsTotalCents(read.items)
299
+ } : {})
300
+ };
301
+ }
302
+ /**
303
+ * The customer floor a win applies to the deal's contact (AGL-2641), after
304
+ * the deal itself is written. A contact write that fails is logged and
305
+ * swallowed: the deal is already won, and a `500` would tell the integrator
306
+ * the move they can see in the console did not happen.
307
+ */ async function floorWonDealContact(ctx, deal) {
308
+ var _deal_contactId;
309
+ const contactId = String((_deal_contactId = deal.contactId) != null ? _deal_contactId : '').trim();
310
+ if (!contactId) return;
311
+ try {
312
+ var _deal_hostId;
313
+ await floorContactLifecycleStage({
314
+ contactRef: ctx.firestore.collection('orgs').doc(ctx.orgId).collection('contacts').doc(contactId),
315
+ org: ctx.org,
316
+ hostId: (_deal_hostId = deal.hostId) != null ? _deal_hostId : null,
317
+ floor: 'customer'
318
+ });
319
+ } catch (error) {
320
+ console.error('[api-v1] deals: the customer floor failed', contactId, error);
321
+ }
322
+ }
323
+ /** The fields a stage move writes, beside the stage itself. */ function stageMove(stage, nowMs) {
324
+ return {
325
+ stageId: stage.id,
326
+ status: stage.kind,
327
+ stageChangedAtMs: nowMs,
328
+ closedAtMs: stage.kind === 'open' ? null : nowMs
329
+ };
330
+ }
331
+ /** The membership and reference checks a deal write makes, merged. */ async function dealRefErrors(ctx, values) {
332
+ var _values_contactId, _values_companyId;
333
+ const [owner, refs] = await Promise.all([
334
+ memberError(ctx, 'ownerUid', values.ownerUid),
335
+ crmRefErrors(ctx, {
336
+ contactId: (_values_contactId = values.contactId) != null ? _values_contactId : undefined,
337
+ companyId: (_values_companyId = values.companyId) != null ? _values_companyId : undefined
338
+ })
339
+ ]);
340
+ return _extends({}, owner, refs);
341
+ }
342
+ /**
343
+ * `POST /v1/deals`. The pipeline is resolved AFTER the claim because
344
+ * resolving it can create one (`resolvePipeline`), and a refusal past that
345
+ * point releases the key so the retry that names a real stage still can.
346
+ */ async function createDeal(request, ctx) {
347
+ const body = await readJsonBody(request);
348
+ const parsed = readDealInput(body, {
349
+ partial: false
350
+ });
351
+ if ('errors' in parsed) return crmValidationFailed(ctx, 'deal', parsed.errors);
352
+ const site = readCrmSite(ctx, 'deal', body);
353
+ if ('response' in site) return site.response;
354
+ const refErrors = await dealRefErrors(ctx, parsed.values);
355
+ if (Object.keys(refErrors).length) return crmValidationFailed(ctx, 'deal', refErrors);
356
+ // Judged against the org's deal definitions, above the claim (AGL-2661).
357
+ const custom = await readCrmCustomBody(ctx, body, 'deal');
358
+ if (custom && 'errors' in custom) return crmValidationFailed(ctx, 'deal', custom.errors);
359
+ const customValues = custom && 'values' in custom ? custom.values : {};
360
+ const collection = crmCollection(ctx, CRM_COLLECTIONS.deals);
361
+ const claimed = await claimWrite(ctx, '*', request.headers.get('Idempotency-Key'), 'deals');
362
+ if ('replay' in claimed) return claimed.replay;
363
+ const { claim } = claimed;
364
+ try {
365
+ var _placed_stage;
366
+ const _parsed_values = parsed.values, { title, pipelineId, stageId, status, lineItems } = _parsed_values, rest = _object_without_properties_loose(_parsed_values, [
367
+ "title",
368
+ "pipelineId",
369
+ "stageId",
370
+ "status",
371
+ "lineItems"
372
+ ]);
373
+ const placedLines = placeLineItems({
374
+ lineItems,
375
+ currency: rest.currency
376
+ }, null);
377
+ if ('errors' in placedLines) {
378
+ await claim.release();
379
+ return crmValidationFailed(ctx, 'deal', placedLines.errors);
380
+ }
381
+ const resolved = await resolvePipeline(ctx, site.siteId, pipelineId);
382
+ if ('error' in resolved) {
383
+ await claim.release();
384
+ return crmValidationFailed(ctx, 'deal', {
385
+ pipelineId: resolved.error
386
+ });
387
+ }
388
+ const placed = resolveStage(resolved.pipeline, {
389
+ stageId,
390
+ status
391
+ });
392
+ if ('errors' in placed) {
393
+ await claim.release();
394
+ return crmValidationFailed(ctx, 'deal', placed.errors);
395
+ }
396
+ const stage = (_placed_stage = placed.stage) != null ? _placed_stage : orderedStages(resolved.pipeline)[0];
397
+ if (!stage) {
398
+ await claim.release();
399
+ return crmValidationFailed(ctx, 'deal', {
400
+ pipelineId: 'This pipeline has no stages'
401
+ });
402
+ }
403
+ // The records band (AGL-2611), asked last so a refusal past the pipeline
404
+ // resolution releases the key exactly as a bad stage does.
405
+ const bandFull = await crmRecordsBandRefusal(ctx);
406
+ if (bandFull) {
407
+ await claim.release();
408
+ return bandFull;
409
+ }
410
+ const id = createResourceUid();
411
+ // One clock: the stage move is stamped with the instant the record's own
412
+ // `createdAt` carries, so a deal created won closed the moment it began.
413
+ const stamp = crmCreateStamp(ctx, site.siteId);
414
+ const record = _extends({
415
+ title,
416
+ titleLower: (title != null ? title : '').toLowerCase(),
417
+ pipelineId: resolved.id
418
+ }, createPayload(_extends({}, rest, placedLines.fields, stageMove(stage, stamp.createdAt.toMillis()))), Object.keys(customValues).length ? {
419
+ custom: createPayload(customValues)
420
+ } : {}, stamp);
421
+ // What the console's Deals list searches and filters by (AGL-3321).
422
+ await collection.doc(id).create(_extends({}, record, crmNewRecordListFields('deals', record)));
423
+ // A deal created won is a won deal: its contact is a customer from birth.
424
+ if (stage.kind === 'won') {
425
+ await floorWonDealContact(ctx, {
426
+ contactId: rest.contactId,
427
+ hostId: site.siteId
428
+ });
429
+ }
430
+ const view = dealView(await collection.doc(id).get());
431
+ await claim.record(200, view);
432
+ return apiJson(view, {
433
+ status: 201,
434
+ headers: ctx.headers
435
+ });
436
+ } catch (error) {
437
+ await claim.release();
438
+ throw error;
439
+ }
440
+ }
441
+ /** `PATCH /v1/deals/{id}`. */ async function updateDeal(request, ctx, ref) {
442
+ var _stored_currency;
443
+ const body = await readJsonBody(request);
444
+ const parsed = readDealInput(body, {
445
+ partial: true
446
+ });
447
+ if ('errors' in parsed) return crmValidationFailed(ctx, 'deal', parsed.errors);
448
+ const snap = await ref.get();
449
+ if (!snap.exists) {
450
+ return ApiErrors.notFound({
451
+ message: 'No such deal',
452
+ headers: ctx.headers
453
+ });
454
+ }
455
+ const refErrors = await dealRefErrors(ctx, parsed.values);
456
+ if (Object.keys(refErrors).length) return crmValidationFailed(ctx, 'deal', refErrors);
457
+ const custom = await readCrmCustomBody(ctx, body, 'deal');
458
+ if (custom && 'errors' in custom) return crmValidationFailed(ctx, 'deal', custom.errors);
459
+ const _parsed_values = parsed.values, { title, stageId, status, lineItems } = _parsed_values, rest = _object_without_properties_loose(_parsed_values, [
460
+ "title",
461
+ "stageId",
462
+ "status",
463
+ "lineItems"
464
+ ]);
465
+ const stored = snap.data();
466
+ const storedLines = Array.isArray(stored.lineItems) && stored.lineItems.length > 0;
467
+ const keepsLines = storedLines && lineItems === undefined;
468
+ if (keepsLines && rest.amountCents !== undefined) {
469
+ return crmValidationFailed(ctx, 'deal', {
470
+ amountCents: "This deal's amount is the sum of its line items — edit lineItems instead"
471
+ });
472
+ }
473
+ if (keepsLines && rest.currency !== undefined && rest.currency !== ((_stored_currency = stored.currency) != null ? _stored_currency : 'usd')) {
474
+ return crmValidationFailed(ctx, 'deal', {
475
+ currency: "Every line item is in the deal's currency — send lineItems in the new currency with it"
476
+ });
477
+ }
478
+ const placedLines = placeLineItems({
479
+ lineItems,
480
+ currency: rest.currency
481
+ }, stored);
482
+ if ('errors' in placedLines) return crmValidationFailed(ctx, 'deal', placedLines.errors);
483
+ const update = _extends({}, title !== undefined ? {
484
+ title,
485
+ titleLower: title.toLowerCase()
486
+ } : {}, updatePayload(_extends({}, rest, placedLines.fields)), custom && 'values' in custom ? crmCustomUpdate(custom.values) : {});
487
+ // The write's one instant: `updatedAt`, and the stage move if there is one,
488
+ // so a deal closed at T reads updated at T.
489
+ const now = Timestamp.now();
490
+ let won = false;
491
+ if (stageId !== undefined || status !== undefined) {
492
+ const current = snap.data();
493
+ const resolved = await resolvePipeline(ctx, current.hostId, current.pipelineId, {
494
+ allowArchived: true
495
+ });
496
+ if ('error' in resolved) {
497
+ return crmValidationFailed(ctx, 'deal', {
498
+ stageId: "This deal's pipeline no longer exists"
499
+ });
500
+ }
501
+ const placed = resolveStage(resolved.pipeline, {
502
+ stageId,
503
+ status
504
+ });
505
+ if ('errors' in placed) return crmValidationFailed(ctx, 'deal', placed.errors);
506
+ if (placed.stage && placed.stage.id !== current.stageId) {
507
+ Object.assign(update, stageMove(placed.stage, now.toMillis()));
508
+ // The transition INTO won, not a won deal edited: a deal already
509
+ // closed won floored its contact when it closed.
510
+ won = placed.stage.kind === 'won' && current.status !== 'won';
511
+ }
512
+ }
513
+ if (Object.keys(update).length > 0) {
514
+ await ref.update(_extends({}, update, {
515
+ updatedAt: now
516
+ }));
517
+ // What the console's Deals list searches (AGL-3321).
518
+ if ('title' in update) await restampCrmListFieldsAt(ref, 'deals');
519
+ }
520
+ if (won) {
521
+ await floorWonDealContact(ctx, {
522
+ contactId: rest.contactId !== undefined ? rest.contactId : stored.contactId,
523
+ hostId: stored.hostId
524
+ });
525
+ }
526
+ return apiJson(dealView(await ref.get()), {
527
+ headers: ctx.headers
528
+ });
529
+ }
530
+ /** `DELETE /v1/deals/{id}` — the deal alone; its tasks and activities stay. */ async function deleteDeal(request, ctx, ref) {
531
+ const claimed = await claimWrite(ctx, '*', request.headers.get('Idempotency-Key'), 'deal-deletes');
532
+ if ('replay' in claimed) return claimed.replay;
533
+ const { claim } = claimed;
534
+ try {
535
+ const snap = await ref.get();
536
+ if (!snap.exists) {
537
+ await claim.release();
538
+ return ApiErrors.notFound({
539
+ message: 'No such deal',
540
+ headers: ctx.headers
541
+ });
542
+ }
543
+ await ref.delete();
544
+ const view = {
545
+ id: ref.id,
546
+ object: 'deal',
547
+ deleted: true
548
+ };
549
+ await claim.record(200, view);
550
+ return apiJson(view, {
551
+ headers: ctx.headers
552
+ });
553
+ } catch (error) {
554
+ await claim.release();
555
+ throw error;
556
+ }
557
+ }
558
+ /**
559
+ * `GET /v1/deals` filters, most selective first — the order `listCrm`
560
+ * indexes them in. `status` is validated against the three values a deal
561
+ * can hold, because `?status=closed` matching nothing is the plausible empty
562
+ * page the conventions refuse to serve.
563
+ */ async function listDeals(ctx, collection, url) {
564
+ const rawStatus = url.searchParams.get('status');
565
+ if (rawStatus !== null && rawStatus.trim() !== '' && !DEAL_STATUSES.includes(rawStatus.trim())) {
566
+ return crmValidationFailed(ctx, 'deal filter', {
567
+ status: `Must be one of: ${DEAL_STATUSES.join(', ')}`
568
+ });
569
+ }
570
+ const filters = readEqualityFilters(url, [
571
+ 'contactId',
572
+ 'companyId',
573
+ 'pipelineId',
574
+ 'ownerUid',
575
+ 'status'
576
+ ]);
577
+ return listCrm(ctx, collection, url, filters, dealView);
578
+ }
579
+ export async function handleDeals(request, ctx, segments, url) {
580
+ const collection = crmCollection(ctx, CRM_COLLECTIONS.deals);
581
+ const [, dealId] = segments;
582
+ if (!dealId) {
583
+ if (request.method === 'GET') {
584
+ const denied = requireScope(ctx, 'crm:read');
585
+ if (denied) return denied;
586
+ return listDeals(ctx, collection, url);
587
+ }
588
+ if (request.method === 'POST') {
589
+ const denied = requireScope(ctx, 'crm:write');
590
+ if (denied) return denied;
591
+ return createDeal(request, ctx);
592
+ }
593
+ return ApiErrors.methodNotAllowed({
594
+ headers: _extends({}, ctx.headers, {
595
+ Allow: 'GET, POST'
596
+ })
597
+ });
598
+ }
599
+ const ref = collection.doc(dealId);
600
+ if (request.method === 'GET') {
601
+ const denied = requireScope(ctx, 'crm:read');
602
+ if (denied) return denied;
603
+ const snap = await ref.get();
604
+ if (!snap.exists) {
605
+ return ApiErrors.notFound({
606
+ message: 'No such deal',
607
+ headers: ctx.headers
608
+ });
609
+ }
610
+ return apiJson(dealView(snap), {
611
+ headers: ctx.headers
612
+ });
613
+ }
614
+ if (request.method === 'PATCH') {
615
+ const denied = requireScope(ctx, 'crm:write');
616
+ if (denied) return denied;
617
+ return updateDeal(request, ctx, ref);
618
+ }
619
+ if (request.method === 'DELETE') {
620
+ const denied = requireScope(ctx, 'crm:write');
621
+ if (denied) return denied;
622
+ return deleteDeal(request, ctx, ref);
623
+ }
624
+ return ApiErrors.methodNotAllowed({
625
+ headers: _extends({}, ctx.headers, {
626
+ Allow: 'GET, PATCH, DELETE'
627
+ })
628
+ });
629
+ }
630
+
631
+ //# sourceMappingURL=crm-deals.js.map