@happyvertical/smrt-sales 0.57.2 → 0.57.3

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 (46) hide show
  1. package/README.md +70 -2
  2. package/agents/commissions.md +1 -1
  3. package/agents/referrals.md +1 -0
  4. package/dist/agreements/collections/AgreementCollection.d.ts +50 -0
  5. package/dist/agreements/collections/AgreementCollection.d.ts.map +1 -0
  6. package/dist/agreements/index.d.ts +2 -0
  7. package/dist/agreements/index.d.ts.map +1 -1
  8. package/dist/agreements/models/Agreement.d.ts +67 -0
  9. package/dist/agreements/models/Agreement.d.ts.map +1 -0
  10. package/dist/agreements/types.d.ts +32 -0
  11. package/dist/agreements/types.d.ts.map +1 -1
  12. package/dist/agreements.d.ts +2 -2
  13. package/dist/agreements.js +3 -3
  14. package/dist/chunks/__smrt-register__-DTUQW7wz.js +6 -0
  15. package/dist/chunks/__smrt-register__-DTUQW7wz.js.map +1 -0
  16. package/dist/chunks/{agreements-BB_tNU0U.js → agreements-jBiVmttD.js} +450 -4
  17. package/dist/chunks/agreements-jBiVmttD.js.map +1 -0
  18. package/dist/chunks/{commissions-Cs4MsxVg.js → commissions-Cip6QZ9Q.js} +350 -6
  19. package/dist/chunks/commissions-Cip6QZ9Q.js.map +1 -0
  20. package/dist/chunks/{referrals-CrFDsdq-.js → referrals-CYgrGkEo.js} +17 -3
  21. package/dist/chunks/{referrals-CrFDsdq-.js.map → referrals-CYgrGkEo.js.map} +1 -1
  22. package/dist/commissions/collections/CommissionAdjustmentOperationCollection.d.ts.map +1 -1
  23. package/dist/commissions/collections/CommissionPlanCollection.d.ts +5 -1
  24. package/dist/commissions/collections/CommissionPlanCollection.d.ts.map +1 -1
  25. package/dist/commissions/models/CommissionPlan.d.ts +20 -1
  26. package/dist/commissions/models/CommissionPlan.d.ts.map +1 -1
  27. package/dist/commissions/services/WaterfallCalculationService.d.ts +22 -0
  28. package/dist/commissions/services/WaterfallCalculationService.d.ts.map +1 -0
  29. package/dist/commissions/services/index.d.ts +1 -0
  30. package/dist/commissions/services/index.d.ts.map +1 -1
  31. package/dist/commissions/types.d.ts +100 -0
  32. package/dist/commissions/types.d.ts.map +1 -1
  33. package/dist/commissions.js +3 -3
  34. package/dist/crm.js +1 -1
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +5 -5
  37. package/dist/manifest.json +1399 -514
  38. package/dist/referrals/services/ReferralQualificationService.d.ts +4 -3
  39. package/dist/referrals/services/ReferralQualificationService.d.ts.map +1 -1
  40. package/dist/referrals.js +2 -2
  41. package/dist/smrt-knowledge.json +985 -34
  42. package/package.json +6 -6
  43. package/dist/chunks/__smrt-register__-CNn8HXhK.js +0 -6
  44. package/dist/chunks/__smrt-register__-CNn8HXhK.js.map +0 -1
  45. package/dist/chunks/agreements-BB_tNU0U.js.map +0 -1
  46. package/dist/chunks/commissions-Cs4MsxVg.js.map +0 -1
