@happyvertical/smrt-sales 0.39.2

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 (201) hide show
  1. package/AGENTS.md +87 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/chunks/__smrt-register__-kWdT7LOt.js +6 -0
  8. package/dist/chunks/__smrt-register__-kWdT7LOt.js.map +1 -0
  9. package/dist/chunks/commissions-BCxqOKVM.js +2181 -0
  10. package/dist/chunks/commissions-BCxqOKVM.js.map +1 -0
  11. package/dist/chunks/crm-DwEz7E2r.js +1428 -0
  12. package/dist/chunks/crm-DwEz7E2r.js.map +1 -0
  13. package/dist/chunks/referrals-BY47p70T.js +2880 -0
  14. package/dist/chunks/referrals-BY47p70T.js.map +1 -0
  15. package/dist/commissions/collections/CommissionAdjustmentCollection.d.ts +26 -0
  16. package/dist/commissions/collections/CommissionAdjustmentCollection.d.ts.map +1 -0
  17. package/dist/commissions/collections/CommissionCollection.d.ts +43 -0
  18. package/dist/commissions/collections/CommissionCollection.d.ts.map +1 -0
  19. package/dist/commissions/collections/CommissionPayoutCollection.d.ts +19 -0
  20. package/dist/commissions/collections/CommissionPayoutCollection.d.ts.map +1 -0
  21. package/dist/commissions/collections/CommissionPlanCollection.d.ts +46 -0
  22. package/dist/commissions/collections/CommissionPlanCollection.d.ts.map +1 -0
  23. package/dist/commissions/collections/EarnerCollection.d.ts +14 -0
  24. package/dist/commissions/collections/EarnerCollection.d.ts.map +1 -0
  25. package/dist/commissions/collections/EarningEventCollection.d.ts +27 -0
  26. package/dist/commissions/collections/EarningEventCollection.d.ts.map +1 -0
  27. package/dist/commissions/collections/index.d.ts +11 -0
  28. package/dist/commissions/collections/index.d.ts.map +1 -0
  29. package/dist/commissions/index.d.ts +20 -0
  30. package/dist/commissions/index.d.ts.map +1 -0
  31. package/dist/commissions/models/Commission.d.ts +150 -0
  32. package/dist/commissions/models/Commission.d.ts.map +1 -0
  33. package/dist/commissions/models/CommissionAdjustment.d.ts +62 -0
  34. package/dist/commissions/models/CommissionAdjustment.d.ts.map +1 -0
  35. package/dist/commissions/models/CommissionPayout.d.ts +119 -0
  36. package/dist/commissions/models/CommissionPayout.d.ts.map +1 -0
  37. package/dist/commissions/models/CommissionPlan.d.ts +117 -0
  38. package/dist/commissions/models/CommissionPlan.d.ts.map +1 -0
  39. package/dist/commissions/models/Earner.d.ts +49 -0
  40. package/dist/commissions/models/Earner.d.ts.map +1 -0
  41. package/dist/commissions/models/EarningEvent.d.ts +89 -0
  42. package/dist/commissions/models/EarningEvent.d.ts.map +1 -0
  43. package/dist/commissions/models/index.d.ts +11 -0
  44. package/dist/commissions/models/index.d.ts.map +1 -0
  45. package/dist/commissions/money.d.ts +42 -0
  46. package/dist/commissions/money.d.ts.map +1 -0
  47. package/dist/commissions/services/CommissionBalanceService.d.ts +14 -0
  48. package/dist/commissions/services/CommissionBalanceService.d.ts.map +1 -0
  49. package/dist/commissions/services/CommissionCalculationService.d.ts +142 -0
  50. package/dist/commissions/services/CommissionCalculationService.d.ts.map +1 -0
  51. package/dist/commissions/services/CommissionPayoutService.d.ts +134 -0
  52. package/dist/commissions/services/CommissionPayoutService.d.ts.map +1 -0
  53. package/dist/commissions/services/CommissionSettlementService.d.ts +42 -0
  54. package/dist/commissions/services/CommissionSettlementService.d.ts.map +1 -0
  55. package/dist/commissions/services/index.d.ts +9 -0
  56. package/dist/commissions/services/index.d.ts.map +1 -0
  57. package/dist/commissions/types.d.ts +243 -0
  58. package/dist/commissions/types.d.ts.map +1 -0
  59. package/dist/commissions.d.ts +2 -0
  60. package/dist/commissions.d.ts.map +1 -0
  61. package/dist/commissions.js +3 -0
  62. package/dist/crm/collections/LeadCollection.d.ts +79 -0
  63. package/dist/crm/collections/LeadCollection.d.ts.map +1 -0
  64. package/dist/crm/collections/OpportunityCollection.d.ts +42 -0
  65. package/dist/crm/collections/OpportunityCollection.d.ts.map +1 -0
  66. package/dist/crm/collections/OpportunityConversionCollection.d.ts +26 -0
  67. package/dist/crm/collections/OpportunityConversionCollection.d.ts.map +1 -0
  68. package/dist/crm/collections/PipelineDefinitionCollection.d.ts +46 -0
  69. package/dist/crm/collections/PipelineDefinitionCollection.d.ts.map +1 -0
  70. package/dist/crm/collections/PipelineStageCollection.d.ts +22 -0
  71. package/dist/crm/collections/PipelineStageCollection.d.ts.map +1 -0
  72. package/dist/crm/collections/SalesActivityCollection.d.ts +24 -0
  73. package/dist/crm/collections/SalesActivityCollection.d.ts.map +1 -0
  74. package/dist/crm/collections/SalesRepresentativeCollection.d.ts +19 -0
  75. package/dist/crm/collections/SalesRepresentativeCollection.d.ts.map +1 -0
  76. package/dist/crm/index.d.ts +27 -0
  77. package/dist/crm/index.d.ts.map +1 -0
  78. package/dist/crm/models/Lead.d.ts +135 -0
  79. package/dist/crm/models/Lead.d.ts.map +1 -0
  80. package/dist/crm/models/Opportunity.d.ts +111 -0
  81. package/dist/crm/models/Opportunity.d.ts.map +1 -0
  82. package/dist/crm/models/OpportunityConversion.d.ts +66 -0
  83. package/dist/crm/models/OpportunityConversion.d.ts.map +1 -0
  84. package/dist/crm/models/PipelineDefinition.d.ts +58 -0
  85. package/dist/crm/models/PipelineDefinition.d.ts.map +1 -0
  86. package/dist/crm/models/PipelineStage.d.ts +66 -0
  87. package/dist/crm/models/PipelineStage.d.ts.map +1 -0
  88. package/dist/crm/models/SalesActivity.d.ts +76 -0
  89. package/dist/crm/models/SalesActivity.d.ts.map +1 -0
  90. package/dist/crm/models/SalesRepresentative.d.ts +60 -0
  91. package/dist/crm/models/SalesRepresentative.d.ts.map +1 -0
  92. package/dist/crm/types.d.ts +228 -0
  93. package/dist/crm/types.d.ts.map +1 -0
  94. package/dist/crm.d.ts +2 -0
  95. package/dist/crm.d.ts.map +1 -0
  96. package/dist/crm.js +3 -0
  97. package/dist/index.d.ts +4 -0
  98. package/dist/index.d.ts.map +1 -0
  99. package/dist/index.js +5 -0
  100. package/dist/manifest.json +9340 -0
  101. package/dist/referrals/collections/AttributionExceptionCollection.d.ts +17 -0
  102. package/dist/referrals/collections/AttributionExceptionCollection.d.ts.map +1 -0
  103. package/dist/referrals/collections/AttributionPolicyCollection.d.ts +50 -0
  104. package/dist/referrals/collections/AttributionPolicyCollection.d.ts.map +1 -0
  105. package/dist/referrals/collections/ReferralAgreementCollection.d.ts +48 -0
  106. package/dist/referrals/collections/ReferralAgreementCollection.d.ts.map +1 -0
  107. package/dist/referrals/collections/ReferralCollection.d.ts +25 -0
  108. package/dist/referrals/collections/ReferralCollection.d.ts.map +1 -0
  109. package/dist/referrals/collections/ReferralLinkCollection.d.ts +96 -0
  110. package/dist/referrals/collections/ReferralLinkCollection.d.ts.map +1 -0
  111. package/dist/referrals/collections/ReferralProgramCollection.d.ts +18 -0
  112. package/dist/referrals/collections/ReferralProgramCollection.d.ts.map +1 -0
  113. package/dist/referrals/collections/ReferralTermSnapshotCollection.d.ts +13 -0
  114. package/dist/referrals/collections/ReferralTermSnapshotCollection.d.ts.map +1 -0
  115. package/dist/referrals/collections/ReferralTouchCollection.d.ts +30 -0
  116. package/dist/referrals/collections/ReferralTouchCollection.d.ts.map +1 -0
  117. package/dist/referrals/collections/ReferrerCollection.d.ts +17 -0
  118. package/dist/referrals/collections/ReferrerCollection.d.ts.map +1 -0
  119. package/dist/referrals/index.d.ts +38 -0
  120. package/dist/referrals/index.d.ts.map +1 -0
  121. package/dist/referrals/models/AttributionException.d.ts +85 -0
  122. package/dist/referrals/models/AttributionException.d.ts.map +1 -0
  123. package/dist/referrals/models/AttributionPolicy.d.ts +128 -0
  124. package/dist/referrals/models/AttributionPolicy.d.ts.map +1 -0
  125. package/dist/referrals/models/Referral.d.ts +112 -0
  126. package/dist/referrals/models/Referral.d.ts.map +1 -0
  127. package/dist/referrals/models/ReferralAgreement.d.ts +125 -0
  128. package/dist/referrals/models/ReferralAgreement.d.ts.map +1 -0
  129. package/dist/referrals/models/ReferralLink.d.ts +71 -0
  130. package/dist/referrals/models/ReferralLink.d.ts.map +1 -0
  131. package/dist/referrals/models/ReferralProgram.d.ts +57 -0
  132. package/dist/referrals/models/ReferralProgram.d.ts.map +1 -0
  133. package/dist/referrals/models/ReferralTermSnapshot.d.ts +61 -0
  134. package/dist/referrals/models/ReferralTermSnapshot.d.ts.map +1 -0
  135. package/dist/referrals/models/ReferralTouch.d.ts +55 -0
  136. package/dist/referrals/models/ReferralTouch.d.ts.map +1 -0
  137. package/dist/referrals/models/Referrer.d.ts +69 -0
  138. package/dist/referrals/models/Referrer.d.ts.map +1 -0
  139. package/dist/referrals/services/AttributionService.d.ts +248 -0
  140. package/dist/referrals/services/AttributionService.d.ts.map +1 -0
  141. package/dist/referrals/services/ReferralCommissionService.d.ts +91 -0
  142. package/dist/referrals/services/ReferralCommissionService.d.ts.map +1 -0
  143. package/dist/referrals/services/ReferralQualificationService.d.ts +135 -0
  144. package/dist/referrals/services/ReferralQualificationService.d.ts.map +1 -0
  145. package/dist/referrals/types.d.ts +226 -0
  146. package/dist/referrals/types.d.ts.map +1 -0
  147. package/dist/referrals.d.ts +2 -0
  148. package/dist/referrals.d.ts.map +1 -0
  149. package/dist/referrals.js +3 -0
  150. package/dist/smrt-knowledge.json +5501 -0
  151. package/dist/svelte/__tests__/format.test.js +68 -0
  152. package/dist/svelte/__tests__/types.test.js +349 -0
  153. package/dist/svelte/components/AttributionConflictQueue.svelte +361 -0
  154. package/dist/svelte/components/AttributionConflictQueue.svelte.d.ts +15 -0
  155. package/dist/svelte/components/AttributionConflictQueue.svelte.d.ts.map +1 -0
  156. package/dist/svelte/components/CommissionBreakdown.svelte +251 -0
  157. package/dist/svelte/components/CommissionBreakdown.svelte.d.ts +11 -0
  158. package/dist/svelte/components/CommissionBreakdown.svelte.d.ts.map +1 -0
  159. package/dist/svelte/components/CommissionExpenseSummary.svelte +186 -0
  160. package/dist/svelte/components/CommissionExpenseSummary.svelte.d.ts +13 -0
  161. package/dist/svelte/components/CommissionExpenseSummary.svelte.d.ts.map +1 -0
  162. package/dist/svelte/components/ExecutedAgreementsList.svelte +136 -0
  163. package/dist/svelte/components/ExecutedAgreementsList.svelte.d.ts +11 -0
  164. package/dist/svelte/components/ExecutedAgreementsList.svelte.d.ts.map +1 -0
  165. package/dist/svelte/components/LeadList.svelte +224 -0
  166. package/dist/svelte/components/LeadList.svelte.d.ts +19 -0
  167. package/dist/svelte/components/LeadList.svelte.d.ts.map +1 -0
  168. package/dist/svelte/components/OpportunityBoard.svelte +230 -0
  169. package/dist/svelte/components/OpportunityBoard.svelte.d.ts +17 -0
  170. package/dist/svelte/components/OpportunityBoard.svelte.d.ts.map +1 -0
  171. package/dist/svelte/components/OpportunityDetail.svelte +472 -0
  172. package/dist/svelte/components/OpportunityDetail.svelte.d.ts +27 -0
  173. package/dist/svelte/components/OpportunityDetail.svelte.d.ts.map +1 -0
  174. package/dist/svelte/components/PayoutBatchReview.svelte +285 -0
  175. package/dist/svelte/components/PayoutBatchReview.svelte.d.ts +21 -0
  176. package/dist/svelte/components/PayoutBatchReview.svelte.d.ts.map +1 -0
  177. package/dist/svelte/components/PayoutHistoryList.svelte +245 -0
  178. package/dist/svelte/components/PayoutHistoryList.svelte.d.ts +11 -0
  179. package/dist/svelte/components/PayoutHistoryList.svelte.d.ts.map +1 -0
  180. package/dist/svelte/components/ReferralLinkManager.svelte +216 -0
  181. package/dist/svelte/components/ReferralLinkManager.svelte.d.ts +17 -0
  182. package/dist/svelte/components/ReferralLinkManager.svelte.d.ts.map +1 -0
  183. package/dist/svelte/components/ReferralStatusList.svelte +118 -0
  184. package/dist/svelte/components/ReferralStatusList.svelte.d.ts +11 -0
  185. package/dist/svelte/components/ReferralStatusList.svelte.d.ts.map +1 -0
  186. package/dist/svelte/components/ReferrerEarningsSummary.svelte +106 -0
  187. package/dist/svelte/components/ReferrerEarningsSummary.svelte.d.ts +11 -0
  188. package/dist/svelte/components/ReferrerEarningsSummary.svelte.d.ts.map +1 -0
  189. package/dist/svelte/components/SalesDashboard.svelte +192 -0
  190. package/dist/svelte/components/SalesDashboard.svelte.d.ts +29 -0
  191. package/dist/svelte/components/SalesDashboard.svelte.d.ts.map +1 -0
  192. package/dist/svelte/format.d.ts +46 -0
  193. package/dist/svelte/format.d.ts.map +1 -0
  194. package/dist/svelte/format.js +75 -0
  195. package/dist/svelte/index.d.ts +50 -0
  196. package/dist/svelte/index.d.ts.map +1 -0
  197. package/dist/svelte/index.js +37 -0
  198. package/dist/svelte/types.d.ts +422 -0
  199. package/dist/svelte/types.d.ts.map +1 -0
  200. package/dist/svelte/types.js +376 -0
  201. package/package.json +89 -0
