@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,2181 @@
1
+ import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
2
+ import { TenantScoped, tenantId } from "@happyvertical/smrt-tenancy";
3
+ //#region src/commissions/models/CommissionAdjustment.ts
4
+ var __defProp$5 = Object.defineProperty;
5
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
6
+ var __decorateClass$5 = (decorators, target, key, kind) => {
7
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(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$5(target, key, result);
10
+ return result;
11
+ };
12
+ var frozenAdjustmentSnapshot = /* @__PURE__ */ new WeakMap();
13
+ var CommissionAdjustment = class extends SmrtObject {
14
+ tenantId = null;
15
+ commissionId = "";
16
+ earnerId = "";
17
+ /** What kind of correction this is. */
18
+ adjustmentKind = "correction";
19
+ /**
20
+ * SIGNED amount in integer cents. Negative claws earnings back (refund,
21
+ * chargeback); positive credits extra.
22
+ */
23
+ amountCents = 0;
24
+ /** ISO 4217 currency — must match the parent commission's. */
25
+ currency = "USD";
26
+ reason = "";
27
+ createdByProfileId = "";
28
+ payoutId = "";
29
+ /** Additional metadata as a JSON string. Frozen once persisted. */
30
+ metadata = "{}";
31
+ constructor(options = {}) {
32
+ super(options);
33
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
34
+ if (options.commissionId !== void 0) this.commissionId = options.commissionId;
35
+ if (options.earnerId !== void 0) this.earnerId = options.earnerId;
36
+ if (options.adjustmentKind !== void 0) this.adjustmentKind = options.adjustmentKind;
37
+ if (options.amountCents !== void 0) this.amountCents = options.amountCents;
38
+ if (options.currency !== void 0) this.currency = options.currency;
39
+ if (options.reason !== void 0) this.reason = options.reason;
40
+ if (options.createdByProfileId !== void 0) this.createdByProfileId = options.createdByProfileId;
41
+ if (options.payoutId !== void 0) this.payoutId = options.payoutId;
42
+ if (options.metadata !== void 0) this.metadata = options.metadata;
43
+ }
44
+ /**
45
+ * Capture the frozen-fields snapshot when the row was loaded from the
46
+ * database — from that moment on, only {@link payoutId} may change.
47
+ */
48
+ async initialize() {
49
+ await super.initialize();
50
+ if (await this.isSaved()) frozenAdjustmentSnapshot.set(this, this.serializeFrozenSnapshot());
51
+ return this;
52
+ }
53
+ /** `true` once a payout batch has stamped {@link payoutId}. */
54
+ isSettled() {
55
+ return !!this.payoutId;
56
+ }
57
+ /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
58
+ getMetadata() {
59
+ if (!this.metadata) return {};
60
+ try {
61
+ const parsed = JSON.parse(this.metadata);
62
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
63
+ } catch {
64
+ return {};
65
+ }
66
+ }
67
+ /** Serialize and store {@link metadata}. */
68
+ setMetadata(data) {
69
+ this.metadata = JSON.stringify(data ?? {});
70
+ }
71
+ /**
72
+ * Save with the append-only guard: once the row has been persisted, every
73
+ * field except `payoutId` must match the captured snapshot. Corrections
74
+ * to a wrong adjustment are new counter-adjustments, never edits.
75
+ */
76
+ async save() {
77
+ this.assertImmutableOncePersisted();
78
+ const result = await super.save();
79
+ if (!frozenAdjustmentSnapshot.has(this)) frozenAdjustmentSnapshot.set(this, this.serializeFrozenSnapshot());
80
+ return result;
81
+ }
82
+ assertImmutableOncePersisted() {
83
+ const captured = frozenAdjustmentSnapshot.get(this);
84
+ if (!captured) return;
85
+ if (captured !== this.serializeFrozenSnapshot()) throw new Error(`CommissionAdjustment ${this.id ?? "<new>"}: adjustments are append-only \u2014 only payoutId may change after creation. Append a counter-adjustment instead of editing this one.`);
86
+ }
87
+ /**
88
+ * Serialize every field EXCEPT `payoutId` (the sole post-create mutable
89
+ * field) with stable key ordering.
90
+ */
91
+ serializeFrozenSnapshot() {
92
+ return JSON.stringify({
93
+ tenantId: this.tenantId,
94
+ commissionId: this.commissionId,
95
+ earnerId: this.earnerId,
96
+ adjustmentKind: this.adjustmentKind,
97
+ amountCents: this.amountCents,
98
+ currency: this.currency,
99
+ reason: this.reason,
100
+ createdByProfileId: this.createdByProfileId,
101
+ metadata: this.metadata
102
+ });
103
+ }
104
+ };
105
+ __decorateClass$5([tenantId({ nullable: true })], CommissionAdjustment.prototype, "tenantId", 2);
106
+ __decorateClass$5([foreignKey("Commission", { required: true })], CommissionAdjustment.prototype, "commissionId", 2);
107
+ __decorateClass$5([foreignKey("Earner", { required: true })], CommissionAdjustment.prototype, "earnerId", 2);
108
+ __decorateClass$5([field({ required: true })], CommissionAdjustment.prototype, "reason", 2);
109
+ __decorateClass$5([crossPackageRef("@happyvertical/smrt-profiles:Profile")], CommissionAdjustment.prototype, "createdByProfileId", 2);
110
+ __decorateClass$5([foreignKey("CommissionPayout")], CommissionAdjustment.prototype, "payoutId", 2);
111
+ CommissionAdjustment = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
112
+ api: { include: [
113
+ "create",
114
+ "list",
115
+ "get"
116
+ ] },
117
+ mcp: { include: ["list", "create"] },
118
+ cli: false
119
+ })], CommissionAdjustment);
120
+ //#endregion
121
+ //#region src/commissions/collections/CommissionAdjustmentCollection.ts
122
+ var CommissionAdjustmentCollection = class extends SmrtCollection {
123
+ static _itemClass = CommissionAdjustment;
124
+ /** All adjustments appended to one commission, oldest first. */
125
+ async findByCommission(commissionId) {
126
+ return await this.list({
127
+ where: { commissionId },
128
+ orderBy: "created_at ASC"
129
+ });
130
+ }
131
+ /** Unsettled adjustments for an earner+currency, oldest first. */
132
+ async findUnsettledByEarner(earnerId, currency) {
133
+ return (await this.list({
134
+ where: {
135
+ earnerId,
136
+ currency
137
+ },
138
+ orderBy: "created_at ASC"
139
+ })).filter((a) => !a.payoutId);
140
+ }
141
+ /**
142
+ * Σ signed amountCents of {@link findUnsettledByEarner} rows (integer
143
+ * cents; clawbacks make it negative).
144
+ */
145
+ async sumUnsettledByEarner(earnerId, currency) {
146
+ return (await this.findUnsettledByEarner(earnerId, currency)).reduce((sum, a) => sum + a.amountCents, 0);
147
+ }
148
+ /** Adjustments settled by one payout batch. */
149
+ async findByPayout(payoutId) {
150
+ return await this.list({
151
+ where: { payoutId },
152
+ orderBy: "created_at ASC"
153
+ });
154
+ }
155
+ /**
156
+ * Conditionally claim adjustment rows for a payout batch — the
157
+ * adjustment twin of `CommissionCollection.claimForPayout`. Rows already
158
+ * claimed by a DIFFERENT payout are skipped; rows already claimed by THIS
159
+ * payout pass through (idempotent retry / repair); every claim is
160
+ * verified by a post-save re-read. Returns the claimed rows.
161
+ */
162
+ async claimForPayout(adjustmentIds, payoutId) {
163
+ const claimed = [];
164
+ for (const id of adjustmentIds) {
165
+ const row = await this.get({ id });
166
+ if (!row) continue;
167
+ if (row.payoutId && row.payoutId !== payoutId) continue;
168
+ if (!row.payoutId) {
169
+ row.payoutId = payoutId;
170
+ await row.save();
171
+ }
172
+ const verified = await this.get({ id });
173
+ if (verified && verified.payoutId === payoutId) claimed.push(verified);
174
+ }
175
+ return claimed;
176
+ }
177
+ };
178
+ //#endregion
179
+ //#region src/commissions/models/Commission.ts
180
+ var __defProp$4 = Object.defineProperty;
181
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
182
+ var __decorateClass$4 = (decorators, target, key, kind) => {
183
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
184
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
185
+ if (kind && result) __defProp$4(target, key, result);
186
+ return result;
187
+ };
188
+ var COMMISSION_STATUS_TRANSITIONS = {
189
+ pending: ["earned"],
190
+ earned: ["approved"],
191
+ approved: ["payable"],
192
+ payable: ["paid"],
193
+ paid: []
194
+ };
195
+ var loadedCommissionStatus = /* @__PURE__ */ new WeakMap();
196
+ var Commission = class extends SmrtObject {
197
+ tenantId = null;
198
+ earnerId = "";
199
+ earningEventId = "";
200
+ /** Snapshot reference: plan key at calculation time. */
201
+ planKey = "";
202
+ /** Snapshot reference: plan version at calculation time. */
203
+ planVersion = 0;
204
+ /** Which plan component produced this commission. */
205
+ componentKey = "";
206
+ /**
207
+ * Generic polymorphic reference to the terms snapshot that governed the
208
+ * calculation (e.g. the referrals module sets
209
+ * `('referral_term_snapshot', <id>)`). Free-form; this module attaches no
210
+ * semantics beyond recording it in the dedupe key and trace.
211
+ */
212
+ termsSnapshotKind = "";
213
+ /** Id of the terms snapshot named by {@link termsSnapshotKind}. */
214
+ termsSnapshotId = "";
215
+ /** How {@link baseAmountCents} was resolved from the event. */
216
+ basis = "gross";
217
+ /** Base amount the rate was applied to, in integer cents. */
218
+ baseAmountCents = 0;
219
+ /** Rate applied (0–1). Recorded as `0` for `fixed`-basis commissions. */
220
+ rate = 0;
221
+ /** Split share applied (0–1). `1.0` for unsplit commissions. */
222
+ shareFraction = 1;
223
+ /**
224
+ * Groups the sibling commissions of one split — every earner sharing an
225
+ * event/component carries the same `splitGroupId`. Empty for unsplit rows.
226
+ */
227
+ splitGroupId = "";
228
+ /** The earned amount in integer cents. */
229
+ amountCents = 0;
230
+ /** ISO 4217 currency (copied from the earning event). */
231
+ currency = "USD";
232
+ /**
233
+ * Lifecycle status — strict chain `pending → earned → approved → payable
234
+ * → paid`. Mutate via the transition methods; the save-time guard rejects
235
+ * illegal edges.
236
+ */
237
+ status = "pending";
238
+ /**
239
+ * End of the clearing window (refund/chargeback holdback). `null` means
240
+ * no clearing applies — the commission is immediately sweepable to
241
+ * `earned` (see `CommissionSettlementService.sweepClearing`).
242
+ */
243
+ clearingEndsAt = null;
244
+ /** When the commission transitioned to `earned`. */
245
+ earnedAt = null;
246
+ /** When the commission transitioned to `approved`. */
247
+ approvedAt = null;
248
+ /** When the commission transitioned to `payable`. */
249
+ payableAt = null;
250
+ /** When the commission transitioned to `paid`. */
251
+ paidAt = null;
252
+ payoutId = "";
253
+ /** Copied from the earning event for reporting (generic source pair). */
254
+ sourceKind = "";
255
+ /** Copied from the earning event for reporting. */
256
+ sourceId = "";
257
+ /**
258
+ * JSON-string {@link CommissionCalculationTrace} — everything needed to
259
+ * reproduce {@link amountCents}. Use {@link getCalculationTrace} /
260
+ * {@link setCalculationTrace}.
261
+ */
262
+ calculationTrace = "{}";
263
+ dedupeKey = "";
264
+ /** Additional metadata as a JSON string. */
265
+ metadata = "{}";
266
+ constructor(options = {}) {
267
+ super(options);
268
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
269
+ if (options.earnerId !== void 0) this.earnerId = options.earnerId;
270
+ if (options.earningEventId !== void 0) this.earningEventId = options.earningEventId;
271
+ if (options.planKey !== void 0) this.planKey = options.planKey;
272
+ if (options.planVersion !== void 0) this.planVersion = options.planVersion;
273
+ if (options.componentKey !== void 0) this.componentKey = options.componentKey;
274
+ if (options.termsSnapshotKind !== void 0) this.termsSnapshotKind = options.termsSnapshotKind;
275
+ if (options.termsSnapshotId !== void 0) this.termsSnapshotId = options.termsSnapshotId;
276
+ if (options.basis !== void 0) this.basis = options.basis;
277
+ if (options.baseAmountCents !== void 0) this.baseAmountCents = options.baseAmountCents;
278
+ if (options.rate !== void 0) this.rate = options.rate;
279
+ if (options.shareFraction !== void 0) this.shareFraction = options.shareFraction;
280
+ if (options.splitGroupId !== void 0) this.splitGroupId = options.splitGroupId;
281
+ if (options.amountCents !== void 0) this.amountCents = options.amountCents;
282
+ if (options.currency !== void 0) this.currency = options.currency;
283
+ if (options.status !== void 0) this.status = options.status;
284
+ if (options.clearingEndsAt !== void 0) this.clearingEndsAt = Commission.coerceDate(options.clearingEndsAt);
285
+ if (options.earnedAt !== void 0) this.earnedAt = Commission.coerceDate(options.earnedAt);
286
+ if (options.approvedAt !== void 0) this.approvedAt = Commission.coerceDate(options.approvedAt);
287
+ if (options.payableAt !== void 0) this.payableAt = Commission.coerceDate(options.payableAt);
288
+ if (options.paidAt !== void 0) this.paidAt = Commission.coerceDate(options.paidAt);
289
+ if (options.payoutId !== void 0) this.payoutId = options.payoutId;
290
+ if (options.sourceKind !== void 0) this.sourceKind = options.sourceKind;
291
+ if (options.sourceId !== void 0) this.sourceId = options.sourceId;
292
+ if (options.calculationTrace !== void 0) this.calculationTrace = options.calculationTrace;
293
+ if (options.dedupeKey !== void 0) this.dedupeKey = options.dedupeKey;
294
+ if (options.metadata !== void 0) this.metadata = options.metadata;
295
+ }
296
+ /**
297
+ * Re-coerce timestamp fields after the framework reapplies raw option /
298
+ * hydrated row values, and record the loaded status for the save guard.
299
+ */
300
+ async initialize() {
301
+ await super.initialize();
302
+ this.clearingEndsAt = Commission.coerceDate(this.clearingEndsAt);
303
+ this.earnedAt = Commission.coerceDate(this.earnedAt);
304
+ this.approvedAt = Commission.coerceDate(this.approvedAt);
305
+ this.payableAt = Commission.coerceDate(this.payableAt);
306
+ this.paidAt = Commission.coerceDate(this.paidAt);
307
+ if (await this.isSaved()) loadedCommissionStatus.set(this, this.status);
308
+ return this;
309
+ }
310
+ isPending() {
311
+ return this.status === "pending";
312
+ }
313
+ isEarned() {
314
+ return this.status === "earned";
315
+ }
316
+ isApproved() {
317
+ return this.status === "approved";
318
+ }
319
+ isPayable() {
320
+ return this.status === "payable";
321
+ }
322
+ isPaid() {
323
+ return this.status === "paid";
324
+ }
325
+ /** `true` once a payout batch has stamped {@link payoutId}. */
326
+ isSettled() {
327
+ return !!this.payoutId;
328
+ }
329
+ /**
330
+ * `pending → earned` (clearing window passed). Stamps {@link earnedAt}.
331
+ * Does NOT save — the caller saves.
332
+ */
333
+ markEarned(now = /* @__PURE__ */ new Date()) {
334
+ this.assertTransitionFrom("pending", "earned");
335
+ this.status = "earned";
336
+ this.earnedAt = now;
337
+ }
338
+ /**
339
+ * `earned → approved` (operator/automation approved the earning).
340
+ * Stamps {@link approvedAt}. Does NOT save — the caller saves.
341
+ */
342
+ approve(now = /* @__PURE__ */ new Date()) {
343
+ this.assertTransitionFrom("earned", "approved");
344
+ this.status = "approved";
345
+ this.approvedAt = now;
346
+ }
347
+ /**
348
+ * `approved → payable` (released for the next payout batch).
349
+ * Stamps {@link payableAt}. Does NOT save — the caller saves.
350
+ */
351
+ markPayable(now = /* @__PURE__ */ new Date()) {
352
+ this.assertTransitionFrom("approved", "payable");
353
+ this.status = "payable";
354
+ this.payableAt = now;
355
+ }
356
+ /**
357
+ * `payable → paid` (its payout batch completed). Stamps {@link paidAt}.
358
+ * Does NOT save — the caller saves.
359
+ */
360
+ markPaid(now = /* @__PURE__ */ new Date()) {
361
+ this.assertTransitionFrom("payable", "paid");
362
+ this.status = "paid";
363
+ this.paidAt = now;
364
+ }
365
+ assertTransitionFrom(expected, next) {
366
+ if (this.status !== expected) throw new Error(`Commission ${this.id ?? "<new>"}: cannot transition to '${next}' from status '${this.status}' (chain is pending \u2192 earned \u2192 approved \u2192 payable \u2192 paid)`);
367
+ }
368
+ /** Parse {@link calculationTrace}; returns `null` on empty/invalid JSON. */
369
+ getCalculationTrace() {
370
+ if (!this.calculationTrace) return null;
371
+ try {
372
+ const parsed = JSON.parse(this.calculationTrace);
373
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
374
+ const trace = parsed;
375
+ return typeof trace.componentKey === "string" && typeof trace.baseAmountCents === "number" ? trace : null;
376
+ } catch {
377
+ return null;
378
+ }
379
+ }
380
+ /** Serialize and store {@link calculationTrace}. */
381
+ setCalculationTrace(trace) {
382
+ this.calculationTrace = JSON.stringify(trace);
383
+ }
384
+ /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
385
+ getMetadata() {
386
+ if (!this.metadata) return {};
387
+ try {
388
+ const parsed = JSON.parse(this.metadata);
389
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
390
+ } catch {
391
+ return {};
392
+ }
393
+ }
394
+ /** Serialize and store {@link metadata}. */
395
+ setMetadata(data) {
396
+ this.metadata = JSON.stringify(data ?? {});
397
+ }
398
+ /**
399
+ * Save-time state-machine guard (commerce pattern). Validates the status
400
+ * transition against the AUTHORITATIVE prior persisted status — re-read
401
+ * from the database so a `create({ id: <existing>, _skipLoad: true })`
402
+ * upsert is correctly treated as an update rather than a guard-free new
403
+ * row. Brand-new rows may start in any status (fixtures/imports); a
404
+ * persisted row may only advance one legal step.
405
+ */
406
+ async save() {
407
+ const prior = await this.resolvePriorStatus();
408
+ this.assertStatusTransition(prior);
409
+ await this.assertDedupeKeyNotTaken();
410
+ const result = await super.save();
411
+ loadedCommissionStatus.set(this, this.status);
412
+ return result;
413
+ }
414
+ /**
415
+ * Refuse a save whose `dedupeKey` already belongs to a DIFFERENT row —
416
+ * commissions are audit rows, and the natural-key upsert would let a
417
+ * fresh instance (generated `create`, or the loser of a calculation
418
+ * race) overwrite the persisted amount/status and rotate the row id.
419
+ * `CommissionCalculationService` treats this refusal as "someone else
420
+ * already earned it" and returns the existing row.
421
+ */
422
+ async assertDedupeKeyNotTaken() {
423
+ if (!this.dedupeKey) return;
424
+ try {
425
+ const res = await this.db.query(`SELECT id FROM ${this.tableName} WHERE dedupe_key = $1`, this.dedupeKey);
426
+ if ((Array.isArray(res) ? res : res.rows ?? []).find((row) => row.id !== this.id)) throw new Error(`Commission (dedupeKey '${this.dedupeKey}'): a commission with this dedupe key already exists \u2014 commissions are immutable audit rows; corrections append CommissionAdjustments.`);
427
+ } catch (error) {
428
+ if (error instanceof Error && error.message.includes("immutable")) throw error;
429
+ }
430
+ }
431
+ async resolvePriorStatus() {
432
+ if (this.id) try {
433
+ const row = await this.db.get(this.tableName, { id: this.id });
434
+ if (row && row.status != null) return row.status;
435
+ } catch {}
436
+ return loadedCommissionStatus.get(this);
437
+ }
438
+ assertStatusTransition(prior) {
439
+ if (prior === void 0) return;
440
+ if (prior === this.status) return;
441
+ if (!(COMMISSION_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`Commission ${this.id}: illegal status transition '${prior}' \u2192 '${this.status}'. Use markEarned() / approve() / markPayable() / markPaid().`);
442
+ }
443
+ static coerceDate(value) {
444
+ if (value == null) return null;
445
+ if (value instanceof Date) return value;
446
+ if (typeof value === "number" || typeof value === "string") {
447
+ const d = new Date(value);
448
+ return Number.isNaN(d.getTime()) ? null : d;
449
+ }
450
+ return null;
451
+ }
452
+ };
453
+ __decorateClass$4([tenantId({ nullable: true })], Commission.prototype, "tenantId", 2);
454
+ __decorateClass$4([foreignKey("Earner", { required: true })], Commission.prototype, "earnerId", 2);
455
+ __decorateClass$4([foreignKey("EarningEvent")], Commission.prototype, "earningEventId", 2);
456
+ __decorateClass$4([foreignKey("CommissionPayout")], Commission.prototype, "payoutId", 2);
457
+ __decorateClass$4([field({ required: true })], Commission.prototype, "dedupeKey", 2);
458
+ Commission = __decorateClass$4([TenantScoped({ mode: "optional" }), smrt({
459
+ conflictColumns: ["dedupe_key"],
460
+ api: { include: [
461
+ "list",
462
+ "get",
463
+ "create"
464
+ ] },
465
+ mcp: { include: ["list", "get"] },
466
+ cli: false
467
+ })], Commission);
468
+ //#endregion
469
+ //#region src/commissions/collections/CommissionCollection.ts
470
+ var CommissionCollection = class extends SmrtCollection {
471
+ static _itemClass = Commission;
472
+ /** All commissions for an earner, newest first. */
473
+ async findByEarner(earnerId) {
474
+ return await this.list({
475
+ where: { earnerId },
476
+ orderBy: "created_at DESC"
477
+ });
478
+ }
479
+ /** All commissions derived from one earning event. */
480
+ async findByEvent(earningEventId) {
481
+ return await this.list({
482
+ where: { earningEventId },
483
+ orderBy: "created_at DESC"
484
+ });
485
+ }
486
+ /** Commissions by lifecycle status, newest first. */
487
+ async findByStatus(status) {
488
+ return await this.list({
489
+ where: { status },
490
+ orderBy: "created_at DESC"
491
+ });
492
+ }
493
+ /** Look up a commission by its idempotency natural key. */
494
+ async findByDedupeKey(dedupeKey) {
495
+ if (!dedupeKey) return null;
496
+ return (await this.list({
497
+ where: { dedupeKey },
498
+ limit: 1
499
+ }))[0] ?? null;
500
+ }
501
+ /**
502
+ * Payable commissions for an earner+currency that no payout batch has
503
+ * settled yet — the rows `CommissionPayoutService.createPayoutBatch`
504
+ * gathers.
505
+ */
506
+ async findPayableUnsettled(earnerId, currency) {
507
+ return (await this.list({
508
+ where: {
509
+ earnerId,
510
+ currency,
511
+ status: "payable"
512
+ },
513
+ orderBy: "created_at ASC"
514
+ })).filter((c) => !c.payoutId);
515
+ }
516
+ /** Σ amountCents of {@link findPayableUnsettled} rows (integer cents). */
517
+ async sumPayableByEarner(earnerId, currency) {
518
+ return (await this.findPayableUnsettled(earnerId, currency)).reduce((sum, c) => sum + c.amountCents, 0);
519
+ }
520
+ /** Commissions settled by one payout batch. */
521
+ async findByPayout(payoutId) {
522
+ return await this.list({
523
+ where: { payoutId },
524
+ orderBy: "created_at ASC"
525
+ });
526
+ }
527
+ /**
528
+ * Conditionally claim rows for a payout batch: each row is re-loaded
529
+ * fresh and stamped with `payoutId` only when it is still payable and
530
+ * unclaimed (or already claimed by THIS payout — the idempotent-retry /
531
+ * repair case). Rows claimed by a DIFFERENT payout are skipped, and every
532
+ * claim is verified by a post-save re-read so a lost race never counts
533
+ * toward the caller's totals.
534
+ *
535
+ * This is the single place claim semantics live. It narrows the
536
+ * concurrent-batch window to the re-read granularity; true compare-and-set
537
+ * needs DB transactions the collection layer doesn't expose (settlement
538
+ * runs are expected to be single-writer per earner — see
539
+ * `CommissionPayoutService`).
540
+ *
541
+ * Returns the claimed rows (freshly loaded, `payoutId` verified).
542
+ */
543
+ async claimForPayout(commissionIds, payoutId) {
544
+ const claimed = [];
545
+ for (const id of commissionIds) {
546
+ const row = await this.get({ id });
547
+ if (!row) continue;
548
+ if (row.payoutId && row.payoutId !== payoutId) continue;
549
+ if (!row.payoutId) {
550
+ if (!row.isPayable()) continue;
551
+ row.payoutId = payoutId;
552
+ await row.save();
553
+ }
554
+ const verified = await this.get({ id });
555
+ if (verified && verified.payoutId === payoutId) claimed.push(verified);
556
+ }
557
+ return claimed;
558
+ }
559
+ };
560
+ //#endregion
561
+ //#region src/commissions/models/CommissionPayout.ts
562
+ var __defProp$3 = Object.defineProperty;
563
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
564
+ var __decorateClass$3 = (decorators, target, key, kind) => {
565
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
566
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
567
+ if (kind && result) __defProp$3(target, key, result);
568
+ return result;
569
+ };
570
+ var PAYOUT_STATUS_TRANSITIONS = {
571
+ pending: ["approved"],
572
+ approved: ["processing", "failed"],
573
+ processing: ["completed", "failed"],
574
+ completed: [],
575
+ failed: ["pending"]
576
+ };
577
+ var loadedPayoutStatus = /* @__PURE__ */ new WeakMap();
578
+ var CommissionPayout = class extends SmrtObject {
579
+ tenantId = null;
580
+ earnerId = "";
581
+ /** Start of the settlement period this batch covers (informational). */
582
+ periodStart = null;
583
+ /** End of the settlement period this batch covers (informational). */
584
+ periodEnd = null;
585
+ /** Σ amountCents of the Commissions this batch settled (integer cents). */
586
+ commissionTotalCents = 0;
587
+ /**
588
+ * Σ signed amountCents of the Adjustments this batch settled (integer
589
+ * cents; clawbacks make it negative).
590
+ */
591
+ adjustmentTotalCents = 0;
592
+ /**
593
+ * Net amount remitted — must equal
594
+ * `commissionTotalCents + adjustmentTotalCents` (enforced on save).
595
+ */
596
+ totalAmountCents = 0;
597
+ /** ISO 4217 currency of the batch. */
598
+ currency = "USD";
599
+ /** Delivery method for this batch (defaulted from the Earner). */
600
+ payoutMethod = "bank_transfer";
601
+ /**
602
+ * Lifecycle status — see the class doc. Mutate via {@link approve} /
603
+ * {@link markProcessing} / {@link complete} / {@link fail} /
604
+ * {@link resetFromFailed}.
605
+ */
606
+ status = "pending";
607
+ /**
608
+ * Payment reference recorded at completion (check number, transfer id,
609
+ * …). Cleared by {@link resetFromFailed}.
610
+ */
611
+ paymentReference = "";
612
+ /**
613
+ * Opaque payout-provider reference (processor batch id, remittance file
614
+ * id, …). Retained across failure/reset for audit.
615
+ */
616
+ providerRef = "";
617
+ /** When the payout completed. */
618
+ paidAt = null;
619
+ invoiceId = "";
620
+ /** Operator notes — approval memos, failure reasons (append-only). */
621
+ notes = "";
622
+ idempotencyKey = "";
623
+ /** Additional metadata as a JSON string. */
624
+ metadata = "{}";
625
+ constructor(options = {}) {
626
+ super(options);
627
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
628
+ if (options.earnerId !== void 0) this.earnerId = options.earnerId;
629
+ if (options.periodStart !== void 0) this.periodStart = CommissionPayout.coerceDate(options.periodStart);
630
+ if (options.periodEnd !== void 0) this.periodEnd = CommissionPayout.coerceDate(options.periodEnd);
631
+ if (options.commissionTotalCents !== void 0) this.commissionTotalCents = options.commissionTotalCents;
632
+ if (options.adjustmentTotalCents !== void 0) this.adjustmentTotalCents = options.adjustmentTotalCents;
633
+ if (options.totalAmountCents !== void 0) this.totalAmountCents = options.totalAmountCents;
634
+ if (options.currency !== void 0) this.currency = options.currency;
635
+ if (options.payoutMethod !== void 0) this.payoutMethod = options.payoutMethod;
636
+ if (options.status !== void 0) this.status = options.status;
637
+ if (options.paymentReference !== void 0) this.paymentReference = options.paymentReference;
638
+ if (options.providerRef !== void 0) this.providerRef = options.providerRef;
639
+ if (options.paidAt !== void 0) this.paidAt = CommissionPayout.coerceDate(options.paidAt);
640
+ if (options.invoiceId !== void 0) this.invoiceId = options.invoiceId;
641
+ if (options.notes !== void 0) this.notes = options.notes;
642
+ if (options.idempotencyKey !== void 0) this.idempotencyKey = options.idempotencyKey;
643
+ if (options.metadata !== void 0) this.metadata = options.metadata;
644
+ }
645
+ /**
646
+ * Re-coerce timestamp fields after the framework reapplies raw option /
647
+ * hydrated row values, and record the loaded status for the save guard.
648
+ */
649
+ async initialize() {
650
+ await super.initialize();
651
+ this.periodStart = CommissionPayout.coerceDate(this.periodStart);
652
+ this.periodEnd = CommissionPayout.coerceDate(this.periodEnd);
653
+ this.paidAt = CommissionPayout.coerceDate(this.paidAt);
654
+ if (await this.isSaved()) loadedPayoutStatus.set(this, this.status);
655
+ return this;
656
+ }
657
+ isPending() {
658
+ return this.status === "pending";
659
+ }
660
+ isApproved() {
661
+ return this.status === "approved";
662
+ }
663
+ isProcessing() {
664
+ return this.status === "processing";
665
+ }
666
+ isCompleted() {
667
+ return this.status === "completed";
668
+ }
669
+ isFailed() {
670
+ return this.status === "failed";
671
+ }
672
+ /** `pending → approved`. Does NOT save — the caller saves. */
673
+ approve() {
674
+ if (this.status !== "pending") throw new Error(`CommissionPayout ${this.id ?? "<new>"}: cannot approve from status '${this.status}'`);
675
+ if (this.totalAmountCents <= 0) throw new Error(`CommissionPayout ${this.id ?? "<new>"}: cannot approve a batch with non-positive total (${this.totalAmountCents} cents)`);
676
+ this.status = "approved";
677
+ }
678
+ /** `approved → processing`. Does NOT save — the caller saves. */
679
+ markProcessing() {
680
+ if (this.status !== "approved") throw new Error(`CommissionPayout ${this.id ?? "<new>"}: cannot mark processing from status '${this.status}'`);
681
+ this.status = "processing";
682
+ }
683
+ /**
684
+ * `processing → completed`. Requires a payment reference — a completed
685
+ * payout with no reference is untraceable. Stamps {@link paidAt}.
686
+ * Does NOT save — the caller saves.
687
+ */
688
+ complete(paymentReference, now = /* @__PURE__ */ new Date()) {
689
+ if (this.status !== "processing") throw new Error(`CommissionPayout ${this.id ?? "<new>"}: cannot complete from status '${this.status}'`);
690
+ if (!paymentReference) throw new Error(`CommissionPayout ${this.id ?? "<new>"}: complete() requires a paymentReference`);
691
+ this.status = "completed";
692
+ this.paymentReference = paymentReference;
693
+ this.paidAt = now;
694
+ }
695
+ /**
696
+ * `approved | processing → failed`. Appends the reason to {@link notes}.
697
+ * Does NOT save — the caller saves.
698
+ */
699
+ fail(reason) {
700
+ if (this.status !== "approved" && this.status !== "processing") throw new Error(`CommissionPayout ${this.id ?? "<new>"}: cannot fail from status '${this.status}'`);
701
+ this.status = "failed";
702
+ const memo = `Failed: ${reason ?? ""}`;
703
+ this.notes = this.notes ? `${this.notes}
704
+ ${memo}` : memo;
705
+ }
706
+ /**
707
+ * Operator-driven reset: `failed → pending` after fixing whatever broke.
708
+ * Clears {@link paymentReference} and {@link paidAt} (the next attempt
709
+ * gets fresh ones) but RETAINS {@link providerRef} and {@link notes} for
710
+ * audit. The only path out of `failed`. Does NOT save — the caller saves.
711
+ */
712
+ resetFromFailed() {
713
+ if (this.status !== "failed") throw new Error(`CommissionPayout ${this.id ?? "<new>"}: cannot reset from status '${this.status}' \u2014 only failed payouts are resettable`);
714
+ this.status = "pending";
715
+ this.paymentReference = "";
716
+ this.paidAt = null;
717
+ }
718
+ /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
719
+ getMetadata() {
720
+ if (!this.metadata) return {};
721
+ try {
722
+ const parsed = JSON.parse(this.metadata);
723
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
724
+ } catch {
725
+ return {};
726
+ }
727
+ }
728
+ /** Serialize and store {@link metadata}. */
729
+ setMetadata(data) {
730
+ this.metadata = JSON.stringify(data ?? {});
731
+ }
732
+ /**
733
+ * Save with two guards (commerce pattern):
734
+ *
735
+ * 1. **Totals invariant** — `totalAmountCents` must equal
736
+ * `commissionTotalCents + adjustmentTotalCents` (exact integer
737
+ * arithmetic, no epsilon).
738
+ * 2. **Status transition** — validated against the AUTHORITATIVE prior
739
+ * persisted status (re-read from the database so a
740
+ * `create({ id, _skipLoad: true })` upsert can't sidestep the guard).
741
+ * A `completed` payout additionally requires a payment reference,
742
+ * matching {@link complete}'s invariant, regardless of how the status
743
+ * was set.
744
+ */
745
+ async save() {
746
+ this.validateTotals();
747
+ const prior = await this.resolvePriorStatus();
748
+ this.assertStatusTransition(prior);
749
+ if (this.status === "completed" && !this.paymentReference) throw new Error(`CommissionPayout ${this.id ?? "<new>"}: a completed payout requires a paymentReference (use complete()).`);
750
+ const result = await super.save();
751
+ loadedPayoutStatus.set(this, this.status);
752
+ return result;
753
+ }
754
+ /** Throws when the totals invariant doesn't hold. */
755
+ validateTotals() {
756
+ for (const [name, value] of [
757
+ ["commissionTotalCents", this.commissionTotalCents],
758
+ ["adjustmentTotalCents", this.adjustmentTotalCents],
759
+ ["totalAmountCents", this.totalAmountCents]
760
+ ]) if (!Number.isInteger(value)) throw new Error(`CommissionPayout ${this.id ?? "<new>"}: ${name} must be integer cents (got ${value}).`);
761
+ const expected = this.commissionTotalCents + this.adjustmentTotalCents;
762
+ if (this.totalAmountCents !== expected) throw new Error(`CommissionPayout ${this.id ?? "<new>"}: totals invariant violated \u2014 commission=${this.commissionTotalCents} adjustment=${this.adjustmentTotalCents} total=${this.totalAmountCents} (expected total=${expected}).`);
763
+ }
764
+ async resolvePriorStatus() {
765
+ if (this.id) try {
766
+ const row = await this.db.get(this.tableName, { id: this.id });
767
+ if (row && row.status != null) return row.status;
768
+ } catch {}
769
+ return loadedPayoutStatus.get(this);
770
+ }
771
+ assertStatusTransition(prior) {
772
+ if (prior === void 0) return;
773
+ if (prior === this.status) return;
774
+ if (!(PAYOUT_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`CommissionPayout ${this.id}: illegal status transition '${prior}' \u2192 '${this.status}'. Use approve() / markProcessing() / complete() / fail() / resetFromFailed().`);
775
+ }
776
+ static coerceDate(value) {
777
+ if (value == null) return null;
778
+ if (value instanceof Date) return value;
779
+ if (typeof value === "number" || typeof value === "string") {
780
+ const d = new Date(value);
781
+ return Number.isNaN(d.getTime()) ? null : d;
782
+ }
783
+ return null;
784
+ }
785
+ };
786
+ __decorateClass$3([tenantId({ nullable: true })], CommissionPayout.prototype, "tenantId", 2);
787
+ __decorateClass$3([foreignKey("Earner", { required: true })], CommissionPayout.prototype, "earnerId", 2);
788
+ __decorateClass$3([crossPackageRef("@happyvertical/smrt-commerce:Invoice")], CommissionPayout.prototype, "invoiceId", 2);
789
+ __decorateClass$3([field({ required: true })], CommissionPayout.prototype, "idempotencyKey", 2);
790
+ CommissionPayout = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
791
+ conflictColumns: ["idempotency_key"],
792
+ api: { include: ["list", "get"] },
793
+ mcp: { include: ["list", "get"] },
794
+ cli: { include: ["list", "get"] }
795
+ })], CommissionPayout);
796
+ //#endregion
797
+ //#region src/commissions/collections/CommissionPayoutCollection.ts
798
+ var CommissionPayoutCollection = class extends SmrtCollection {
799
+ static _itemClass = CommissionPayout;
800
+ /** All payout batches for an earner, newest first. */
801
+ async findByEarner(earnerId) {
802
+ return await this.list({
803
+ where: { earnerId },
804
+ orderBy: "created_at DESC"
805
+ });
806
+ }
807
+ /** Payout batches by status, newest first. */
808
+ async findByStatus(status) {
809
+ return await this.list({
810
+ where: { status },
811
+ orderBy: "created_at DESC"
812
+ });
813
+ }
814
+ /** Look up a payout by its idempotency natural key. */
815
+ async findByIdempotencyKey(idempotencyKey) {
816
+ if (!idempotencyKey) return null;
817
+ return (await this.list({
818
+ where: { idempotencyKey },
819
+ limit: 1
820
+ }))[0] ?? null;
821
+ }
822
+ /**
823
+ * Σ totalAmountCents of COMPLETED payouts for an earner+currency —
824
+ * lifetime settled earnings (integer cents).
825
+ */
826
+ async sumPaidByEarner(earnerId, currency) {
827
+ return (await this.list({ where: {
828
+ earnerId,
829
+ currency,
830
+ status: "completed"
831
+ } })).reduce((sum, p) => sum + p.totalAmountCents, 0);
832
+ }
833
+ };
834
+ //#endregion
835
+ //#region src/commissions/types.ts
836
+ var EARNER_STATUSES = [
837
+ "pending",
838
+ "active",
839
+ "suspended"
840
+ ];
841
+ var PAYOUT_METHODS = [
842
+ "bank_transfer",
843
+ "check",
844
+ "paypal",
845
+ "credit",
846
+ "other"
847
+ ];
848
+ var COMMISSION_PLAN_STATUSES = [
849
+ "draft",
850
+ "active",
851
+ "superseded",
852
+ "retired"
853
+ ];
854
+ var COMMISSION_STATUSES = [
855
+ "pending",
856
+ "earned",
857
+ "approved",
858
+ "payable",
859
+ "paid"
860
+ ];
861
+ var COMMISSION_BASES = [
862
+ "fixed",
863
+ "gross",
864
+ "net",
865
+ "margin",
866
+ "custom"
867
+ ];
868
+ var COMMISSION_ADJUSTMENT_KINDS = [
869
+ "refund",
870
+ "credit",
871
+ "chargeback",
872
+ "dispute",
873
+ "correction"
874
+ ];
875
+ var COMMISSION_PAYOUT_STATUSES = [
876
+ "pending",
877
+ "approved",
878
+ "processing",
879
+ "completed",
880
+ "failed"
881
+ ];
882
+ var EARNING_EVENT_KINDS = [
883
+ "conversion",
884
+ "agreement_execution",
885
+ "invoice_payment",
886
+ "collected_revenue",
887
+ "recognized_margin",
888
+ "milestone"
889
+ ];
890
+ var ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES = [
891
+ "earned",
892
+ "approved",
893
+ "payable",
894
+ "paid"
895
+ ];
896
+ //#endregion
897
+ //#region src/commissions/models/CommissionPlan.ts
898
+ var __defProp$2 = Object.defineProperty;
899
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
900
+ var __decorateClass$2 = (decorators, target, key, kind) => {
901
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
902
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
903
+ if (kind && result) __defProp$2(target, key, result);
904
+ return result;
905
+ };
906
+ var PLAN_STATUS_TRANSITIONS = {
907
+ draft: ["active", "retired"],
908
+ active: ["superseded", "retired"],
909
+ superseded: [],
910
+ retired: []
911
+ };
912
+ var loadedPlanStatus = /* @__PURE__ */ new WeakMap();
913
+ var frozenPlanSnapshot = /* @__PURE__ */ new WeakMap();
914
+ function validateCommissionPlanComponents(components) {
915
+ if (!Array.isArray(components)) throw new Error("CommissionPlan components must be an array");
916
+ const seen = /* @__PURE__ */ new Set();
917
+ for (const component of components) {
918
+ const label = component?.key || "<missing key>";
919
+ if (!component || typeof component !== "object") throw new Error("CommissionPlan component must be an object");
920
+ if (!component.key || typeof component.key !== "string") throw new Error("CommissionPlan component requires a non-empty key");
921
+ if (seen.has(component.key)) throw new Error(`CommissionPlan component keys must be unique \u2014 duplicate '${component.key}'`);
922
+ seen.add(component.key);
923
+ if (!component.trigger || typeof component.trigger !== "string") throw new Error(`CommissionPlan component '${label}' requires a non-empty trigger ('*' matches all kinds)`);
924
+ if (!COMMISSION_BASES.includes(component.basis)) throw new Error(`CommissionPlan component '${label}' has invalid basis '${component.basis}'`);
925
+ if (component.basis === "fixed") {
926
+ if (typeof component.fixedAmountCents !== "number" || !Number.isInteger(component.fixedAmountCents)) throw new Error(`CommissionPlan component '${label}' with basis 'fixed' requires an integer fixedAmountCents`);
927
+ } else if (typeof component.rate !== "number" || !Number.isFinite(component.rate) || component.rate < 0 || component.rate > 1) throw new Error(`CommissionPlan component '${label}' with basis '${component.basis}' requires a rate in [0, 1]`);
928
+ if (component.basis === "custom" && !component.customBasisKey) throw new Error(`CommissionPlan component '${label}' with basis 'custom' requires a customBasisKey`);
929
+ const recurrence = component.recurrence;
930
+ if (recurrence !== void 0) {
931
+ if (recurrence.kind !== "one_time" && recurrence.kind !== "recurring") throw new Error(`CommissionPlan component '${label}' recurrence.kind must be 'one_time' or 'recurring'`);
932
+ for (const [name, value] of [["maxOccurrences", recurrence.maxOccurrences], ["windowMonths", recurrence.windowMonths]]) if (value !== void 0 && (!Number.isInteger(value) || value <= 0)) throw new Error(`CommissionPlan component '${label}' recurrence.${name} must be a positive integer`);
933
+ }
934
+ }
935
+ }
936
+ var CommissionPlan = class extends SmrtObject {
937
+ tenantId = null;
938
+ planKey = "";
939
+ /** Monotonic version within `planKey`. Amendments insert `max + 1`. */
940
+ version = 1;
941
+ /** Human-readable plan name. */
942
+ name = "";
943
+ /** Longer human-readable description of the terms. */
944
+ description = "";
945
+ /**
946
+ * Lifecycle status — see {@link PLAN_STATUS_TRANSITIONS}. Mutate via
947
+ * {@link activate} / {@link supersede} / {@link retire} (or a legal
948
+ * single-step assignment; the save-time guard rejects illegal edges).
949
+ */
950
+ status = "draft";
951
+ /** When this version takes effect. Frozen once the plan activates. */
952
+ effectiveFrom = null;
953
+ /** ISO 4217 currency the plan's terms are denominated in. */
954
+ currency = "USD";
955
+ /**
956
+ * Calculation components as a JSON-string array — see
957
+ * {@link CommissionPlanComponent}. Use {@link getComponents} /
958
+ * {@link setComponents} (the setter validates).
959
+ */
960
+ components = "[]";
961
+ /** Additional metadata as a JSON string. */
962
+ metadata = "{}";
963
+ constructor(options = {}) {
964
+ super(options);
965
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
966
+ if (options.planKey !== void 0) this.planKey = options.planKey;
967
+ if (options.version !== void 0) this.version = options.version;
968
+ if (options.name !== void 0) this.name = options.name;
969
+ if (options.description !== void 0) this.description = options.description;
970
+ if (options.status !== void 0) this.status = options.status;
971
+ if (options.effectiveFrom !== void 0) this.effectiveFrom = CommissionPlan.coerceDate(options.effectiveFrom);
972
+ if (options.currency !== void 0) this.currency = options.currency;
973
+ if (options.components !== void 0) this.components = options.components;
974
+ if (options.metadata !== void 0) this.metadata = options.metadata;
975
+ }
976
+ /**
977
+ * Re-coerce date fields after the framework reapplies raw option values,
978
+ * record the loaded status for the transition guard, and capture the
979
+ * frozen snapshot when the row arrived already activated. The snapshot is
980
+ * captured for every non-draft status (not just `active`) so a superseded
981
+ * or retired version — history — can't be rewritten either.
982
+ */
983
+ async initialize() {
984
+ await super.initialize();
985
+ this.effectiveFrom = CommissionPlan.coerceDate(this.effectiveFrom);
986
+ if (await this.isSaved()) {
987
+ loadedPlanStatus.set(this, this.status);
988
+ if (this.status !== "draft") frozenPlanSnapshot.set(this, this.serializeFrozenSnapshot());
989
+ }
990
+ return this;
991
+ }
992
+ isDraft() {
993
+ return this.status === "draft";
994
+ }
995
+ isActive() {
996
+ return this.status === "active";
997
+ }
998
+ /** Parse {@link components}; returns `[]` on empty/invalid JSON. */
999
+ getComponents() {
1000
+ if (!this.components) return [];
1001
+ try {
1002
+ const parsed = JSON.parse(this.components);
1003
+ return Array.isArray(parsed) ? parsed : [];
1004
+ } catch {
1005
+ return [];
1006
+ }
1007
+ }
1008
+ /**
1009
+ * Validate and store the components array. Throws on invalid components —
1010
+ * see {@link validateCommissionPlanComponents} for the rules.
1011
+ */
1012
+ setComponents(components) {
1013
+ validateCommissionPlanComponents(components);
1014
+ this.components = JSON.stringify(components);
1015
+ }
1016
+ /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
1017
+ getMetadata() {
1018
+ if (!this.metadata) return {};
1019
+ try {
1020
+ const parsed = JSON.parse(this.metadata);
1021
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1022
+ } catch {
1023
+ return {};
1024
+ }
1025
+ }
1026
+ /** Serialize and store {@link metadata}. */
1027
+ setMetadata(data) {
1028
+ this.metadata = JSON.stringify(data ?? {});
1029
+ }
1030
+ /**
1031
+ * Transition `draft → active`. Validates components first so no active
1032
+ * plan can carry malformed terms.
1033
+ */
1034
+ activate() {
1035
+ if (this.status !== "draft") throw new Error(`CommissionPlan ${this.planKey}@${this.version}: cannot activate from status '${this.status}'`);
1036
+ validateCommissionPlanComponents(this.getComponents());
1037
+ this.status = "active";
1038
+ }
1039
+ /** Transition `active → superseded` (a newer version took over). */
1040
+ supersede() {
1041
+ if (this.status !== "active") throw new Error(`CommissionPlan ${this.planKey}@${this.version}: cannot supersede from status '${this.status}'`);
1042
+ this.status = "superseded";
1043
+ }
1044
+ /** Transition `draft | active → retired` (terminal). */
1045
+ retire() {
1046
+ if (this.status !== "draft" && this.status !== "active") throw new Error(`CommissionPlan ${this.planKey}@${this.version}: cannot retire from status '${this.status}'`);
1047
+ this.status = "retired";
1048
+ }
1049
+ /**
1050
+ * Save with two guards:
1051
+ *
1052
+ * 1. **Status transition** — the about-to-be-written status must be a
1053
+ * legal edge from the authoritative prior persisted status (re-read
1054
+ * from the DB so a `create({ id: <existing>, _skipLoad: true })` upsert
1055
+ * can't sidestep the guard — commerce pattern).
1056
+ * 2. **Frozen calculation identity** — once the row has been saved
1057
+ * non-draft, `components` / `currency` / `planKey` / `version` /
1058
+ * `effectiveFrom` must match the captured snapshot. Amend by inserting
1059
+ * a new version instead.
1060
+ *
1061
+ * Activating saves also re-validate components, so an `active` row always
1062
+ * carries well-formed terms regardless of which write path set them.
1063
+ */
1064
+ async save() {
1065
+ const prior = await this.resolvePriorStatus();
1066
+ this.assertStatusTransition(prior);
1067
+ this.assertFrozenIdentityUnchanged();
1068
+ await this.assertNaturalKeyNotTaken();
1069
+ if (this.status === "active") validateCommissionPlanComponents(this.getComponents());
1070
+ const result = await super.save();
1071
+ loadedPlanStatus.set(this, this.status);
1072
+ if (this.status !== "draft" && !frozenPlanSnapshot.has(this)) frozenPlanSnapshot.set(this, this.serializeFrozenSnapshot());
1073
+ return result;
1074
+ }
1075
+ /**
1076
+ * Refuse a save whose `(tenantId, planKey, version)` natural key already
1077
+ * belongs to a DIFFERENT row. The frozen-identity guard above is
1078
+ * instance-local (WeakMap), so a FRESH instance carrying an existing
1079
+ * natural key would otherwise sail through and the conflict-column
1080
+ * upsert would rewrite the persisted terms (and rotate the row id).
1081
+ * Edit drafts by hydrating them; change terms with
1082
+ * `CommissionPlanCollection.createAmendment()`.
1083
+ */
1084
+ async assertNaturalKeyNotTaken() {
1085
+ if (!this.planKey) return;
1086
+ try {
1087
+ const res = await this.db.query(`SELECT id, tenant_id FROM ${this.tableName} WHERE plan_key = $1 AND version = $2`, this.planKey, this.version);
1088
+ if ((Array.isArray(res) ? res : res.rows ?? []).find((row) => (row.tenant_id ?? null) === (this.tenantId ?? null) && row.id !== this.id)) throw new Error(`CommissionPlan ${this.planKey}@${this.version}: this version already exists for the tenant \u2014 plan versions are immutable records. Hydrate the existing row to edit a draft, or create new terms with CommissionPlanCollection.createAmendment().`);
1089
+ } catch (error) {
1090
+ if (error instanceof Error && error.message.includes("immutable")) throw error;
1091
+ }
1092
+ }
1093
+ /**
1094
+ * Resolve the AUTHORITATIVE prior status from the database; fall back to
1095
+ * the loaded-status WeakMap only when the DB is unavailable. `undefined`
1096
+ * means no persisted row exists (genuinely new).
1097
+ */
1098
+ async resolvePriorStatus() {
1099
+ if (this.id) try {
1100
+ const row = await this.db.get(this.tableName, { id: this.id });
1101
+ if (row && row.status != null) return row.status;
1102
+ } catch {}
1103
+ return loadedPlanStatus.get(this);
1104
+ }
1105
+ assertStatusTransition(prior) {
1106
+ if (prior === void 0) return;
1107
+ if (prior === this.status) return;
1108
+ if (!(PLAN_STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`CommissionPlan ${this.planKey}@${this.version}: illegal status transition '${prior}' \u2192 '${this.status}'. Use activate() / supersede() / retire().`);
1109
+ }
1110
+ assertFrozenIdentityUnchanged() {
1111
+ const captured = frozenPlanSnapshot.get(this);
1112
+ if (!captured) return;
1113
+ if (captured !== this.serializeFrozenSnapshot()) throw new Error(`CommissionPlan ${this.planKey}@${this.version}: components, currency, planKey, version, and effectiveFrom are immutable once the plan has been active. Create an amendment (CommissionPlanCollection.createAmendment) instead of editing this version.`);
1114
+ }
1115
+ serializeFrozenSnapshot() {
1116
+ return JSON.stringify({
1117
+ planKey: this.planKey,
1118
+ version: this.version,
1119
+ currency: this.currency,
1120
+ components: this.components,
1121
+ effectiveFrom: this.effectiveFrom ? this.effectiveFrom.toISOString() : null
1122
+ });
1123
+ }
1124
+ static coerceDate(value) {
1125
+ if (value == null) return null;
1126
+ if (value instanceof Date) return value;
1127
+ if (typeof value === "number" || typeof value === "string") {
1128
+ const d = new Date(value);
1129
+ return Number.isNaN(d.getTime()) ? null : d;
1130
+ }
1131
+ return null;
1132
+ }
1133
+ };
1134
+ __decorateClass$2([tenantId({ nullable: true })], CommissionPlan.prototype, "tenantId", 2);
1135
+ __decorateClass$2([field({ required: true })], CommissionPlan.prototype, "planKey", 2);
1136
+ CommissionPlan = __decorateClass$2([TenantScoped({ mode: "optional" }), smrt({
1137
+ conflictColumns: [
1138
+ "tenant_id",
1139
+ "plan_key",
1140
+ "version"
1141
+ ],
1142
+ api: { include: [
1143
+ "list",
1144
+ "get",
1145
+ "create"
1146
+ ] },
1147
+ mcp: { include: ["list", "get"] },
1148
+ cli: true
1149
+ })], CommissionPlan);
1150
+ //#endregion
1151
+ //#region src/commissions/collections/CommissionPlanCollection.ts
1152
+ var CommissionPlanCollection = class extends SmrtCollection {
1153
+ static _itemClass = CommissionPlan;
1154
+ /** Every version of a plan, newest version first. */
1155
+ async findByPlanKey(planKey) {
1156
+ return await this.list({
1157
+ where: { planKey },
1158
+ orderBy: "version DESC"
1159
+ });
1160
+ }
1161
+ /** Plans by status. */
1162
+ async findByStatus(status) {
1163
+ return await this.list({
1164
+ where: { status },
1165
+ orderBy: "created_at DESC"
1166
+ });
1167
+ }
1168
+ /**
1169
+ * The highest ACTIVE version of a plan already IN EFFECT at `at`, or
1170
+ * `null` when none is. This is what calculation callers resolve terms
1171
+ * from when no frozen snapshot pins a specific version. A future-dated
1172
+ * amendment can be activated ahead of its effective date without
1173
+ * governing earlier qualifications (`effectiveFrom: null` = effective
1174
+ * immediately).
1175
+ */
1176
+ async latestActiveByKey(planKey, at = /* @__PURE__ */ new Date(), tenantId) {
1177
+ const inEffect = (await this.list({
1178
+ where: {
1179
+ planKey,
1180
+ status: "active"
1181
+ },
1182
+ orderBy: "version DESC"
1183
+ })).filter((plan) => plan.effectiveFrom === null || plan.effectiveFrom <= at);
1184
+ if (tenantId === void 0) return inEffect[0] ?? null;
1185
+ return inEffect.find((plan) => plan.tenantId === tenantId) ?? inEffect.find((plan) => plan.tenantId === null) ?? null;
1186
+ }
1187
+ /**
1188
+ * Create an amendment: insert a new DRAFT row with
1189
+ * `version = max(existing versions) + 1`, copying the latest existing
1190
+ * version's fields and then applying `changes`. The source version is not
1191
+ * touched — activate the draft (and supersede the prior active version)
1192
+ * as a separate, explicit step.
1193
+ *
1194
+ * Throws when no version of `planKey` exists (nothing to amend — use
1195
+ * `create` for a brand-new plan).
1196
+ */
1197
+ async createAmendment(planKey, changes = {}) {
1198
+ const latest = (await this.findByPlanKey(planKey))[0];
1199
+ if (!latest) throw new Error(`CommissionPlanCollection.createAmendment: no versions exist for plan key '${planKey}' \u2014 create the plan first`);
1200
+ if (changes.components !== void 0) validateCommissionPlanComponents(changes.components);
1201
+ return await this.create({
1202
+ tenantId: latest.tenantId,
1203
+ planKey,
1204
+ version: latest.version + 1,
1205
+ status: "draft",
1206
+ name: changes.name ?? latest.name,
1207
+ description: changes.description ?? latest.description,
1208
+ currency: changes.currency ?? latest.currency,
1209
+ effectiveFrom: changes.effectiveFrom !== void 0 ? changes.effectiveFrom : latest.effectiveFrom,
1210
+ components: changes.components !== void 0 ? JSON.stringify(changes.components) : latest.components,
1211
+ metadata: changes.metadata !== void 0 ? JSON.stringify(changes.metadata) : latest.metadata
1212
+ });
1213
+ }
1214
+ };
1215
+ //#endregion
1216
+ //#region src/commissions/models/Earner.ts
1217
+ var __defProp$1 = Object.defineProperty;
1218
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
1219
+ var __decorateClass$1 = (decorators, target, key, kind) => {
1220
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
1221
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1222
+ if (kind && result) __defProp$1(target, key, result);
1223
+ return result;
1224
+ };
1225
+ var Earner = class extends SmrtObject {
1226
+ tenantId = null;
1227
+ profileId = "";
1228
+ /** Human-readable display name for portals and operator views. */
1229
+ displayName = "";
1230
+ /** Account lifecycle: `pending` (default) → `active` / `suspended`. */
1231
+ status = "pending";
1232
+ /** Preferred payout delivery method. */
1233
+ payoutMethod = "bank_transfer";
1234
+ /**
1235
+ * Minimum unsettled balance (integer cents) before a payout batch is
1236
+ * created. Default $50.00 = 5000 cents.
1237
+ */
1238
+ payoutThresholdCents = 5e3;
1239
+ /**
1240
+ * Payout cadence key. Open string so applications can define their own
1241
+ * schedules (`manual`, `monthly`, `weekly`, `net_30`, …); `manual` means
1242
+ * an operator triggers batches explicitly.
1243
+ */
1244
+ payoutScheduleKey = "manual";
1245
+ /** ISO 4217 currency all of this earner's balances settle in. */
1246
+ currency = "USD";
1247
+ /**
1248
+ * Additional metadata as a JSON string (tax info, payout-rail details,
1249
+ * …). Use {@link getMetadata}/{@link setMetadata}.
1250
+ */
1251
+ metadata = "{}";
1252
+ constructor(options = {}) {
1253
+ super(options);
1254
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1255
+ if (options.profileId !== void 0) this.profileId = options.profileId;
1256
+ if (options.displayName !== void 0) this.displayName = options.displayName;
1257
+ if (options.status !== void 0) this.status = options.status;
1258
+ if (options.payoutMethod !== void 0) this.payoutMethod = options.payoutMethod;
1259
+ if (options.payoutThresholdCents !== void 0) this.payoutThresholdCents = options.payoutThresholdCents;
1260
+ if (options.payoutScheduleKey !== void 0) this.payoutScheduleKey = options.payoutScheduleKey;
1261
+ if (options.currency !== void 0) this.currency = options.currency;
1262
+ if (options.metadata !== void 0) this.metadata = options.metadata;
1263
+ }
1264
+ isActive() {
1265
+ return this.status === "active";
1266
+ }
1267
+ isPending() {
1268
+ return this.status === "pending";
1269
+ }
1270
+ isSuspended() {
1271
+ return this.status === "suspended";
1272
+ }
1273
+ /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
1274
+ getMetadata() {
1275
+ if (!this.metadata) return {};
1276
+ try {
1277
+ const parsed = JSON.parse(this.metadata);
1278
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1279
+ } catch {
1280
+ return {};
1281
+ }
1282
+ }
1283
+ /** Serialize and store {@link metadata}. */
1284
+ setMetadata(data) {
1285
+ this.metadata = JSON.stringify(data ?? {});
1286
+ }
1287
+ };
1288
+ __decorateClass$1([tenantId({ nullable: true })], Earner.prototype, "tenantId", 2);
1289
+ __decorateClass$1([crossPackageRef("@happyvertical/smrt-profiles:Profile")], Earner.prototype, "profileId", 2);
1290
+ Earner = __decorateClass$1([TenantScoped({ mode: "optional" }), smrt({
1291
+ api: { include: [
1292
+ "list",
1293
+ "get",
1294
+ "create",
1295
+ "update"
1296
+ ] },
1297
+ mcp: { include: [
1298
+ "list",
1299
+ "get",
1300
+ "create"
1301
+ ] },
1302
+ cli: true
1303
+ })], Earner);
1304
+ //#endregion
1305
+ //#region src/commissions/collections/EarnerCollection.ts
1306
+ var EarnerCollection = class extends SmrtCollection {
1307
+ static _itemClass = Earner;
1308
+ /** Earners linked to a smrt-profiles Profile. */
1309
+ async findByProfile(profileId) {
1310
+ return await this.list({
1311
+ where: { profileId },
1312
+ orderBy: "created_at DESC"
1313
+ });
1314
+ }
1315
+ /** Earners by status. */
1316
+ async findByStatus(status) {
1317
+ return await this.list({
1318
+ where: { status },
1319
+ orderBy: "created_at DESC"
1320
+ });
1321
+ }
1322
+ /** All active earners. */
1323
+ async findActive() {
1324
+ return await this.findByStatus("active");
1325
+ }
1326
+ };
1327
+ //#endregion
1328
+ //#region src/commissions/models/EarningEvent.ts
1329
+ var __defProp = Object.defineProperty;
1330
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
1331
+ var __decorateClass = (decorators, target, key, kind) => {
1332
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
1333
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1334
+ if (kind && result) __defProp(target, key, result);
1335
+ return result;
1336
+ };
1337
+ var persistedEventState = /* @__PURE__ */ new WeakMap();
1338
+ var EarningEvent = class extends SmrtObject {
1339
+ tenantId = null;
1340
+ eventKind = "";
1341
+ /** When the commercial event occurred (not when it was ingested). */
1342
+ occurredAt = /* @__PURE__ */ new Date();
1343
+ /**
1344
+ * Generic earning-source discriminator (`referral`, `opportunity`,
1345
+ * `subscription`, `ad_event`, …). Free-form; this module attaches no
1346
+ * semantics to it.
1347
+ */
1348
+ sourceKind = "";
1349
+ /** Id of the source record named by {@link sourceKind}. */
1350
+ sourceId = "";
1351
+ /** Gross amount of the event in integer cents. */
1352
+ grossAmountCents = 0;
1353
+ netAmountCents = null;
1354
+ marginCents = null;
1355
+ /** ISO 4217 currency of the event's amounts. */
1356
+ currency = "USD";
1357
+ /**
1358
+ * JSON map of `basisKey → integer cents` for `custom`-basis plan
1359
+ * components. Use {@link getCustomBases}/{@link setCustomBases}.
1360
+ */
1361
+ customBases = "{}";
1362
+ dedupeKey = "";
1363
+ /** Additional metadata as a JSON string. */
1364
+ metadata = "{}";
1365
+ constructor(options = {}) {
1366
+ super(options);
1367
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1368
+ if (options.eventKind !== void 0) this.eventKind = options.eventKind;
1369
+ if (options.occurredAt !== void 0) this.occurredAt = EarningEvent.coerceDate(options.occurredAt) ?? /* @__PURE__ */ new Date();
1370
+ if (options.sourceKind !== void 0) this.sourceKind = options.sourceKind;
1371
+ if (options.sourceId !== void 0) this.sourceId = options.sourceId;
1372
+ if (options.grossAmountCents !== void 0) this.grossAmountCents = options.grossAmountCents;
1373
+ if (options.netAmountCents !== void 0) this.netAmountCents = options.netAmountCents;
1374
+ if (options.marginCents !== void 0) this.marginCents = options.marginCents;
1375
+ if (options.currency !== void 0) this.currency = options.currency;
1376
+ if (options.customBases !== void 0) this.customBases = options.customBases;
1377
+ if (options.dedupeKey !== void 0) this.dedupeKey = options.dedupeKey;
1378
+ if (options.metadata !== void 0) this.metadata = options.metadata;
1379
+ }
1380
+ /**
1381
+ * Re-coerce {@link occurredAt} after the framework reapplies raw option /
1382
+ * hydrated row values (SQLite hands back ISO strings), and capture the
1383
+ * persisted state for the immutability guard when this instance hydrated
1384
+ * an existing row.
1385
+ */
1386
+ async initialize() {
1387
+ await super.initialize();
1388
+ this.occurredAt = EarningEvent.coerceDate(this.occurredAt) ?? /* @__PURE__ */ new Date();
1389
+ if (await this.isSaved()) persistedEventState.set(this, this.serializeState());
1390
+ return this;
1391
+ }
1392
+ /**
1393
+ * Save with the evidence-immutability guard. EarningEvents are immutable
1394
+ * commercial evidence; three write vectors are closed:
1395
+ *
1396
+ * - a HYDRATED persisted row must serialize identically to its captured
1397
+ * state (no-op re-saves pass, any change throws);
1398
+ * - an instance carrying an existing id WITHOUT having hydrated it
1399
+ * (`create({ id, _skipLoad: true })`) is rejected outright;
1400
+ * - a NEW instance whose `dedupeKey` already belongs to another row is
1401
+ * refused outright: the natural-key upsert would not only rewrite the
1402
+ * evidence values but ROTATE the row's id (orphaning any Commission
1403
+ * whose `earningEventId` points at it). Idempotent ingestion goes
1404
+ * through `EarningEventCollection.getOrCreateByDedupeKey()`, which
1405
+ * finds first and never upserts.
1406
+ */
1407
+ async save() {
1408
+ const captured = persistedEventState.get(this);
1409
+ if (captured !== void 0) {
1410
+ if (captured !== this.serializeState()) throw new Error(`EarningEvent ${this.id ?? "<new>"}: earning events are immutable evidence \u2014 record a correcting event (or a CommissionAdjustment downstream) instead of editing this row.`);
1411
+ } else if (this.id && await this.isSaved()) throw new Error(`EarningEvent ${this.id}: refusing to overwrite an existing event row from a non-hydrated instance \u2014 earning events are immutable evidence.`);
1412
+ else if (this.dedupeKey) try {
1413
+ const row = await this.db.get(this.tableName, { dedupe_key: this.dedupeKey });
1414
+ if (row && row.id !== this.id) throw new Error(`EarningEvent (dedupeKey '${this.dedupeKey}'): an event with this dedupe key already exists \u2014 earning events are immutable evidence, and the natural-key upsert would rotate the existing row's id (orphaning commissions that reference it). Use EarningEventCollection.getOrCreateByDedupeKey() for idempotent ingestion, or record a new event under its own dedupe key.`);
1415
+ } catch (error) {
1416
+ if (error instanceof Error && error.message.includes("immutable evidence")) throw error;
1417
+ }
1418
+ const result = await super.save();
1419
+ persistedEventState.set(this, this.serializeState());
1420
+ return result;
1421
+ }
1422
+ serializeState() {
1423
+ return JSON.stringify({
1424
+ tenantId: this.tenantId,
1425
+ eventKind: this.eventKind,
1426
+ occurredAt: this.occurredAt.toISOString(),
1427
+ sourceKind: this.sourceKind,
1428
+ sourceId: this.sourceId,
1429
+ grossAmountCents: this.grossAmountCents,
1430
+ netAmountCents: this.netAmountCents,
1431
+ marginCents: this.marginCents,
1432
+ currency: this.currency,
1433
+ customBases: this.customBases,
1434
+ dedupeKey: this.dedupeKey,
1435
+ metadata: this.metadata
1436
+ });
1437
+ }
1438
+ /**
1439
+ * Parse {@link customBases} into a `basisKey → cents` map; non-numeric
1440
+ * values are dropped. Returns `{}` on empty/invalid JSON.
1441
+ */
1442
+ getCustomBases() {
1443
+ if (!this.customBases) return {};
1444
+ try {
1445
+ const parsed = JSON.parse(this.customBases);
1446
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
1447
+ const out = {};
1448
+ for (const [key, value] of Object.entries(parsed)) if (typeof value === "number" && Number.isFinite(value)) out[key] = value;
1449
+ return out;
1450
+ } catch {
1451
+ return {};
1452
+ }
1453
+ }
1454
+ /** Serialize and store {@link customBases}. */
1455
+ setCustomBases(bases) {
1456
+ this.customBases = JSON.stringify(bases ?? {});
1457
+ }
1458
+ /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
1459
+ getMetadata() {
1460
+ if (!this.metadata) return {};
1461
+ try {
1462
+ const parsed = JSON.parse(this.metadata);
1463
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1464
+ } catch {
1465
+ return {};
1466
+ }
1467
+ }
1468
+ /** Serialize and store {@link metadata}. */
1469
+ setMetadata(data) {
1470
+ this.metadata = JSON.stringify(data ?? {});
1471
+ }
1472
+ static coerceDate(value) {
1473
+ if (value == null) return null;
1474
+ if (value instanceof Date) return value;
1475
+ if (typeof value === "number" || typeof value === "string") {
1476
+ const d = new Date(value);
1477
+ return Number.isNaN(d.getTime()) ? null : d;
1478
+ }
1479
+ return null;
1480
+ }
1481
+ };
1482
+ __decorateClass([tenantId({ nullable: true })], EarningEvent.prototype, "tenantId", 2);
1483
+ __decorateClass([field({ required: true })], EarningEvent.prototype, "eventKind", 2);
1484
+ __decorateClass([field({
1485
+ type: "integer",
1486
+ nullable: true
1487
+ })], EarningEvent.prototype, "netAmountCents", 2);
1488
+ __decorateClass([field({
1489
+ type: "integer",
1490
+ nullable: true
1491
+ })], EarningEvent.prototype, "marginCents", 2);
1492
+ __decorateClass([field({ required: true })], EarningEvent.prototype, "dedupeKey", 2);
1493
+ EarningEvent = __decorateClass([TenantScoped({ mode: "optional" }), smrt({
1494
+ conflictColumns: ["dedupe_key"],
1495
+ api: { include: [
1496
+ "create",
1497
+ "list",
1498
+ "get"
1499
+ ] },
1500
+ mcp: { include: ["list", "create"] },
1501
+ cli: false
1502
+ })], EarningEvent);
1503
+ //#endregion
1504
+ //#region src/commissions/collections/EarningEventCollection.ts
1505
+ var EarningEventCollection = class extends SmrtCollection {
1506
+ static _itemClass = EarningEvent;
1507
+ /** Look up an event by its idempotency natural key. */
1508
+ async findByDedupeKey(dedupeKey) {
1509
+ if (!dedupeKey) return null;
1510
+ return (await this.list({
1511
+ where: { dedupeKey },
1512
+ limit: 1
1513
+ }))[0] ?? null;
1514
+ }
1515
+ /**
1516
+ * Idempotent ingestion: if an event with `options.dedupeKey` already
1517
+ * exists, return it untouched (`created: false`) — evidence is immutable,
1518
+ * so a replay never updates the stored row. Otherwise create the event.
1519
+ *
1520
+ * `dedupeKey` is required — callers embed tenant/source identity in it
1521
+ * (e.g. `` `${tenantId}:${sourceKind}:${sourceId}:${eventKind}` ``);
1522
+ * an empty key would silently disable idempotency, so it throws instead.
1523
+ */
1524
+ async getOrCreateByDedupeKey(options) {
1525
+ const dedupeKey = options.dedupeKey ?? "";
1526
+ if (!dedupeKey) throw new Error("EarningEventCollection.getOrCreateByDedupeKey requires a dedupeKey");
1527
+ const existing = await this.findByDedupeKey(dedupeKey);
1528
+ if (existing) return {
1529
+ event: existing,
1530
+ created: false
1531
+ };
1532
+ const { occurredAt, ...rest } = options;
1533
+ return {
1534
+ event: await this.create({
1535
+ ...rest,
1536
+ ...occurredAt !== void 0 ? { occurredAt: new Date(occurredAt) } : {}
1537
+ }),
1538
+ created: true
1539
+ };
1540
+ }
1541
+ /** Events for one generic earning source, newest occurrence first. */
1542
+ async findBySource(sourceKind, sourceId) {
1543
+ return await this.list({
1544
+ where: {
1545
+ sourceKind,
1546
+ sourceId
1547
+ },
1548
+ orderBy: "occurred_at DESC"
1549
+ });
1550
+ }
1551
+ /** Events by kind, newest occurrence first. */
1552
+ async findByKind(eventKind) {
1553
+ return await this.list({
1554
+ where: { eventKind },
1555
+ orderBy: "occurred_at DESC"
1556
+ });
1557
+ }
1558
+ };
1559
+ //#endregion
1560
+ //#region src/commissions/money.ts
1561
+ function roundCents(value) {
1562
+ const rounded = Math.sign(value) * Math.round(Math.abs(value));
1563
+ return rounded === 0 ? 0 : rounded;
1564
+ }
1565
+ function centsToAmount(cents) {
1566
+ return cents / 100;
1567
+ }
1568
+ function amountToCents(amount) {
1569
+ return roundCents(amount * 100);
1570
+ }
1571
+ function calculateCommissionAmountCents(baseCents, rate, shareFraction) {
1572
+ return roundCents(baseCents * rate * (shareFraction ?? 1));
1573
+ }
1574
+ //#endregion
1575
+ //#region src/commissions/services/CommissionBalanceService.ts
1576
+ var CommissionBalanceService = class CommissionBalanceService {
1577
+ constructor(commissions, adjustments) {
1578
+ this.commissions = commissions;
1579
+ this.adjustments = adjustments;
1580
+ }
1581
+ commissions;
1582
+ adjustments;
1583
+ static async create(classOptions = {}) {
1584
+ return new CommissionBalanceService(await CommissionCollection.create(classOptions), await CommissionAdjustmentCollection.create(classOptions));
1585
+ }
1586
+ /** Compute the {@link EarnerBalance} for one earner in one currency. */
1587
+ async getBalance(earnerId, currency) {
1588
+ const rows = await this.commissions.list({ where: {
1589
+ earnerId,
1590
+ currency
1591
+ } });
1592
+ const sumByStatus = (status) => rows.filter((c) => c.status === status).reduce((sum, c) => sum + c.amountCents, 0);
1593
+ const pendingCents = sumByStatus("pending");
1594
+ const earnedCents = sumByStatus("earned");
1595
+ const approvedCents = sumByStatus("approved");
1596
+ const payableCents = rows.filter((c) => c.status === "payable" && !c.payoutId).reduce((sum, c) => sum + c.amountCents, 0);
1597
+ const statusById = /* @__PURE__ */ new Map();
1598
+ for (const c of rows) if (c.id) statusById.set(c.id, c.status);
1599
+ const unsettled = await this.adjustments.findUnsettledByEarner(earnerId, currency);
1600
+ let unsettledAdjustmentCents = 0;
1601
+ for (const adjustment of unsettled) {
1602
+ const parentStatus = statusById.get(adjustment.commissionId);
1603
+ if (parentStatus !== void 0 && ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES.includes(parentStatus)) unsettledAdjustmentCents += adjustment.amountCents;
1604
+ }
1605
+ return {
1606
+ earnerId,
1607
+ currency,
1608
+ payableCents,
1609
+ pendingCents,
1610
+ earnedCents,
1611
+ approvedCents,
1612
+ unsettledAdjustmentCents,
1613
+ netPayableCents: payableCents + unsettledAdjustmentCents
1614
+ };
1615
+ }
1616
+ };
1617
+ //#endregion
1618
+ //#region src/commissions/services/CommissionCalculationService.ts
1619
+ var MS_PER_DAY = 1440 * 60 * 1e3;
1620
+ var CommissionCalculationService = class CommissionCalculationService {
1621
+ constructor(commissions, earners) {
1622
+ this.commissions = commissions;
1623
+ this.earners = earners;
1624
+ }
1625
+ commissions;
1626
+ earners;
1627
+ static async create(classOptions = {}) {
1628
+ return new CommissionCalculationService(await CommissionCollection.create(classOptions), await EarnerCollection.create(classOptions));
1629
+ }
1630
+ /**
1631
+ * Calculate commissions for one event × one earner × a component set.
1632
+ *
1633
+ * For each component whose `trigger` matches `event.eventKind` (or `'*'`
1634
+ * — non-matching components are silently filtered, not "skipped"):
1635
+ *
1636
+ * 1. **Idempotency** — if a Commission already exists for this
1637
+ * (event, terms, component, earner) tuple, it is returned in
1638
+ * `existing` and nothing else runs for the component.
1639
+ * 2. **Currency** — `input.currency` (when given) must equal the event's;
1640
+ * otherwise skip `'currency_mismatch'`.
1641
+ * 3. **Recurrence** — `one_time` components skip
1642
+ * `'occurrence_limit_reached'` once the resolver reports ≥ 1 prior
1643
+ * occurrence; `recurring` components honor `maxOccurrences` and
1644
+ * `windowMonths` (events after `anchorAt + windowMonths` skip
1645
+ * `'outside_recurrence_window'`).
1646
+ * 4. **Basis** — gross → `grossAmountCents`; net → `netAmountCents`
1647
+ * (skip `'net_basis_undefined'` when null — net is explicit, NEVER
1648
+ * derived from gross); margin → `marginCents` (skip
1649
+ * `'margin_basis_undefined'` when null); fixed → `fixedAmountCents`;
1650
+ * custom → `getCustomBases()[customBasisKey]` (skip
1651
+ * `'custom_basis_missing'`).
1652
+ * 5. **Amount** — `roundCents(base * rate * shareFraction)`; for `fixed`,
1653
+ * `roundCents(fixedAmountCents * shareFraction)` with `rate` recorded
1654
+ * as `0`. Rounding happens exactly once, on the final product.
1655
+ *
1656
+ * Every created Commission is persisted `pending`, carries the event's
1657
+ * tenant/currency/source, a complete {@link CommissionCalculationTrace},
1658
+ * `clearingEndsAt` when `clearingDays` was given, and the dedupe key
1659
+ * `` `${event.dedupeKey}:${termsSnapshotId || planKey + '@' + planVersion}:${componentKey}:${earnerId}:${occurrenceIndex}` ``.
1660
+ */
1661
+ async calculateForEvent(input) {
1662
+ const { event } = input;
1663
+ if (!event.id) throw new Error("CommissionCalculationService.calculateForEvent requires a persisted event (missing id)");
1664
+ if (!input.earnerId) throw new Error("CommissionCalculationService.calculateForEvent requires an earnerId");
1665
+ const shareFraction = input.shareFraction ?? 1;
1666
+ if (!Number.isFinite(shareFraction) || shareFraction < 0 || shareFraction > 1) throw new Error(`CommissionCalculationService.calculateForEvent: shareFraction must be a finite number in [0, 1], got ${String(shareFraction)}`);
1667
+ validateCommissionPlanComponents(input.components);
1668
+ if (this.earners) {
1669
+ const earner = await this.earners.get({ id: input.earnerId });
1670
+ if (earner && earner.tenantId !== null && (event.tenantId ?? null) !== null && earner.tenantId !== event.tenantId) throw new Error(`CommissionCalculationService.calculateForEvent: earner '${input.earnerId}' belongs to tenant '${earner.tenantId}' but the event belongs to tenant '${event.tenantId}' \u2014 cross-tenant commissions are refused.`);
1671
+ }
1672
+ const termsRef = input.termsSnapshotId || `${input.planKey}@${input.planVersion}`;
1673
+ const created = [];
1674
+ const skipped = [];
1675
+ const existing = [];
1676
+ for (const component of input.components) {
1677
+ if (component.trigger !== "*" && component.trigger !== event.eventKind) continue;
1678
+ const priorForEvent = await this.commissions.list({
1679
+ where: {
1680
+ earningEventId: event.id,
1681
+ earnerId: input.earnerId,
1682
+ componentKey: component.key,
1683
+ planKey: input.planKey,
1684
+ planVersion: input.planVersion,
1685
+ termsSnapshotId: input.termsSnapshotId ?? ""
1686
+ },
1687
+ limit: 1
1688
+ });
1689
+ if (priorForEvent[0]) {
1690
+ existing.push(priorForEvent[0]);
1691
+ continue;
1692
+ }
1693
+ if (input.currency !== void 0 && input.currency !== event.currency) {
1694
+ skipped.push({
1695
+ componentKey: component.key,
1696
+ reason: "currency_mismatch"
1697
+ });
1698
+ continue;
1699
+ }
1700
+ const occurrenceCount = input.occurrenceCountResolver ? await input.occurrenceCountResolver(component.key) : 0;
1701
+ const recurrence = component.recurrence;
1702
+ if (recurrence) {
1703
+ if (recurrence.kind === "one_time" && occurrenceCount >= 1) {
1704
+ skipped.push({
1705
+ componentKey: component.key,
1706
+ reason: "occurrence_limit_reached"
1707
+ });
1708
+ continue;
1709
+ }
1710
+ if (recurrence.kind === "recurring" && recurrence.maxOccurrences !== void 0 && occurrenceCount >= recurrence.maxOccurrences) {
1711
+ skipped.push({
1712
+ componentKey: component.key,
1713
+ reason: "occurrence_limit_reached"
1714
+ });
1715
+ continue;
1716
+ }
1717
+ if (recurrence.windowMonths !== void 0 && input.anchorAt !== void 0 && event.occurredAt.getTime() > CommissionCalculationService.addMonths(input.anchorAt, recurrence.windowMonths).getTime()) {
1718
+ skipped.push({
1719
+ componentKey: component.key,
1720
+ reason: "outside_recurrence_window"
1721
+ });
1722
+ continue;
1723
+ }
1724
+ }
1725
+ let baseAmountCents;
1726
+ switch (component.basis) {
1727
+ case "gross":
1728
+ baseAmountCents = event.grossAmountCents;
1729
+ break;
1730
+ case "net":
1731
+ if (event.netAmountCents === null) {
1732
+ skipped.push({
1733
+ componentKey: component.key,
1734
+ reason: "net_basis_undefined"
1735
+ });
1736
+ continue;
1737
+ }
1738
+ baseAmountCents = event.netAmountCents;
1739
+ break;
1740
+ case "margin":
1741
+ if (event.marginCents === null) {
1742
+ skipped.push({
1743
+ componentKey: component.key,
1744
+ reason: "margin_basis_undefined"
1745
+ });
1746
+ continue;
1747
+ }
1748
+ baseAmountCents = event.marginCents;
1749
+ break;
1750
+ case "fixed":
1751
+ if (typeof component.fixedAmountCents !== "number") {
1752
+ skipped.push({
1753
+ componentKey: component.key,
1754
+ reason: "fixed_amount_missing"
1755
+ });
1756
+ continue;
1757
+ }
1758
+ baseAmountCents = component.fixedAmountCents;
1759
+ break;
1760
+ case "custom": {
1761
+ const bases = event.getCustomBases();
1762
+ const key = component.customBasisKey ?? "";
1763
+ const value = key ? bases[key] : void 0;
1764
+ if (typeof value !== "number") {
1765
+ skipped.push({
1766
+ componentKey: component.key,
1767
+ reason: "custom_basis_missing"
1768
+ });
1769
+ continue;
1770
+ }
1771
+ baseAmountCents = value;
1772
+ break;
1773
+ }
1774
+ }
1775
+ let rate;
1776
+ let amountCents;
1777
+ if (component.basis === "fixed") {
1778
+ rate = 0;
1779
+ amountCents = roundCents(baseAmountCents * shareFraction);
1780
+ } else {
1781
+ if (typeof component.rate !== "number") {
1782
+ skipped.push({
1783
+ componentKey: component.key,
1784
+ reason: "rate_missing"
1785
+ });
1786
+ continue;
1787
+ }
1788
+ rate = component.rate;
1789
+ amountCents = calculateCommissionAmountCents(baseAmountCents, rate, shareFraction);
1790
+ }
1791
+ const occurrenceIndex = occurrenceCount;
1792
+ const dedupeKey = `${event.dedupeKey}:${termsRef}:${component.key}:${input.earnerId}:${occurrenceIndex}`;
1793
+ const priorByKey = await this.commissions.findByDedupeKey(dedupeKey);
1794
+ if (priorByKey) {
1795
+ existing.push(priorByKey);
1796
+ continue;
1797
+ }
1798
+ const trace = {
1799
+ planKey: input.planKey,
1800
+ planVersion: input.planVersion,
1801
+ componentKey: component.key,
1802
+ basis: component.basis,
1803
+ baseAmountCents,
1804
+ rate,
1805
+ shareFraction,
1806
+ occurrenceIndex,
1807
+ earningEventId: event.id,
1808
+ roundingMode: "half_away_from_zero"
1809
+ };
1810
+ let commission;
1811
+ try {
1812
+ commission = await this.commissions.create({
1813
+ tenantId: event.tenantId,
1814
+ earnerId: input.earnerId,
1815
+ earningEventId: event.id,
1816
+ planKey: input.planKey,
1817
+ planVersion: input.planVersion,
1818
+ componentKey: component.key,
1819
+ termsSnapshotKind: input.termsSnapshotKind ?? "",
1820
+ termsSnapshotId: input.termsSnapshotId ?? "",
1821
+ basis: component.basis,
1822
+ baseAmountCents,
1823
+ rate,
1824
+ shareFraction,
1825
+ splitGroupId: input.splitGroupId ?? "",
1826
+ amountCents,
1827
+ currency: event.currency,
1828
+ status: "pending",
1829
+ clearingEndsAt: input.clearingDays !== void 0 ? new Date(event.occurredAt.getTime() + input.clearingDays * MS_PER_DAY) : null,
1830
+ sourceKind: event.sourceKind,
1831
+ sourceId: event.sourceId,
1832
+ calculationTrace: JSON.stringify(trace),
1833
+ dedupeKey
1834
+ });
1835
+ } catch (error) {
1836
+ if (error instanceof Error && error.message.includes("immutable audit rows")) {
1837
+ const winner = await this.commissions.list({
1838
+ where: { dedupeKey },
1839
+ limit: 1
1840
+ });
1841
+ if (winner[0]) {
1842
+ existing.push(winner[0]);
1843
+ continue;
1844
+ }
1845
+ }
1846
+ throw error;
1847
+ }
1848
+ created.push(commission);
1849
+ }
1850
+ return {
1851
+ created,
1852
+ skipped,
1853
+ existing
1854
+ };
1855
+ }
1856
+ /**
1857
+ * Calendar-month addition (UTC). JS `setUTCMonth` semantics: day-of-month
1858
+ * overflow rolls into the next month (Jan 31 + 1 month → Mar 2/3), which
1859
+ * is acceptable for coarse recurrence windows.
1860
+ */
1861
+ static addMonths(date, months) {
1862
+ const result = new Date(date.getTime());
1863
+ result.setUTCMonth(result.getUTCMonth() + months);
1864
+ return result;
1865
+ }
1866
+ };
1867
+ //#endregion
1868
+ //#region src/commissions/services/CommissionPayoutService.ts
1869
+ var CommissionPayoutService = class CommissionPayoutService {
1870
+ constructor(deps) {
1871
+ this.deps = deps;
1872
+ }
1873
+ deps;
1874
+ static async create(classOptions = {}) {
1875
+ return new CommissionPayoutService({
1876
+ earners: await EarnerCollection.create(classOptions),
1877
+ commissions: await CommissionCollection.create(classOptions),
1878
+ adjustments: await CommissionAdjustmentCollection.create(classOptions),
1879
+ payouts: await CommissionPayoutCollection.create(classOptions)
1880
+ });
1881
+ }
1882
+ /**
1883
+ * Create a settlement batch for one earner in one currency.
1884
+ *
1885
+ * Flow:
1886
+ * 1. **Idempotency + repair** — an existing payout with the (defaulted)
1887
+ * key is returned as `{ payout, created: false }`. A clean replay
1888
+ * touches nothing (new payable work is never swept into an existing
1889
+ * batch). A PENDING payout whose stored totals disagree with the rows
1890
+ * stamped with its id — the signature of an interrupted claim pass —
1891
+ * is repaired: the claim pass re-runs and the totals are reconciled
1892
+ * from the verified membership. Past `pending` the batch is frozen.
1893
+ * 2. **Gather** — payable unsettled commissions for the earner/currency,
1894
+ * plus unsettled adjustments whose parent commission is
1895
+ * earned/approved/payable/paid (same eligibility as the balance
1896
+ * service, so the batch settles exactly what the balance reports).
1897
+ * 3. **Refuse** — `netTotal <= 0` → `'nothing_payable'`;
1898
+ * `netTotal < threshold` (earner default, overridable) →
1899
+ * `'below_threshold'`. Nothing is minted or stamped on refusal.
1900
+ * 4. **Mint, claim, reconcile** — create the `pending` payout, then
1901
+ * CLAIM the gathered rows through the collections' conditional
1902
+ * `claimForPayout` (rows grabbed by another batch in the interim are
1903
+ * skipped, never double-claimed), and finally store totals computed
1904
+ * from the rows that were VERIFIABLY claimed — the payout's totals
1905
+ * are always reproducible from its member rows.
1906
+ *
1907
+ * Concurrency: claims are conditional with post-save verification, which
1908
+ * narrows but does not eliminate races between batches with different
1909
+ * keys (the collection layer exposes no cross-row transaction — the same
1910
+ * stance as commerce/ledgers compensation). Settlement runs are expected
1911
+ * to be single-writer per earner; totals are correct-by-construction from
1912
+ * claimed rows either way.
1913
+ */
1914
+ async createPayoutBatch(input) {
1915
+ const now = input.now ?? /* @__PURE__ */ new Date();
1916
+ const earner = await this.deps.earners.get({ id: input.earnerId });
1917
+ if (!earner) throw new Error(`CommissionPayoutService: earner '${input.earnerId}' not found`);
1918
+ const idempotencyKey = input.idempotencyKey ?? CommissionPayoutService.defaultIdempotencyKey(input.earnerId, input.currency, input.periodEnd ?? now);
1919
+ const existingPayout = await this.deps.payouts.findByIdempotencyKey(idempotencyKey);
1920
+ if (existingPayout) {
1921
+ if (existingPayout.isPending() && !await this.membershipConsistent(existingPayout)) return {
1922
+ ...await this.claimAndReconcile(existingPayout, input),
1923
+ created: false
1924
+ };
1925
+ return {
1926
+ payout: existingPayout,
1927
+ created: false,
1928
+ settledCommissionIds: [],
1929
+ settledAdjustmentIds: []
1930
+ };
1931
+ }
1932
+ const commissions = await this.deps.commissions.findPayableUnsettled(input.earnerId, input.currency);
1933
+ const eligibleAdjustments = await this.findEligibleUnsettledAdjustments(input.earnerId, input.currency);
1934
+ const commissionTotalCents = commissions.reduce((sum, c) => sum + c.amountCents, 0);
1935
+ const adjustmentTotalCents = eligibleAdjustments.reduce((sum, a) => sum + a.amountCents, 0);
1936
+ const netTotalCents = commissionTotalCents + adjustmentTotalCents;
1937
+ if (netTotalCents <= 0) return {
1938
+ payout: null,
1939
+ created: false,
1940
+ reason: "nothing_payable",
1941
+ settledCommissionIds: [],
1942
+ settledAdjustmentIds: []
1943
+ };
1944
+ if (netTotalCents < (input.minimumThresholdCents ?? earner.payoutThresholdCents)) return {
1945
+ payout: null,
1946
+ created: false,
1947
+ reason: "below_threshold",
1948
+ settledCommissionIds: [],
1949
+ settledAdjustmentIds: []
1950
+ };
1951
+ const minted = await this.deps.payouts.create({
1952
+ tenantId: earner.tenantId,
1953
+ earnerId: input.earnerId,
1954
+ currency: input.currency,
1955
+ periodStart: input.periodStart ?? null,
1956
+ periodEnd: input.periodEnd ?? null,
1957
+ payoutMethod: input.payoutMethod ?? earner.payoutMethod,
1958
+ status: "pending",
1959
+ commissionTotalCents,
1960
+ adjustmentTotalCents,
1961
+ totalAmountCents: netTotalCents,
1962
+ idempotencyKey
1963
+ });
1964
+ const payout = await this.deps.payouts.findByIdempotencyKey(idempotencyKey) ?? minted;
1965
+ return {
1966
+ ...await this.claimAndReconcile(payout, input),
1967
+ created: true
1968
+ };
1969
+ }
1970
+ /**
1971
+ * Whether a payout's stored totals are reproducible from the rows
1972
+ * actually stamped with its id — the invariant an interrupted claim pass
1973
+ * breaks. Clean replays short-circuit on this; repair runs only when it
1974
+ * fails.
1975
+ */
1976
+ async membershipConsistent(payout) {
1977
+ const payoutId = payout.id ?? "";
1978
+ const members = await this.deps.commissions.findByPayout(payoutId);
1979
+ const memberAdjustments = await this.deps.adjustments.findByPayout(payoutId);
1980
+ const commissionTotalCents = members.reduce((sum, c) => sum + c.amountCents, 0);
1981
+ const adjustmentTotalCents = memberAdjustments.reduce((sum, a) => sum + a.amountCents, 0);
1982
+ return payout.commissionTotalCents === commissionTotalCents && payout.adjustmentTotalCents === adjustmentTotalCents && payout.totalAmountCents === commissionTotalCents + adjustmentTotalCents;
1983
+ }
1984
+ /**
1985
+ * Claim pass + totals reconciliation for a PENDING payout.
1986
+ *
1987
+ * The claim set is the union of rows already stamped with this payout
1988
+ * (an interrupted earlier pass) and the currently gathered eligible
1989
+ * rows. Claims go through the collections' conditional `claimForPayout`
1990
+ * (rows owned by another batch are skipped); totals are then recomputed
1991
+ * from the claimed rows and saved when they drift from what the payout
1992
+ * carries. In the pathological all-rows-raced-away case the payout keeps
1993
+ * zero totals and a note — auditable, never double-paid.
1994
+ */
1995
+ async claimAndReconcile(payout, input) {
1996
+ const payoutId = payout.id ?? "";
1997
+ const previouslyClaimed = await this.deps.commissions.findByPayout(payoutId);
1998
+ const gathered = await this.deps.commissions.findPayableUnsettled(input.earnerId, input.currency);
1999
+ const commissionIds = [...new Set([...previouslyClaimed, ...gathered].map((c) => c.id).filter((id) => !!id))];
2000
+ const claimedCommissions = await this.deps.commissions.claimForPayout(commissionIds, payoutId);
2001
+ const previouslyClaimedAdjustments = await this.deps.adjustments.findByPayout(payoutId);
2002
+ const gatheredAdjustments = await this.findEligibleUnsettledAdjustments(input.earnerId, input.currency);
2003
+ const adjustmentIds = [...new Set([...previouslyClaimedAdjustments, ...gatheredAdjustments].map((a) => a.id).filter((id) => !!id))];
2004
+ const claimedAdjustments = await this.deps.adjustments.claimForPayout(adjustmentIds, payoutId);
2005
+ const commissionTotalCents = claimedCommissions.reduce((sum, c) => sum + c.amountCents, 0);
2006
+ const adjustmentTotalCents = claimedAdjustments.reduce((sum, a) => sum + a.amountCents, 0);
2007
+ const totalAmountCents = commissionTotalCents + adjustmentTotalCents;
2008
+ if (payout.commissionTotalCents !== commissionTotalCents || payout.adjustmentTotalCents !== adjustmentTotalCents || payout.totalAmountCents !== totalAmountCents) {
2009
+ payout.commissionTotalCents = commissionTotalCents;
2010
+ payout.adjustmentTotalCents = adjustmentTotalCents;
2011
+ payout.totalAmountCents = totalAmountCents;
2012
+ if (claimedCommissions.length === 0 && claimedAdjustments.length === 0) payout.notes = "no rows claimed (raced by a concurrent batch); nothing will be paid";
2013
+ await payout.save();
2014
+ }
2015
+ return {
2016
+ payout,
2017
+ settledCommissionIds: claimedCommissions.map((c) => c.id).filter((id) => !!id),
2018
+ settledAdjustmentIds: claimedAdjustments.map((a) => a.id).filter((id) => !!id)
2019
+ };
2020
+ }
2021
+ /**
2022
+ * Complete a payout: flip the batch's settled commissions
2023
+ * `payable → paid` FIRST, then `payout.complete(paymentReference)`
2024
+ * (requires status `processing`). Ordering matters for recoverability —
2025
+ * if a member save fails mid-loop the payout is still `processing`, so a
2026
+ * retry finishes the remaining members (already-paid ones are skipped)
2027
+ * and then finalizes; the terminal transition never strands `payable`
2028
+ * members behind a `completed` payout. Adjustments carry no status —
2029
+ * stamping `payoutId` at batch time already settled them.
2030
+ */
2031
+ async completePayout(payoutId, paymentReference, now = /* @__PURE__ */ new Date()) {
2032
+ const payout = await this.requirePayout(payoutId);
2033
+ if (!payout.isProcessing()) throw new Error(`CommissionPayout ${payout.id ?? "<new>"}: cannot complete from status '${payout.status}'`);
2034
+ if (!paymentReference) throw new Error(`CommissionPayout ${payout.id ?? "<new>"}: complete() requires a paymentReference`);
2035
+ const members = await this.deps.commissions.findByPayout(payoutId);
2036
+ for (const commission of members) if (commission.isPayable()) {
2037
+ commission.markPaid(now);
2038
+ await commission.save();
2039
+ }
2040
+ payout.complete(paymentReference, now);
2041
+ await payout.save();
2042
+ return payout;
2043
+ }
2044
+ /**
2045
+ * Fail a payout (`approved | processing → failed`). The batch's rows stay
2046
+ * stamped — after `resetFromFailed()` the SAME payout retries the SAME
2047
+ * rows; releasing the rows to a different batch would double-pay them if
2048
+ * the failed remittance later settled.
2049
+ */
2050
+ async failPayout(payoutId, reason) {
2051
+ const payout = await this.requirePayout(payoutId);
2052
+ payout.fail(reason);
2053
+ await payout.save();
2054
+ return payout;
2055
+ }
2056
+ /**
2057
+ * Unsettled adjustments for the earner/currency whose parent commission
2058
+ * is earned/approved/payable/paid — the same eligibility rule the balance
2059
+ * service applies, so batches settle exactly what balances report.
2060
+ */
2061
+ async findEligibleUnsettledAdjustments(earnerId, currency) {
2062
+ const unsettled = await this.deps.adjustments.findUnsettledByEarner(earnerId, currency);
2063
+ if (unsettled.length === 0) return unsettled;
2064
+ const parentIds = [...new Set(unsettled.map((a) => a.commissionId).filter(Boolean))];
2065
+ const parents = await this.deps.commissions.listByIds(parentIds);
2066
+ const parentStatusById = /* @__PURE__ */ new Map();
2067
+ for (const parent of parents) if (parent.id) parentStatusById.set(parent.id, parent.status);
2068
+ return unsettled.filter((adjustment) => {
2069
+ const parentStatus = parentStatusById.get(adjustment.commissionId);
2070
+ return parentStatus !== void 0 && ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES.includes(parentStatus);
2071
+ });
2072
+ }
2073
+ async requirePayout(payoutId) {
2074
+ const payout = await this.deps.payouts.get({ id: payoutId });
2075
+ if (!payout) throw new Error(`CommissionPayoutService: payout '${payoutId}' not found`);
2076
+ return payout;
2077
+ }
2078
+ /** `${earnerId}:${currency}:${YYYY-MM-DD of periodEnd}` — see the input doc. */
2079
+ static defaultIdempotencyKey(earnerId, currency, periodEnd) {
2080
+ return `${earnerId}:${currency}:${periodEnd.toISOString().slice(0, 10)}`;
2081
+ }
2082
+ };
2083
+ //#endregion
2084
+ //#region src/commissions/services/CommissionSettlementService.ts
2085
+ var CommissionSettlementService = class CommissionSettlementService {
2086
+ constructor(commissions) {
2087
+ this.commissions = commissions;
2088
+ }
2089
+ commissions;
2090
+ static async create(classOptions = {}) {
2091
+ return new CommissionSettlementService(await CommissionCollection.create(classOptions));
2092
+ }
2093
+ /**
2094
+ * Sweep the clearing window: every `pending` commission whose
2095
+ * `clearingEndsAt` is `<= now` — or whose `clearingEndsAt` is `null`
2096
+ * (null means NO clearing window applies, so the row is immediately
2097
+ * sweepable) — transitions to `earned` and is saved.
2098
+ *
2099
+ * @returns The commissions that were marked earned by this sweep.
2100
+ */
2101
+ async sweepClearing(now = /* @__PURE__ */ new Date()) {
2102
+ const pending = await this.commissions.findByStatus("pending");
2103
+ const swept = [];
2104
+ for (const commission of pending) {
2105
+ const clearingEndsAt = commission.clearingEndsAt;
2106
+ if (clearingEndsAt !== null && clearingEndsAt.getTime() > now.getTime()) continue;
2107
+ commission.markEarned(now);
2108
+ await commission.save();
2109
+ swept.push(commission);
2110
+ }
2111
+ return swept;
2112
+ }
2113
+ /**
2114
+ * Approve `earned` commissions by id (`earned → approved`). Strict: a
2115
+ * missing id or a commission in any other status throws — the caller
2116
+ * names exact rows, so a mismatch is a bug worth surfacing, not skipping.
2117
+ */
2118
+ async approveCommissions(ids, now = /* @__PURE__ */ new Date()) {
2119
+ return await this.transitionByIds(ids, (commission) => {
2120
+ commission.approve(now);
2121
+ });
2122
+ }
2123
+ /**
2124
+ * Release `approved` commissions to `payable` by id. Strict — see
2125
+ * {@link approveCommissions}.
2126
+ */
2127
+ async markPayable(ids, now = /* @__PURE__ */ new Date()) {
2128
+ return await this.transitionByIds(ids, (commission) => {
2129
+ commission.markPayable(now);
2130
+ });
2131
+ }
2132
+ /**
2133
+ * Convenience chain: advance each commission from wherever it currently
2134
+ * sits up to `payable` (`pending → earned → approved → payable`), saving
2135
+ * after EACH step — the save-time guard only admits single-step edges, so
2136
+ * every intermediate state is persisted (each with its timestamp). Rows
2137
+ * already `payable` or `paid` are returned untouched (idempotent). Note
2138
+ * this deliberately bypasses the clearing window — it's the "operator
2139
+ * says pay these now" path.
2140
+ */
2141
+ async settleUpToPayable(ids, now = /* @__PURE__ */ new Date()) {
2142
+ const updated = [];
2143
+ for (const id of ids) {
2144
+ const commission = await this.requireCommission(id);
2145
+ if (commission.isPending()) {
2146
+ commission.markEarned(now);
2147
+ await commission.save();
2148
+ }
2149
+ if (commission.isEarned()) {
2150
+ commission.approve(now);
2151
+ await commission.save();
2152
+ }
2153
+ if (commission.isApproved()) {
2154
+ commission.markPayable(now);
2155
+ await commission.save();
2156
+ }
2157
+ updated.push(commission);
2158
+ }
2159
+ return updated;
2160
+ }
2161
+ async transitionByIds(ids, transition) {
2162
+ const updated = [];
2163
+ for (const id of ids) {
2164
+ const commission = await this.requireCommission(id);
2165
+ const statusBefore = commission.status;
2166
+ transition(commission);
2167
+ if (commission.status !== statusBefore) await commission.save();
2168
+ updated.push(commission);
2169
+ }
2170
+ return updated;
2171
+ }
2172
+ async requireCommission(id) {
2173
+ const commission = await this.commissions.get({ id });
2174
+ if (!commission) throw new Error(`CommissionSettlementService: commission '${id}' not found`);
2175
+ return commission;
2176
+ }
2177
+ };
2178
+ //#endregion
2179
+ export { CommissionAdjustment as A, EARNING_EVENT_KINDS as C, CommissionCollection as D, CommissionPayout as E, Commission as O, EARNER_STATUSES as S, CommissionPayoutCollection as T, COMMISSION_ADJUSTMENT_KINDS as _, amountToCents as a, COMMISSION_PLAN_STATUSES as b, roundCents as c, EarnerCollection as d, Earner as f, ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES as g, validateCommissionPlanComponents as h, CommissionBalanceService as i, CommissionAdjustmentCollection as k, EarningEventCollection as l, CommissionPlan as m, CommissionPayoutService as n, calculateCommissionAmountCents as o, CommissionPlanCollection as p, CommissionCalculationService as r, centsToAmount as s, CommissionSettlementService as t, EarningEvent as u, COMMISSION_BASES as v, PAYOUT_METHODS as w, COMMISSION_STATUSES as x, COMMISSION_PAYOUT_STATUSES as y };
2180
+
2181
+ //# sourceMappingURL=commissions-BCxqOKVM.js.map