@@ -1,7 +1,14 @@
1
- import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
2
- import { TenantScoped, requireTenantId, tenantId } from "@happyvertical/smrt-tenancy";
1
+ import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, invalidateCollectionCache, isEmbeddedDatabase, isPostgresDatabase, resolveDbCacheKey, smrt, withEmbeddedWriteTransaction } from "@happyvertical/smrt-core";
2
+ import { TenantScoped, assertTenantReadAllowed, getCurrentTenant, requireTenantId, tenantId } from "@happyvertical/smrt-tenancy";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  //#region src/agreements/types.ts
5
+ var AGREEMENT_STATUSES = [
6
+ "draft",
7
+ "cancelled",
8
+ "active",
9
+ "superseded",
10
+ "terminated"
11
+ ];
5
12
  var AGREEMENT_EXECUTION_STATUSES = [
6
13
  "prepared",
7
14
  "sent",
@@ -46,6 +53,445 @@ function coerceAgreementDate(value) {
46
53
  return null;
47
54
  }
48
55
  //#endregion
56
+ //#region src/agreements/models/Agreement.ts
57
+ var __defProp$3 = Object.defineProperty;
58
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
59
+ var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, {
60
+ enumerable: true,
61
+ configurable: true,
62
+ writable: true,
63
+ value
64
+ }) : obj[key] = value;
65
+ var __decorateClass$3 = (decorators, target, key, kind) => {
66
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
67
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
68
+ if (kind && result) __defProp$3(target, key, result);
69
+ return result;
70
+ };
71
+ var __publicField$3 = (obj, key, value) => __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
72
+ var STATUS_TRANSITIONS = {
73
+ draft: ["active", "cancelled"],
74
+ cancelled: [],
75
+ active: ["superseded", "terminated"],
76
+ superseded: [],
77
+ terminated: []
78
+ };
79
+ var loadedStatus = /* @__PURE__ */ new WeakMap();
80
+ var frozenTerms = /* @__PURE__ */ new WeakMap();
81
+ var frozenEnd = /* @__PURE__ */ new WeakMap();
82
+ function serializeAgreementParty(party) {
83
+ if (!party || typeof party !== "object" || typeof party.kind !== "string" || !party.kind.trim() || typeof party.id !== "string" || !party.id.trim()) throw new Error("Agreement party requires non-empty kind and id");
84
+ return JSON.stringify({
85
+ kind: party.kind,
86
+ id: party.id
87
+ });
88
+ }
89
+ function parseAgreementParty(value) {
90
+ try {
91
+ const parsed = JSON.parse(value);
92
+ if (parsed && typeof parsed === "object" && typeof parsed.kind === "string" && parsed.kind.trim() && typeof parsed.id === "string" && parsed.id.trim()) return {
93
+ kind: parsed.kind,
94
+ id: parsed.id
95
+ };
96
+ } catch {}
97
+ return null;
98
+ }
99
+ var Agreement = class extends SmrtObject {
100
+ tenantId = null;
101
+ kind = "";
102
+ partyA = "";
103
+ partyB = "";
104
+ /** Monotonic version within tenant, kind, and ordered counterparties. */
105
+ version = 1;
106
+ status = "draft";
107
+ /** JSON object containing consumer-defined, frozen commercial terms. */
108
+ terms = "{}";
109
+ /** Inclusive start of the effective interval; null means no lower bound. */
110
+ effectiveFrom = null;
111
+ /**
112
+ * Exclusive end of the effective interval; null means no upper bound.
113
+ * It is the one lifecycle-derived field: `AgreementCollection.activateAmendment`
114
+ * closes a predecessor at the new version's start in the same transition.
115
+ */
116
+ effectiveTo = null;
117
+ executedAgreementId = null;
118
+ amendmentPredecessorId = null;
119
+ amendmentPredecessorEnd = null;
120
+ metadata = "{}";
121
+ constructor(options = {}) {
122
+ super(options);
123
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
124
+ if (options.kind !== void 0) this.kind = options.kind;
125
+ if (options.partyA !== void 0) this.partyA = options.partyA;
126
+ if (options.partyB !== void 0) this.partyB = options.partyB;
127
+ if (options.version !== void 0) this.version = options.version;
128
+ if (options.status !== void 0) this.status = options.status;
129
+ if (options.terms !== void 0) this.terms = options.terms;
130
+ if (options.effectiveFrom !== void 0) this.effectiveFrom = coerceAgreementDate(options.effectiveFrom);
131
+ if (options.effectiveTo !== void 0) this.effectiveTo = coerceAgreementDate(options.effectiveTo);
132
+ if (options.executedAgreementId !== void 0) this.executedAgreementId = options.executedAgreementId;
133
+ if (options.amendmentPredecessorId !== void 0) this.amendmentPredecessorId = options.amendmentPredecessorId;
134
+ if (options.amendmentPredecessorEnd !== void 0) this.amendmentPredecessorEnd = coerceAgreementDate(options.amendmentPredecessorEnd);
135
+ if (options.metadata !== void 0) this.metadata = options.metadata;
136
+ }
137
+ async initialize() {
138
+ await super.initialize();
139
+ this.effectiveFrom = coerceAgreementDate(this.effectiveFrom);
140
+ this.effectiveTo = coerceAgreementDate(this.effectiveTo);
141
+ this.amendmentPredecessorEnd = coerceAgreementDate(this.amendmentPredecessorEnd);
142
+ if (this.isPersisted) {
143
+ loadedStatus.set(this, this.status);
144
+ if (this.status !== "draft") {
145
+ frozenTerms.set(this, this.serializeFrozenTerms());
146
+ frozenEnd.set(this, this.effectiveTo?.getTime() ?? null);
147
+ }
148
+ }
149
+ return this;
150
+ }
151
+ isDraft() {
152
+ return this.status === "draft";
153
+ }
154
+ isActive() {
155
+ return this.status === "active";
156
+ }
157
+ /**
158
+ * Tests the half-open effective interval `[effectiveFrom, effectiveTo)`.
159
+ * This makes an amendment starting at `T` and its predecessor ending at
160
+ * `T` non-overlapping. Lifecycle status is intentionally not considered.
161
+ */
162
+ isEffectiveAt(at) {
163
+ return !(this.effectiveFrom && at < this.effectiveFrom || this.effectiveTo && at >= this.effectiveTo);
164
+ }
165
+ getPartyA() {
166
+ return parseAgreementParty(this.partyA);
167
+ }
168
+ getPartyB() {
169
+ return parseAgreementParty(this.partyB);
170
+ }
171
+ setPartyA(party) {
172
+ this.partyA = serializeAgreementParty(party);
173
+ }
174
+ setPartyB(party) {
175
+ this.partyB = serializeAgreementParty(party);
176
+ }
177
+ getTerms() {
178
+ return parseObject(this.terms) ?? {};
179
+ }
180
+ setTerms(terms) {
181
+ if (!terms || typeof terms !== "object" || Array.isArray(terms)) throw new Error("Agreement terms must be a JSON object");
182
+ this.terms = JSON.stringify(terms);
183
+ }
184
+ activate() {
185
+ if (this.status !== "draft") throw new Error(`Agreement: cannot activate from status '${this.status}'`);
186
+ this.assertValidTerms();
187
+ this.status = "active";
188
+ }
189
+ supersede() {
190
+ if (this.status !== "active") throw new Error(`Agreement: cannot supersede from status '${this.status}'`);
191
+ if (!this.effectiveTo) throw new Error("Agreement supersession requires an effective end boundary");
192
+ this.assertValidTerms();
193
+ this.status = "superseded";
194
+ }
195
+ terminate(at = /* @__PURE__ */ new Date()) {
196
+ if (this.status !== "draft" && this.status !== "active") throw new Error(`Agreement: cannot terminate from status '${this.status}'`);
197
+ if (this.status === "active" && (!this.effectiveTo || this.effectiveTo > at)) this.effectiveTo = at;
198
+ this.status = this.status === "draft" ? "cancelled" : "terminated";
199
+ }
200
+ async save() {
201
+ const partyA = this.getPartyA();
202
+ const partyB = this.getPartyB();
203
+ if (partyA) this.partyA = serializeAgreementParty(partyA);
204
+ if (partyB) this.partyB = serializeAgreementParty(partyB);
205
+ await this.assertActivationProvenanceUnchanged();
206
+ const prior = await this.resolvePriorStatus();
207
+ this.assertStatusTransition(prior);
208
+ this.assertEffectiveIntervalUnchanged(prior);
209
+ this.assertFrozenTermsUnchanged();
210
+ await this.assertNaturalKeyNotTaken();
211
+ if (this.status !== "draft" && this.status !== "cancelled") this.assertValidTerms();
212
+ if (!this.isPersisted) this.requireInsertOnSave();
213
+ const result = await super.save();
214
+ loadedStatus.set(this, this.status);
215
+ if (this.status !== "draft" && !frozenTerms.has(this)) frozenTerms.set(this, this.serializeFrozenTerms());
216
+ if (this.status !== "draft") frozenEnd.set(this, this.effectiveTo?.getTime() ?? null);
217
+ return result;
218
+ }
219
+ /** @internal Complete observation of a collection-owned guarded transition. */
220
+ async refreshAfterLifecycleTransition() {
221
+ if (!this.isPersisted || !this.id) throw new Error("Agreement is not persisted");
222
+ const durableRow = await this.db.get(this.tableName, { id: this.id });
223
+ if (!durableRow) throw new Error("Agreement disappeared during transition");
224
+ await this.completePersistedUpdate(durableRow);
225
+ loadedStatus.set(this, this.status);
226
+ frozenTerms.set(this, this.serializeFrozenTerms());
227
+ frozenEnd.set(this, this.effectiveTo?.getTime() ?? null);
228
+ }
229
+ async assertActivationProvenanceUnchanged() {
230
+ const persisted = this.isPersisted && this.id ? await this.db.get(this.tableName, { id: this.id }) : null;
231
+ const priorId = persisted?.amendment_predecessor_id ?? null;
232
+ const priorEnd = coerceAgreementDate(persisted?.amendment_predecessor_end)?.getTime() ?? null;
233
+ if (this.amendmentPredecessorId !== priorId || (this.amendmentPredecessorEnd?.getTime() ?? null) !== priorEnd) throw new Error("Agreement activation provenance is owned by activateAmendment");
234
+ }
235
+ assertEffectiveIntervalUnchanged(prior) {
236
+ const captured = frozenEnd.get(this);
237
+ const current = this.effectiveTo?.getTime() ?? null;
238
+ if (captured === void 0 || captured === current) return;
239
+ if (!(prior === "active" && (this.status === "superseded" || this.status === "terminated")) || current === null || captured !== null && current > captured) throw new Error("Agreement effective interval is immutable except for lifecycle closure");
240
+ }
241
+ assertValidTerms() {
242
+ if (this.status === "superseded" && !this.effectiveTo) throw new Error("Agreement supersession requires an effective end boundary");
243
+ if (!this.kind.trim()) throw new Error("Agreement kind is required");
244
+ if (!parseAgreementParty(this.partyA) || !parseAgreementParty(this.partyB)) throw new Error("Agreement requires valid partyA and partyB descriptors");
245
+ if (!parseObject(this.terms)) throw new Error("Agreement terms must be a JSON object");
246
+ if (!Number.isSafeInteger(this.version) || this.version < 1) throw new Error("Agreement version must be a positive integer");
247
+ if (this.effectiveFrom && this.effectiveTo && this.effectiveTo <= this.effectiveFrom) throw new Error("Agreement effectiveTo must be after effectiveFrom");
248
+ }
249
+ async assertNaturalKeyNotTaken() {
250
+ if (!this.kind || !this.partyA || !this.partyB) return;
251
+ try {
252
+ const res = await this.db.query(`SELECT id, tenant_id FROM ${this.tableName} WHERE kind = $1 AND party_a = $2 AND party_b = $3 AND version = $4`, this.kind, this.partyA, this.partyB, this.version);
253
+ if ((Array.isArray(res) ? res : res.rows ?? []).some((row) => (row.tenant_id ?? null) === (this.tenantId ?? null) && row.id !== this.id)) throw new Error(`Agreement ${this.kind}@${this.version}: this immutable version already exists; create an amendment instead`);
254
+ } catch (error) {
255
+ if (error instanceof Error && error.message.includes("immutable version")) throw error;
256
+ }
257
+ }
258
+ async resolvePriorStatus() {
259
+ if (this.id) try {
260
+ const row = await this.db.get(this.tableName, { id: this.id });
261
+ if (row?.status != null) return row.status;
262
+ } catch {}
263
+ return loadedStatus.get(this);
264
+ }
265
+ assertStatusTransition(prior) {
266
+ if (prior === void 0 || prior === this.status) return;
267
+ if (!(STATUS_TRANSITIONS[prior] ?? []).includes(this.status)) throw new Error(`Agreement: illegal status transition '${prior}' \u2192 '${this.status}'`);
268
+ }
269
+ assertFrozenTermsUnchanged() {
270
+ const captured = frozenTerms.get(this);
271
+ if (captured && captured !== this.serializeFrozenTerms()) throw new Error("Agreement terms are immutable once active; create an amendment instead");
272
+ }
273
+ serializeFrozenTerms() {
274
+ return JSON.stringify({
275
+ kind: this.kind,
276
+ partyA: this.partyA,
277
+ partyB: this.partyB,
278
+ version: this.version,
279
+ terms: this.terms,
280
+ effectiveFrom: this.effectiveFrom?.toISOString() ?? null,
281
+ executedAgreementId: this.executedAgreementId,
282
+ amendmentPredecessorId: this.amendmentPredecessorId,
283
+ amendmentPredecessorEnd: this.amendmentPredecessorEnd?.toISOString() ?? null
284
+ });
285
+ }
286
+ };
287
+ __publicField$3(Agreement, "__smrtPackage__", "@happyvertical/smrt-sales");
288
+ __decorateClass$3([tenantId({ nullable: true })], Agreement.prototype, "tenantId", 2);
289
+ __decorateClass$3([field({ required: true })], Agreement.prototype, "kind", 2);
290
+ __decorateClass$3([field({ required: true })], Agreement.prototype, "partyA", 2);
291
+ __decorateClass$3([field({ required: true })], Agreement.prototype, "partyB", 2);
292
+ __decorateClass$3([foreignKey("ExecutedAgreement")], Agreement.prototype, "executedAgreementId", 2);
293
+ __decorateClass$3([foreignKey("Agreement")], Agreement.prototype, "amendmentPredecessorId", 2);
294
+ Agreement = __decorateClass$3([TenantScoped({ mode: "optional" }), smrt({
295
+ conflictColumns: [
296
+ "tenant_id",
297
+ "kind",
298
+ "party_a",
299
+ "party_b",
300
+ "version"
301
+ ],
302
+ api: { include: [
303
+ "list",
304
+ "get",
305
+ "create"
306
+ ] },
307
+ mcp: { include: ["list", "get"] },
308
+ cli: { skipApiCheck: true }
309
+ })], Agreement);
310
+ function parseObject(value) {
311
+ try {
312
+ const parsed = JSON.parse(value);
313
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
314
+ } catch {
315
+ return null;
316
+ }
317
+ }
318
+ //#endregion
319
+ //#region src/agreements/collections/AgreementCollection.ts
320
+ var AgreementCollection = class AgreementCollection extends SmrtCollection {
321
+ static _itemClass = Agreement;
322
+ async findVersions(kind, partyA, partyB) {
323
+ return await this.findGoverningCandidates(kind, serializeAgreementParty(partyA), serializeAgreementParty(partyB), this.ambientTenantId());
324
+ }
325
+ /**
326
+ * Resolves the sole version governing `at`. This is a historical query:
327
+ * a superseded or terminated row can still govern before its exclusive
328
+ * `effectiveTo`; today's lifecycle status alone is never used to answer a
329
+ * record-date lookup. Drafts never govern.
330
+ *
331
+ * Generated amendments are non-overlapping because the predecessor ends at
332
+ * the successor's inclusive start. If imported or manually-created rows do
333
+ * overlap, this helper throws instead of silently selecting a version.
334
+ */
335
+ async activeFor(kind, partyA, partyB, at = /* @__PURE__ */ new Date(), tenantId) {
336
+ const partyAValue = serializeAgreementParty(partyA);
337
+ const partyBValue = serializeAgreementParty(partyB);
338
+ const lane = tenantId === void 0 ? this.ambientTenantId() : tenantId;
339
+ const governs = (agreement) => agreement.status !== "draft" && agreement.status !== "cancelled" && agreement.isEffectiveAt(at);
340
+ const candidates = await this.findGoverningCandidates(kind, partyAValue, partyBValue, lane, false);
341
+ let matches = candidates.filter(governs);
342
+ const laneHasBegun = candidates.some((agreement) => agreement.status !== "draft" && agreement.status !== "cancelled" && (!agreement.effectiveFrom || agreement.effectiveFrom <= at));
343
+ if (matches.length === 0 && lane != null && !laneHasBegun) matches = (await this.findGoverningCandidates(kind, partyAValue, partyBValue, null, false)).filter(governs);
344
+ if (matches.length > 1) throw new Error(`Agreement overlap for '${kind}' parties at ${at.toISOString()}; resolve the conflicting effective intervals before using these terms`);
345
+ return matches[0] ?? null;
346
+ }
347
+ /** Creates the next unsigned/draft version without changing prior history. */
348
+ async createAmendment(kind, partyA, partyB, changes = {}) {
349
+ const createdId = await this.runTransaction(async (agreements) => {
350
+ let versions = await agreements.findVersions(kind, partyA, partyB);
351
+ if (!versions.length) throw new Error(`AgreementCollection.createAmendment: no versions exist for '${kind}' parties \u2014 create the initial agreement first`);
352
+ if (versions[0].tenantId !== (this.ambientTenantId() ?? null)) throw new Error("Create an initial tenant agreement before amending global terms");
353
+ if (isPostgresDatabase(this.db)) {
354
+ const anchor = versions[versions.length - 1];
355
+ await agreements.db.query(`SELECT id FROM ${agreements.tableName} WHERE id = $1 FOR UPDATE`, anchor.id);
356
+ versions = await agreements.findVersions(kind, partyA, partyB);
357
+ }
358
+ const latest = versions[0];
359
+ if (changes.terms !== void 0 && Array.isArray(changes.terms)) throw new Error("Agreement amendment terms must be a JSON object");
360
+ return (await agreements.create({
361
+ tenantId: latest.tenantId,
362
+ kind,
363
+ partyA: serializeAgreementParty(partyA),
364
+ partyB: serializeAgreementParty(partyB),
365
+ version: latest.version + 1,
366
+ status: "draft",
367
+ terms: changes.terms !== void 0 ? JSON.stringify(changes.terms) : latest.terms,
368
+ effectiveFrom: changes.effectiveFrom !== void 0 ? changes.effectiveFrom : latest.effectiveFrom,
369
+ effectiveTo: changes.effectiveTo !== void 0 ? changes.effectiveTo : latest.effectiveTo,
370
+ executedAgreementId: changes.executedAgreementId ?? null,
371
+ metadata: changes.metadata !== void 0 ? JSON.stringify(changes.metadata) : latest.metadata
372
+ })).id;
373
+ });
374
+ invalidateCollectionCache(resolveDbCacheKey(this.db), this.tableName);
375
+ const created = await this.get({ id: createdId });
376
+ if (!created) throw new Error(`Agreement '${createdId}' disappeared`);
377
+ return created;
378
+ }
379
+ /**
380
+ * Activates an amendment and closes any currently governing predecessor at
381
+ * the new version's start. The interval is `[from, to)`, so equal boundary
382
+ * timestamps do not overlap. A future-dated amendment may be activated in
383
+ * advance; record-date lookup still selects its predecessor until `from`.
384
+ */
385
+ async activateAmendment(id) {
386
+ const activatedId = await this.runTransaction(async (agreements) => {
387
+ const draft = await agreements.get({ id });
388
+ if (!draft) throw new Error(`Agreement '${id}' was not found`);
389
+ if (!draft.isDraft()) throw new Error("Only a draft Agreement amendment can be activated");
390
+ if (!draft.effectiveFrom) throw new Error("Agreement amendment requires effectiveFrom");
391
+ const partyA = draft.getPartyA();
392
+ const partyB = draft.getPartyB();
393
+ if (!partyA || !partyB) throw new Error("Agreement has invalid parties");
394
+ const versions = await agreements.findGoverningCandidates(draft.kind, serializeAgreementParty(partyA), serializeAgreementParty(partyB), draft.tenantId, false);
395
+ const predecessors = versions.filter((version) => version.id !== draft.id && version.status === "active" && version.version < draft.version && version.isEffectiveAt(draft.effectiveFrom));
396
+ if (predecessors.length > 1) throw new Error("Agreement amendment has overlapping active predecessors");
397
+ const predecessor = predecessors[0];
398
+ if (versions.some((version) => {
399
+ if (version.id === draft.id || version.id === predecessor?.id || version.status === "draft" || version.status === "cancelled") return false;
400
+ return (!draft.effectiveTo || !version.effectiveFrom || version.effectiveFrom < draft.effectiveTo) && (!version.effectiveTo || version.effectiveTo > draft.effectiveFrom);
401
+ })) throw new Error("Agreement amendment would overlap governing history");
402
+ if (predecessor?.effectiveFrom && predecessor.effectiveFrom >= draft.effectiveFrom) throw new Error("Agreement amendment would overlap the predecessor start");
403
+ const originalEnd = predecessor?.effectiveTo ?? null;
404
+ if (predecessor) {
405
+ predecessor.effectiveTo = draft.effectiveFrom;
406
+ predecessor.supersede();
407
+ await predecessor.save();
408
+ }
409
+ draft.activate();
410
+ await draft.save();
411
+ if (predecessor) {
412
+ await claimLifecycleRevision(draft);
413
+ if (returnedRows(await agreements.db.query(`UPDATE ${agreements.tableName} SET amendment_predecessor_id = $1, amendment_predecessor_end = $2 WHERE id = $3 AND status = 'active' RETURNING id`, predecessor.id, originalEnd?.toISOString() ?? null, draft.id)).length !== 1) throw new Error("Agreement activation changed concurrently");
414
+ await draft.refreshAfterLifecycleTransition();
415
+ }
416
+ return draft.id;
417
+ });
418
+ invalidateCollectionCache(resolveDbCacheKey(this.db), this.tableName);
419
+ const activated = await this.get({ id: activatedId });
420
+ if (!activated) throw new Error(`Agreement '${activatedId}' disappeared`);
421
+ return activated;
422
+ }
423
+ /** Withdraw a scheduled successor before it has ever governed.
424
+ * Restores only its persisted predecessor boundary, in the same transaction.
425
+ * Repeated/concurrent withdrawal refuses rather than applying recovery twice.
426
+ */
427
+ async withdrawAmendment(id) {
428
+ const db = this.db;
429
+ if (typeof db.transaction !== "function") throw new Error("Agreement withdrawal requires transaction support");
430
+ await withEmbeddedWriteTransaction(db, isEmbeddedDatabase(db), async (tx) => {
431
+ const agreements = await AgreementCollection.create({ db: tx });
432
+ const successor = await agreements.get({ id });
433
+ const now = /* @__PURE__ */ new Date();
434
+ if (!successor || successor.status !== "active" || !successor.effectiveFrom || successor.effectiveFrom <= now || !successor.amendmentPredecessorId) throw new Error("Only a scheduled active amendment with predecessor provenance can be withdrawn");
435
+ const predecessor = await agreements.get({ id: successor.amendmentPredecessorId });
436
+ if (!predecessor || predecessor.tenantId !== successor.tenantId || predecessor.kind !== successor.kind || predecessor.partyA !== successor.partyA || predecessor.partyB !== successor.partyB || predecessor.version >= successor.version || predecessor.status !== "superseded" || predecessor.effectiveTo?.getTime() !== successor.effectiveFrom.getTime()) throw new Error("Agreement withdrawal predecessor no longer matches activation provenance");
437
+ const restoredEnd = successor.amendmentPredecessorEnd;
438
+ if (restoredEnd && restoredEnd <= successor.effectiveFrom) throw new Error("Agreement withdrawal has invalid predecessor boundary provenance");
439
+ if ((await agreements.findGoverningCandidates(successor.kind, successor.partyA, successor.partyB, successor.tenantId, false)).some((row) => row.id !== successor.id && row.id !== predecessor.id && row.status !== "draft" && row.status !== "cancelled" && (!restoredEnd || !row.effectiveFrom || row.effectiveFrom < restoredEnd) && (!row.effectiveTo || row.effectiveTo > successor.effectiveFrom))) throw new Error("Agreement withdrawal would overlap governing history");
440
+ await claimLifecycleRevision(successor);
441
+ if (returnedRows(await tx.query(`UPDATE ${agreements.tableName} SET status = 'cancelled' WHERE id = $1 AND status = 'active' AND effective_from > $2 RETURNING id`, id, (/* @__PURE__ */ new Date()).toISOString())).length !== 1) throw new Error("Agreement withdrawal lost its scheduled active transition");
442
+ await claimLifecycleRevision(predecessor);
443
+ if (returnedRows(await tx.query(`UPDATE ${agreements.tableName} SET status = 'active', effective_to = $1 WHERE id = $2 AND status = 'superseded' AND effective_to = $3 RETURNING id`, restoredEnd?.toISOString() ?? null, predecessor.id, successor.effectiveFrom.toISOString())).length !== 1) throw new Error("Agreement withdrawal predecessor changed concurrently");
444
+ await successor.refreshAfterLifecycleTransition();
445
+ await predecessor.refreshAfterLifecycleTransition();
446
+ });
447
+ invalidateCollectionCache(resolveDbCacheKey(this.db), this.tableName);
448
+ const withdrawn = await this.get({ id });
449
+ if (!withdrawn) throw new Error(`Agreement '${id}' disappeared`);
450
+ return withdrawn;
451
+ }
452
+ async findGoverningCandidates(kind, partyA, partyB, tenantId, includeGlobalFallback = true) {
453
+ if (tenantId === null) return await this.query(`SELECT * FROM ${this.tableName} WHERE tenant_id IS NULL AND kind = ? AND party_a = ? AND party_b = ? ORDER BY version DESC`, [
454
+ kind,
455
+ partyA,
456
+ partyB
457
+ ], { allowRawOnTenantScoped: true });
458
+ if (tenantId === void 0) return await this.query(`SELECT * FROM ${this.tableName} WHERE tenant_id IS NULL AND kind = ? AND party_a = ? AND party_b = ? ORDER BY version DESC`, [
459
+ kind,
460
+ partyA,
461
+ partyB
462
+ ], { allowRawOnTenantScoped: true });
463
+ assertTenantReadAllowed(tenantId, "AgreementCollection.activeFor");
464
+ const tenantRows = await this.query(`SELECT * FROM ${this.tableName} WHERE tenant_id = ? AND kind = ? AND party_a = ? AND party_b = ? ORDER BY version DESC`, [
465
+ tenantId,
466
+ kind,
467
+ partyA,
468
+ partyB
469
+ ], { allowRawOnTenantScoped: true });
470
+ if (tenantRows.length > 0 || !includeGlobalFallback) return tenantRows;
471
+ return await this.query(`SELECT * FROM ${this.tableName} WHERE tenant_id IS NULL AND kind = ? AND party_a = ? AND party_b = ? ORDER BY version DESC`, [
472
+ kind,
473
+ partyA,
474
+ partyB
475
+ ], { allowRawOnTenantScoped: true });
476
+ }
477
+ ambientTenantId() {
478
+ return getCurrentTenant()?.tenantId;
479
+ }
480
+ async runTransaction(fn) {
481
+ const db = this.db;
482
+ if (typeof db.transaction !== "function") throw new Error("Agreement amendment activation requires transaction support");
483
+ return await withEmbeddedWriteTransaction(db, isEmbeddedDatabase(db), async (tx) => fn(await AgreementCollection.create({ db: tx })));
484
+ }
485
+ };
486
+ function returnedRows(result) {
487
+ if (Array.isArray(result)) return result;
488
+ return result?.rows ?? [];
489
+ }
490
+ async function claimLifecycleRevision(agreement) {
491
+ if (!agreement.updated_at) throw new Error("Agreement has no persisted revision");
492
+ await agreement.claimRevision(agreement.updated_at);
493
+ }
494
+ //#endregion
49
495
  //#region src/agreements/models/AgreementExecution.ts