@@ -0,0 +1,1428 @@
1
+ import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
2
+ import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
3
+ //#region src/crm/models/Lead.ts
4
+ var __defProp$6 = Object.defineProperty;
5
+ var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
6
+ var __decorateClass$6 = (decorators, target, key, kind) => {
7
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
8
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
9
+ if (kind && result) __defProp$6(target, key, result);
10
+ return result;
11
+ };
12
+ var LEAD_STATUS_TRANSITIONS = {
13
+ new: [
14
+ "working",
15
+ "qualified",
16
+ "disqualified",
17
+ "merged"
18
+ ],
19
+ working: [
20
+ "qualified",
21
+ "disqualified",
22
+ "merged"
23
+ ],
24
+ qualified: ["merged"],
25
+ disqualified: ["working", "merged"],
26
+ merged: []
27
+ };
28
+ var loadedLeadStatus = /* @__PURE__ */ new WeakMap();
29
+ var Lead = class extends SmrtObject {
30
+ tenantId = null;
31
+ name = "";
32
+ /** Primary human contact name. */
33
+ contactName = "";
34
+ /** Primary contact email. */
35
+ email = "";
36
+ /** Primary contact phone. */
37
+ phone = "";
38
+ /** Prospect organization/company name. */
39
+ organizationName = "";
40
+ profileId = "";
41
+ ownerRepId = "";
42
+ /** Lifecycle status; transitions are save-guarded (see module map). */
43
+ status = "new";
44
+ /**
45
+ * Generic acquisition-source kind (open string): `'campaign'`,
46
+ * `'referral'`, `'import'`, `'web_form'`, … CRM stores the pointer without
47
+ * interpreting it — referral attribution lives in the referrals module.
48
+ */
49
+ sourceKind = "";
50
+ /** Identifier within the `sourceKind` namespace. */
51
+ sourceId = "";
52
+ /**
53
+ * Preserved acquisition history as a JSON object string. Use
54
+ * {@link getAcquisitionContext}/{@link setAcquisitionContext}. Merges append
55
+ * the losing lead's context under a `mergedSources` array — both sides'
56
+ * histories survive.
57
+ */
58
+ acquisitionContext = "{}";
59
+ /**
60
+ * Generic external intake-draft reference (open string) — e.g. the id of a
61
+ * form submission, imported CSV row, or referral intake draft that this
62
+ * lead was materialized from.
63
+ */
64
+ intakeRef = "";
65
+ mergedIntoId = "";
66
+ /** When the lead was qualified into an opportunity. */
67
+ qualifiedAt = null;
68
+ /**
69
+ * Free-form JSON object stored as a string. Use
70
+ * {@link getMetadata}/{@link setMetadata} instead of parsing manually.
71
+ */
72
+ metadata = "{}";
73
+ constructor(options = {}) {
74
+ super(options);
75
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
76
+ if (options.name !== void 0) this.name = options.name;
77
+ if (options.contactName !== void 0) this.contactName = options.contactName;
78
+ if (options.email !== void 0) this.email = options.email;
79
+ if (options.phone !== void 0) this.phone = options.phone;
80
+ if (options.organizationName !== void 0) this.organizationName = options.organizationName;
81
+ if (options.profileId !== void 0) this.profileId = options.profileId;
82
+ if (options.ownerRepId !== void 0) this.ownerRepId = options.ownerRepId;
83
+ if (options.status !== void 0) this.status = options.status;
84
+ if (options.sourceKind !== void 0) this.sourceKind = options.sourceKind;
85
+ if (options.sourceId !== void 0) this.sourceId = options.sourceId;
86
+ if (options.acquisitionContext !== void 0) this.acquisitionContext = options.acquisitionContext;
87
+ if (options.intakeRef !== void 0) this.intakeRef = options.intakeRef;
88
+ if (options.mergedIntoId !== void 0) this.mergedIntoId = options.mergedIntoId;
89
+ if (options.qualifiedAt !== void 0) this.qualifiedAt = options.qualifiedAt;
90
+ if (options.metadata !== void 0) this.metadata = options.metadata;
91
+ }
92
+ /** Whether the lead has been folded into another lead (terminal). */
93
+ isMerged() {
94
+ return this.status === "merged";
95
+ }
96
+ /** Whether the lead has been qualified into an opportunity. */
97
+ isQualified() {
98
+ return this.status === "qualified";
99
+ }
100
+ /** Parse the acquisition-context JSON string; `{}` on malformed content. */
101
+ getAcquisitionContext() {
102
+ try {
103
+ const parsed = JSON.parse(this.acquisitionContext);
104
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
105
+ return {};
106
+ } catch {
107
+ return {};
108
+ }
109
+ }
110
+ /** Serialize and store the acquisition-context object. */
111
+ setAcquisitionContext(context) {
112
+ this.acquisitionContext = JSON.stringify(context);
113
+ }
114
+ /** Parse the metadata JSON string; returns `{}` on malformed content. */
115
+ getMetadata() {
116
+ try {
117
+ const parsed = JSON.parse(this.metadata);
118
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
119
+ return {};
120
+ } catch {
121
+ return {};
122
+ }
123
+ }
124
+ /** Serialize and store the metadata object. */
125
+ setMetadata(metadata) {
126
+ this.metadata = JSON.stringify(metadata);
127
+ }
128
+ /**
129
+ * Capture the status the row was loaded with so the save-time transition
130
+ * guard can reject illegal status flips made via raw field assignment
131
+ * (mass-assignment on the generated update route, a stale caller, etc.).
132
+ * Only persisted rows carry a prior status.
133
+ */
134
+ async initialize() {
135
+ await super.initialize();
136
+ if (await this.isSaved()) loadedLeadStatus.set(this, this.status);
137
+ return this;
138
+ }
139
+ /**
140
+ * Validate the status transition before persisting, then save. A forged
141
+ * `status` (e.g. un-merging a merged lead) is rejected here regardless of
142
+ * how the instance was constructed.
143
+ */
144
+ async save() {
145
+ const prior = await this.resolvePriorStatus();
146
+ this.assertLeadStatusTransition(prior);
147
+ const result = await super.save();
148
+ loadedLeadStatus.set(this, this.status);
149
+ return result;
150
+ }
151
+ /**
152
+ * Reject an illegal status flip done via raw assignment. No-op transitions
153
+ * and brand-new rows are always allowed.
154
+ */
155
+ assertLeadStatusTransition(prior) {
156
+ if (prior === void 0) return;
157
+ if (prior === this.status) return;
158
+ if (!(LEAD_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`Lead ${this.name || this.id}: illegal status transition '${prior}' \u2192 '${this.status}'.`);
159
+ }
160
+ /**
161
+ * Resolve the AUTHORITATIVE prior status (commerce Contract pattern). The
162
+ * WeakMap is only populated when {@link initialize} loaded the row from the
163
+ * DB; it is empty for an instance built via
164
+ * `collection.create({ id: <existing>, _skipLoad: true })` — the upsert
165
+ * path that writes onto an existing row without hydrating it. Trusting an
166
+ * empty WeakMap there would treat the write as a brand-new row and skip the
167
+ * guard entirely (a poisonable prior-state).
168
+ *
169
+ * So when this instance carries an `id`, read the persisted row straight
170
+ * from the database and treat its `status` as the prior. Only when no row
171
+ * exists (truly new) do we fall back to the WeakMap. The raw row's `status`
172
+ * column is single-word (no snake_case transform).
173
+ */
174
+ async resolvePriorStatus() {
175
+ if (this.id) try {
176
+ const row = await this.db.get(this.tableName, { id: this.id });
177
+ if (row && row.status != null) return row.status;
178
+ } catch {}
179
+ return loadedLeadStatus.get(this);
180
+ }
181
+ };
182
+ __decorateClass$6([tenantId({ nullable: true })], Lead.prototype, "tenantId", 2);
183
+ __decorateClass$6([field({ required: true })], Lead.prototype, "name", 2);
184
+ __decorateClass$6([crossPackageRef("@happyvertical/smrt-profiles:Profile")], Lead.prototype, "profileId", 2);
185
+ __decorateClass$6([foreignKey("SalesRepresentative")], Lead.prototype, "ownerRepId", 2);
186
+ __decorateClass$6([foreignKey("Lead")], Lead.prototype, "mergedIntoId", 2);
187
+ Lead = __decorateClass$6([TenantScoped({ mode: "optional" }), smrt({
188
+ api: { include: [
189
+ "list",
190
+ "get",
191
+ "create",
192
+ "update"
193
+ ] },
194
+ mcp: { include: [
195
+ "list",
196
+ "get",
197
+ "create"
198
+ ] },
199
+ cli: true
200
+ })], Lead);
201
+ //#endregion
202
+ //#region src/crm/models/Opportunity.ts
203
+ var __defProp$5 = Object.defineProperty;
204
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
205
+ var __decorateClass$5 = (decorators, target, key, kind) => {
206
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
207
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
208
+ if (kind && result) __defProp$5(target, key, result);
209
+ return result;
210
+ };
211
+ var OPPORTUNITY_STATUS_TRANSITIONS = {
212
+ open: ["won", "lost"],
213
+ won: [],
214
+ lost: []
215
+ };
216
+ var loadedOpportunityStatus = /* @__PURE__ */ new WeakMap();
217
+ var Opportunity = class extends SmrtObject {
218
+ tenantId = null;
219
+ name = "";
220
+ leadId = "";
221
+ ownerRepId = "";
222
+ pipelineId = "";
223
+ stageId = "";
224
+ /** Expected deal value in integer cents (INTEGER column). */
225
+ expectedValueCents = 0;
226
+ /** ISO 4217 currency code for `expectedValueCents`. */
227
+ currency = "USD";
228
+ /**
229
+ * Win probability (`0`–`1`, DECIMAL). Adopted from the current stage's
230
+ * default on stage moves unless explicitly overridden.
231
+ */
232
+ probability = 0;
233
+ /** Forecasted close date. */
234
+ expectedCloseAt = null;
235
+ /** Lifecycle status; `open → won|lost` is save-guarded, terminal after. */
236
+ status = "open";
237
+ /**
238
+ * Human-readable outcome note — conventionally the loss reason
239
+ * (`'budget cut'`) or a win annotation.
240
+ */
241
+ outcomeReason = "";
242
+ /** When the deal closed as won. */
243
+ wonAt = null;
244
+ /** When the deal closed as lost. */
245
+ lostAt = null;
246
+ /**
247
+ * Acquisition source copied from the originating lead at qualification
248
+ * time (open string), kept denormalized for reporting.
249
+ */
250
+ sourceKind = "";
251
+ /** Identifier within the `sourceKind` namespace (copied from the lead). */
252
+ sourceId = "";
253
+ /**
254
+ * Free-form JSON object stored as a string. Use
255
+ * {@link getMetadata}/{@link setMetadata} instead of parsing manually.
256
+ */
257
+ metadata = "{}";
258
+ constructor(options = {}) {
259
+ super(options);
260
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
261
+ if (options.name !== void 0) this.name = options.name;
262
+ if (options.leadId !== void 0) this.leadId = options.leadId;
263
+ if (options.ownerRepId !== void 0) this.ownerRepId = options.ownerRepId;
264
+ if (options.pipelineId !== void 0) this.pipelineId = options.pipelineId;
265
+ if (options.stageId !== void 0) this.stageId = options.stageId;
266
+ if (options.expectedValueCents !== void 0) this.expectedValueCents = options.expectedValueCents;
267
+ if (options.currency !== void 0) this.currency = options.currency;
268
+ if (options.probability !== void 0) this.probability = options.probability;
269
+ if (options.expectedCloseAt !== void 0) this.expectedCloseAt = options.expectedCloseAt;
270
+ if (options.status !== void 0) this.status = options.status;
271
+ if (options.outcomeReason !== void 0) this.outcomeReason = options.outcomeReason;
272
+ if (options.wonAt !== void 0) this.wonAt = options.wonAt;
273
+ if (options.lostAt !== void 0) this.lostAt = options.lostAt;
274
+ if (options.sourceKind !== void 0) this.sourceKind = options.sourceKind;
275
+ if (options.sourceId !== void 0) this.sourceId = options.sourceId;
276
+ if (options.metadata !== void 0) this.metadata = options.metadata;
277
+ }
278
+ /** Whether the deal is still in play. */
279
+ isOpen() {
280
+ return this.status === "open";
281
+ }
282
+ /** Whether the deal closed (won or lost). */
283
+ isClosed() {
284
+ return this.status !== "open";
285
+ }
286
+ /** Parse the metadata JSON string; returns `{}` on malformed content. */
287
+ getMetadata() {
288
+ try {
289
+ const parsed = JSON.parse(this.metadata);
290
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
291
+ return {};
292
+ } catch {
293
+ return {};
294
+ }
295
+ }
296
+ /** Serialize and store the metadata object. */
297
+ setMetadata(metadata) {
298
+ this.metadata = JSON.stringify(metadata);
299
+ }
300
+ /**
301
+ * Capture the status the row was loaded with so the save-time transition
302
+ * guard can reject illegal status flips made via raw field assignment.
303
+ * Only persisted rows carry a prior status.
304
+ */
305
+ async initialize() {
306
+ await super.initialize();
307
+ if (await this.isSaved()) loadedOpportunityStatus.set(this, this.status);
308
+ return this;
309
+ }
310
+ /**
311
+ * Validate the status transition before persisting, then save. A forged
312
+ * `status` (e.g. re-opening a lost deal) is rejected here regardless of
313
+ * how the instance was constructed.
314
+ */
315
+ async save() {
316
+ const prior = await this.resolvePriorStatus();
317
+ this.assertOpportunityStatusTransition(prior);
318
+ const result = await super.save();
319
+ loadedOpportunityStatus.set(this, this.status);
320
+ return result;
321
+ }
322
+ /**
323
+ * Reject an illegal status flip done via raw assignment. No-op transitions
324
+ * and brand-new rows are always allowed.
325
+ */
326
+ assertOpportunityStatusTransition(prior) {
327
+ if (prior === void 0) return;
328
+ if (prior === this.status) return;
329
+ if (!(OPPORTUNITY_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`Opportunity ${this.name || this.id}: illegal status transition '${prior}' \u2192 '${this.status}'.`);
330
+ }
331
+ /**
332
+ * Resolve the AUTHORITATIVE prior status (commerce Contract pattern): when
333
+ * this instance carries an `id`, re-read the persisted row and use its
334
+ * `status` as the prior — a create-onto-existing is an update, and an
335
+ * un-hydrated instance must not bypass the guard. Falls back to the
336
+ * load-time WeakMap (empty for truly new rows → `undefined` = new).
337
+ */
338
+ async resolvePriorStatus() {
339
+ if (this.id) try {
340
+ const row = await this.db.get(this.tableName, { id: this.id });
341
+ if (row && row.status != null) return row.status;
342
+ } catch {}
343
+ return loadedOpportunityStatus.get(this);
344
+ }
345
+ };
346
+ __decorateClass$5([tenantId({ nullable: true })], Opportunity.prototype, "tenantId", 2);
347
+ __decorateClass$5([field({ required: true })], Opportunity.prototype, "name", 2);
348
+ __decorateClass$5([foreignKey("Lead")], Opportunity.prototype, "leadId", 2);
349
+ __decorateClass$5([foreignKey("SalesRepresentative")], Opportunity.prototype, "ownerRepId", 2);
350
+ __decorateClass$5([foreignKey("PipelineDefinition")], Opportunity.prototype, "pipelineId", 2);
351
+ __decorateClass$5([foreignKey("PipelineStage")], Opportunity.prototype, "stageId", 2);
352
+ Opportunity = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
353
+ api: { include: [
354
+ "list",
355
+ "get",
356
+ "create",
357
+ "update"
358
+ ] },
359
+ mcp: { include: ["list", "get"] },
360
+ cli: true
361
+ })], Opportunity);
362
+ //#endregion
363
+ //#region src/crm/models/PipelineStage.ts
364
+ var __defProp$4 = Object.defineProperty;
365
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
366
+ var __decorateClass$4 = (decorators, target, key, kind) => {
367
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
368
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
369
+ if (kind && result) __defProp$4(target, key, result);
370
+ return result;
371
+ };
372
+ var PipelineStage = class extends SmrtObject {
373
+ tenantId = null;
374
+ pipelineId = "";
375
+ key = "";
376
+ /** Human-readable stage name shown on board columns. */
377
+ name = "";
378
+ /** Ordering within the pipeline (ascending). Seeded in steps of 10. */
379
+ sortOrder = 0;
380
+ /**
381
+ * Default win probability (`0`–`1`, DECIMAL) adopted by an opportunity
382
+ * entering this stage unless the move overrides it.
383
+ */
384
+ probability = 0;
385
+ /** Terminal flag: moving an opportunity here closes it as `won`. */
386
+ isWon = false;
387
+ /** Terminal flag: moving an opportunity here closes it as `lost`. */
388
+ isLost = false;
389
+ /**
390
+ * Free-form JSON object stored as a string. Use
391
+ * {@link getMetadata}/{@link setMetadata} instead of parsing manually.
392
+ */
393
+ metadata = "{}";
394
+ constructor(options = {}) {
395
+ super(options);
396
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
397
+ if (options.pipelineId !== void 0) this.pipelineId = options.pipelineId;
398
+ if (options.key !== void 0) this.key = options.key;
399
+ if (options.name !== void 0) this.name = options.name;
400
+ if (options.sortOrder !== void 0) this.sortOrder = options.sortOrder;
401
+ if (options.probability !== void 0) this.probability = options.probability;
402
+ if (options.isWon !== void 0) this.isWon = options.isWon;
403
+ if (options.isLost !== void 0) this.isLost = options.isLost;
404
+ if (options.metadata !== void 0) this.metadata = options.metadata;
405
+ }
406
+ /** Whether entering this stage closes the opportunity (won or lost). */
407
+ isTerminal() {
408
+ return this.isWon || this.isLost;
409
+ }
410
+ /** Parse the metadata JSON string; returns `{}` on malformed content. */
411
+ getMetadata() {
412
+ try {
413
+ const parsed = JSON.parse(this.metadata);
414
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
415
+ return {};
416
+ } catch {
417
+ return {};
418
+ }
419
+ }
420
+ /** Serialize and store the metadata object. */
421
+ setMetadata(metadata) {
422
+ this.metadata = JSON.stringify(metadata);
423
+ }
424
+ };
425
+ __decorateClass$4([tenantId({ nullable: true })], PipelineStage.prototype, "tenantId", 2);
426
+ __decorateClass$4([foreignKey("PipelineDefinition", { required: true })], PipelineStage.prototype, "pipelineId", 2);
427
+ __decorateClass$4([field({ required: true })], PipelineStage.prototype, "key", 2);
428
+ PipelineStage = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
429
+ api: { include: [
430
+ "list",
431
+ "get",
432
+ "create",
433
+ "update"
434
+ ] },
435
+ mcp: { include: ["list", "get"] },
436
+ cli: false,
437
+ conflictColumns: ["pipeline_id", "key"]
438
+ })], PipelineStage);
439
+ //#endregion
440
+ //#region src/crm/collections/PipelineStageCollection.ts
441
+ var PipelineStageCollection = class extends SmrtCollection {
442
+ static _itemClass = PipelineStage;
443
+ /**
444
+ * List a pipeline's stages in board order.
445
+ *
446
+ * @param pipelineId - Owning PipelineDefinition id
447
+ * @returns Stages ordered by `sortOrder` ascending
448
+ */
449
+ async findByPipeline(pipelineId) {
450
+ return await this.list({
451
+ where: { pipelineId },
452
+ orderBy: "sort_order ASC"
453
+ });
454
+ }
455
+ /**
456
+ * Find one stage by its natural key within a pipeline.
457
+ *
458
+ * @param pipelineId - Owning PipelineDefinition id
459
+ * @param key - Stage machine key (e.g. `'proposal'`)
460
+ * @returns The stage, or null when the key does not exist in the pipeline
461
+ */
462
+ async findByKey(pipelineId, key) {
463
+ return (await this.list({
464
+ where: {
465
+ pipelineId,
466
+ key
467
+ },
468
+ limit: 1
469
+ }))[0] ?? null;
470
+ }
471
+ };
472
+ //#endregion
473
+ //#region src/crm/models/SalesActivity.ts
474
+ var __defProp$3 = Object.defineProperty;
475
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
476
+ var __decorateClass$3 = (decorators, target, key, kind) => {
477
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
478
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
479
+ if (kind && result) __defProp$3(target, key, result);
480
+ return result;
481
+ };
482
+ var SalesActivity = class extends SmrtObject {
483
+ tenantId = null;
484
+ /** Which model the activity attaches to: `'lead'` or `'opportunity'`. */
485
+ subjectKind = "lead";
486
+ subjectId = "";
487
+ /**
488
+ * Kind of activity — OPEN string so consumers can extend the vocabulary
489
+ * without schema changes. See `SALES_ACTIVITY_KINDS` for the recommended
490
+ * kinds (framework-written rows use `qualification`, `stage_change`,
491
+ * `merge`).
492
+ */
493
+ activityKind = "note";
494
+ /** One-line human-readable description. */
495
+ summary = "";
496
+ /** Next-action due date — set for `task`-like activities. */
497
+ dueAt = null;
498
+ /** When the next action was completed; `null` while open. */
499
+ completedAt = null;
500
+ actorProfileId = "";
501
+ /**
502
+ * Free-form JSON object stored as a string (framework audit rows carry
503
+ * structured detail here, e.g. `stage_change` from/to ids or the full
504
+ * loser snapshot on `merge`). Use {@link getMetadata}/{@link setMetadata}.
505
+ */
506
+ metadata = "{}";
507
+ constructor(options = {}) {
508
+ super(options);
509
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
510
+ if (options.subjectKind !== void 0) this.subjectKind = options.subjectKind;
511
+ if (options.subjectId !== void 0) this.subjectId = options.subjectId;
512
+ if (options.activityKind !== void 0) this.activityKind = options.activityKind;
513
+ if (options.summary !== void 0) this.summary = options.summary;
514
+ if (options.dueAt !== void 0) this.dueAt = options.dueAt;
515
+ if (options.completedAt !== void 0) this.completedAt = options.completedAt;
516
+ if (options.actorProfileId !== void 0) this.actorProfileId = options.actorProfileId;
517
+ if (options.metadata !== void 0) this.metadata = options.metadata;
518
+ }
519
+ /** Whether this is an open next action (due date set, not completed). */
520
+ isOpenTask() {
521
+ return this.dueAt !== null && this.completedAt === null;
522
+ }
523
+ /** Parse the metadata JSON string; returns `{}` on malformed content. */
524
+ getMetadata() {
525
+ try {
526
+ const parsed = JSON.parse(this.metadata);
527
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
528
+ return {};
529
+ } catch {
530
+ return {};
531
+ }
532
+ }
533
+ /** Serialize and store the metadata object. */
534
+ setMetadata(metadata) {
535
+ this.metadata = JSON.stringify(metadata);
536
+ }
537
+ };
538
+ __decorateClass$3([tenantId({ nullable: true })], SalesActivity.prototype, "tenantId", 2);
539
+ __decorateClass$3([field({ required: true })], SalesActivity.prototype, "subjectId", 2);
540
+ __decorateClass$3([crossPackageRef("@happyvertical/smrt-profiles:Profile")], SalesActivity.prototype, "actorProfileId", 2);
541
+ SalesActivity = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
542
+ api: { include: [
543
+ "create",
544
+ "list",
545
+ "get"
546
+ ] },
547
+ mcp: { include: ["list", "create"] },
548
+ cli: false
549
+ })], SalesActivity);
550
+ //#endregion
551
+ //#region src/crm/collections/SalesActivityCollection.ts
552
+ var SalesActivityCollection = class extends SmrtCollection {
553
+ static _itemClass = SalesActivity;
554
+ /**
555
+ * Full trail for one subject, oldest first (chronological reading order).
556
+ *
557
+ * @param subjectKind - `'lead'` or `'opportunity'`
558
+ * @param subjectId - Subject row id
559
+ */
560
+ async findBySubject(subjectKind, subjectId) {
561
+ return await this.list({
562
+ where: {
563
+ subjectKind,
564
+ subjectId
565
+ },
566
+ orderBy: "created_at ASC"
567
+ });
568
+ }
569
+ /**
570
+ * Open next actions for one subject: activities with a due date
571
+ * (`dueAt` set) that have not been completed (`completedAt` null),
572
+ * soonest due first.
573
+ *
574
+ * @param subjectKind - `'lead'` or `'opportunity'`
575
+ * @param subjectId - Subject row id
576
+ */
577
+ async findOpenTasks(subjectKind, subjectId) {
578
+ return await this.list({
579
+ where: {
580
+ subjectKind,
581
+ subjectId,
582
+ "dueAt !=": null,
583
+ completedAt: null
584
+ },
585
+ orderBy: "due_at ASC"
586
+ });
587
+ }
588
+ };
589
+ //#endregion
590
+ //#region src/crm/collections/OpportunityCollection.ts
591
+ var OpportunityCollection = class extends SmrtCollection {
592
+ static _itemClass = Opportunity;
593
+ stageCollectionPromise = null;
594
+ activityCollectionPromise = null;
595
+ /** Sibling stage collection sharing this collection's DB connection. */
596
+ async getStageCollection() {
597
+ if (!this.stageCollectionPromise) this.stageCollectionPromise = PipelineStageCollection.create({ db: this.db });
598
+ return this.stageCollectionPromise;
599
+ }
600
+ /** Sibling activity collection sharing this collection's DB connection. */
601
+ async getActivityCollection() {
602
+ if (!this.activityCollectionPromise) this.activityCollectionPromise = SalesActivityCollection.create({ db: this.db });
603
+ return this.activityCollectionPromise;
604
+ }
605
+ /**
606
+ * Opportunities originating from a given lead, oldest first (the first row
607
+ * is the one `LeadCollection.qualify()` treats as canonical).
608
+ */
609
+ async findByLead(leadId) {
610
+ return await this.list({
611
+ where: { leadId },
612
+ orderBy: "created_at ASC"
613
+ });
614
+ }
615
+ /**
616
+ * Open (still in play) opportunities, optionally narrowed to one pipeline.
617
+ */
618
+ async findOpen(pipelineId) {
619
+ return await this.list({
620
+ where: {
621
+ status: "open",
622
+ ...pipelineId ? { pipelineId } : {}
623
+ },
624
+ orderBy: "created_at ASC"
625
+ });
626
+ }
627
+ /**
628
+ * Move an opportunity to another stage of ITS pipeline, with the full
629
+ * consequence set:
630
+ *
631
+ * - validates the target stage belongs to the opportunity's pipeline
632
+ * (an opportunity created without a pipeline adopts the stage's);
633
+ * - adopts the stage's default `probability` unless
634
+ * `probabilityOverride` is given;
635
+ * - terminal stages close the deal: `isWon` sets `status: 'won'` +
636
+ * `wonAt`; `isLost` sets `status: 'lost'` + `lostAt` (record the loss
637
+ * reason via `outcomeReason`);
638
+ * - writes a `stage_change` SalesActivity carrying from/to detail.
639
+ *
640
+ * Moves after a terminal status are rejected — a closed deal's history is
641
+ * settled; reopening is not supported (create a new opportunity instead).
642
+ *
643
+ * @returns The updated opportunity
644
+ */
645
+ async moveToStage(params) {
646
+ const opportunity = await this.get({ id: params.opportunityId });
647
+ if (!opportunity) throw new Error(`OpportunityCollection.moveToStage: opportunity '${params.opportunityId}' not found`);
648
+ if (opportunity.status !== "open") throw new Error(`OpportunityCollection.moveToStage: opportunity '${params.opportunityId}' is '${opportunity.status}' \u2014 stage moves after a terminal status are not allowed`);
649
+ const stage = await (await this.getStageCollection()).get({ id: params.stageId });
650
+ if (!stage) throw new Error(`OpportunityCollection.moveToStage: stage '${params.stageId}' not found`);
651
+ if (opportunity.pipelineId && stage.pipelineId !== opportunity.pipelineId) throw new Error(`OpportunityCollection.moveToStage: stage '${stage.key || stage.id}' belongs to pipeline '${stage.pipelineId}', not the opportunity's pipeline '${opportunity.pipelineId}'`);
652
+ const fromStageId = opportunity.stageId;
653
+ const fromStatus = opportunity.status;
654
+ if (!opportunity.pipelineId) opportunity.pipelineId = stage.pipelineId;
655
+ opportunity.stageId = stage.id ?? "";
656
+ opportunity.probability = params.probabilityOverride ?? stage.probability;
657
+ if (params.outcomeReason !== void 0) opportunity.outcomeReason = params.outcomeReason;
658
+ if (stage.isWon) {
659
+ opportunity.status = "won";
660
+ opportunity.wonAt = params.now ?? /* @__PURE__ */ new Date();
661
+ } else if (stage.isLost) {
662
+ opportunity.status = "lost";
663
+ opportunity.lostAt = params.now ?? /* @__PURE__ */ new Date();
664
+ }
665
+ await opportunity.save();
666
+ await (await this.getActivityCollection()).create({
667
+ ...opportunity.tenantId !== null ? { tenantId: opportunity.tenantId } : {},
668
+ subjectKind: "opportunity",
669
+ subjectId: opportunity.id ?? "",
670
+ activityKind: "stage_change",
671
+ summary: `Moved to stage '${stage.key}'`,
672
+ actorProfileId: params.actorProfileId ?? "",
673
+ metadata: JSON.stringify({
674
+ fromStageId,
675
+ toStageId: stage.id ?? "",
676
+ toStageKey: stage.key,
677
+ fromStatus,
678
+ toStatus: opportunity.status,
679
+ probability: opportunity.probability,
680
+ ...params.outcomeReason !== void 0 ? { outcomeReason: params.outcomeReason } : {}
681
+ })
682
+ });
683
+ return opportunity;
684
+ }
685
+ };
686
+ //#endregion
687
+ //#region src/crm/models/PipelineDefinition.ts
688
+ var __defProp$2 = Object.defineProperty;
689
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
690
+ var __decorateClass$2 = (decorators, target, key, kind) => {
691
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
692
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
693
+ if (kind && result) __defProp$2(target, key, result);
694
+ return result;
695
+ };
696
+ var PipelineDefinition = class extends SmrtObject {
697
+ tenantId = null;
698
+ key = "";
699
+ /** Human-readable pipeline name shown on CRM surfaces. */
700
+ name = "";
701
+ /**
702
+ * Marks the tenant's default pipeline. Informational for pickers/UI; the
703
+ * seeded default pipeline sets it `true`.
704
+ */
705
+ isDefault = false;
706
+ /** `active` pipelines accept new opportunities; `archived` are read-only history. */
707
+ status = "active";
708
+ /**
709
+ * Free-form JSON object stored as a string. Use
710
+ * {@link getMetadata}/{@link setMetadata} instead of parsing manually.
711
+ */
712
+ metadata = "{}";
713
+ constructor(options = {}) {
714
+ super(options);
715
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
716
+ if (options.key !== void 0) this.key = options.key;
717
+ if (options.name !== void 0) this.name = options.name;
718
+ if (options.isDefault !== void 0) this.isDefault = options.isDefault;
719
+ if (options.status !== void 0) this.status = options.status;
720
+ if (options.metadata !== void 0) this.metadata = options.metadata;
721
+ }
722
+ /** Whether the pipeline accepts new opportunities. */
723
+ isActive() {
724
+ return this.status === "active";
725
+ }
726
+ /** Parse the metadata JSON string; returns `{}` on malformed content. */
727
+ getMetadata() {
728
+ try {
729
+ const parsed = JSON.parse(this.metadata);
730
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
731
+ return {};
732
+ } catch {
733
+ return {};
734
+ }
735
+ }
736
+ /** Serialize and store the metadata object. */
737
+ setMetadata(metadata) {
738
+ this.metadata = JSON.stringify(metadata);
739
+ }
740
+ };
741
+ __decorateClass$2([tenantId({ nullable: true })], PipelineDefinition.prototype, "tenantId", 2);
742
+ __decorateClass$2([field({ required: true })], PipelineDefinition.prototype, "key", 2);
743
+ PipelineDefinition = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
744
+ api: { include: [
745
+ "list",
746
+ "get",
747
+ "create",
748
+ "update"
749
+ ] },
750
+ mcp: { include: ["list", "get"] },
751
+ cli: true,
752
+ conflictColumns: ["tenant_id", "key"]
753
+ })], PipelineDefinition);
754
+ //#endregion
755
+ //#region src/crm/types.ts
756
+ var SALES_REPRESENTATIVE_STATUSES = [
757
+ "active",
758
+ "inactive",
759
+ "suspended"
760
+ ];
761
+ var LEAD_STATUSES = [
762
+ "new",
763
+ "working",
764
+ "qualified",
765
+ "disqualified",
766
+ "merged"
767
+ ];
768
+ var PIPELINE_STATUSES = ["active", "archived"];
769
+ var OPPORTUNITY_STATUSES = [
770
+ "open",
771
+ "won",
772
+ "lost"
773
+ ];
774
+ var SALES_ACTIVITY_SUBJECT_KINDS = ["lead", "opportunity"];
775
+ var SALES_ACTIVITY_KINDS = [
776
+ "note",
777
+ "call",
778
+ "email",
779
+ "meeting",
780
+ "task",
781
+ "assignment",
782
+ "qualification",
783
+ "stage_change",
784
+ "merge",
785
+ "status_change"
786
+ ];
787
+ var DEFAULT_PIPELINE_KEY = "default";
788
+ var DEFAULT_PIPELINE_STAGES = [
789
+ {
790
+ key: "new",
791
+ name: "New",
792
+ probability: .1
793
+ },
794
+ {
795
+ key: "qualified",
796
+ name: "Qualified",
797
+ probability: .25
798
+ },
799
+ {
800
+ key: "discovery",
801
+ name: "Discovery",
802
+ probability: .4
803
+ },
804
+ {
805
+ key: "proposal",
806
+ name: "Proposal",
807
+ probability: .6
808
+ },
809
+ {
810
+ key: "negotiation",
811
+ name: "Negotiation",
812
+ probability: .8
813
+ },
814
+ {
815
+ key: "closed_won",
816
+ name: "Closed Won",
817
+ probability: 1,
818
+ isWon: true
819
+ },
820
+ {
821
+ key: "closed_lost",
822
+ name: "Closed Lost",
823
+ probability: 0,
824
+ isLost: true
825
+ }
826
+ ];
827
+ //#endregion
828
+ //#region src/crm/collections/PipelineDefinitionCollection.ts
829
+ function requireId$1(value, what) {
830
+ if (!value) throw new Error(`PipelineDefinitionCollection: ${what} has no id`);
831
+ return value;
832
+ }
833
+ var PipelineDefinitionCollection = class extends SmrtCollection {
834
+ static _itemClass = PipelineDefinition;
835
+ stageCollectionPromise = null;
836
+ /** Sibling stage collection sharing this collection's DB connection. */
837
+ async getStageCollection() {
838
+ if (!this.stageCollectionPromise) this.stageCollectionPromise = PipelineStageCollection.create({ db: this.db });
839
+ return this.stageCollectionPromise;
840
+ }
841
+ /**
842
+ * Find one pipeline by its machine key (natural key within the tenant
843
+ * scope — ambient tenancy auto-filters the lookup).
844
+ */
845
+ async findByKey(key) {
846
+ return (await this.list({
847
+ where: { key },
848
+ limit: 1
849
+ }))[0] ?? null;
850
+ }
851
+ /**
852
+ * Find the pipeline flagged `isDefault` (informational designation).
853
+ */
854
+ async findDefault() {
855
+ return (await this.list({
856
+ where: { isDefault: true },
857
+ orderBy: "created_at ASC",
858
+ limit: 1
859
+ }))[0] ?? null;
860
+ }
861
+ /**
862
+ * A pipeline's stages in board order (`sortOrder` ascending).
863
+ */
864
+ async getStages(pipelineId) {
865
+ return await (await this.getStageCollection()).findByPipeline(pipelineId);
866
+ }
867
+ /**
868
+ * Idempotently find-or-create the seeded default pipeline (key
869
+ * `'default'`, `isDefault: true`) and its 7 default stages:
870
+ * `new(0.1) → qualified(0.25) → discovery(0.4) → proposal(0.6) →
871
+ * negotiation(0.8) → closed_won(1.0, isWon) → closed_lost(0.0, isLost)`.
872
+ *
873
+ * Idempotency is per stage KEY: a second call creates nothing, and a
874
+ * partially-seeded pipeline (e.g. an interrupted first run) is healed by
875
+ * creating only the missing keys. Existing stages are never overwritten —
876
+ * renames, re-weights, reorders, and custom stages added via normal
877
+ * PipelineStage operations all survive re-runs.
878
+ *
879
+ * Tenancy: under an ambient `withTenant()` context the pipeline/stages
880
+ * auto-scope to that tenant; pass `tenantId` to seed for a specific tenant
881
+ * from a system/global context. Lookup is by natural key, so each tenant
882
+ * gets exactly one seeded default pipeline.
883
+ *
884
+ * @returns The pipeline plus ALL of its stages ordered by `sortOrder`
885
+ * (including any custom stages added since seeding)
886
+ */
887
+ async ensureDefaultPipeline(params = {}) {
888
+ const tenantScope = params.tenantId !== void 0 ? { tenantId: params.tenantId } : {};
889
+ let pipeline = (await this.list({
890
+ where: {
891
+ key: DEFAULT_PIPELINE_KEY,
892
+ ...tenantScope
893
+ },
894
+ limit: 1
895
+ }))[0];
896
+ if (!pipeline) pipeline = await this.create({
897
+ ...tenantScope,
898
+ key: DEFAULT_PIPELINE_KEY,
899
+ name: "Default Sales Pipeline",
900
+ isDefault: true,
901
+ status: "active"
902
+ });
903
+ const pipelineId = requireId$1(pipeline.id, "default pipeline");
904
+ const stagesCollection = await this.getStageCollection();
905
+ const existing = await stagesCollection.findByPipeline(pipelineId);
906
+ const existingKeys = new Set(existing.map((stage) => stage.key));
907
+ const stageTenantScope = pipeline.tenantId !== null ? { tenantId: pipeline.tenantId } : tenantScope;
908
+ for (const [index, seed] of DEFAULT_PIPELINE_STAGES.entries()) {
909
+ if (existingKeys.has(seed.key)) continue;
910
+ await stagesCollection.create({
911
+ ...stageTenantScope,
912
+ pipelineId,
913
+ key: seed.key,
914
+ name: seed.name,
915
+ sortOrder: (index + 1) * 10,
916
+ probability: seed.probability,
917
+ isWon: seed.isWon ?? false,
918
+ isLost: seed.isLost ?? false
919
+ });
920
+ }
921
+ const stages = await stagesCollection.findByPipeline(pipelineId);
922
+ return {
923
+ pipeline,
924
+ stages
925
+ };
926
+ }
927
+ };
928
+ //#endregion
929
+ //#region src/crm/collections/LeadCollection.ts
930
+ var MERGE_FILL_CONTACT_FIELDS = [
931
+ "contactName",
932
+ "email",
933
+ "phone",
934
+ "organizationName"
935
+ ];
936
+ function requireId(value, what) {
937
+ if (!value) throw new Error(`LeadCollection: ${what} has no id`);
938
+ return value;
939
+ }
940
+ var LeadCollection = class extends SmrtCollection {
941
+ static _itemClass = Lead;
942
+ opportunityCollectionPromise = null;
943
+ pipelineCollectionPromise = null;
944
+ activityCollectionPromise = null;
945
+ /** Sibling opportunity collection sharing this collection's DB connection. */
946
+ async getOpportunityCollection() {
947
+ if (!this.opportunityCollectionPromise) this.opportunityCollectionPromise = OpportunityCollection.create({ db: this.db });
948
+ return this.opportunityCollectionPromise;
949
+ }
950
+ /** Sibling pipeline collection sharing this collection's DB connection. */
951
+ async getPipelineCollection() {
952
+ if (!this.pipelineCollectionPromise) this.pipelineCollectionPromise = PipelineDefinitionCollection.create({ db: this.db });
953
+ return this.pipelineCollectionPromise;
954
+ }
955
+ /** Sibling activity collection sharing this collection's DB connection. */
956
+ async getActivityCollection() {
957
+ if (!this.activityCollectionPromise) this.activityCollectionPromise = SalesActivityCollection.create({ db: this.db });
958
+ return this.activityCollectionPromise;
959
+ }
960
+ /** Leads in a given lifecycle status, newest first. */
961
+ async findByStatus(status) {
962
+ return await this.list({
963
+ where: { status },
964
+ orderBy: "created_at DESC"
965
+ });
966
+ }
967
+ /** Leads owned by a given sales rep, newest first. */
968
+ async findByOwner(ownerRepId) {
969
+ return await this.list({
970
+ where: { ownerRepId },
971
+ orderBy: "created_at DESC"
972
+ });
973
+ }
974
+ /**
975
+ * Qualify a lead into an Opportunity.
976
+ *
977
+ * Transitions the lead to `qualified` (stamping `qualifiedAt` once) and
978
+ * creates an Opportunity at the FIRST stage of the target pipeline —
979
+ * `pipelineId` when given, otherwise the seeded default pipeline
980
+ * (`ensureDefaultPipeline()`). The opportunity adopts the first stage's
981
+ * probability, copies the lead's `sourceKind`/`sourceId` for reporting,
982
+ * and defaults its name/owner from the lead. A `qualification`
983
+ * SalesActivity is written on BOTH the lead and the opportunity.
984
+ *
985
+ * IDEMPOTENT: when the lead is already `qualified` and an opportunity
986
+ * exists for it, that opportunity is returned unchanged (no new rows, no
987
+ * new activities). A qualified lead with no opportunity (interrupted
988
+ * earlier run) is healed by creating the missing opportunity.
989
+ *
990
+ * Illegal source statuses are rejected by the lead's save-time transition
991
+ * guard (`disqualified`/`merged` leads cannot be qualified).
992
+ *
993
+ * @returns The (existing or newly created) opportunity
994
+ */
995
+ async qualify(params) {
996
+ const lead = await this.get({ id: params.leadId });
997
+ if (!lead) throw new Error(`LeadCollection.qualify: lead '${params.leadId}' not found`);
998
+ const leadId = requireId(lead.id, `lead '${params.leadId}'`);
999
+ const opportunities = await this.getOpportunityCollection();
1000
+ if (lead.status === "qualified") {
1001
+ const existing = await opportunities.findByLead(leadId);
1002
+ if (existing[0]) return existing[0];
1003
+ }
1004
+ const pipelines = await this.getPipelineCollection();
1005
+ let pipeline;
1006
+ let stages;
1007
+ if (params.pipelineId) {
1008
+ const found = await pipelines.get({ id: params.pipelineId });
1009
+ if (!found) throw new Error(`LeadCollection.qualify: pipeline '${params.pipelineId}' not found`);
1010
+ pipeline = found;
1011
+ stages = await pipelines.getStages(requireId(pipeline.id, "pipeline"));
1012
+ } else ({pipeline, stages} = await pipelines.ensureDefaultPipeline({ ...lead.tenantId !== null ? { tenantId: lead.tenantId } : {} }));
1013
+ const firstStage = stages[0];
1014
+ if (!firstStage) throw new Error(`LeadCollection.qualify: pipeline '${pipeline.key || pipeline.id}' has no stages`);
1015
+ if (lead.status !== "qualified") {
1016
+ lead.status = "qualified";
1017
+ if (!lead.qualifiedAt) lead.qualifiedAt = params.now ?? /* @__PURE__ */ new Date();
1018
+ await lead.save();
1019
+ }
1020
+ const opportunity = await opportunities.create({
1021
+ ...lead.tenantId !== null ? { tenantId: lead.tenantId } : {},
1022
+ name: params.opportunityName ?? lead.name,
1023
+ leadId,
1024
+ ownerRepId: params.ownerRepId ?? lead.ownerRepId,
1025
+ pipelineId: pipeline.id ?? "",
1026
+ stageId: firstStage.id ?? "",
1027
+ probability: firstStage.probability,
1028
+ expectedValueCents: params.expectedValueCents ?? 0,
1029
+ currency: params.currency ?? "USD",
1030
+ expectedCloseAt: params.expectedCloseAt ?? null,
1031
+ status: "open",
1032
+ sourceKind: lead.sourceKind,
1033
+ sourceId: lead.sourceId
1034
+ });
1035
+ const opportunityId = requireId(opportunity.id, "created opportunity");
1036
+ const activities = await this.getActivityCollection();
1037
+ const tenantScope = lead.tenantId !== null ? { tenantId: lead.tenantId } : {};
1038
+ await activities.create({
1039
+ ...tenantScope,
1040
+ subjectKind: "lead",
1041
+ subjectId: leadId,
1042
+ activityKind: "qualification",
1043
+ summary: `Qualified into opportunity '${opportunity.name}'`,
1044
+ metadata: JSON.stringify({
1045
+ opportunityId,
1046
+ pipelineId: pipeline.id ?? "",
1047
+ stageId: firstStage.id ?? "",
1048
+ stageKey: firstStage.key
1049
+ })
1050
+ });
1051
+ await activities.create({
1052
+ ...tenantScope,
1053
+ subjectKind: "opportunity",
1054
+ subjectId: opportunityId,
1055
+ activityKind: "qualification",
1056
+ summary: `Created from qualified lead '${lead.name}'`,
1057
+ metadata: JSON.stringify({
1058
+ leadId,
1059
+ pipelineId: pipeline.id ?? "",
1060
+ stageId: firstStage.id ?? "",
1061
+ stageKey: firstStage.key
1062
+ })
1063
+ });
1064
+ return opportunity;
1065
+ }
1066
+ /**
1067
+ * Audited duplicate merge: fold the `loserId` lead into the `winnerId`
1068
+ * lead.
1069
+ *
1070
+ * Validations: the ids must differ, both leads must exist, the loser must
1071
+ * not already be merged, the winner must not itself be merged (merge into
1072
+ * the chain head instead), and the merge must not create a
1073
+ * `mergedIntoId` cycle.
1074
+ *
1075
+ * Effects:
1076
+ * - EMPTY winner contact fields (`contactName`/`email`/`phone`/
1077
+ * `organizationName`) are filled from the loser; non-empty winner data
1078
+ * is never overwritten;
1079
+ * - the loser's acquisition context is appended into the winner's under a
1080
+ * `mergedSources` array (both histories preserved verbatim);
1081
+ * - the loser becomes terminal: `status: 'merged'` +
1082
+ * `mergedIntoId: winnerId`;
1083
+ * - a `merge` SalesActivity is written on BOTH leads, each carrying a full
1084
+ * pre-merge loser snapshot in metadata;
1085
+ * - the loser's existing activities STAY attached to the loser (history
1086
+ * preserved in place) — read the combined trail via
1087
+ * {@link activitiesIncludingMerged}.
1088
+ */
1089
+ async mergeLeads(params) {
1090
+ const { winnerId, loserId } = params;
1091
+ if (winnerId === loserId) throw new Error(`LeadCollection.mergeLeads: winner and loser must be distinct leads (got '${winnerId}' twice)`);
1092
+ const winner = await this.get({ id: winnerId });
1093
+ if (!winner) throw new Error(`LeadCollection.mergeLeads: winner lead '${winnerId}' not found`);
1094
+ const loser = await this.get({ id: loserId });
1095
+ if (!loser) throw new Error(`LeadCollection.mergeLeads: loser lead '${loserId}' not found`);
1096
+ if (loser.status === "merged") throw new Error(`LeadCollection.mergeLeads: lead '${loserId}' is already merged into '${loser.mergedIntoId}'`);
1097
+ if (winner.status === "merged") throw new Error(`LeadCollection.mergeLeads: winner lead '${winnerId}' has itself been merged into '${winner.mergedIntoId}' \u2014 merge into the chain head instead`);
1098
+ const visited = /* @__PURE__ */ new Set([winnerId]);
1099
+ let cursor = winner.mergedIntoId;
1100
+ while (cursor) {
1101
+ if (cursor === loserId) throw new Error(`LeadCollection.mergeLeads: merging '${loserId}' into '${winnerId}' would create a merge cycle`);
1102
+ if (visited.has(cursor)) break;
1103
+ visited.add(cursor);
1104
+ cursor = (await this.get({ id: cursor }))?.mergedIntoId ?? "";
1105
+ }
1106
+ const loserSnapshot = loser.toJSON();
1107
+ const filledFields = [];
1108
+ for (const fieldName of MERGE_FILL_CONTACT_FIELDS) if (winner[fieldName] === "" && loser[fieldName] !== "") {
1109
+ winner[fieldName] = loser[fieldName];
1110
+ filledFields.push(fieldName);
1111
+ }
1112
+ const winnerContext = winner.getAcquisitionContext();
1113
+ const priorSources = winnerContext.mergedSources;
1114
+ const mergedSources = Array.isArray(priorSources) ? [...priorSources] : [];
1115
+ mergedSources.push({
1116
+ leadId: loserId,
1117
+ mergedAt: (/* @__PURE__ */ new Date()).toISOString(),
1118
+ sourceKind: loser.sourceKind,
1119
+ sourceId: loser.sourceId,
1120
+ acquisitionContext: loser.getAcquisitionContext()
1121
+ });
1122
+ winner.setAcquisitionContext({
1123
+ ...winnerContext,
1124
+ mergedSources
1125
+ });
1126
+ await winner.save();
1127
+ loser.status = "merged";
1128
+ loser.mergedIntoId = winnerId;
1129
+ await loser.save();
1130
+ const activities = await this.getActivityCollection();
1131
+ const actorProfileId = params.actorProfileId ?? "";
1132
+ const auditDetail = {
1133
+ winnerId,
1134
+ loserId,
1135
+ reason: params.reason ?? "",
1136
+ filledFields,
1137
+ loserSnapshot
1138
+ };
1139
+ await activities.create({
1140
+ ...winner.tenantId !== null ? { tenantId: winner.tenantId } : {},
1141
+ subjectKind: "lead",
1142
+ subjectId: winnerId,
1143
+ activityKind: "merge",
1144
+ summary: `Merged duplicate lead '${loser.name}' into this lead`,
1145
+ actorProfileId,
1146
+ metadata: JSON.stringify({
1147
+ ...auditDetail,
1148
+ direction: "absorbed"
1149
+ })
1150
+ });
1151
+ await activities.create({
1152
+ ...loser.tenantId !== null ? { tenantId: loser.tenantId } : {},
1153
+ subjectKind: "lead",
1154
+ subjectId: loserId,
1155
+ activityKind: "merge",
1156
+ summary: `Merged into lead '${winner.name}'`,
1157
+ actorProfileId,
1158
+ metadata: JSON.stringify({
1159
+ ...auditDetail,
1160
+ direction: "merged_away"
1161
+ })
1162
+ });
1163
+ return {
1164
+ winner,
1165
+ loser
1166
+ };
1167
+ }
1168
+ /**
1169
+ * The lead's activity trail INCLUDING the trails of every lead merged into
1170
+ * it, transitively (losers keep their own activities; this read
1171
+ * re-assembles the full history across `mergedIntoId` chains).
1172
+ *
1173
+ * Traversal is breadth-first over merge children with a visited-set guard,
1174
+ * so malformed cyclic data cannot loop. Results are in chronological
1175
+ * order.
1176
+ */
1177
+ async activitiesIncludingMerged(leadId) {
1178
+ const visited = /* @__PURE__ */ new Set();
1179
+ let frontier = [leadId];
1180
+ while (frontier.length > 0) {
1181
+ const batch = frontier.filter((id) => id && !visited.has(id));
1182
+ if (batch.length === 0) break;
1183
+ for (const id of batch) visited.add(id);
1184
+ frontier = (await this.list({ where: { "mergedIntoId in": batch } })).map((child) => child.id ?? "").filter((id) => id !== "");
1185
+ }
1186
+ return await (await this.getActivityCollection()).list({
1187
+ where: {
1188
+ subjectKind: "lead",
1189
+ "subjectId in": Array.from(visited)
1190
+ },
1191
+ orderBy: "created_at ASC"
1192
+ });
1193
+ }
1194
+ };
1195
+ //#endregion
1196
+ //#region src/crm/models/OpportunityConversion.ts
1197
+ var __defProp$1 = Object.defineProperty;
1198
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
1199
+ var __decorateClass$1 = (decorators, target, key, kind) => {
1200
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
1201
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1202
+ if (kind && result) __defProp$1(target, key, result);
1203
+ return result;
1204
+ };
1205
+ var OpportunityConversion = class extends SmrtObject {
1206
+ tenantId = null;
1207
+ opportunityId = "";
1208
+ targetKind = "";
1209
+ targetId = "";
1210
+ /** Optional human-readable note about the conversion. */
1211
+ note = "";
1212
+ /**
1213
+ * Free-form JSON object stored as a string. Use
1214
+ * {@link getMetadata}/{@link setMetadata} instead of parsing manually.
1215
+ */
1216
+ metadata = "{}";
1217
+ constructor(options = {}) {
1218
+ super(options);
1219
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1220
+ if (options.opportunityId !== void 0) this.opportunityId = options.opportunityId;
1221
+ if (options.targetKind !== void 0) this.targetKind = options.targetKind;
1222
+ if (options.targetId !== void 0) this.targetId = options.targetId;
1223
+ if (options.note !== void 0) this.note = options.note;
1224
+ if (options.metadata !== void 0) this.metadata = options.metadata;
1225
+ }
1226
+ /** Parse the metadata JSON string; returns `{}` on malformed content. */
1227
+ getMetadata() {
1228
+ try {
1229
+ const parsed = JSON.parse(this.metadata);
1230
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
1231
+ return {};
1232
+ } catch {
1233
+ return {};
1234
+ }
1235
+ }
1236
+ /** Serialize and store the metadata object. */
1237
+ setMetadata(metadata) {
1238
+ this.metadata = JSON.stringify(metadata);
1239
+ }
1240
+ /**
1241
+ * Save with the append-only guard: a fresh instance whose
1242
+ * `(opportunityId, targetKind, targetId)` natural key already belongs to
1243
+ * a DIFFERENT row is refused — the conflict-column upsert would replace
1244
+ * the existing conversion link (note and id) without the won-opportunity
1245
+ * validation. Idempotent linking goes through
1246
+ * `OpportunityConversionCollection.recordConversion()`, which finds
1247
+ * first and reports `{ created: false }`.
1248
+ */
1249
+ async save() {
1250
+ if (this.opportunityId && this.targetKind && this.targetId) try {
1251
+ const res = await this.db.query(`SELECT id FROM ${this.tableName} WHERE opportunity_id = $1 AND target_kind = $2 AND target_id = $3`, this.opportunityId, this.targetKind, this.targetId);
1252
+ if ((Array.isArray(res) ? res : res.rows ?? []).find((row) => row.id !== this.id)) throw new Error(`OpportunityConversion ${this.opportunityId}/${this.targetKind}/${this.targetId}: this conversion link already exists \u2014 conversion links are append-only. Use OpportunityConversionCollection.recordConversion() for idempotent linking.`);
1253
+ } catch (error) {
1254
+ if (error instanceof Error && error.message.includes("append-only")) throw error;
1255
+ }
1256
+ return await super.save();
1257
+ }
1258
+ };
1259
+ __decorateClass$1([tenantId({ nullable: true })], OpportunityConversion.prototype, "tenantId", 2);
1260
+ __decorateClass$1([foreignKey("Opportunity", { required: true })], OpportunityConversion.prototype, "opportunityId", 2);
1261
+ __decorateClass$1([field({ required: true })], OpportunityConversion.prototype, "targetKind", 2);
1262
+ __decorateClass$1([field({ required: true })], OpportunityConversion.prototype, "targetId", 2);
1263
+ OpportunityConversion = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
1264
+ api: { include: [
1265
+ "list",
1266
+ "get",
1267
+ "create"
1268
+ ] },
1269
+ mcp: { include: ["list", "get"] },
1270
+ cli: false,
1271
+ conflictColumns: [
1272
+ "opportunity_id",
1273
+ "target_kind",
1274
+ "target_id"
1275
+ ]
1276
+ })], OpportunityConversion);
1277
+ //#endregion
1278
+ //#region src/crm/collections/OpportunityConversionCollection.ts
1279
+ var OpportunityConversionCollection = class extends SmrtCollection {
1280
+ static _itemClass = OpportunityConversion;
1281
+ opportunityCollectionPromise = null;
1282
+ /** Sibling opportunity collection sharing this collection's DB connection. */
1283
+ async getOpportunityCollection() {
1284
+ if (!this.opportunityCollectionPromise) this.opportunityCollectionPromise = OpportunityCollection.create({ db: this.db });
1285
+ return this.opportunityCollectionPromise;
1286
+ }
1287
+ /** All conversion links recorded for one opportunity, oldest first. */
1288
+ async findByOpportunity(opportunityId) {
1289
+ return await this.list({
1290
+ where: { opportunityId },
1291
+ orderBy: "created_at ASC"
1292
+ });
1293
+ }
1294
+ /**
1295
+ * Record that a WON opportunity materialized into a downstream record,
1296
+ * idempotently on the natural key `(opportunityId, targetKind, targetId)`.
1297
+ *
1298
+ * Preconditions: the opportunity must exist and its status must be `'won'`
1299
+ * — conversions from open or lost deals are rejected with a clear error.
1300
+ * CRM only records the link; creating the downstream record itself
1301
+ * (client, project, contract, subscription, …) is the caller's job.
1302
+ *
1303
+ * @returns `{ conversion, created }` — `created: false` when the link
1304
+ * already existed (the existing row is returned untouched)
1305
+ */
1306
+ async recordConversion(params) {
1307
+ const opportunity = await (await this.getOpportunityCollection()).get({ id: params.opportunityId });
1308
+ if (!opportunity) throw new Error(`OpportunityConversionCollection.recordConversion: opportunity '${params.opportunityId}' not found`);
1309
+ if (opportunity.status !== "won") throw new Error(`OpportunityConversionCollection.recordConversion: opportunity '${params.opportunityId}' has status '${opportunity.status}' \u2014 conversions can only be recorded for won opportunities`);
1310
+ const existing = await this.list({
1311
+ where: {
1312
+ opportunityId: params.opportunityId,
1313
+ targetKind: params.targetKind,
1314
+ targetId: params.targetId
1315
+ },
1316
+ limit: 1
1317
+ });
1318
+ if (existing[0]) return {
1319
+ conversion: existing[0],
1320
+ created: false
1321
+ };
1322
+ return {
1323
+ conversion: await this.create({
1324
+ ...opportunity.tenantId !== null ? { tenantId: opportunity.tenantId } : {},
1325
+ opportunityId: params.opportunityId,
1326
+ targetKind: params.targetKind,
1327
+ targetId: params.targetId,
1328
+ note: params.note ?? ""
1329
+ }),
1330
+ created: true
1331
+ };
1332
+ }
1333
+ };
1334
+ //#endregion
1335
+ //#region src/crm/models/SalesRepresentative.ts
1336
+ var __defProp = Object.defineProperty;
1337
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
1338
+ var __decorateClass = (decorators, target, key, kind) => {
1339
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
1340
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1341
+ if (kind && result) __defProp(target, key, result);
1342
+ return result;
1343
+ };
1344
+ var SalesRepresentative = class extends SmrtObject {
1345
+ tenantId = null;
1346
+ profileId = "";
1347
+ earnerId = "";
1348
+ /** Role status — only `active` reps should receive new assignments. */
1349
+ status = "active";
1350
+ /** Job title shown on CRM surfaces (e.g. `'Account Executive'`). */
1351
+ title = "";
1352
+ /**
1353
+ * Free-form JSON object stored as a string. Use
1354
+ * {@link getMetadata}/{@link setMetadata} instead of parsing manually.
1355
+ */
1356
+ metadata = "{}";
1357
+ constructor(options = {}) {
1358
+ super(options);
1359
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1360
+ if (options.profileId !== void 0) this.profileId = options.profileId;
1361
+ if (options.earnerId !== void 0) this.earnerId = options.earnerId;
1362
+ if (options.status !== void 0) this.status = options.status;
1363
+ if (options.title !== void 0) this.title = options.title;
1364
+ if (options.metadata !== void 0) this.metadata = options.metadata;
1365
+ }
1366
+ /** Whether the rep may receive new lead/opportunity assignments. */
1367
+ isActive() {
1368
+ return this.status === "active";
1369
+ }
1370
+ /** Parse the metadata JSON string; returns `{}` on malformed content. */
1371
+ getMetadata() {
1372
+ try {
1373
+ const parsed = JSON.parse(this.metadata);
1374
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
1375
+ return {};
1376
+ } catch {
1377
+ return {};
1378
+ }
1379
+ }
1380
+ /** Serialize and store the metadata object. */
1381
+ setMetadata(metadata) {
1382
+ this.metadata = JSON.stringify(metadata);
1383
+ }
1384
+ };
1385
+ __decorateClass([tenantId({ nullable: true })], SalesRepresentative.prototype, "tenantId", 2);
1386
+ __decorateClass([crossPackageRef("@happyvertical/smrt-profiles:Profile")], SalesRepresentative.prototype, "profileId", 2);
1387
+ __decorateClass([foreignKey("Earner")], SalesRepresentative.prototype, "earnerId", 2);
1388
+ SalesRepresentative = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
1389
+ api: { include: [
1390
+ "list",
1391
+ "get",
1392
+ "create",
1393
+ "update"
1394
+ ] },
1395
+ mcp: { include: ["list", "get"] },
1396
+ cli: true
1397
+ })], SalesRepresentative);
1398
+ //#endregion
1399
+ //#region src/crm/collections/SalesRepresentativeCollection.ts
1400
+ var SalesRepresentativeCollection = class extends SmrtCollection {
1401
+ static _itemClass = SalesRepresentative;
1402
+ /**
1403
+ * Find rep roles held by a given profile (a profile may hold the role in
1404
+ * several tenants/contexts).
1405
+ *
1406
+ * @param profileId - smrt-profiles Profile id
1407
+ * @returns Reps for that profile, newest first
1408
+ */
1409
+ async findByProfile(profileId) {
1410
+ return await this.list({
1411
+ where: { profileId },
1412
+ orderBy: "created_at DESC"
1413
+ });
1414
+ }
1415
+ /**
1416
+ * Find all active reps (eligible for new lead/opportunity assignment).
1417
+ */
1418
+ async findActive() {
1419
+ return await this.list({
1420
+ where: { status: "active" },
1421
+ orderBy: "created_at DESC"
1422
+ });
1423
+ }
1424
+ };
1425
+ //#endregion
1426
+ export { Lead as S, SalesActivityCollection as _, LeadCollection as a, PipelineStage as b, DEFAULT_PIPELINE_STAGES as c, PIPELINE_STATUSES as d, SALES_ACTIVITY_KINDS as f, OpportunityCollection as g, PipelineDefinition as h, OpportunityConversion as i, LEAD_STATUSES as l, SALES_REPRESENTATIVE_STATUSES as m, SalesRepresentative as n, PipelineDefinitionCollection as o, SALES_ACTIVITY_SUBJECT_KINDS as p, OpportunityConversionCollection as r, DEFAULT_PIPELINE_KEY as s, SalesRepresentativeCollection as t, OPPORTUNITY_STATUSES as u, SalesActivity as v, Opportunity as x, PipelineStageCollection as y };
1427
+
1428
+ //# sourceMappingURL=crm-DwEz7E2r.js.map