50
496
  var __defProp$2 = Object.defineProperty;
51
497
  var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
@@ -1659,6 +2105,6 @@ function canAdvanceStatus(current, next) {
1659
2105
  return (STATUS_PROGRESS.get(next) ?? -1) >= (STATUS_PROGRESS.get(current) ?? -1);
1660
2106
  }
1661
2107
  //#endregion
1662
- export { AgreementExecutionEvent as a, AGREEMENT_EXECUTION_STATUSES as c, sanitizeSignerIntent as d, AgreementExecutionEventCollection as i, coerceAgreementDate as l, ExecutedAgreementCollection as n, AgreementExecutionCollection as o, ExecutedAgreement as r, AgreementExecution as s, AgreementExecutionService as t, sanitizeSignerEvidence as u };
2108
+ export { AgreementExecutionEvent as a, AgreementCollection as c, serializeAgreementParty as d, AGREEMENT_EXECUTION_STATUSES as f, sanitizeSignerIntent as g, sanitizeSignerEvidence as h, AgreementExecutionEventCollection as i, Agreement as l, coerceAgreementDate as m, ExecutedAgreementCollection as n, AgreementExecutionCollection as o, AGREEMENT_STATUSES as p, ExecutedAgreement as r, AgreementExecution as s, AgreementExecutionService as t, parseAgreementParty as u };
1663
2109
 
1664
- //# sourceMappingURL=agreements-BB_tNU0U.js.map
2110
+ //# sourceMappingURL=agreements-jBiVmttD.js.map