@happyvertical/smrt-sales 0.40.11 → 0.40.13

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.
@@ -1 +1 @@
1
- {"version":3,"file":"commissions-CelWwvjZ.js","names":["results","payout"],"sources":["../../src/commissions/models/CommissionAdjustment.ts","../../src/commissions/collections/CommissionAdjustmentCollection.ts","../../src/commissions/models/Commission.ts","../../src/commissions/collections/CommissionCollection.ts","../../src/commissions/models/CommissionPayout.ts","../../src/commissions/collections/CommissionPayoutCollection.ts","../../src/commissions/types.ts","../../src/commissions/models/CommissionPlan.ts","../../src/commissions/collections/CommissionPlanCollection.ts","../../src/commissions/models/Earner.ts","../../src/commissions/collections/EarnerCollection.ts","../../src/commissions/models/EarnerSourceAttribution.ts","../../src/commissions/collections/EarnerSourceAttributionCollection.ts","../../src/commissions/models/EarningEvent.ts","../../src/commissions/collections/EarningEventCollection.ts","../../src/commissions/money.ts","../../src/commissions/models/CommissionAdjustmentOperation.ts","../../src/commissions/collections/CommissionAdjustmentOperationCollection.ts","../../src/commissions/services/CommissionAdjustmentService.ts","../../src/commissions/services/CommissionBalanceService.ts","../../src/commissions/services/CommissionCalculationService.ts","../../src/commissions/services/CommissionPayoutService.ts","../../src/commissions/services/CommissionSettlementService.ts","../../src/commissions/services/EarnerAttributionService.ts"],"sourcesContent":["/**\n * CommissionAdjustment — append-only correction against a Commission.\n *\n * Earned/paid Commissions are NEVER rewritten. A refund, credit,\n * chargeback, dispute outcome, or manual correction appends one of these\n * rows instead. `amountCents` is SIGNED — negative amounts claw earnings\n * back; positive amounts credit extra.\n *\n * Immutability contract: once persisted, an adjustment's substance\n * (`commissionId`, `earnerId`, `adjustmentKind`, `amountCents`, `currency`,\n * `reason`, `createdByProfileId`, `metadata`, `tenantId`) is frozen — the\n * save-time guard rejects any change via a WeakMap snapshot compare (the\n * commerce LicenseSale pattern). The ONLY post-create mutation allowed is\n * stamping/clearing `payoutId` when a settlement batch picks the row up.\n * A wrong adjustment is corrected by appending a counter-adjustment.\n *\n * @packageDocumentation\n */\n\nimport {\n crossPackageRef,\n field,\n foreignKey,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n CommissionAdjustmentKind,\n CommissionAdjustmentOptions,\n} from '../types.js';\n\n/**\n * Module-scoped record of the frozen-fields snapshot each persisted\n * adjustment was loaded with (or first saved as). WeakMap keeps it out of\n * the schema and GCs with the instance — commerce LicenseSale pattern.\n */\nconst frozenAdjustmentSnapshot = new WeakMap<CommissionAdjustment, string>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // Append-only audit rows: create/list/get only — no generated update or\n // delete on any surface.\n api: { include: ['create', 'list', 'get'] },\n mcp: { include: ['list', 'create'] },\n cli: false,\n})\nexport class CommissionAdjustment extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global rows). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The {@link Commission} this adjustment corrects. Required. */\n @foreignKey('Commission', { required: true })\n commissionId: string = '';\n\n /**\n * The {@link Earner} the adjustment applies to — denormalized from the\n * parent commission so balance queries never need a join. Required.\n */\n @foreignKey('Earner', { required: true })\n earnerId: string = '';\n\n /** What kind of correction this is. */\n adjustmentKind: CommissionAdjustmentKind = 'correction';\n\n /**\n * SIGNED amount in integer cents. Negative claws earnings back (refund,\n * chargeback); positive credits extra.\n */\n amountCents: number = 0;\n\n /** ISO 4217 currency — must match the parent commission's. */\n currency: string = 'USD';\n\n /** Human-readable justification. Required — audit rows explain themselves. */\n @field({ required: true })\n reason: string = '';\n\n /**\n * Profile of the operator/automation that created the adjustment\n * (cross-package string reference to smrt-profiles).\n */\n @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n createdByProfileId: string = '';\n\n /**\n * The {@link CommissionPayout} batch that settled this adjustment. Empty\n * until stamped. This is the ONLY field mutable after creation.\n */\n @foreignKey('CommissionPayout')\n payoutId: string = '';\n\n /** Additional metadata as a JSON string. Frozen once persisted. */\n metadata: string = '{}';\n\n constructor(options: CommissionAdjustmentOptions = {}) {\n super(options);\n if ('operationId' in (options as unknown as Record<string, unknown>)) {\n throw new Error(\n 'CommissionAdjustment has no public operationId field; use ' +\n 'CommissionAdjustmentService.createAdjustment()',\n );\n }\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.commissionId !== undefined)\n this.commissionId = options.commissionId;\n if (options.earnerId !== undefined) this.earnerId = options.earnerId;\n if (options.adjustmentKind !== undefined)\n this.adjustmentKind = options.adjustmentKind;\n if (options.amountCents !== undefined)\n this.amountCents = options.amountCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.reason !== undefined) this.reason = options.reason;\n if (options.createdByProfileId !== undefined)\n this.createdByProfileId = options.createdByProfileId;\n if (options.payoutId !== undefined) this.payoutId = options.payoutId;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Capture the frozen-fields snapshot when the row was loaded from the\n * database — from that moment on, only {@link payoutId} may change.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n if (await this.isSaved()) {\n frozenAdjustmentSnapshot.set(this, this.serializeFrozenSnapshot());\n }\n return this;\n }\n\n /** `true` once a payout batch has stamped {@link payoutId}. */\n isSettled(): boolean {\n return !!this.payoutId;\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n /**\n * Save with the append-only guard: once the row has been persisted, every\n * field except `payoutId` must match the captured snapshot. Corrections\n * to a wrong adjustment are new counter-adjustments, never edits.\n */\n override async save(): Promise<this> {\n this.assertImmutableOncePersisted();\n const result = (await super.save()) as this;\n if (!frozenAdjustmentSnapshot.has(this)) {\n frozenAdjustmentSnapshot.set(this, this.serializeFrozenSnapshot());\n }\n return result;\n }\n\n private assertImmutableOncePersisted(): void {\n const captured = frozenAdjustmentSnapshot.get(this);\n if (!captured) return; // brand-new row — first save captures below\n const current = this.serializeFrozenSnapshot();\n if (captured !== current) {\n throw new Error(\n `CommissionAdjustment ${this.id ?? '<new>'}: adjustments are ` +\n 'append-only — only payoutId may change after creation. Append a ' +\n 'counter-adjustment instead of editing this one.',\n );\n }\n }\n\n /**\n * Serialize every field EXCEPT `payoutId` (the sole post-create mutable\n * field) with stable key ordering.\n */\n private serializeFrozenSnapshot(): string {\n return JSON.stringify({\n tenantId: this.tenantId,\n commissionId: this.commissionId,\n earnerId: this.earnerId,\n adjustmentKind: this.adjustmentKind,\n amountCents: this.amountCents,\n currency: this.currency,\n reason: this.reason,\n createdByProfileId: this.createdByProfileId,\n metadata: this.metadata,\n });\n }\n}\n\nexport default CommissionAdjustment;\n","/**\n * CommissionAdjustmentCollection — collection manager for\n * {@link CommissionAdjustment}.\n *\n * \"Unsettled\" means `payoutId` is empty (checked in memory so `''`/`NULL`\n * storage differences don't matter). NOTE: the collection-level queries do\n * NOT apply the parent-commission-status eligibility rule — that lives in\n * `CommissionBalanceService` / `CommissionPayoutService`, which filter\n * unsettled adjustments to those whose parent commission is\n * earned/approved/payable/paid.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { CommissionAdjustment } from '../models/CommissionAdjustment.js';\n\nexport class CommissionAdjustmentCollection extends SmrtCollection<CommissionAdjustment> {\n static readonly _itemClass = CommissionAdjustment;\n\n /** All adjustments appended to one commission, oldest first. */\n async findByCommission(\n commissionId: string,\n ): Promise<CommissionAdjustment[]> {\n return await this.list({\n where: { commissionId },\n orderBy: 'created_at ASC',\n });\n }\n\n /** Unsettled adjustments for an earner+currency, oldest first. */\n async findUnsettledByEarner(\n earnerId: string,\n currency: string,\n ): Promise<CommissionAdjustment[]> {\n const rows = await this.list({\n where: { earnerId, currency },\n orderBy: 'created_at ASC',\n });\n return rows.filter((a) => !a.payoutId);\n }\n\n /**\n * Σ signed amountCents of {@link findUnsettledByEarner} rows (integer\n * cents; clawbacks make it negative).\n */\n async sumUnsettledByEarner(\n earnerId: string,\n currency: string,\n ): Promise<number> {\n const rows = await this.findUnsettledByEarner(earnerId, currency);\n return rows.reduce((sum, a) => sum + a.amountCents, 0);\n }\n\n /** Adjustments settled by one payout batch. */\n async findByPayout(payoutId: string): Promise<CommissionAdjustment[]> {\n return await this.list({\n where: { payoutId },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Adjustments settled by ANY of the given payout batches, in one `IN`\n * query — the adjustment twin of `CommissionCollection.findByPayouts`.\n * Empty input returns `[]` without querying.\n */\n async findByPayouts(payoutIds: string[]): Promise<CommissionAdjustment[]> {\n const ids = [...new Set(payoutIds.filter(Boolean))];\n if (ids.length === 0) return [];\n return await this.list({\n where: { payoutId: ids },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Conditionally claim adjustment rows for a payout batch — the adjustment\n * twin of `CommissionCollection.claimForPayout`. Rows already claimed by a\n * DIFFERENT payout are skipped; rows already claimed by THIS payout pass\n * through (idempotent retry / repair); every claim is verified by a\n * post-save re-read. Reads/writes go through the model layer, so this\n * respects the tenancy interceptor and the dialect's empty-FK encoding.\n * Not a cross-row transaction — safe concurrency relies on disjoint batch\n * scopes (see `CommissionPayoutService`). Returns the claimed rows.\n */\n async claimForPayout(\n adjustmentIds: string[],\n payoutId: string,\n ): Promise<CommissionAdjustment[]> {\n const claimed: CommissionAdjustment[] = [];\n for (const id of adjustmentIds) {\n const row = await this.get({ id });\n if (!row) continue;\n if (row.payoutId && row.payoutId !== payoutId) continue; // other batch\n if (!row.payoutId) {\n row.payoutId = payoutId;\n await row.save();\n }\n const verified = await this.get({ id });\n if (verified && verified.payoutId === payoutId) {\n claimed.push(verified);\n }\n }\n return claimed;\n }\n}\n\nexport default CommissionAdjustmentCollection;\n","/**\n * Commission — one earning record for one earner, one plan component, one\n * earning-event occurrence.\n *\n * Amounts are integer cents; `rate`/`shareFraction` are decimals in 0–1.\n * Every row stores snapshot references (`planKey`/`planVersion` plus a\n * generic polymorphic `termsSnapshotKind`/`termsSnapshotId` — the referrals\n * module points the latter at its ReferralTermSnapshot) and a JSON\n * `calculationTrace` sufficient to reproduce `amountCents`, so earnings stay\n * auditable after plans are superseded.\n *\n * Lifecycle is a STRICT forward chain — `pending → earned → approved →\n * payable → paid` — enforced at save time against the AUTHORITATIVE prior\n * persisted status (re-read from the database, commerce pattern), so neither\n * raw mass-assignment nor a `create({ id, _skipLoad: true })` upsert can skip\n * steps or roll back. Use the transition methods ({@link markEarned} /\n * {@link approve} / {@link markPayable} / {@link markPaid}); they mutate and\n * stamp timestamps but DO NOT save — the caller saves (one explicit\n * persistence point per mutation, matching commerce's markSent/markConfirmed\n * convention).\n *\n * Commissions are audit rows: the generated surface has no update or delete.\n * Corrections append {@link CommissionAdjustment} rows instead of editing.\n *\n * @packageDocumentation\n */\n\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n CommissionBasis,\n CommissionCalculationTrace,\n CommissionOptions,\n CommissionStatus,\n} from '../types.js';\n\n/**\n * Legal status transitions — the strict chain, keyed by prior persisted\n * status. No-op re-saves and brand-new rows are always permitted (imports /\n * fixtures may seed any status); this map governs *changes* to persisted\n * rows only.\n */\nconst COMMISSION_STATUS_TRANSITIONS: Record<\n CommissionStatus,\n CommissionStatus[]\n> = {\n pending: ['earned'],\n earned: ['approved'],\n approved: ['payable'],\n payable: ['paid'],\n paid: [],\n};\n\n/**\n * Module-scoped record of the status each Commission instance was loaded\n * with — fallback for the save-time guard when the DB re-read is\n * unavailable. WeakMap keeps it out of the schema (commerce pattern).\n */\nconst loadedCommissionStatus = new WeakMap<Commission, CommissionStatus>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // Idempotent creation: dedupeKey is the natural key so a retried\n // calculation upserts instead of duplicating.\n conflictColumns: ['dedupe_key'],\n // Audit rows: create/list/get only — no generated update or delete.\n // Lifecycle mutations happen through the guarded transition methods and\n // the settlement/payout services.\n api: { include: ['list', 'get', 'create'] },\n mcp: { include: ['list', 'get'] },\n // High volume and mutation-sensitive — no CLI surface.\n cli: false,\n})\nexport class Commission extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global rows). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The {@link Earner} this commission belongs to. Required. */\n @foreignKey('Earner', { required: true })\n earnerId: string = '';\n\n /** The {@link EarningEvent} evidence row this commission derives from. */\n @foreignKey('EarningEvent')\n earningEventId: string = '';\n\n /** Snapshot reference: plan key at calculation time. */\n planKey: string = '';\n\n /** Snapshot reference: plan version at calculation time. */\n planVersion: number = 0;\n\n /** Which plan component produced this commission. */\n componentKey: string = '';\n\n /**\n * Generic polymorphic reference to the terms snapshot that governed the\n * calculation (e.g. the referrals module sets\n * `('referral_term_snapshot', <id>)`). Free-form; this module attaches no\n * semantics beyond recording it in the dedupe key and trace.\n */\n termsSnapshotKind: string = '';\n\n /** Id of the terms snapshot named by {@link termsSnapshotKind}. */\n termsSnapshotId: string = '';\n\n /** How {@link baseAmountCents} was resolved from the event. */\n basis: CommissionBasis = 'gross';\n\n /** Base amount the rate was applied to, in integer cents. */\n baseAmountCents: number = 0;\n\n /** Rate applied (0–1). Recorded as `0` for `fixed`-basis commissions. */\n rate: number = 0.0;\n\n /** Split share applied (0–1). `1.0` for unsplit commissions. */\n shareFraction: number = 1.0;\n\n /**\n * Groups the sibling commissions of one split — every earner sharing an\n * event/component carries the same `splitGroupId`. Empty for unsplit rows.\n */\n splitGroupId: string = '';\n\n /** The earned amount in integer cents. */\n amountCents: number = 0;\n\n /** ISO 4217 currency (copied from the earning event). */\n currency: string = 'USD';\n\n /**\n * Lifecycle status — strict chain `pending → earned → approved → payable\n * → paid`. Mutate via the transition methods; the save-time guard rejects\n * illegal edges.\n */\n status: CommissionStatus = 'pending';\n\n /**\n * End of the clearing window (refund/chargeback holdback). `null` means\n * no clearing applies — the commission is immediately sweepable to\n * `earned` (see `CommissionSettlementService.sweepClearing`).\n */\n clearingEndsAt: Date | null = null;\n\n /** When the commission transitioned to `earned`. */\n earnedAt: Date | null = null;\n\n /** When the commission transitioned to `approved`. */\n approvedAt: Date | null = null;\n\n /** When the commission transitioned to `payable`. */\n payableAt: Date | null = null;\n\n /** When the commission transitioned to `paid`. */\n paidAt: Date | null = null;\n\n /**\n * The {@link CommissionPayout} batch that settled this commission. Empty\n * until a payout batch stamps it.\n */\n @foreignKey('CommissionPayout')\n payoutId: string = '';\n\n /** Copied from the earning event for reporting (generic source pair). */\n sourceKind: string = '';\n\n /** Copied from the earning event for reporting. */\n sourceId: string = '';\n\n /**\n * JSON-string {@link CommissionCalculationTrace} — everything needed to\n * reproduce {@link amountCents}. Use {@link getCalculationTrace} /\n * {@link setCalculationTrace}.\n */\n calculationTrace: string = '{}';\n\n /**\n * Idempotency natural key —\n * `` `${event.dedupeKey}:${terms}:${componentKey}:${earnerId}:${occurrenceIndex}` ``\n * (see `CommissionCalculationService`). Required.\n */\n @field({ required: true })\n dedupeKey: string = '';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: CommissionOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.earnerId !== undefined) this.earnerId = options.earnerId;\n if (options.earningEventId !== undefined)\n this.earningEventId = options.earningEventId;\n if (options.planKey !== undefined) this.planKey = options.planKey;\n if (options.planVersion !== undefined)\n this.planVersion = options.planVersion;\n if (options.componentKey !== undefined)\n this.componentKey = options.componentKey;\n if (options.termsSnapshotKind !== undefined)\n this.termsSnapshotKind = options.termsSnapshotKind;\n if (options.termsSnapshotId !== undefined)\n this.termsSnapshotId = options.termsSnapshotId;\n if (options.basis !== undefined) this.basis = options.basis;\n if (options.baseAmountCents !== undefined)\n this.baseAmountCents = options.baseAmountCents;\n if (options.rate !== undefined) this.rate = options.rate;\n if (options.shareFraction !== undefined)\n this.shareFraction = options.shareFraction;\n if (options.splitGroupId !== undefined)\n this.splitGroupId = options.splitGroupId;\n if (options.amountCents !== undefined)\n this.amountCents = options.amountCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.status !== undefined) this.status = options.status;\n if (options.clearingEndsAt !== undefined)\n this.clearingEndsAt = Commission.coerceDate(options.clearingEndsAt);\n if (options.earnedAt !== undefined)\n this.earnedAt = Commission.coerceDate(options.earnedAt);\n if (options.approvedAt !== undefined)\n this.approvedAt = Commission.coerceDate(options.approvedAt);\n if (options.payableAt !== undefined)\n this.payableAt = Commission.coerceDate(options.payableAt);\n if (options.paidAt !== undefined)\n this.paidAt = Commission.coerceDate(options.paidAt);\n if (options.payoutId !== undefined) this.payoutId = options.payoutId;\n if (options.sourceKind !== undefined) this.sourceKind = options.sourceKind;\n if (options.sourceId !== undefined) this.sourceId = options.sourceId;\n if (options.calculationTrace !== undefined)\n this.calculationTrace = options.calculationTrace;\n if (options.dedupeKey !== undefined) this.dedupeKey = options.dedupeKey;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Re-coerce timestamp fields after the framework reapplies raw option /\n * hydrated row values, and record the loaded status for the save guard.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n this.clearingEndsAt = Commission.coerceDate(this.clearingEndsAt);\n this.earnedAt = Commission.coerceDate(this.earnedAt);\n this.approvedAt = Commission.coerceDate(this.approvedAt);\n this.payableAt = Commission.coerceDate(this.payableAt);\n this.paidAt = Commission.coerceDate(this.paidAt);\n if (await this.isSaved()) {\n loadedCommissionStatus.set(this, this.status);\n }\n return this;\n }\n\n // -------- Status predicates --------\n\n isPending(): boolean {\n return this.status === 'pending';\n }\n\n isEarned(): boolean {\n return this.status === 'earned';\n }\n\n isApproved(): boolean {\n return this.status === 'approved';\n }\n\n isPayable(): boolean {\n return this.status === 'payable';\n }\n\n isPaid(): boolean {\n return this.status === 'paid';\n }\n\n /** `true` once a payout batch has stamped {@link payoutId}. */\n isSettled(): boolean {\n return !!this.payoutId;\n }\n\n // -------- Transition methods (mutate only — caller saves) --------\n\n /**\n * `pending → earned` (clearing window passed). Stamps {@link earnedAt}.\n * Does NOT save — the caller saves.\n */\n markEarned(now: Date = new Date()): void {\n this.assertTransitionFrom('pending', 'earned');\n this.status = 'earned';\n this.earnedAt = now;\n }\n\n /**\n * `earned → approved` (operator/automation approved the earning).\n * Stamps {@link approvedAt}. Does NOT save — the caller saves.\n */\n approve(now: Date = new Date()): void {\n this.assertTransitionFrom('earned', 'approved');\n this.status = 'approved';\n this.approvedAt = now;\n }\n\n /**\n * `approved → payable` (released for the next payout batch).\n * Stamps {@link payableAt}. Does NOT save — the caller saves.\n */\n markPayable(now: Date = new Date()): void {\n this.assertTransitionFrom('approved', 'payable');\n this.status = 'payable';\n this.payableAt = now;\n }\n\n /**\n * `payable → paid` (its payout batch completed). Stamps {@link paidAt}.\n * Does NOT save — the caller saves.\n */\n markPaid(now: Date = new Date()): void {\n this.assertTransitionFrom('payable', 'paid');\n this.status = 'paid';\n this.paidAt = now;\n }\n\n private assertTransitionFrom(\n expected: CommissionStatus,\n next: CommissionStatus,\n ): void {\n if (this.status !== expected) {\n throw new Error(\n `Commission ${this.id ?? '<new>'}: cannot transition to '${next}' ` +\n `from status '${this.status}' (chain is pending → earned → ` +\n 'approved → payable → paid)',\n );\n }\n }\n\n // -------- Trace / metadata helpers --------\n\n /** Parse {@link calculationTrace}; returns `null` on empty/invalid JSON. */\n getCalculationTrace(): CommissionCalculationTrace | null {\n if (!this.calculationTrace) return null;\n try {\n const parsed = JSON.parse(this.calculationTrace) as unknown;\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return null;\n }\n const trace = parsed as CommissionCalculationTrace;\n return typeof trace.componentKey === 'string' &&\n typeof trace.baseAmountCents === 'number'\n ? trace\n : null;\n } catch {\n return null;\n }\n }\n\n /** Serialize and store {@link calculationTrace}. */\n setCalculationTrace(trace: CommissionCalculationTrace): void {\n this.calculationTrace = JSON.stringify(trace);\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n // -------- Save-time guard --------\n\n /**\n * Save-time state-machine guard (commerce pattern). Validates the status\n * transition against the AUTHORITATIVE prior persisted status — re-read\n * from the database so a `create({ id: <existing>, _skipLoad: true })`\n * upsert is correctly treated as an update rather than a guard-free new\n * row. Brand-new rows may start in any status (fixtures/imports); a\n * persisted row may only advance one legal step.\n */\n override async save(): Promise<this> {\n const prior = await this.resolvePriorStatus();\n this.assertStatusTransition(prior);\n await this.assertDedupeKeyNotTaken();\n const result = (await super.save()) as this;\n loadedCommissionStatus.set(this, this.status);\n return result;\n }\n\n /**\n * Refuse a save whose `dedupeKey` already belongs to a DIFFERENT row —\n * commissions are audit rows, and the natural-key upsert would let a\n * fresh instance (generated `create`, or the loser of a calculation\n * race) overwrite the persisted amount/status and rotate the row id.\n * `CommissionCalculationService` treats this refusal as \"someone else\n * already earned it\" and returns the existing row.\n */\n private async assertDedupeKeyNotTaken(): Promise<void> {\n if (!this.dedupeKey) return;\n try {\n const res = await this.db.query(\n `SELECT id FROM ${this.tableName} WHERE dedupe_key = $1`,\n this.dedupeKey,\n );\n const rows = Array.isArray(res)\n ? (res as Record<string, unknown>[])\n : ((res as { rows?: Record<string, unknown>[] }).rows ?? []);\n const taken = rows.find((row) => row.id !== this.id);\n if (taken) {\n throw new Error(\n `Commission (dedupeKey '${this.dedupeKey}'): a commission with ` +\n 'this dedupe key already exists — commissions are immutable ' +\n 'audit rows; corrections append CommissionAdjustments.',\n );\n }\n } catch (error) {\n if (error instanceof Error && error.message.includes('immutable')) {\n throw error;\n }\n // DB not ready / table absent — nothing persisted to collide with.\n }\n }\n\n private async resolvePriorStatus(): Promise<CommissionStatus | undefined> {\n if (this.id) {\n try {\n const row = await this.db.get(this.tableName, { id: this.id });\n if (row && row.status != null) {\n return row.status as CommissionStatus;\n }\n } catch {\n // DB not ready — fall through to the in-memory record.\n }\n }\n return loadedCommissionStatus.get(this);\n }\n\n private assertStatusTransition(prior: CommissionStatus | undefined): void {\n if (prior === undefined) return; // new row\n if (prior === this.status) return; // no-op re-save\n const allowed = COMMISSION_STATUS_TRANSITIONS[prior] ?? [];\n if (!allowed.includes(this.status)) {\n throw new Error(\n `Commission ${this.id}: illegal status transition '${prior}' → ` +\n `'${this.status}'. Use markEarned() / approve() / markPayable() ` +\n '/ markPaid().',\n );\n }\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n return null;\n }\n}\n\nexport default Commission;\n","/**\n * CommissionCollection — collection manager for {@link Commission}.\n *\n * \"Unsettled\" throughout means `payoutId` is empty — the row has not been\n * gathered into a {@link CommissionPayout} batch yet. The emptiness check is\n * applied in memory (`!c.payoutId`) rather than as a `WHERE payout_id = ''`\n * filter so the semantics hold whether an adapter stores the empty\n * reference as `''` or `NULL`.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Commission } from '../models/Commission.js';\nimport type { CommissionStatus } from '../types.js';\n\nexport class CommissionCollection extends SmrtCollection<Commission> {\n static readonly _itemClass = Commission;\n\n /** All commissions for an earner, newest first. */\n async findByEarner(earnerId: string): Promise<Commission[]> {\n return await this.list({\n where: { earnerId },\n orderBy: 'created_at DESC',\n });\n }\n\n /** All commissions derived from one earning event. */\n async findByEvent(earningEventId: string): Promise<Commission[]> {\n return await this.list({\n where: { earningEventId },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Commissions by lifecycle status, newest first. */\n async findByStatus(status: CommissionStatus): Promise<Commission[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Look up a commission by its idempotency natural key. */\n async findByDedupeKey(dedupeKey: string): Promise<Commission | null> {\n if (!dedupeKey) return null;\n const results = await this.list({ where: { dedupeKey }, limit: 1 });\n return results[0] ?? null;\n }\n\n /**\n * Payable commissions for an earner+currency that no payout batch has\n * settled yet — the rows `CommissionPayoutService.createPayoutBatch`\n * gathers.\n *\n * Pass `scope` to narrow the gather to one earning source (e.g. a single\n * ad network): only commissions whose `(sourceKind, sourceId)` match are\n * returned. This lets a caller cut a payout batch that claims *only* its\n * network's commissions, so concurrent per-network batches settle\n * disjoint sets instead of one sweeping the other's rows.\n */\n async findPayableUnsettled(\n earnerId: string,\n currency: string,\n scope?: { sourceKind: string; sourceId: string },\n ): Promise<Commission[]> {\n const where: Record<string, unknown> = {\n earnerId,\n currency,\n status: 'payable',\n };\n if (scope) {\n where.sourceKind = scope.sourceKind;\n where.sourceId = scope.sourceId;\n }\n const payable = await this.list({ where, orderBy: 'created_at ASC' });\n return payable.filter((c) => !c.payoutId);\n }\n\n /** Σ amountCents of {@link findPayableUnsettled} rows (integer cents). */\n async sumPayableByEarner(\n earnerId: string,\n currency: string,\n ): Promise<number> {\n const payable = await this.findPayableUnsettled(earnerId, currency);\n return payable.reduce((sum, c) => sum + c.amountCents, 0);\n }\n\n /** Commissions settled by one payout batch. */\n async findByPayout(payoutId: string): Promise<Commission[]> {\n return await this.list({\n where: { payoutId },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Commissions settled by ANY of the given payout batches, in one `IN`\n * query — the batched-membership primitive behind the source-scoped\n * payout-history verification (one query per page instead of one per\n * payout). Empty input returns `[]` without querying.\n */\n async findByPayouts(payoutIds: string[]): Promise<Commission[]> {\n const ids = [...new Set(payoutIds.filter(Boolean))];\n if (ids.length === 0) return [];\n return await this.list({\n where: { payoutId: ids },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Conditionally claim rows for a payout batch: each row is re-loaded\n * fresh and stamped with `payoutId` only when it is still payable and\n * unclaimed (or already claimed by THIS payout — the idempotent-retry /\n * repair case). Rows claimed by a DIFFERENT payout are skipped, and every\n * claim is verified by a post-save re-read so a lost race never counts\n * toward the caller's totals.\n *\n * Reads and writes go through the model layer (`get` / `save`), so this\n * respects the tenancy interceptor (a cross-tenant id resolves to `null`\n * and is skipped, never mutated) and the DB dialect (an empty FK is `''`\n * on SQLite / `NULL` on the native-`uuid` Postgres/DuckDB columns — the\n * model normalizes both).\n *\n * This is the single place claim semantics live. It narrows the\n * concurrent-batch window to the re-read granularity but is NOT a\n * cross-row transaction — safe concurrent settlement relies on batches\n * using DISJOINT scopes (see `CommissionPayoutService.createPayoutBatch`);\n * overlapping concurrent scopes must be serialized by the caller.\n *\n * Returns the claimed rows (freshly loaded, `payoutId` verified).\n */\n async claimForPayout(\n commissionIds: string[],\n payoutId: string,\n ): Promise<Commission[]> {\n const claimed: Commission[] = [];\n for (const id of commissionIds) {\n const row = await this.get({ id });\n if (!row) continue;\n if (row.payoutId && row.payoutId !== payoutId) continue; // other batch\n if (!row.payoutId) {\n if (!row.isPayable()) continue; // no longer eligible\n row.payoutId = payoutId;\n await row.save();\n }\n const verified = await this.get({ id });\n if (verified && verified.payoutId === payoutId) {\n claimed.push(verified);\n }\n }\n return claimed;\n }\n}\n\nexport default CommissionCollection;\n","/**\n * CommissionPayout — settlement batch for one earner in one currency.\n *\n * A payout gathers an earner's payable unsettled Commissions plus the\n * unsettled Adjustments of eligible commissions, stamps `payoutId` on those\n * EXACT rows, and records the totals it settled\n * (`totalAmountCents = commissionTotalCents + adjustmentTotalCents` —\n * enforced at save time). Batches are minted by\n * `CommissionPayoutService.createPayoutBatch`, idempotently via the\n * `idempotencyKey` natural key.\n *\n * The model is named CommissionPayout (table `commission_payouts`) to avoid\n * the global table-name collision with commerce `Payout` / legacy affiliates\n * `Payout` (`payouts`).\n *\n * Generated surface is FULLY read-only — list/get on api, mcp, AND cli.\n * Commerce Payout precedent (\"a payout has no safe generated write\"): the\n * status drives an outgoing remittance and the totals are the integrity\n * core, so the only legitimate writes are the service's creation path and\n * the guarded transition helpers below. The CLI is an independently\n * configured write surface — `cli: true` would regenerate the exact\n * create/update vector closed on api/mcp, so it is locked to list/get too.\n *\n * Lifecycle: `pending → approved → processing → completed | failed`, with\n * `failed` reachable from approved/processing and resettable to `pending`\n * only via {@link resetFromFailed}, and `rejected` the terminal\n * operator-decline exit from pending/approved (see {@link reject} — the\n * membership release lives in\n * `CommissionPayoutService.transitionPayoutForSource`). Transition helpers\n * mutate and stamp but DO NOT save — the caller saves (same convention as\n * Commission).\n *\n * @packageDocumentation\n */\n\nimport {\n crossPackageRef,\n field,\n foreignKey,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n CommissionPayoutOptions,\n CommissionPayoutStatus,\n PayoutMethod,\n} from '../types.js';\n\n/**\n * Legal status transitions, keyed by the prior persisted status.\n * `failed → pending` exists only for {@link resetFromFailed}. `completed`\n * is terminal. No-op re-saves and brand-new rows are always permitted;\n * this map governs *changes* to persisted rows only (commerce pattern).\n */\nconst PAYOUT_STATUS_TRANSITIONS: Record<\n CommissionPayoutStatus,\n CommissionPayoutStatus[]\n> = {\n pending: ['approved', 'rejected'],\n approved: ['processing', 'failed', 'rejected'],\n processing: ['completed', 'failed'],\n completed: [],\n // FAILED is resettable to PENDING via resetFromFailed().\n failed: ['pending'],\n // REJECTED is terminal — the batch was declined and its membership\n // released; the released rows settle through a FUTURE batch instead.\n rejected: [],\n};\n\n/**\n * Module-scoped record of the status each payout instance was loaded with —\n * fallback for the save-time guard when the DB re-read is unavailable.\n */\nconst loadedPayoutStatus = new WeakMap<\n CommissionPayout,\n CommissionPayoutStatus\n>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // Idempotent settlement: a retried batch with the same idempotencyKey\n // resolves to the existing payout instead of double-paying.\n conflictColumns: ['idempotency_key'],\n // Fully read-only generated surface on ALL THREE surfaces — see the\n // class doc. Writes go through CommissionPayoutService and the guarded\n // transition helpers only.\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n})\nexport class CommissionPayout extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global payouts). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The {@link Earner} being paid. Required. */\n @foreignKey('Earner', { required: true })\n earnerId: string = '';\n\n /** Start of the settlement period this batch covers (informational). */\n periodStart: Date | null = null;\n\n /** End of the settlement period this batch covers (informational). */\n periodEnd: Date | null = null;\n\n /** Σ amountCents of the Commissions this batch settled (integer cents). */\n commissionTotalCents: number = 0;\n\n /**\n * Σ signed amountCents of the Adjustments this batch settled (integer\n * cents; clawbacks make it negative).\n */\n adjustmentTotalCents: number = 0;\n\n /**\n * Net amount remitted — must equal\n * `commissionTotalCents + adjustmentTotalCents` (enforced on save).\n */\n totalAmountCents: number = 0;\n\n /** ISO 4217 currency of the batch. */\n currency: string = 'USD';\n\n /** Delivery method for this batch (defaulted from the Earner). */\n payoutMethod: PayoutMethod = 'bank_transfer';\n\n /**\n * Lifecycle status — see the class doc. Mutate via {@link approve} /\n * {@link markProcessing} / {@link complete} / {@link fail} /\n * {@link resetFromFailed}.\n */\n status: CommissionPayoutStatus = 'pending';\n\n /**\n * Payment reference recorded at completion (check number, transfer id,\n * …). Cleared by {@link resetFromFailed}.\n */\n paymentReference: string = '';\n\n /**\n * Opaque payout-provider reference (processor batch id, remittance file\n * id, …). Retained across failure/reset for audit.\n */\n providerRef: string = '';\n\n /** When the payout completed. */\n paidAt: Date | null = null;\n\n /**\n * Optional link to the commerce Invoice that papers this payout\n * (cross-package string reference — never a DDL foreign key).\n */\n @crossPackageRef('@happyvertical/smrt-commerce:Invoice')\n invoiceId: string = '';\n\n /** Operator notes — approval memos, failure reasons (append-only). */\n notes: string = '';\n\n /**\n * Idempotency natural key. Required. The payout service defaults it to\n * `` `${earnerId}:${currency}:${periodEnd ISO date}` `` when the caller\n * doesn't supply one.\n */\n @field({ required: true })\n idempotencyKey: string = '';\n\n /**\n * DERIVED single-source stamp: when every member commission — and every\n * member adjustment's parent commission — shares exactly one non-empty\n * `(sourceKind, sourceId)`, that source is stamped here; otherwise both\n * stay `''` (mixed-source, unknown-source, or empty membership). The\n * payout service maintains the stamp from VERIFIED claimed membership at\n * batch/repair time (`restampPayoutSource` is the backfill for payouts\n * minted before the stamp existed). It is the index behind the\n * source-scoped payout-history listing, which still re-verifies\n * membership per page — never an authorization input by itself.\n */\n sourceKind: string = '';\n\n /** Id half of the derived single-source stamp — see {@link sourceKind}. */\n @field({ indexed: true })\n sourceId: string = '';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: CommissionPayoutOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.earnerId !== undefined) this.earnerId = options.earnerId;\n if (options.periodStart !== undefined)\n this.periodStart = CommissionPayout.coerceDate(options.periodStart);\n if (options.periodEnd !== undefined)\n this.periodEnd = CommissionPayout.coerceDate(options.periodEnd);\n if (options.commissionTotalCents !== undefined)\n this.commissionTotalCents = options.commissionTotalCents;\n if (options.adjustmentTotalCents !== undefined)\n this.adjustmentTotalCents = options.adjustmentTotalCents;\n if (options.totalAmountCents !== undefined)\n this.totalAmountCents = options.totalAmountCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.payoutMethod !== undefined)\n this.payoutMethod = options.payoutMethod;\n if (options.status !== undefined) this.status = options.status;\n if (options.paymentReference !== undefined)\n this.paymentReference = options.paymentReference;\n if (options.providerRef !== undefined)\n this.providerRef = options.providerRef;\n if (options.paidAt !== undefined)\n this.paidAt = CommissionPayout.coerceDate(options.paidAt);\n if (options.invoiceId !== undefined) this.invoiceId = options.invoiceId;\n if (options.notes !== undefined) this.notes = options.notes;\n if (options.idempotencyKey !== undefined)\n this.idempotencyKey = options.idempotencyKey;\n if (options.sourceKind !== undefined) this.sourceKind = options.sourceKind;\n if (options.sourceId !== undefined) this.sourceId = options.sourceId;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Re-coerce timestamp fields after the framework reapplies raw option /\n * hydrated row values, and record the loaded status for the save guard.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n this.periodStart = CommissionPayout.coerceDate(this.periodStart);\n this.periodEnd = CommissionPayout.coerceDate(this.periodEnd);\n this.paidAt = CommissionPayout.coerceDate(this.paidAt);\n if (await this.isSaved()) {\n loadedPayoutStatus.set(this, this.status);\n }\n return this;\n }\n\n // -------- Status predicates --------\n\n isPending(): boolean {\n return this.status === 'pending';\n }\n\n isApproved(): boolean {\n return this.status === 'approved';\n }\n\n isProcessing(): boolean {\n return this.status === 'processing';\n }\n\n isCompleted(): boolean {\n return this.status === 'completed';\n }\n\n isFailed(): boolean {\n return this.status === 'failed';\n }\n\n isRejected(): boolean {\n return this.status === 'rejected';\n }\n\n // -------- Transition methods (mutate only — caller saves) --------\n\n /** `pending → approved`. Does NOT save — the caller saves. */\n approve(): void {\n if (this.status !== 'pending') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot approve from status '${this.status}'`,\n );\n }\n // A batch whose reconciled membership nets to nothing (or a clawback\n // surplus) must never move toward remittance — such payouts exist only\n // as audit artifacts of a raced/interrupted claim pass.\n if (this.totalAmountCents <= 0) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot approve a batch with ` +\n `non-positive total (${this.totalAmountCents} cents)`,\n );\n }\n this.status = 'approved';\n }\n\n /** `approved → processing`. Does NOT save — the caller saves. */\n markProcessing(): void {\n if (this.status !== 'approved') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot mark processing from status '${this.status}'`,\n );\n }\n this.status = 'processing';\n }\n\n /**\n * `processing → completed`. Requires a payment reference — a completed\n * payout with no reference is untraceable. Stamps {@link paidAt}.\n * Does NOT save — the caller saves.\n */\n complete(paymentReference: string, now: Date = new Date()): void {\n if (this.status !== 'processing') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot complete from status '${this.status}'`,\n );\n }\n if (!paymentReference) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: complete() requires a paymentReference`,\n );\n }\n this.status = 'completed';\n this.paymentReference = paymentReference;\n this.paidAt = now;\n }\n\n /**\n * `pending | approved → rejected` (operator declined the batch before\n * remittance started). Terminal — there is no reset from rejected; the\n * released membership settles through a future batch. Requires a reason,\n * appended to {@link notes}. This mutates the payout only: use\n * `CommissionPayoutService.transitionPayoutForSource` to reject, which\n * also RELEASES the batch's membership (clears `payoutId` on its\n * commissions and adjustments) in the same operation — a rejected payout\n * that kept its rows stamped would strand them unsettleable forever.\n * Does NOT save — the caller saves.\n */\n reject(reason: string): void {\n if (this.status !== 'pending' && this.status !== 'approved') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot reject from status '${this.status}' — processing/terminal batches use fail()/resetFromFailed()`,\n );\n }\n if (!reason) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: reject() requires a reason`,\n );\n }\n this.status = 'rejected';\n const memo = `Rejected: ${reason}`;\n this.notes = this.notes ? `${this.notes}\\n${memo}` : memo;\n }\n\n /**\n * `approved | processing → failed`. Appends the reason to {@link notes}.\n * Does NOT save — the caller saves.\n */\n fail(reason: string): void {\n if (this.status !== 'approved' && this.status !== 'processing') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot fail from status '${this.status}'`,\n );\n }\n this.status = 'failed';\n const memo = `Failed: ${reason ?? ''}`;\n this.notes = this.notes ? `${this.notes}\\n${memo}` : memo;\n }\n\n /**\n * Operator-driven reset: `failed → pending` after fixing whatever broke.\n * Clears {@link paymentReference} and {@link paidAt} (the next attempt\n * gets fresh ones) but RETAINS {@link providerRef} and {@link notes} for\n * audit. The only path out of `failed`. Does NOT save — the caller saves.\n */\n resetFromFailed(): void {\n if (this.status !== 'failed') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot reset from status '${this.status}' — only failed payouts are resettable`,\n );\n }\n this.status = 'pending';\n this.paymentReference = '';\n this.paidAt = null;\n }\n\n // -------- Metadata helpers --------\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n // -------- Save-time guards --------\n\n /**\n * Save with two guards (commerce pattern):\n *\n * 1. **Totals invariant** — `totalAmountCents` must equal\n * `commissionTotalCents + adjustmentTotalCents` (exact integer\n * arithmetic, no epsilon).\n * 2. **Status transition** — validated against the AUTHORITATIVE prior\n * persisted status (re-read from the database so a\n * `create({ id, _skipLoad: true })` upsert can't sidestep the guard).\n * A `completed` payout additionally requires a payment reference,\n * matching {@link complete}'s invariant, regardless of how the status\n * was set.\n */\n override async save(): Promise<this> {\n this.validateTotals();\n const prior = await this.resolvePriorStatus();\n this.assertStatusTransition(prior);\n if (this.status === 'completed' && !this.paymentReference) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: a completed payout requires a paymentReference (use complete()).`,\n );\n }\n const result = (await super.save()) as this;\n loadedPayoutStatus.set(this, this.status);\n return result;\n }\n\n /** Throws when the totals invariant doesn't hold. */\n validateTotals(): void {\n for (const [name, value] of [\n ['commissionTotalCents', this.commissionTotalCents],\n ['adjustmentTotalCents', this.adjustmentTotalCents],\n ['totalAmountCents', this.totalAmountCents],\n ] as const) {\n if (!Number.isInteger(value)) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: ${name} must be integer cents (got ${value}).`,\n );\n }\n }\n const expected = this.commissionTotalCents + this.adjustmentTotalCents;\n if (this.totalAmountCents !== expected) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: totals invariant violated — ` +\n `commission=${this.commissionTotalCents} adjustment=${this.adjustmentTotalCents} ` +\n `total=${this.totalAmountCents} (expected total=${expected}).`,\n );\n }\n }\n\n private async resolvePriorStatus(): Promise<\n CommissionPayoutStatus | undefined\n > {\n if (this.id) {\n try {\n const row = await this.db.get(this.tableName, { id: this.id });\n if (row && row.status != null) {\n return row.status as CommissionPayoutStatus;\n }\n } catch {\n // DB not ready — fall through to the in-memory record.\n }\n }\n return loadedPayoutStatus.get(this);\n }\n\n private assertStatusTransition(\n prior: CommissionPayoutStatus | undefined,\n ): void {\n if (prior === undefined) return; // new row\n if (prior === this.status) return; // no-op re-save\n const allowed = PAYOUT_STATUS_TRANSITIONS[prior] ?? [];\n if (!allowed.includes(this.status)) {\n throw new Error(\n `CommissionPayout ${this.id}: illegal status transition '${prior}' ` +\n `→ '${this.status}'. Use approve() / markProcessing() / ` +\n 'complete() / fail() / reject() / resetFromFailed().',\n );\n }\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n return null;\n }\n}\n\nexport default CommissionPayout;\n","/**\n * CommissionPayoutCollection — collection manager for\n * {@link CommissionPayout}.\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { CommissionPayout } from '../models/CommissionPayout.js';\nimport type { CommissionPayoutStatus } from '../types.js';\n\nexport class CommissionPayoutCollection extends SmrtCollection<CommissionPayout> {\n static readonly _itemClass = CommissionPayout;\n\n /** All payout batches for an earner, newest first. */\n async findByEarner(earnerId: string): Promise<CommissionPayout[]> {\n return await this.list({\n where: { earnerId },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Payout batches by status, newest first. */\n async findByStatus(\n status: CommissionPayoutStatus,\n ): Promise<CommissionPayout[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Look up a payout by its idempotency natural key. */\n async findByIdempotencyKey(\n idempotencyKey: string,\n ): Promise<CommissionPayout | null> {\n if (!idempotencyKey) return null;\n const results = await this.list({ where: { idempotencyKey }, limit: 1 });\n return results[0] ?? null;\n }\n\n /**\n * One page of payouts carrying the derived single-source stamp for\n * `(sourceKind, sourceId)`, newest first with a deterministic id\n * tiebreak. This is the RAW indexed page — stamped rows only, membership\n * unverified. Consumers want\n * `CommissionPayoutService.getSourcePayoutHistory`, which re-verifies\n * each page's membership and fails closed on rows the stamp alone cannot\n * prove.\n */\n async findBySource(\n sourceKind: string,\n sourceId: string,\n page: { limit: number; offset: number },\n ): Promise<CommissionPayout[]> {\n if (!sourceKind || !sourceId) return [];\n return await this.list({\n where: { sourceKind, sourceId },\n orderBy: ['created_at DESC', 'id DESC'],\n limit: page.limit,\n offset: page.offset,\n });\n }\n\n /**\n * Σ totalAmountCents of COMPLETED payouts for an earner+currency —\n * lifetime settled earnings (integer cents).\n */\n async sumPaidByEarner(earnerId: string, currency: string): Promise<number> {\n const completed = await this.list({\n where: { earnerId, currency, status: 'completed' },\n });\n return completed.reduce((sum, p) => sum + p.totalAmountCents, 0);\n }\n}\n\nexport default CommissionPayoutCollection;\n","/**\n * Shared types for the neutral commissions financial core.\n *\n * Statuses are string-literal unions derived from `as const` arrays (no TS\n * enums) so downstream code can iterate the legal values and the types stay\n * erasable. All monetary fields across the module are integer cents with a\n * `*Cents` suffix; rates/fractions are decimals in the range 0–1.\n *\n * This module is the neutral financial core of `@happyvertical/smrt-sales`:\n * it never imports from the `crm` or `referrals` modules and never assumes\n * advertising, Referral, Lead, or Opportunity semantics. Earning sources are\n * generic `(sourceKind, sourceId)` string pairs.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtObjectOptions } from '@happyvertical/smrt-core';\n\n// ---------------------------------------------------------------------------\n// Status / kind vocabularies\n// ---------------------------------------------------------------------------\n\n/** Lifecycle of an {@link Earner} payout account. */\nexport const EARNER_STATUSES = ['pending', 'active', 'suspended'] as const;\nexport type EarnerStatus = (typeof EARNER_STATUSES)[number];\n\n/**\n * Lifecycle of an {@link EarnerSourceAttribution} mapping row. `inactive`\n * rows are retained for audit but never resolve through the attribution\n * lookups.\n */\nexport const EARNER_SOURCE_ATTRIBUTION_STATUSES = [\n 'active',\n 'inactive',\n] as const;\nexport type EarnerSourceAttributionStatus =\n (typeof EARNER_SOURCE_ATTRIBUTION_STATUSES)[number];\n\n/**\n * How a payout is delivered. Shared between {@link Earner} (preference) and\n * {@link CommissionPayout} (what a specific batch will use).\n */\nexport const PAYOUT_METHODS = [\n 'bank_transfer',\n 'check',\n 'paypal',\n 'credit',\n 'other',\n] as const;\nexport type PayoutMethod = (typeof PAYOUT_METHODS)[number];\n\n/**\n * Lifecycle of a versioned {@link CommissionPlan} row.\n *\n * `draft → active | retired`; `active → superseded | retired`;\n * `superseded` / `retired` are terminal. Amendments never mutate an active\n * row — they insert a new `(planKey, version + 1)` draft.\n */\nexport const COMMISSION_PLAN_STATUSES = [\n 'draft',\n 'active',\n 'superseded',\n 'retired',\n] as const;\nexport type CommissionPlanStatus = (typeof COMMISSION_PLAN_STATUSES)[number];\n\n/**\n * Lifecycle of a {@link Commission} earning record. STRICT forward chain:\n * `pending → earned → approved → payable → paid` — no skips, no reversals.\n * Corrections to earned/paid commissions are appended as\n * {@link CommissionAdjustment} rows, never edits.\n */\nexport const COMMISSION_STATUSES = [\n 'pending',\n 'earned',\n 'approved',\n 'payable',\n 'paid',\n] as const;\nexport type CommissionStatus = (typeof COMMISSION_STATUSES)[number];\n\n/** How a commission amount is derived from its earning event. */\nexport const COMMISSION_BASES = [\n 'fixed',\n 'gross',\n 'net',\n 'margin',\n 'custom',\n] as const;\nexport type CommissionBasis = (typeof COMMISSION_BASES)[number];\n\n/** Kinds of append-only {@link CommissionAdjustment} corrections. */\nexport const COMMISSION_ADJUSTMENT_KINDS = [\n 'refund',\n 'credit',\n 'chargeback',\n 'dispute',\n 'correction',\n] as const;\nexport type CommissionAdjustmentKind =\n (typeof COMMISSION_ADJUSTMENT_KINDS)[number];\n\n/**\n * Lifecycle of a {@link CommissionPayout} settlement batch.\n *\n * `pending → approved → processing → completed | failed`, with `failed`\n * reachable from `approved`/`processing` and resettable to `pending` only via\n * the dedicated `resetFromFailed()` helper. `rejected` is the terminal\n * operator-decline exit from `pending`/`approved` — rejecting releases the\n * batch's membership back to unsettled so a future batch can re-gather it\n * (`reject()` on the model mutates status only; the release lives in\n * `CommissionPayoutService.transitionPayoutForSource`).\n */\nexport const COMMISSION_PAYOUT_STATUSES = [\n 'pending',\n 'approved',\n 'processing',\n 'completed',\n 'failed',\n 'rejected',\n] as const;\nexport type CommissionPayoutStatus =\n (typeof COMMISSION_PAYOUT_STATUSES)[number];\n\n/**\n * Recommended earning-event kinds. The `EarningEvent.eventKind` field stays an\n * open string so applications can define their own commercial vocabulary —\n * these are the kinds the framework's own modules emit and recognize.\n */\nexport const EARNING_EVENT_KINDS = [\n 'conversion',\n 'agreement_execution',\n 'invoice_payment',\n 'collected_revenue',\n 'recognized_margin',\n 'milestone',\n] as const;\nexport type EarningEventKind = (typeof EARNING_EVENT_KINDS)[number];\n\n/**\n * Commission statuses whose unsettled adjustments count toward an earner's\n * net payable balance (and are gathered into payout batches). Adjustments\n * against a still-`pending` commission stay out of settlement until the\n * underlying earning clears.\n */\nexport const ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES = [\n 'earned',\n 'approved',\n 'payable',\n 'paid',\n] as const satisfies readonly CommissionStatus[];\n\n// ---------------------------------------------------------------------------\n// Plan components\n// ---------------------------------------------------------------------------\n\n/** Recurrence contract for a {@link CommissionPlanComponent}. */\nexport interface CommissionPlanComponentRecurrence {\n /** `one_time` fires at most once per earner+terms; `recurring` repeats. */\n kind: 'one_time' | 'recurring';\n /** Maximum number of occurrences for `recurring` components. */\n maxOccurrences?: number;\n /**\n * Only events whose `occurredAt` falls within `anchorAt + windowMonths`\n * qualify (the anchor — e.g. an agreement's effective date — is supplied by\n * the caller at calculation time).\n */\n windowMonths?: number;\n}\n\n/**\n * One calculation term inside a {@link CommissionPlan}'s `components` JSON\n * array. Each earning event is matched against every component whose\n * `trigger` equals the event's `eventKind` (or `'*'`).\n */\nexport interface CommissionPlanComponent {\n /** Unique key within the plan (stable across versions by convention). */\n key: string;\n /** Earning-event kind this component fires on, or `'*'` for any kind. */\n trigger: string;\n /** How the commission base amount is resolved from the event. */\n basis: CommissionBasis;\n /** Rate in the range 0–1. Required for every basis except `fixed`. */\n rate?: number;\n /** Flat amount in integer cents. Required for basis `fixed`. */\n fixedAmountCents?: number;\n /** Optional recurrence limits; omitted means unlimited. */\n recurrence?: CommissionPlanComponentRecurrence;\n /**\n * For basis `custom`: the key into the earning event's `customBases`\n * JSON map (`basisKey → cents`) that supplies the base amount.\n */\n customBasisKey?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Calculation trace\n// ---------------------------------------------------------------------------\n\n/**\n * Everything needed to reproduce a Commission's `amountCents` from first\n * principles. Persisted as a JSON string on every Commission so amounts stay\n * auditable even after plans are superseded.\n */\nexport interface CommissionCalculationTrace {\n planKey: string;\n planVersion: number;\n componentKey: string;\n basis: CommissionBasis;\n /** Base amount the rate was applied to, in integer cents. */\n baseAmountCents: number;\n /** Rate applied (0–1). Recorded as `0` for `fixed`-basis components. */\n rate: number;\n /** Split share applied (0–1; `1` for unsplit commissions). */\n shareFraction: number;\n /** Zero-based occurrence index within the component's recurrence. */\n occurrenceIndex: number;\n /** Id of the {@link EarningEvent} evidence row. */\n earningEventId: string;\n /** Rounding contract used by `roundCents()`. */\n roundingMode: 'half_away_from_zero';\n}\n\n// ---------------------------------------------------------------------------\n// Balances\n// ---------------------------------------------------------------------------\n\n/**\n * Computed (never stored) per-earner, per-currency balance snapshot.\n * All figures in integer cents.\n */\nexport interface EarnerBalance {\n earnerId: string;\n currency: string;\n /** Σ unsettled `payable` commissions. */\n payableCents: number;\n /** Σ `pending` commissions (still clearing). */\n pendingCents: number;\n /** Σ `earned` commissions (cleared, awaiting approval). */\n earnedCents: number;\n /** Σ `approved` commissions (awaiting payable release). */\n approvedCents: number;\n /**\n * Σ unsettled adjustments whose parent commission is\n * earned/approved/payable/paid (signed — clawbacks are negative).\n */\n unsettledAdjustmentCents: number;\n /** `payableCents + unsettledAdjustmentCents`. May be negative. */\n netPayableCents: number;\n}\n\n// ---------------------------------------------------------------------------\n// Model constructor options\n// ---------------------------------------------------------------------------\n\n/** Options for constructing an {@link Earner}. */\nexport interface EarnerOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n profileId?: string;\n displayName?: string;\n status?: EarnerStatus;\n payoutMethod?: PayoutMethod;\n payoutThresholdCents?: number;\n payoutScheduleKey?: string;\n currency?: string;\n metadata?: string;\n}\n\n/** Options for constructing an {@link EarnerSourceAttribution}. */\nexport interface EarnerSourceAttributionOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n earnerId?: string;\n sourceKind?: string;\n sourceId?: string;\n status?: EarnerSourceAttributionStatus;\n metadata?: string;\n}\n\n/** Options for constructing a {@link CommissionPlan}. */\nexport interface CommissionPlanOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n planKey?: string;\n version?: number;\n name?: string;\n description?: string;\n status?: CommissionPlanStatus;\n effectiveFrom?: Date | string | number | null;\n currency?: string;\n components?: string;\n metadata?: string;\n}\n\n/** Options for constructing an {@link EarningEvent}. */\nexport interface EarningEventOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n eventKind?: string;\n occurredAt?: Date | string | number;\n sourceKind?: string;\n sourceId?: string;\n grossAmountCents?: number;\n netAmountCents?: number | null;\n marginCents?: number | null;\n currency?: string;\n customBases?: string;\n dedupeKey?: string;\n metadata?: string;\n}\n\n/** Options for constructing a {@link Commission}. */\nexport interface CommissionOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n earnerId?: string;\n earningEventId?: string;\n planKey?: string;\n planVersion?: number;\n componentKey?: string;\n termsSnapshotKind?: string;\n termsSnapshotId?: string;\n basis?: CommissionBasis;\n baseAmountCents?: number;\n rate?: number;\n shareFraction?: number;\n splitGroupId?: string;\n amountCents?: number;\n currency?: string;\n status?: CommissionStatus;\n clearingEndsAt?: Date | string | number | null;\n earnedAt?: Date | string | number | null;\n approvedAt?: Date | string | number | null;\n payableAt?: Date | string | number | null;\n paidAt?: Date | string | number | null;\n payoutId?: string;\n sourceKind?: string;\n sourceId?: string;\n calculationTrace?: string;\n dedupeKey?: string;\n metadata?: string;\n}\n\n/** Options for constructing a {@link CommissionAdjustment}. */\nexport interface CommissionAdjustmentOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n commissionId?: string;\n earnerId?: string;\n adjustmentKind?: CommissionAdjustmentKind;\n amountCents?: number;\n currency?: string;\n reason?: string;\n createdByProfileId?: string;\n payoutId?: string;\n metadata?: string;\n}\n\n/** Options for constructing a {@link CommissionPayout}. */\nexport interface CommissionPayoutOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n earnerId?: string;\n periodStart?: Date | string | number | null;\n periodEnd?: Date | string | number | null;\n commissionTotalCents?: number;\n adjustmentTotalCents?: number;\n totalAmountCents?: number;\n currency?: string;\n payoutMethod?: PayoutMethod;\n status?: CommissionPayoutStatus;\n paymentReference?: string;\n providerRef?: string;\n paidAt?: Date | string | number | null;\n invoiceId?: string;\n notes?: string;\n idempotencyKey?: string;\n sourceKind?: string;\n sourceId?: string;\n metadata?: string;\n}\n","/**\n * CommissionPlan — versioned commission calculation terms.\n *\n * A plan is identified by its `(planKey, version)` natural key. Versions are\n * ROWS, not edits: amending a plan inserts a `version + 1` draft (see\n * `CommissionPlanCollection.createAmendment`) and the prior version is never\n * rewritten. Once a row has been saved with status `active`, its calculation\n * identity (`components`, `currency`, `planKey`, `version`, `effectiveFrom`)\n * is frozen — re-saving with any of them changed throws (same\n * WeakMap-serialize pattern as commerce's `LicenseSale` rights snapshot).\n * Status transitions remain allowed on frozen rows.\n *\n * Generated write surface is deliberately narrow: `create` only (drafts),\n * NO generated update route — amendments are new rows, and status moves go\n * through the guarded transition methods / legal save-time edges.\n *\n * @packageDocumentation\n */\n\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport {\n COMMISSION_BASES,\n type CommissionPlanComponent,\n type CommissionPlanOptions,\n type CommissionPlanStatus,\n} from '../types.js';\n\n/**\n * Legal status transitions, keyed by the prior persisted status.\n * `draft → active | retired`; `active → superseded | retired`;\n * `superseded` / `retired` are terminal. No-op re-saves and brand-new rows\n * are always permitted; this map governs *changes* to persisted rows only\n * (commerce pattern, S5 audit #1390 lineage).\n */\nconst PLAN_STATUS_TRANSITIONS: Record<\n CommissionPlanStatus,\n CommissionPlanStatus[]\n> = {\n draft: ['active', 'retired'],\n active: ['superseded', 'retired'],\n superseded: [],\n retired: [],\n};\n\n/**\n * Module-scoped record of the status each plan instance was loaded with —\n * fallback for the save-time transition guard when the DB re-read is\n * unavailable. WeakMap keeps it out of the schema and GCs with the instance.\n */\nconst loadedPlanStatus = new WeakMap<CommissionPlan, CommissionPlanStatus>();\n\n/**\n * Module-scoped record of the frozen calculation-identity snapshot captured\n * when a plan row is (or becomes) non-draft. Same rationale as commerce\n * `LicenseSale`: an instance field would become a persisted column, a\n * `Meta<T>` would round-trip and tautologically match — a WeakMap keyed by\n * the instance has no schema interaction and GCs with the instance.\n */\nconst frozenPlanSnapshot = new WeakMap<CommissionPlan, string>();\n\n/**\n * Validate a components array. Throws a descriptive error on the first\n * violation. Exported for reuse by the calculation service's input guards\n * and by referral-terms builders that assemble component arrays.\n *\n * Rules:\n * - component keys are non-empty and unique within the plan\n * - `trigger` is a non-empty string (`'*'` matches every event kind)\n * - `basis` is one of {@link COMMISSION_BASES}\n * - basis `fixed` requires an integer `fixedAmountCents`\n * - every other basis requires `rate` in `[0, 1]`\n * - basis `custom` additionally requires a non-empty `customBasisKey`\n * - `recurrence.kind` (when present) is `one_time` or `recurring`;\n * `maxOccurrences` / `windowMonths` (when present) are positive integers\n */\nexport function validateCommissionPlanComponents(\n components: CommissionPlanComponent[],\n): void {\n if (!Array.isArray(components)) {\n throw new Error('CommissionPlan components must be an array');\n }\n const seen = new Set<string>();\n for (const component of components) {\n const label = component?.key || '<missing key>';\n if (!component || typeof component !== 'object') {\n throw new Error('CommissionPlan component must be an object');\n }\n if (!component.key || typeof component.key !== 'string') {\n throw new Error('CommissionPlan component requires a non-empty key');\n }\n if (seen.has(component.key)) {\n throw new Error(\n `CommissionPlan component keys must be unique — duplicate '${component.key}'`,\n );\n }\n seen.add(component.key);\n if (!component.trigger || typeof component.trigger !== 'string') {\n throw new Error(\n `CommissionPlan component '${label}' requires a non-empty trigger ('*' matches all kinds)`,\n );\n }\n if (!COMMISSION_BASES.includes(component.basis)) {\n throw new Error(\n `CommissionPlan component '${label}' has invalid basis '${component.basis}'`,\n );\n }\n if (component.basis === 'fixed') {\n if (\n typeof component.fixedAmountCents !== 'number' ||\n !Number.isInteger(component.fixedAmountCents)\n ) {\n throw new Error(\n `CommissionPlan component '${label}' with basis 'fixed' requires an integer fixedAmountCents`,\n );\n }\n } else {\n if (\n typeof component.rate !== 'number' ||\n !Number.isFinite(component.rate) ||\n component.rate < 0 ||\n component.rate > 1\n ) {\n throw new Error(\n `CommissionPlan component '${label}' with basis '${component.basis}' requires a rate in [0, 1]`,\n );\n }\n }\n if (component.basis === 'custom' && !component.customBasisKey) {\n throw new Error(\n `CommissionPlan component '${label}' with basis 'custom' requires a customBasisKey`,\n );\n }\n const recurrence = component.recurrence;\n if (recurrence !== undefined) {\n if (recurrence.kind !== 'one_time' && recurrence.kind !== 'recurring') {\n throw new Error(\n `CommissionPlan component '${label}' recurrence.kind must be 'one_time' or 'recurring'`,\n );\n }\n for (const [name, value] of [\n ['maxOccurrences', recurrence.maxOccurrences],\n ['windowMonths', recurrence.windowMonths],\n ] as const) {\n if (\n value !== undefined &&\n (!Number.isInteger(value) || (value as number) <= 0)\n ) {\n throw new Error(\n `CommissionPlan component '${label}' recurrence.${name} must be a positive integer`,\n );\n }\n }\n }\n }\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // (tenantId, planKey, version) is the natural key — a retried create of\n // the same version upserts instead of duplicating, and two tenants can\n // both own a plan key like 'default' without colliding. NULL-tenant\n // (global) rows opt out of upsert dedup on adapters where NULLs compare\n // distinct — the PaymentIntent natural-key convention.\n conflictColumns: ['tenant_id', 'plan_key', 'version'],\n // NO generated update route: amendments are new rows\n // (CommissionPlanCollection.createAmendment) and status transitions go\n // through the guarded methods. The save-time guards below still protect\n // any server-side write path.\n api: { include: ['list', 'get', 'create'] },\n mcp: { include: ['list', 'get'] },\n cli: true,\n})\nexport class CommissionPlan extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global plans). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** Stable plan identity shared by every version of the plan. */\n @field({ required: true })\n planKey: string = '';\n\n /** Monotonic version within `planKey`. Amendments insert `max + 1`. */\n version: number = 1;\n\n /** Human-readable plan name. */\n name: string = '';\n\n /** Longer human-readable description of the terms. */\n description: string = '';\n\n /**\n * Lifecycle status — see {@link PLAN_STATUS_TRANSITIONS}. Mutate via\n * {@link activate} / {@link supersede} / {@link retire} (or a legal\n * single-step assignment; the save-time guard rejects illegal edges).\n */\n status: CommissionPlanStatus = 'draft';\n\n /** When this version takes effect. Frozen once the plan activates. */\n effectiveFrom: Date | null = null;\n\n /** ISO 4217 currency the plan's terms are denominated in. */\n currency: string = 'USD';\n\n /**\n * Calculation components as a JSON-string array — see\n * {@link CommissionPlanComponent}. Use {@link getComponents} /\n * {@link setComponents} (the setter validates).\n */\n components: string = '[]';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: CommissionPlanOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.planKey !== undefined) this.planKey = options.planKey;\n if (options.version !== undefined) this.version = options.version;\n if (options.name !== undefined) this.name = options.name;\n if (options.description !== undefined)\n this.description = options.description;\n if (options.status !== undefined) this.status = options.status;\n if (options.effectiveFrom !== undefined)\n this.effectiveFrom = CommissionPlan.coerceDate(options.effectiveFrom);\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.components !== undefined) this.components = options.components;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Re-coerce date fields after the framework reapplies raw option values,\n * record the loaded status for the transition guard, and capture the\n * frozen snapshot when the row arrived already activated. The snapshot is\n * captured for every non-draft status (not just `active`) so a superseded\n * or retired version — history — can't be rewritten either.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n this.effectiveFrom = CommissionPlan.coerceDate(this.effectiveFrom);\n if (await this.isSaved()) {\n loadedPlanStatus.set(this, this.status);\n if (this.status !== 'draft') {\n frozenPlanSnapshot.set(this, this.serializeFrozenSnapshot());\n }\n }\n return this;\n }\n\n // -------- Status predicates --------\n\n isDraft(): boolean {\n return this.status === 'draft';\n }\n\n isActive(): boolean {\n return this.status === 'active';\n }\n\n // -------- Components / metadata helpers --------\n\n /** Parse {@link components}; returns `[]` on empty/invalid JSON. */\n getComponents(): CommissionPlanComponent[] {\n if (!this.components) return [];\n try {\n const parsed = JSON.parse(this.components) as unknown;\n return Array.isArray(parsed) ? (parsed as CommissionPlanComponent[]) : [];\n } catch {\n return [];\n }\n }\n\n /**\n * Validate and store the components array. Throws on invalid components —\n * see {@link validateCommissionPlanComponents} for the rules.\n */\n setComponents(components: CommissionPlanComponent[]): void {\n validateCommissionPlanComponents(components);\n this.components = JSON.stringify(components);\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n // -------- Status transitions --------\n\n /**\n * Transition `draft → active`. Validates components first so no active\n * plan can carry malformed terms.\n */\n activate(): void {\n if (this.status !== 'draft') {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: cannot activate from status '${this.status}'`,\n );\n }\n validateCommissionPlanComponents(this.getComponents());\n this.status = 'active';\n }\n\n /** Transition `active → superseded` (a newer version took over). */\n supersede(): void {\n if (this.status !== 'active') {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: cannot supersede from status '${this.status}'`,\n );\n }\n this.status = 'superseded';\n }\n\n /** Transition `draft | active → retired` (terminal). */\n retire(): void {\n if (this.status !== 'draft' && this.status !== 'active') {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: cannot retire from status '${this.status}'`,\n );\n }\n this.status = 'retired';\n }\n\n // -------- Save-time guards --------\n\n /**\n * Save with two guards:\n *\n * 1. **Status transition** — the about-to-be-written status must be a\n * legal edge from the authoritative prior persisted status (re-read\n * from the DB so a `create({ id: <existing>, _skipLoad: true })` upsert\n * can't sidestep the guard — commerce pattern).\n * 2. **Frozen calculation identity** — once the row has been saved\n * non-draft, `components` / `currency` / `planKey` / `version` /\n * `effectiveFrom` must match the captured snapshot. Amend by inserting\n * a new version instead.\n *\n * Activating saves also re-validate components, so an `active` row always\n * carries well-formed terms regardless of which write path set them.\n */\n override async save(): Promise<this> {\n const prior = await this.resolvePriorStatus();\n this.assertStatusTransition(prior);\n this.assertFrozenIdentityUnchanged();\n await this.assertNaturalKeyNotTaken();\n if (this.status === 'active') {\n validateCommissionPlanComponents(this.getComponents());\n }\n const result = (await super.save()) as this;\n loadedPlanStatus.set(this, this.status);\n if (this.status !== 'draft' && !frozenPlanSnapshot.has(this)) {\n frozenPlanSnapshot.set(this, this.serializeFrozenSnapshot());\n }\n return result;\n }\n\n /**\n * Refuse a save whose `(tenantId, planKey, version)` natural key already\n * belongs to a DIFFERENT row. The frozen-identity guard above is\n * instance-local (WeakMap), so a FRESH instance carrying an existing\n * natural key would otherwise sail through and the conflict-column\n * upsert would rewrite the persisted terms (and rotate the row id).\n * Edit drafts by hydrating them; change terms with\n * `CommissionPlanCollection.createAmendment()`.\n */\n private async assertNaturalKeyNotTaken(): Promise<void> {\n if (!this.planKey) return;\n try {\n const res = await this.db.query(\n `SELECT id, tenant_id FROM ${this.tableName} WHERE plan_key = $1 AND version = $2`,\n this.planKey,\n this.version,\n );\n const rows = Array.isArray(res)\n ? (res as Record<string, unknown>[])\n : ((res as { rows?: Record<string, unknown>[] }).rows ?? []);\n const taken = rows.find(\n (row) =>\n (row.tenant_id ?? null) === (this.tenantId ?? null) &&\n row.id !== this.id,\n );\n if (taken) {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: this version ` +\n 'already exists for the tenant — plan versions are immutable ' +\n 'records. Hydrate the existing row to edit a draft, or create ' +\n 'new terms with CommissionPlanCollection.createAmendment().',\n );\n }\n } catch (error) {\n if (error instanceof Error && error.message.includes('immutable')) {\n throw error;\n }\n // DB not ready / table absent — nothing persisted to collide with.\n }\n }\n\n /**\n * Resolve the AUTHORITATIVE prior status from the database; fall back to\n * the loaded-status WeakMap only when the DB is unavailable. `undefined`\n * means no persisted row exists (genuinely new).\n */\n private async resolvePriorStatus(): Promise<\n CommissionPlanStatus | undefined\n > {\n if (this.id) {\n try {\n const row = await this.db.get(this.tableName, { id: this.id });\n if (row && row.status != null) {\n return row.status as CommissionPlanStatus;\n }\n } catch {\n // DB not ready — fall through to the in-memory record.\n }\n }\n return loadedPlanStatus.get(this);\n }\n\n private assertStatusTransition(\n prior: CommissionPlanStatus | undefined,\n ): void {\n if (prior === undefined) return; // new row — any starting status\n if (prior === this.status) return; // no-op re-save\n const allowed = PLAN_STATUS_TRANSITIONS[prior] ?? [];\n if (!allowed.includes(this.status)) {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: illegal status ` +\n `transition '${prior}' → '${this.status}'. Use activate() / ` +\n 'supersede() / retire().',\n );\n }\n }\n\n private assertFrozenIdentityUnchanged(): void {\n const captured = frozenPlanSnapshot.get(this);\n if (!captured) return;\n const current = this.serializeFrozenSnapshot();\n if (captured !== current) {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: components, ` +\n 'currency, planKey, version, and effectiveFrom are immutable once ' +\n 'the plan has been active. Create an amendment ' +\n '(CommissionPlanCollection.createAmendment) instead of editing ' +\n 'this version.',\n );\n }\n }\n\n private serializeFrozenSnapshot(): string {\n // Stable key ordering so a no-op re-serialization matches.\n return JSON.stringify({\n planKey: this.planKey,\n version: this.version,\n currency: this.currency,\n components: this.components,\n effectiveFrom: this.effectiveFrom\n ? this.effectiveFrom.toISOString()\n : null,\n });\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n return null;\n }\n}\n\nexport default CommissionPlan;\n","/**\n * CommissionPlanCollection — collection manager for {@link CommissionPlan}.\n *\n * Plans are versioned rows: amendments insert `(planKey, maxVersion + 1)`\n * drafts via {@link createAmendment}; existing versions are never rewritten.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { assertTenantReadAllowed } from '@happyvertical/smrt-tenancy';\nimport {\n CommissionPlan,\n validateCommissionPlanComponents,\n} from '../models/CommissionPlan.js';\nimport type {\n CommissionPlanComponent,\n CommissionPlanStatus,\n} from '../types.js';\n\n/**\n * Fields an amendment may change relative to the version it copies.\n * `planKey` is fixed (it identifies the plan), `version` is computed, and\n * `status` is always `draft` — a caller cannot mint a pre-activated\n * amendment.\n */\nexport interface CommissionPlanAmendmentChanges {\n name?: string;\n description?: string;\n components?: CommissionPlanComponent[];\n currency?: string;\n effectiveFrom?: Date | null;\n metadata?: Record<string, unknown>;\n}\n\nexport class CommissionPlanCollection extends SmrtCollection<CommissionPlan> {\n static readonly _itemClass = CommissionPlan;\n\n /** Every version of a plan, newest version first. */\n async findByPlanKey(planKey: string): Promise<CommissionPlan[]> {\n return await this.list({\n where: { planKey },\n orderBy: 'version DESC',\n });\n }\n\n /** Plans by status. */\n async findByStatus(status: CommissionPlanStatus): Promise<CommissionPlan[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /**\n * The highest ACTIVE version of a plan already IN EFFECT at `at`, or\n * `null` when none is. This is what calculation callers resolve terms\n * from when no frozen snapshot pins a specific version. A future-dated\n * amendment can be activated ahead of its effective date without\n * governing earlier qualifications (`effectiveFrom: null` = effective\n * immediately).\n */\n async latestActiveByKey(\n planKey: string,\n at: Date = new Date(),\n tenantId?: string | null,\n ): Promise<CommissionPlan | null> {\n if (tenantId === undefined) {\n // No explicit scope: ambient tenant scoping (when present) applies.\n const results = await this.list({\n where: { planKey, status: 'active' },\n orderBy: 'version DESC',\n });\n return (\n results.find(\n (plan) => plan.effectiveFrom === null || plan.effectiveFrom <= at,\n ) ?? null\n );\n }\n\n // Explicit scope (system/background paths run without ambient tenant\n // context): the tenant's own versions form their own key-space; global\n // (NULL-tenant) versions are the fallback. Never another tenant's.\n const atIso = at.toISOString();\n if (tenantId === null) {\n const results = await this.query(\n `SELECT * FROM ${this.tableName}\n WHERE tenant_id IS NULL\n AND plan_key = ?\n AND status = ?\n AND (effective_from IS NULL OR effective_from <= ?)\n ORDER BY version DESC\n LIMIT 1`,\n [planKey, 'active', atIso],\n { allowRawOnTenantScoped: true },\n );\n return results[0] ?? null;\n }\n\n assertTenantReadAllowed(\n tenantId,\n 'CommissionPlanCollection.latestActiveByKey',\n );\n const results = await this.query(\n `SELECT * FROM ${this.tableName}\n WHERE (tenant_id = ? OR tenant_id IS NULL)\n AND plan_key = ?\n AND status = ?\n AND (effective_from IS NULL OR effective_from <= ?)\n ORDER BY CASE WHEN tenant_id = ? THEN 0 ELSE 1 END, version DESC\n LIMIT 1`,\n [tenantId, planKey, 'active', atIso, tenantId],\n { allowRawOnTenantScoped: true },\n );\n return results[0] ?? null;\n }\n\n /**\n * Create an amendment: insert a new DRAFT row with\n * `version = max(existing versions) + 1`, copying the latest existing\n * version's fields and then applying `changes`. The source version is not\n * touched — activate the draft (and supersede the prior active version)\n * as a separate, explicit step.\n *\n * Throws when no version of `planKey` exists (nothing to amend — use\n * `create` for a brand-new plan).\n */\n async createAmendment(\n planKey: string,\n changes: CommissionPlanAmendmentChanges = {},\n ): Promise<CommissionPlan> {\n const versions = await this.findByPlanKey(planKey);\n const latest = versions[0];\n if (!latest) {\n throw new Error(\n `CommissionPlanCollection.createAmendment: no versions exist for plan key '${planKey}' — create the plan first`,\n );\n }\n // Validate amended components BEFORE persisting anything, so a bad\n // amendment fails cleanly instead of leaving a malformed draft row.\n if (changes.components !== undefined) {\n validateCommissionPlanComponents(changes.components);\n }\n\n const draft = await this.create({\n tenantId: latest.tenantId,\n planKey,\n version: latest.version + 1,\n status: 'draft',\n name: changes.name ?? latest.name,\n description: changes.description ?? latest.description,\n currency: changes.currency ?? latest.currency,\n effectiveFrom:\n changes.effectiveFrom !== undefined\n ? changes.effectiveFrom\n : latest.effectiveFrom,\n components:\n changes.components !== undefined\n ? JSON.stringify(changes.components)\n : latest.components,\n metadata:\n changes.metadata !== undefined\n ? JSON.stringify(changes.metadata)\n : latest.metadata,\n });\n return draft;\n }\n}\n\nexport default CommissionPlanCollection;\n","/**\n * Earner — neutral financial payout account.\n *\n * Replaces the financial half of legacy smrt-affiliates' `Partner`: it holds\n * everything money-related about a party that earns commissions (payout\n * method, threshold, schedule, currency, status) and NOTHING role-related.\n * Role models (a CRM `SalesRepresentative`, a referrals `Referrer`, or any\n * application-defined role) each hold their own `earnerId` pointing here, so\n * one person acting in several roles still settles through a single account.\n *\n * @packageDocumentation\n */\n\nimport { crossPackageRef, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { EarnerOptions, EarnerStatus, PayoutMethod } from '../types.js';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n api: { include: ['list', 'get', 'create', 'update'] },\n mcp: { include: ['list', 'get', 'create'] },\n cli: true,\n})\nexport class Earner extends SmrtObject {\n /**\n * Tenant ID for multi-tenant isolation. Nullable so global/operator-level\n * earners remain possible; unlike legacy affiliates, sales earners are\n * tenant-owned by default.\n */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * Identity link to a smrt-profiles Profile (cross-package string\n * reference — never a DDL foreign key).\n */\n @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n profileId: string = '';\n\n /** Human-readable display name for portals and operator views. */\n displayName: string = '';\n\n /** Account lifecycle: `pending` (default) → `active` / `suspended`. */\n status: EarnerStatus = 'pending';\n\n /** Preferred payout delivery method. */\n payoutMethod: PayoutMethod = 'bank_transfer';\n\n /**\n * Minimum unsettled balance (integer cents) before a payout batch is\n * created. Default $50.00 = 5000 cents.\n */\n payoutThresholdCents: number = 5000;\n\n /**\n * Payout cadence key. Open string so applications can define their own\n * schedules (`manual`, `monthly`, `weekly`, `net_30`, …); `manual` means\n * an operator triggers batches explicitly.\n */\n payoutScheduleKey: string = 'manual';\n\n /** ISO 4217 currency all of this earner's balances settle in. */\n currency: string = 'USD';\n\n /**\n * Additional metadata as a JSON string (tax info, payout-rail details,\n * …). Use {@link getMetadata}/{@link setMetadata}.\n */\n metadata: string = '{}';\n\n constructor(options: EarnerOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.profileId !== undefined) this.profileId = options.profileId;\n if (options.displayName !== undefined)\n this.displayName = options.displayName;\n if (options.status !== undefined) this.status = options.status;\n if (options.payoutMethod !== undefined)\n this.payoutMethod = options.payoutMethod;\n if (options.payoutThresholdCents !== undefined)\n this.payoutThresholdCents = options.payoutThresholdCents;\n if (options.payoutScheduleKey !== undefined)\n this.payoutScheduleKey = options.payoutScheduleKey;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n isActive(): boolean {\n return this.status === 'active';\n }\n\n isPending(): boolean {\n return this.status === 'pending';\n }\n\n isSuspended(): boolean {\n return this.status === 'suspended';\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n}\n\nexport default Earner;\n","/**\n * EarnerCollection — collection manager for {@link Earner}.\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Earner } from '../models/Earner.js';\nimport type { EarnerStatus } from '../types.js';\n\nexport class EarnerCollection extends SmrtCollection<Earner> {\n static readonly _itemClass = Earner;\n\n /** Earners linked to a smrt-profiles Profile. */\n async findByProfile(profileId: string): Promise<Earner[]> {\n return await this.list({\n where: { profileId },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Earners by status. */\n async findByStatus(status: EarnerStatus): Promise<Earner[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /** All active earners. */\n async findActive(): Promise<Earner[]> {\n return await this.findByStatus('active');\n }\n}\n\nexport default EarnerCollection;\n","/**\n * EarnerSourceAttribution — indexed external attribution mapping for an\n * {@link Earner}.\n *\n * Maps a generic external key `(sourceKind, sourceId)` — an ad-network\n * property, a marketplace storefront, a partner account, any\n * application-defined attribution surface — to the Earner credited for it.\n * High-volume ingestion resolves earners through the indexed lookups on\n * `EarnerSourceAttributionCollection` / `EarnerAttributionService` instead of\n * scanning every active earner's JSON metadata.\n *\n * The kind space is the CONSUMER's to define. It may — but need not —\n * coincide with the `(sourceKind, sourceId)` earning-source pairs recorded on\n * EarningEvents and Commissions: an application can attribute earners by\n * property while its earning events carry the network as their source.\n *\n * ## Uniqueness and tenancy\n *\n * Natural key `(tenant_id, source_kind, source_id)` (`conflictColumns`): one\n * mapping per external key per tenant. A `create` for an existing key\n * UPSERTS — it re-points the mapping to the new `earnerId` (idempotent\n * registration; use `EarnerAttributionService.registerAttribution` to observe\n * whether a call created or re-pointed). The adapters' null-aware upsert\n * dedups NULL-tenant (global) keys too, but the unique INDEX itself treats\n * NULLs as distinct, so duplicate global rows can still arrive outside the\n * model layer (raw-SQL imports, pre-null-aware data) — the lookups treat\n * more than one ACTIVE row for a key as ambiguous and fail closed instead\n * of picking one.\n *\n * With an active tenant context, lookups resolve within that tenant only\n * (global rows are invisible). Without tenant context (`optional` mode)\n * lookups see every row, so operator-level resolution across tenants can\n * surface an ambiguity that per-tenant resolution would not — tenant-scoped\n * applications should resolve inside `withTenant()`.\n *\n * ## Migrating metadata-based associations\n *\n * Consumers that previously stashed the association in `Earner.metadata`\n * migrate with a one-time loop — `registerAttribution` is the idempotent\n * backfill primitive (re-running the loop upserts, never duplicates):\n *\n * ```typescript\n * const service = await EarnerAttributionService.create({ db });\n * for (const earner of await earners.list({})) {\n * const propertyIds = (earner.getMetadata().propertyIds ?? []) as string[];\n * for (const propertyId of propertyIds) {\n * await service.registerAttribution({\n * earnerId: earner.id!,\n * sourceKind: 'ad_network_property',\n * sourceId: propertyId,\n * tenantId: earner.tenantId,\n * });\n * }\n * }\n * // Verify via resolveActiveEarnersBySources(), then drop the metadata key.\n * ```\n *\n * @packageDocumentation\n */\n\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n TenantScoped,\n tenantId,\n} from '@happyvertical/smrt-tenancy';\nimport type {\n EarnerSourceAttributionOptions,\n EarnerSourceAttributionStatus,\n} from '../types.js';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // One mapping per external key per tenant — a retried registration\n // upserts (re-points) instead of duplicating. NULL-tenant rows opt out of\n // dedup (see the class doc); the lookups fail closed on the resulting\n // ambiguity.\n conflictColumns: ['tenant_id', 'source_kind', 'source_id'],\n // Configuration rows: full read plus create/update (deactivate via\n // status). No generated delete on ANY surface — deactivation preserves\n // the audit trail of who was credited for a surface (a bare `cli: true`\n // would regenerate the delete verb this contract closes).\n api: { include: ['list', 'get', 'create', 'update'] },\n mcp: { include: ['list', 'get', 'create'] },\n cli: { include: ['list', 'get', 'create', 'update'] },\n})\nexport class EarnerSourceAttribution extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global mappings). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The {@link Earner} credited for this external key. Required. */\n @foreignKey('Earner', { required: true })\n earnerId: string = '';\n\n /**\n * Consumer-defined attribution kind (`ad_network_property`,\n * `marketplace_storefront`, …). Required.\n */\n @field({ required: true })\n sourceKind: string = '';\n\n /**\n * External identifier within {@link sourceKind}. Required. Indexed so\n * batched ingestion lookups stay bounded by the requested ids even\n * without a tenant predicate (the natural-key index is led by\n * `tenant_id`, which tenant-context lookups use).\n */\n @field({ required: true, indexed: true })\n sourceId: string = '';\n\n /**\n * Mapping lifecycle: only `active` rows resolve through the lookups.\n * `inactive` retains the row for audit.\n */\n status: EarnerSourceAttributionStatus = 'active';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: EarnerSourceAttributionOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.earnerId !== undefined) this.earnerId = options.earnerId;\n if (options.sourceKind !== undefined) this.sourceKind = options.sourceKind;\n if (options.sourceId !== undefined) this.sourceId = options.sourceId;\n if (options.status !== undefined) this.status = options.status;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n isActive(): boolean {\n return this.status === 'active';\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n /**\n * Save with two guards:\n *\n * 1. **Completeness** — a mapping without an earner or a full external\n * key can never resolve, so it must never persist.\n * 2. **Tenant coherence** — the mapping's tenant must equal its earner's\n * tenant (both normalized; `''` and `NULL` mean \"no tenant\"). A tenant\n * A mapping crediting a tenant B earner would be unresolvable in\n * tenant scope yet credit across tenants in operator scope — fail\n * closed at the model boundary, for the generated create/update\n * surface as much as the service. The earner row is read RAW (no\n * tenant interception) because the guard must see the earner's true\n * tenant even when saving from another tenant's context.\n */\n override async save(): Promise<this> {\n if (!this.earnerId || !this.sourceKind || !this.sourceId) {\n throw new Error(\n `EarnerSourceAttribution ${this.id ?? '<new>'}: earnerId, ` +\n 'sourceKind, and sourceId are all required.',\n );\n }\n await this.assertEarnerTenantCoherence();\n return (await super.save()) as this;\n }\n\n private async assertEarnerTenantCoherence(): Promise<void> {\n let earnerRow: Record<string, unknown> | null = null;\n try {\n earnerRow = await this.db.get('earners', { id: this.earnerId });\n } catch {\n // DB not ready / earners table absent — nothing to compare against\n // (the FK layer owns pure existence).\n return;\n }\n if (!earnerRow) {\n throw new Error(\n `EarnerSourceAttribution ${this.id ?? '<new>'}: earner ` +\n `'${this.earnerId}' does not exist.`,\n );\n }\n const tenantOf = (value: unknown) => (value ? String(value) : null);\n const earnerTenant = tenantOf(earnerRow.tenant_id);\n // Compare against the EFFECTIVE tenant: an unset tenantId is\n // auto-stamped from the active tenant context by the tenancy\n // interceptor during save, after this guard runs.\n const mappingTenant =\n tenantOf(this.tenantId) ?? tenantOf(getCurrentTenant()?.tenantId);\n if (earnerTenant !== mappingTenant) {\n throw new Error(\n `EarnerSourceAttribution ${this.id ?? '<new>'}: mapping tenant ` +\n `'${mappingTenant ?? 'global'}' does not match earner tenant ` +\n `'${earnerTenant ?? 'global'}' — a mapping must live in its ` +\n \"earner's tenant.\",\n );\n }\n }\n}\n\nexport default EarnerSourceAttribution;\n","/**\n * EarnerSourceAttributionCollection — collection manager for\n * {@link EarnerSourceAttribution}.\n *\n * The queries here are the indexed primitives; the earner-resolving lookups\n * (single + batched, active-earner filtered, ambiguity fail-closed) live on\n * `EarnerAttributionService`.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { EarnerSourceAttribution } from '../models/EarnerSourceAttribution.js';\n\nexport class EarnerSourceAttributionCollection extends SmrtCollection<EarnerSourceAttribution> {\n static readonly _itemClass = EarnerSourceAttribution;\n\n /** Every mapping for one external key (any status), oldest first. */\n async findBySource(\n sourceKind: string,\n sourceId: string,\n ): Promise<EarnerSourceAttribution[]> {\n if (!sourceKind || !sourceId) return [];\n return await this.list({\n where: { sourceKind, sourceId },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Every mapping for a batch of external keys sharing one kind (any\n * status), in one indexed `IN` query. Empty/duplicate ids are dropped;\n * an empty batch returns `[]` without querying.\n */\n async findBySources(\n sourceKind: string,\n sourceIds: string[],\n ): Promise<EarnerSourceAttribution[]> {\n if (!sourceKind) return [];\n const ids = [...new Set(sourceIds.filter(Boolean))];\n if (ids.length === 0) return [];\n return await this.list({\n where: { sourceKind, sourceId: ids },\n orderBy: 'created_at ASC',\n });\n }\n\n /** All mappings held by one earner (any status), oldest first. */\n async findByEarner(earnerId: string): Promise<EarnerSourceAttribution[]> {\n return await this.list({\n where: { earnerId },\n orderBy: 'created_at ASC',\n });\n }\n}\n\nexport default EarnerSourceAttributionCollection;\n","/**\n * EarningEvent — immutable commercial-event evidence.\n *\n * An EarningEvent records that something commission-worthy happened: a\n * conversion, an agreement execution, an invoice payment, collected revenue,\n * recognized margin, a milestone — or any application-defined kind\n * (`eventKind` is an open string; see `EARNING_EVENT_KINDS` for the\n * recommended vocabulary). The source is a generic `(sourceKind, sourceId)`\n * string pair — this module never assumes advertising, Referral, Lead, or\n * Opportunity semantics.\n *\n * Immutability contract: events are evidence. The generated surface exposes\n * `create`/`list`/`get` only (no update, no delete), and application code\n * must treat persisted rows as append-only — commissions reference an\n * event's amounts in their calculation traces, so rewriting an event would\n * silently orphan the audit trail. Corrections are modelled as NEW events\n * (e.g. a `refund`-kind event) or as `CommissionAdjustment` rows.\n *\n * Idempotent ingestion: `dedupeKey` is the natural key\n * (`conflictColumns: ['dedupe_key']`). Callers embed tenant and source\n * identity in the key — e.g.\n * `` `${tenantId}:${sourceKind}:${sourceId}:${eventKind}:${occurrence}` `` —\n * so a retried ingest resolves to the existing row (see\n * `EarningEventCollection.getOrCreateByDedupeKey`).\n *\n * @packageDocumentation\n */\n\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { EarningEventOptions } from '../types.js';\n\n/**\n * Serialized state of each persisted instance, for the save-time\n * immutability guard (WeakMap keeps it out of the schema and GCs with the\n * instance — the ReferralTermSnapshot pattern).\n */\nconst persistedEventState = new WeakMap<EarningEvent, string>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // Natural key for idempotent ingestion — a retried create with the same\n // dedupeKey upserts instead of duplicating.\n conflictColumns: ['dedupe_key'],\n // Immutable evidence: create/list/get only — no update or delete on any\n // generated surface.\n api: { include: ['create', 'list', 'get'] },\n mcp: { include: ['list', 'create'] },\n // High-volume evidence rows are not useful from the CLI.\n cli: false,\n})\nexport class EarningEvent extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global events). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * What kind of commercial event this is. Open string — see\n * `EARNING_EVENT_KINDS` for the recommended vocabulary. Plan components\n * match on this via their `trigger`.\n */\n @field({ required: true })\n eventKind: string = '';\n\n /** When the commercial event occurred (not when it was ingested). */\n occurredAt: Date = new Date();\n\n /**\n * Generic earning-source discriminator (`referral`, `opportunity`,\n * `subscription`, `ad_event`, …). Free-form; this module attaches no\n * semantics to it.\n */\n sourceKind: string = '';\n\n /** Id of the source record named by {@link sourceKind}. */\n sourceId: string = '';\n\n /** Gross amount of the event in integer cents. */\n grossAmountCents: number = 0;\n\n /**\n * Net amount in integer cents, when the ingesting system defines one.\n * `null` means \"net is not defined for this event\" — `net`-basis\n * components then SKIP rather than falling back to gross (net is never\n * derived).\n */\n @field({ type: 'integer', nullable: true })\n netAmountCents: number | null = null;\n\n /**\n * Recognized margin in integer cents, when defined. `null` skips\n * `margin`-basis components — margin is never derived.\n */\n @field({ type: 'integer', nullable: true })\n marginCents: number | null = null;\n\n /** ISO 4217 currency of the event's amounts. */\n currency: string = 'USD';\n\n /**\n * JSON map of `basisKey → integer cents` for `custom`-basis plan\n * components. Use {@link getCustomBases}/{@link setCustomBases}.\n */\n customBases: string = '{}';\n\n /**\n * Idempotency natural key. Required. Callers embed tenant/source identity\n * (see the class doc) — the framework does not synthesize it.\n */\n @field({ required: true })\n dedupeKey: string = '';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: EarningEventOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.eventKind !== undefined) this.eventKind = options.eventKind;\n if (options.occurredAt !== undefined)\n this.occurredAt =\n EarningEvent.coerceDate(options.occurredAt) ?? new Date();\n if (options.sourceKind !== undefined) this.sourceKind = options.sourceKind;\n if (options.sourceId !== undefined) this.sourceId = options.sourceId;\n if (options.grossAmountCents !== undefined)\n this.grossAmountCents = options.grossAmountCents;\n if (options.netAmountCents !== undefined)\n this.netAmountCents = options.netAmountCents;\n if (options.marginCents !== undefined)\n this.marginCents = options.marginCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.customBases !== undefined)\n this.customBases = options.customBases;\n if (options.dedupeKey !== undefined) this.dedupeKey = options.dedupeKey;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Re-coerce {@link occurredAt} after the framework reapplies raw option /\n * hydrated row values (SQLite hands back ISO strings), and capture the\n * persisted state for the immutability guard when this instance hydrated\n * an existing row.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n this.occurredAt = EarningEvent.coerceDate(this.occurredAt) ?? new Date();\n if (await this.isSaved()) {\n persistedEventState.set(this, this.serializeState());\n }\n return this;\n }\n\n /**\n * Save with the evidence-immutability guard. EarningEvents are immutable\n * commercial evidence; three write vectors are closed:\n *\n * - a HYDRATED persisted row must serialize identically to its captured\n * state (no-op re-saves pass, any change throws);\n * - an instance carrying an existing id WITHOUT having hydrated it\n * (`create({ id, _skipLoad: true })`) is rejected outright;\n * - a NEW instance whose `dedupeKey` already belongs to another row is\n * refused outright: the natural-key upsert would not only rewrite the\n * evidence values but ROTATE the row's id (orphaning any Commission\n * whose `earningEventId` points at it). Idempotent ingestion goes\n * through `EarningEventCollection.getOrCreateByDedupeKey()`, which\n * finds first and never upserts.\n */\n override async save(): Promise<this> {\n const captured = persistedEventState.get(this);\n if (captured !== undefined) {\n if (captured !== this.serializeState()) {\n throw new Error(\n `EarningEvent ${this.id ?? '<new>'}: earning events are immutable ` +\n 'evidence — record a correcting event (or a CommissionAdjustment ' +\n 'downstream) instead of editing this row.',\n );\n }\n } else if (this.id && (await this.isSaved())) {\n throw new Error(\n `EarningEvent ${this.id}: refusing to overwrite an existing event ` +\n 'row from a non-hydrated instance — earning events are immutable ' +\n 'evidence.',\n );\n } else if (this.dedupeKey) {\n // Fresh instance (create() pre-assigns an id, so key off \"no captured\n // state and not a persisted id\" rather than a missing id): its\n // natural key may collide with existing evidence via the upsert.\n try {\n const row = await this.db.get(this.tableName, {\n dedupe_key: this.dedupeKey,\n });\n if (row && row.id !== this.id) {\n throw new Error(\n `EarningEvent (dedupeKey '${this.dedupeKey}'): an event with ` +\n 'this dedupe key already exists — earning events are ' +\n 'immutable evidence, and the natural-key upsert would rotate ' +\n \"the existing row's id (orphaning commissions that reference \" +\n 'it). Use EarningEventCollection.getOrCreateByDedupeKey() ' +\n 'for idempotent ingestion, or record a new event under its ' +\n 'own dedupe key.',\n );\n }\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes('immutable evidence')\n ) {\n throw error;\n }\n // DB not ready / table absent — nothing persisted to protect yet.\n }\n }\n const result = (await super.save()) as this;\n persistedEventState.set(this, this.serializeState());\n return result;\n }\n\n private serializeState(): string {\n // Stable key ordering so a no-op re-serialization matches.\n return JSON.stringify({\n tenantId: this.tenantId,\n eventKind: this.eventKind,\n occurredAt: this.occurredAt.toISOString(),\n sourceKind: this.sourceKind,\n sourceId: this.sourceId,\n grossAmountCents: this.grossAmountCents,\n netAmountCents: this.netAmountCents,\n marginCents: this.marginCents,\n currency: this.currency,\n customBases: this.customBases,\n dedupeKey: this.dedupeKey,\n metadata: this.metadata,\n });\n }\n\n /**\n * Parse {@link customBases} into a `basisKey → cents` map; non-numeric\n * values are dropped. Returns `{}` on empty/invalid JSON.\n */\n getCustomBases(): Record<string, number> {\n if (!this.customBases) return {};\n try {\n const parsed = JSON.parse(this.customBases) as unknown;\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return {};\n }\n const out: Record<string, number> = {};\n for (const [key, value] of Object.entries(\n parsed as Record<string, unknown>,\n )) {\n if (typeof value === 'number' && Number.isFinite(value)) {\n out[key] = value;\n }\n }\n return out;\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link customBases}. */\n setCustomBases(bases: Record<string, number>): void {\n this.customBases = JSON.stringify(bases ?? {});\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n return null;\n }\n}\n\nexport default EarningEvent;\n","/**\n * EarningEventCollection — collection manager for {@link EarningEvent}.\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { EarningEvent } from '../models/EarningEvent.js';\nimport type { EarningEventOptions } from '../types.js';\n\nexport class EarningEventCollection extends SmrtCollection<EarningEvent> {\n static readonly _itemClass = EarningEvent;\n\n /** Look up an event by its idempotency natural key. */\n async findByDedupeKey(dedupeKey: string): Promise<EarningEvent | null> {\n if (!dedupeKey) return null;\n const results = await this.list({ where: { dedupeKey }, limit: 1 });\n return results[0] ?? null;\n }\n\n /**\n * Idempotent ingestion: if an event with `options.dedupeKey` already\n * exists, return it untouched (`created: false`) — evidence is immutable,\n * so a replay never updates the stored row. Otherwise create the event.\n *\n * `dedupeKey` is required — callers embed tenant/source identity in it\n * (e.g. `` `${tenantId}:${sourceKind}:${sourceId}:${eventKind}` ``);\n * an empty key would silently disable idempotency, so it throws instead.\n */\n async getOrCreateByDedupeKey(\n options: EarningEventOptions,\n ): Promise<{ event: EarningEvent; created: boolean }> {\n const dedupeKey = options.dedupeKey ?? '';\n if (!dedupeKey) {\n throw new Error(\n 'EarningEventCollection.getOrCreateByDedupeKey requires a dedupeKey',\n );\n }\n const existing = await this.findByDedupeKey(dedupeKey);\n if (existing) {\n return { event: existing, created: false };\n }\n // Coerce the Date-ish option here so the create input is a real Date\n // (the model would coerce at initialize anyway; this keeps types exact).\n const { occurredAt, ...rest } = options;\n const event = await this.create({\n ...rest,\n ...(occurredAt !== undefined ? { occurredAt: new Date(occurredAt) } : {}),\n });\n return { event, created: true };\n }\n\n /** Events for one generic earning source, newest occurrence first. */\n async findBySource(\n sourceKind: string,\n sourceId: string,\n ): Promise<EarningEvent[]> {\n return await this.list({\n where: { sourceKind, sourceId },\n orderBy: 'occurred_at DESC',\n });\n }\n\n /** Events by kind, newest occurrence first. */\n async findByKind(eventKind: string): Promise<EarningEvent[]> {\n return await this.list({\n where: { eventKind },\n orderBy: 'occurred_at DESC',\n });\n }\n}\n\nexport default EarningEventCollection;\n","/**\n * Integer-cents money helpers for the commissions module.\n *\n * Every monetary field in this module is stored as integer cents (`*Cents`\n * suffix). Rounding happens exactly once per calculation step via\n * {@link roundCents} using half-away-from-zero semantics, and every\n * Commission persists a `calculationTrace` naming that rounding mode so\n * amounts stay reproducible.\n *\n * @packageDocumentation\n */\n\n/**\n * Round to the nearest integer cent, half away from zero.\n *\n * `Math.round` alone rounds -2.5 to -2 (half toward +∞); financial\n * conventions want symmetric behaviour, so the sign is factored out first:\n * `Math.sign(v) * Math.round(Math.abs(v))` → `roundCents(2.5) === 3` and\n * `roundCents(-2.5) === -3`.\n */\nexport function roundCents(value: number): number {\n const rounded = Math.sign(value) * Math.round(Math.abs(value));\n // Normalize -0 to 0 so strict equality (Object.is) comparisons behave.\n return rounded === 0 ? 0 : rounded;\n}\n\n/** Convert integer cents to a decimal major-unit amount (`/ 100`). */\nexport function centsToAmount(cents: number): number {\n return cents / 100;\n}\n\n/**\n * Convert a decimal major-unit amount to integer cents, rounding half away\n * from zero (`roundCents(amount * 100)`).\n */\nexport function amountToCents(amount: number): number {\n return roundCents(amount * 100);\n}\n\n/**\n * Calculate a commission amount in integer cents:\n * `roundCents(baseCents * rate * (shareFraction ?? 1))`.\n *\n * Rounding is applied once, on the final product, so split siblings each\n * round independently and their sum can differ from the unsplit amount by at\n * most one cent per sibling — the calculation trace records the inputs so any\n * such drift is auditable.\n *\n * @param baseCents - Base amount in integer cents\n * @param rate - Commission rate (0–1)\n * @param shareFraction - Optional split share (0–1); defaults to 1\n */\nexport function calculateCommissionAmountCents(\n baseCents: number,\n rate: number,\n shareFraction?: number,\n): number {\n return roundCents(baseCents * rate * (shareFraction ?? 1));\n}\n","/** Persisted serialization fence for idempotent CommissionAdjustment writes. */\n\nimport {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n\ninterface CommissionAdjustmentOperationOptions extends SmrtObjectOptions {\n tenantId?: string;\n adjustmentId?: string;\n}\n\n/**\n * One globally unique adjustment operation UUID mapped to its adjustment.\n *\n * This is package-owned infrastructure for `CommissionAdjustmentService`, not\n * a second financial record. The operation UUID is stored as the table's\n * primary `id`, which gives every supported database a persisted uniqueness\n * fence without adding a constrained column to the existing adjustments\n * table. The service inserts this fence and the adjustment in one transaction.\n */\n@TenantScoped({ mode: 'required' })\n@smrt({\n api: false,\n mcp: false,\n cli: false,\n})\nexport class CommissionAdjustmentOperation extends SmrtObject {\n /** Owning tenant; the operation UUID itself remains globally unique. */\n @tenantId()\n tenantId: string = '';\n\n /** Adjustment that the operation creates in the same transaction. */\n // Deliberately not a database foreign key: the fence is inserted first in\n // the transaction, then its adjustment. Atomic commit plus replay\n // verification preserve integrity without requiring deferred constraints.\n @field({ sqlType: 'UUID', required: true, readonly: true, indexed: true })\n adjustmentId!: string;\n\n constructor(options: CommissionAdjustmentOperationOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.adjustmentId !== undefined)\n this.adjustmentId = options.adjustmentId;\n }\n}\n\nexport default CommissionAdjustmentOperation;\n","/** Database serialization primitive for adjustment operation UUIDs. */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { requireTenantId } from '@happyvertical/smrt-tenancy';\nimport { CommissionAdjustmentOperation } from '../models/CommissionAdjustmentOperation.js';\n\nexport interface ClaimCommissionAdjustmentOperationInput {\n operationId: string;\n tenantId: string;\n adjustmentId: string;\n}\n\nexport interface ClaimCommissionAdjustmentOperationResult {\n operation: CommissionAdjustmentOperation | null;\n claimed: boolean;\n}\n\nexport class CommissionAdjustmentOperationCollection extends SmrtCollection<CommissionAdjustmentOperation> {\n static readonly _itemClass = CommissionAdjustmentOperation;\n\n /** Tenant-scoped lookup; foreign-tenant operation payloads stay invisible. */\n async findByOperationId(\n operationId: string,\n ): Promise<CommissionAdjustmentOperation | null> {\n const tenantId = requireTenantId();\n const [operation] = await this.query(\n `SELECT\n id, slug, context, created_at, updated_at, tenant_id,\n CAST(adjustment_id AS TEXT) AS adjustment_id\n FROM ${this.tableName}\n WHERE id = ? AND tenant_id = ?\n LIMIT 1`,\n [operationId, tenantId],\n { allowRawOnTenantScoped: true },\n );\n return operation ?? null;\n }\n\n /**\n * Claim the globally unique operation UUID without changing an existing\n * winner. This must run inside the same transaction that creates the\n * corresponding adjustment.\n */\n async claim(\n input: ClaimCommissionAdjustmentOperationInput,\n ): Promise<ClaimCommissionAdjustmentOperationResult> {\n if (requireTenantId().toLowerCase() !== input.tenantId.toLowerCase()) {\n throw new Error('CommissionAdjustment operation tenant mismatch');\n }\n\n const inserted = await this.query(\n `INSERT INTO ${this.tableName} (\n id, slug, context, tenant_id, adjustment_id\n ) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO NOTHING\n RETURNING id`,\n [\n input.operationId,\n input.operationId,\n '',\n input.tenantId,\n input.adjustmentId,\n ],\n { allowRawOnTenantScoped: true },\n );\n\n const operation = await this.findByOperationId(input.operationId);\n return { operation, claimed: inserted.length === 1 };\n }\n}\n\nexport default CommissionAdjustmentOperationCollection;\n","/** Tenant-safe, idempotent creation of immutable commission adjustments. */\n\nimport { randomUUID } from 'node:crypto';\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport {\n requireTenantId,\n TenantContextError,\n} from '@happyvertical/smrt-tenancy';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { CommissionAdjustmentCollection } from '../collections/CommissionAdjustmentCollection.js';\nimport { CommissionAdjustmentOperationCollection } from '../collections/CommissionAdjustmentOperationCollection.js';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport { EarnerCollection } from '../collections/EarnerCollection.js';\nimport type { CommissionAdjustment } from '../models/CommissionAdjustment.js';\nimport {\n COMMISSION_ADJUSTMENT_KINDS,\n type CommissionAdjustmentKind,\n} from '../types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\ninterface CommissionAdjustmentServiceDeps {\n commissions: CommissionCollection;\n adjustments: CommissionAdjustmentCollection;\n operations: CommissionAdjustmentOperationCollection;\n earners: EarnerCollection;\n}\n\ninterface TransactionCapableDatabase extends DatabaseInterface {\n transaction?<T>(fn: (tx: DatabaseInterface) => Promise<T>): Promise<T>;\n}\n\n/** One immutable operator correction intent. */\nexport interface CreateCommissionAdjustmentInput {\n /** Stable caller-supplied UUID; retries MUST reuse it. */\n operationId: string;\n tenantId: string;\n commissionId: string;\n /** Denormalized value, validated against the parent Commission. */\n earnerId: string;\n adjustmentKind: CommissionAdjustmentKind;\n /** Signed integer cents; zero is not an adjustment. */\n amountCents: number;\n /** ISO currency, validated against the parent Commission. */\n currency: string;\n reason: string;\n /** Cross-package Profile UUID identifying the operator/automation. */\n createdByProfileId: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface CreateCommissionAdjustmentResult {\n adjustment: CommissionAdjustment;\n /** `true` only when this invocation persisted the row. */\n created: boolean;\n}\n\nexport type CommissionAdjustmentReplayMismatchField =\n | 'tenantId'\n | 'commissionId'\n | 'earnerId'\n | 'adjustmentKind'\n | 'amountCents'\n | 'currency'\n | 'reason'\n | 'createdByProfileId'\n | 'metadata';\n\n/** Typed fail-closed result for a reused operation UUID with another intent. */\nexport class CommissionAdjustmentReplayConflictError extends Error {\n readonly code = 'COMMISSION_ADJUSTMENT_REPLAY_CONFLICT' as const;\n\n constructor(\n readonly operationId: string,\n readonly mismatches: readonly CommissionAdjustmentReplayMismatchField[],\n ) {\n super(\n `Commission adjustment operation '${operationId}' was already used with ` +\n `different immutable ${mismatches.length === 1 ? 'field' : 'fields'}: ` +\n mismatches.join(', '),\n );\n this.name = 'CommissionAdjustmentReplayConflictError';\n }\n}\n\nexport type CommissionAdjustmentValidationReason =\n | 'tenant_context_mismatch'\n | 'invalid_operation_id'\n | 'invalid_tenant_id'\n | 'invalid_commission_id'\n | 'invalid_earner_id'\n | 'invalid_operator_profile_id'\n | 'invalid_adjustment_kind'\n | 'invalid_amount'\n | 'invalid_currency'\n | 'invalid_metadata'\n | 'reason_required'\n | 'commission_not_found'\n | 'commission_tenant_mismatch'\n | 'earner_not_found'\n | 'earner_tenant_mismatch'\n | 'earner_mismatch'\n | 'currency_mismatch'\n | 'transaction_unavailable'\n | 'operation_adjustment_missing';\n\n/** Actionable validation refusal raised before any adjustment is persisted. */\nexport class CommissionAdjustmentValidationError extends Error {\n readonly code = 'COMMISSION_ADJUSTMENT_VALIDATION_ERROR' as const;\n\n constructor(\n readonly reason: CommissionAdjustmentValidationReason,\n message: string,\n ) {\n super(message);\n this.name = 'CommissionAdjustmentValidationError';\n }\n}\n\ninterface CanonicalAdjustmentIntent {\n operationId: string;\n tenantId: string;\n commissionId: string;\n earnerId: string;\n adjustmentKind: CommissionAdjustmentKind;\n amountCents: number;\n currency: string;\n reason: string;\n createdByProfileId: string;\n metadata: string;\n}\n\nexport class CommissionAdjustmentService {\n private constructor(private readonly deps: CommissionAdjustmentServiceDeps) {}\n\n static async create(\n options: SmrtClassOptions = {},\n ): Promise<CommissionAdjustmentService> {\n return new CommissionAdjustmentService({\n commissions: await CommissionCollection.create(options),\n adjustments: await CommissionAdjustmentCollection.create(options),\n operations: await CommissionAdjustmentOperationCollection.create(options),\n earners: await EarnerCollection.create(options),\n });\n }\n\n /**\n * Create exactly one immutable adjustment for `operationId`.\n *\n * The operation fence table's primary UUID is the serialization point. The\n * fence and adjustment are committed in one transaction. An exact replay\n * returns the persisted row; any changed immutable input raises\n * {@link CommissionAdjustmentReplayConflictError}. The claim primitive uses\n * `ON CONFLICT DO NOTHING`, so a losing PostgreSQL transaction remains\n * usable and can read/verify the committed winner rather than entering an\n * aborted transaction state.\n */\n async createAdjustment(\n input: CreateCommissionAdjustmentInput,\n ): Promise<CreateCommissionAdjustmentResult> {\n const intent = this.canonicalize(input);\n this.assertTenant(intent.tenantId);\n return await this.runTransaction(async (deps) => {\n const existingOperation = await deps.operations.findByOperationId(\n intent.operationId,\n );\n if (existingOperation) {\n return await this.replayFromOperation(deps, existingOperation, intent);\n }\n\n await this.assertParentAndEarner(deps, intent);\n const adjustmentId = randomUUID();\n const claimed = await deps.operations.claim({\n operationId: intent.operationId,\n tenantId: intent.tenantId,\n adjustmentId,\n });\n\n if (!claimed.operation) {\n // The operation UUID exists outside this tenant. Tenant-scoped reads\n // deliberately reveal no foreign row or payload; report only that the\n // caller's tenant is not the owner of the globally unique operation.\n throw new CommissionAdjustmentReplayConflictError(intent.operationId, [\n 'tenantId',\n ]);\n }\n if (!claimed.claimed) {\n return await this.replayFromOperation(deps, claimed.operation, intent);\n }\n\n const { operationId: _operationId, ...adjustmentIntent } = intent;\n const adjustment = await deps.adjustments.create({\n id: adjustmentId,\n ...adjustmentIntent,\n });\n this.assertExactReplay(adjustment, intent);\n return { adjustment, created: true };\n });\n }\n\n private canonicalize(\n input: CreateCommissionAdjustmentInput,\n ): CanonicalAdjustmentIntent {\n this.assertUuid(input.operationId, 'invalid_operation_id', 'operationId');\n this.assertUuid(input.tenantId, 'invalid_tenant_id', 'tenantId');\n if (input.tenantId !== input.tenantId.toLowerCase()) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_tenant_id',\n 'Commission adjustment tenantId must use canonical lowercase UUID casing',\n );\n }\n this.assertUuid(\n input.commissionId,\n 'invalid_commission_id',\n 'commissionId',\n );\n this.assertUuid(input.earnerId, 'invalid_earner_id', 'earnerId');\n this.assertUuid(\n input.createdByProfileId,\n 'invalid_operator_profile_id',\n 'createdByProfileId',\n );\n if (!COMMISSION_ADJUSTMENT_KINDS.includes(input.adjustmentKind)) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_adjustment_kind',\n `Unknown commission adjustment kind '${String(input.adjustmentKind)}'`,\n );\n }\n if (!Number.isSafeInteger(input.amountCents) || input.amountCents === 0) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_amount',\n 'Commission adjustment amountCents must be a non-zero safe integer',\n );\n }\n const currency = input.currency.trim().toUpperCase();\n if (!/^[A-Z]{3}$/.test(currency)) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_currency',\n 'Commission adjustment currency must be a three-letter ISO code',\n );\n }\n const reason = input.reason.trim();\n if (!reason) {\n throw new CommissionAdjustmentValidationError(\n 'reason_required',\n 'Commission adjustment reason is required',\n );\n }\n\n return {\n operationId: input.operationId.toLowerCase(),\n tenantId: input.tenantId.toLowerCase(),\n commissionId: input.commissionId.toLowerCase(),\n earnerId: input.earnerId.toLowerCase(),\n adjustmentKind: input.adjustmentKind,\n amountCents: input.amountCents,\n currency,\n reason,\n createdByProfileId: input.createdByProfileId.toLowerCase(),\n metadata: this.canonicalMetadata(\n input.metadata === undefined ? {} : input.metadata,\n ),\n };\n }\n\n private assertTenant(tenantId: string): void {\n let activeTenantId: string;\n try {\n activeTenantId = requireTenantId();\n } catch (error) {\n if (!(error instanceof TenantContextError)) throw error;\n throw new CommissionAdjustmentValidationError(\n 'tenant_context_mismatch',\n 'Commission adjustment creation requires an active tenant context',\n );\n }\n if (\n activeTenantId !== activeTenantId.toLowerCase() ||\n activeTenantId !== tenantId\n ) {\n throw new CommissionAdjustmentValidationError(\n 'tenant_context_mismatch',\n 'Commission adjustment tenant must exactly match the canonical lowercase active tenant',\n );\n }\n }\n\n private async assertParentAndEarner(\n deps: CommissionAdjustmentServiceDeps,\n intent: CanonicalAdjustmentIntent,\n ): Promise<void> {\n const commission = await deps.commissions.get({\n id: intent.commissionId,\n });\n if (!commission) {\n throw new CommissionAdjustmentValidationError(\n 'commission_not_found',\n `Commission '${intent.commissionId}' was not found in the active tenant`,\n );\n }\n if (commission.tenantId?.toLowerCase() !== intent.tenantId) {\n throw new CommissionAdjustmentValidationError(\n 'commission_tenant_mismatch',\n 'Commission does not belong to the adjustment tenant',\n );\n }\n if (commission.earnerId.toLowerCase() !== intent.earnerId) {\n throw new CommissionAdjustmentValidationError(\n 'earner_mismatch',\n `Adjustment earner '${intent.earnerId}' does not match Commission earner '${commission.earnerId}'`,\n );\n }\n if (commission.currency.toUpperCase() !== intent.currency) {\n throw new CommissionAdjustmentValidationError(\n 'currency_mismatch',\n `Adjustment currency '${intent.currency}' does not match Commission currency '${commission.currency}'`,\n );\n }\n\n const earner = await deps.earners.get({ id: intent.earnerId });\n if (!earner) {\n throw new CommissionAdjustmentValidationError(\n 'earner_not_found',\n `Earner '${intent.earnerId}' was not found in the active tenant`,\n );\n }\n if (earner.tenantId?.toLowerCase() !== intent.tenantId) {\n throw new CommissionAdjustmentValidationError(\n 'earner_tenant_mismatch',\n 'Earner does not belong to the adjustment tenant',\n );\n }\n }\n\n private assertExactReplay(\n existing: CommissionAdjustment,\n intent: CanonicalAdjustmentIntent,\n ): void {\n const mismatches: CommissionAdjustmentReplayMismatchField[] = [];\n if (existing.tenantId?.toLowerCase() !== intent.tenantId)\n mismatches.push('tenantId');\n if (existing.commissionId.toLowerCase() !== intent.commissionId)\n mismatches.push('commissionId');\n if (existing.earnerId.toLowerCase() !== intent.earnerId)\n mismatches.push('earnerId');\n if (existing.adjustmentKind !== intent.adjustmentKind)\n mismatches.push('adjustmentKind');\n if (existing.amountCents !== intent.amountCents)\n mismatches.push('amountCents');\n if (existing.currency.toUpperCase() !== intent.currency)\n mismatches.push('currency');\n if (existing.reason !== intent.reason) mismatches.push('reason');\n if (existing.createdByProfileId.toLowerCase() !== intent.createdByProfileId)\n mismatches.push('createdByProfileId');\n if (canonicalizePersistedJson(existing.metadata) !== intent.metadata)\n mismatches.push('metadata');\n if (mismatches.length > 0) {\n throw new CommissionAdjustmentReplayConflictError(\n intent.operationId,\n mismatches,\n );\n }\n }\n\n private assertUuid(\n value: string,\n reason: CommissionAdjustmentValidationReason,\n field: string,\n ): void {\n if (typeof value !== 'string' || !UUID_RE.test(value)) {\n throw new CommissionAdjustmentValidationError(\n reason,\n `Commission adjustment ${field} must be a UUID`,\n );\n }\n }\n\n private canonicalMetadata(metadata: Record<string, unknown>): string {\n try {\n if (!isPlainJsonObject(metadata)) {\n throw new TypeError('metadata must be a plain JSON object');\n }\n return stableJson(metadata);\n } catch (error) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_metadata',\n `Commission adjustment metadata must be JSON-serializable: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n }\n\n private async replayFromOperation(\n deps: CommissionAdjustmentServiceDeps,\n operation: { adjustmentId: string },\n intent: CanonicalAdjustmentIntent,\n ): Promise<CreateCommissionAdjustmentResult> {\n const adjustment = await deps.adjustments.get({\n id: operation.adjustmentId,\n });\n if (!adjustment) {\n throw new CommissionAdjustmentValidationError(\n 'operation_adjustment_missing',\n `Commission adjustment operation '${intent.operationId}' has no visible adjustment`,\n );\n }\n this.assertExactReplay(adjustment, intent);\n return { adjustment, created: false };\n }\n\n private async runTransaction<T>(\n fn: (deps: CommissionAdjustmentServiceDeps) => Promise<T>,\n ): Promise<T> {\n const db = this.deps.adjustments.db as TransactionCapableDatabase;\n if (typeof db.transaction !== 'function') {\n throw new CommissionAdjustmentValidationError(\n 'transaction_unavailable',\n 'Commission adjustment creation requires a transaction-capable database adapter',\n );\n }\n return await db.transaction(async (tx) =>\n fn({\n commissions: await CommissionCollection.create({ db: tx }),\n adjustments: await CommissionAdjustmentCollection.create({ db: tx }),\n operations: await CommissionAdjustmentOperationCollection.create({\n db: tx,\n }),\n earners: await EarnerCollection.create({ db: tx }),\n }),\n );\n }\n}\n\nfunction canonicalizePersistedJson(value: string): string {\n try {\n return stableJson(JSON.parse(value) as unknown);\n } catch {\n return value;\n }\n}\n\nfunction stableJson(value: unknown): string {\n return JSON.stringify(sortJsonValue(value, new Set<object>()));\n}\n\nfunction isPlainJsonObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction sortJsonValue(value: unknown, ancestors: Set<object>): unknown {\n if (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'boolean' ||\n (typeof value === 'number' && Number.isFinite(value))\n ) {\n return value;\n }\n if (Array.isArray(value)) {\n if (ancestors.has(value)) throw new TypeError('metadata must be acyclic');\n ancestors.add(value);\n try {\n return value.map((item) => sortJsonValue(item, ancestors));\n } finally {\n ancestors.delete(value);\n }\n }\n if (value && typeof value === 'object') {\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new TypeError('metadata objects must be plain JSON objects');\n }\n if (ancestors.has(value)) throw new TypeError('metadata must be acyclic');\n ancestors.add(value);\n try {\n const sorted = Object.create(null) as Record<string, unknown>;\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n const item = (value as Record<string, unknown>)[key];\n if (item !== undefined) sorted[key] = sortJsonValue(item, ancestors);\n }\n return sorted;\n } finally {\n ancestors.delete(value);\n }\n }\n throw new TypeError('metadata must contain only JSON values');\n}\n\nexport default CommissionAdjustmentService;\n","/**\n * CommissionBalanceService — computed (never stored) per-earner balances.\n *\n * `payableCents` is the sum of unsettled `payable` commissions;\n * `unsettledAdjustmentCents` is the signed sum of unsettled adjustments\n * whose parent commission is earned/approved/payable/paid (adjustments\n * against still-`pending` commissions wait for the earning to clear);\n * `netPayableCents = payableCents + unsettledAdjustmentCents` and can go\n * NEGATIVE when clawbacks against already-paid commissions exceed what is\n * currently payable. The pending/earned/approved breakdowns feed portal\n * views.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CommissionAdjustmentCollection } from '../collections/CommissionAdjustmentCollection.js';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport type { Commission } from '../models/Commission.js';\nimport {\n ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES,\n type CommissionStatus,\n type EarnerBalance,\n} from '../types.js';\n\nexport class CommissionBalanceService {\n constructor(\n private readonly commissions: CommissionCollection,\n private readonly adjustments: CommissionAdjustmentCollection,\n ) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CommissionBalanceService> {\n return new CommissionBalanceService(\n await CommissionCollection.create(classOptions),\n await CommissionAdjustmentCollection.create(classOptions),\n );\n }\n\n /** Compute the {@link EarnerBalance} for one earner in one currency. */\n async getBalance(earnerId: string, currency: string): Promise<EarnerBalance> {\n const rows = await this.commissions.list({\n where: { earnerId, currency },\n });\n\n const sumByStatus = (status: CommissionStatus): number =>\n rows\n .filter((c: Commission) => c.status === status)\n .reduce((sum: number, c: Commission) => sum + c.amountCents, 0);\n\n const pendingCents = sumByStatus('pending');\n const earnedCents = sumByStatus('earned');\n const approvedCents = sumByStatus('approved');\n const payableCents = rows\n .filter((c: Commission) => c.status === 'payable' && !c.payoutId)\n .reduce((sum: number, c: Commission) => sum + c.amountCents, 0);\n\n const statusById = new Map<string, CommissionStatus>();\n for (const c of rows) {\n if (c.id) statusById.set(c.id, c.status);\n }\n\n const unsettled = await this.adjustments.findUnsettledByEarner(\n earnerId,\n currency,\n );\n let unsettledAdjustmentCents = 0;\n for (const adjustment of unsettled) {\n const parentStatus = statusById.get(adjustment.commissionId);\n if (\n parentStatus !== undefined &&\n (\n ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES as readonly CommissionStatus[]\n ).includes(parentStatus)\n ) {\n unsettledAdjustmentCents += adjustment.amountCents;\n }\n }\n\n return {\n earnerId,\n currency,\n payableCents,\n pendingCents,\n earnedCents,\n approvedCents,\n unsettledAdjustmentCents,\n netPayableCents: payableCents + unsettledAdjustmentCents,\n };\n }\n}\n\nexport default CommissionBalanceService;\n","/**\n * CommissionCalculationService — turns one {@link EarningEvent} into\n * Commission rows for one earner, driven by a set of plan components.\n *\n * The service deliberately takes COMPONENTS (plus `planKey`/`planVersion`\n * snapshot refs), not a live plan: callers that calculate from frozen terms\n * — e.g. the referrals module's term snapshots — pass the components they\n * froze, so a later plan amendment can never leak into an already-agreed\n * calculation.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport { EarnerCollection } from '../collections/EarnerCollection.js';\nimport type { Commission } from '../models/Commission.js';\nimport { validateCommissionPlanComponents } from '../models/CommissionPlan.js';\nimport type { EarningEvent } from '../models/EarningEvent.js';\nimport { calculateCommissionAmountCents, roundCents } from '../money.js';\nimport type {\n CommissionCalculationTrace,\n CommissionPlanComponent,\n} from '../types.js';\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n/** Input for {@link CommissionCalculationService.calculateForEvent}. */\nexport interface CommissionCalculationInput {\n /** The (persisted) earning event to calculate from. */\n event: EarningEvent;\n /** Snapshot reference recorded on every created Commission. */\n planKey: string;\n /** Snapshot reference recorded on every created Commission. */\n planVersion: number;\n /** The calculation terms to apply (typically from a frozen snapshot). */\n components: CommissionPlanComponent[];\n /** The earner the commissions belong to. */\n earnerId: string;\n /**\n * Split share (0–1) this earner receives; defaults to 1. Callers running\n * a split invoke the service once per earner with the shares and a shared\n * `splitGroupId`.\n */\n shareFraction?: number;\n /** Groups the sibling commissions of one split. */\n splitGroupId?: string;\n /** Generic polymorphic terms-snapshot reference (kind). */\n termsSnapshotKind?: string;\n /** Generic polymorphic terms-snapshot reference (id). */\n termsSnapshotId?: string;\n /**\n * Clearing window in days: created commissions get\n * `clearingEndsAt = event.occurredAt + clearingDays`. Omitted → no\n * clearing (`clearingEndsAt: null`, immediately sweepable).\n */\n clearingDays?: number;\n /**\n * Currency the plan/terms are denominated in. When provided and different\n * from `event.currency`, every matching component skips with reason\n * `'currency_mismatch'` (this module performs no FX). Omitted → the event\n * currency is taken as authoritative and no mismatch is possible.\n */\n currency?: string;\n /**\n * Resolves how many occurrences of a component this earner has already\n * consumed under these terms (commissions from PRIOR events — the current\n * event must not be counted). Drives `one_time` / `maxOccurrences` limits\n * and the `occurrenceIndex` in the dedupe key and trace. Omitted →\n * occurrence count `0`, i.e. recurrence limits are NOT enforced.\n */\n occurrenceCountResolver?: (componentKey: string) => Promise<number>;\n /**\n * Anchor for `windowMonths` recurrence checks (e.g. an agreement's\n * effective date). Omitted → window checks are skipped.\n */\n anchorAt?: Date;\n}\n\n/** One component the calculation declined, and why. */\nexport interface CommissionComponentSkip {\n componentKey: string;\n /**\n * `'net_basis_undefined'` | `'margin_basis_undefined'` |\n * `'fixed_amount_missing'` | `'rate_missing'` | `'custom_basis_missing'`\n * | `'currency_mismatch'` | `'occurrence_limit_reached'` |\n * `'outside_recurrence_window'`\n */\n reason: string;\n}\n\n/** Result of {@link CommissionCalculationService.calculateForEvent}. */\nexport interface CommissionCalculationResult {\n /** Commissions newly created by THIS call. */\n created: Commission[];\n /** Components that produced nothing, with reasons. */\n skipped: CommissionComponentSkip[];\n /**\n * Idempotent replays: commissions that already existed for this\n * (event, terms, component, earner) tuple. Never re-created, never\n * mutated, and never in `created`.\n */\n existing: Commission[];\n}\n\nexport class CommissionCalculationService {\n constructor(\n private readonly commissions: CommissionCollection,\n /**\n * Optional earner lookup for the tenant-lane guard. When provided,\n * `calculateForEvent` refuses an earner from a different tenant lane\n * than the event (a cross-tenant `earnerId` would create a commission\n * payable to another tenant's account). `static create()` always wires\n * it; direct constructors may omit it for narrow test fixtures.\n */\n private readonly earners?: EarnerCollection,\n ) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CommissionCalculationService> {\n return new CommissionCalculationService(\n await CommissionCollection.create(classOptions),\n await EarnerCollection.create(classOptions),\n );\n }\n\n /**\n * Calculate commissions for one event × one earner × a component set.\n *\n * For each component whose `trigger` matches `event.eventKind` (or `'*'`\n * — non-matching components are silently filtered, not \"skipped\"):\n *\n * 1. **Idempotency** — if a Commission already exists for this\n * (event, terms, component, earner) tuple, it is returned in\n * `existing` and nothing else runs for the component.\n * 2. **Currency** — `input.currency` (when given) must equal the event's;\n * otherwise skip `'currency_mismatch'`.\n * 3. **Recurrence** — `one_time` components skip\n * `'occurrence_limit_reached'` once the resolver reports ≥ 1 prior\n * occurrence; `recurring` components honor `maxOccurrences` and\n * `windowMonths` (events after `anchorAt + windowMonths` skip\n * `'outside_recurrence_window'`).\n * 4. **Basis** — gross → `grossAmountCents`; net → `netAmountCents`\n * (skip `'net_basis_undefined'` when null — net is explicit, NEVER\n * derived from gross); margin → `marginCents` (skip\n * `'margin_basis_undefined'` when null); fixed → `fixedAmountCents`;\n * custom → `getCustomBases()[customBasisKey]` (skip\n * `'custom_basis_missing'`).\n * 5. **Amount** — `roundCents(base * rate * shareFraction)`; for `fixed`,\n * `roundCents(fixedAmountCents * shareFraction)` with `rate` recorded\n * as `0`. Rounding happens exactly once, on the final product.\n *\n * Every created Commission is persisted `pending`, carries the event's\n * tenant/currency/source, a complete {@link CommissionCalculationTrace},\n * `clearingEndsAt` when `clearingDays` was given, and the dedupe key\n * `` `${event.dedupeKey}:${termsSnapshotId || planKey + '@' + planVersion}:${componentKey}:${earnerId}:${occurrenceIndex}` ``.\n */\n async calculateForEvent(\n input: CommissionCalculationInput,\n ): Promise<CommissionCalculationResult> {\n const { event } = input;\n if (!event.id) {\n throw new Error(\n 'CommissionCalculationService.calculateForEvent requires a persisted event (missing id)',\n );\n }\n if (!input.earnerId) {\n throw new Error(\n 'CommissionCalculationService.calculateForEvent requires an earnerId',\n );\n }\n\n const shareFraction = input.shareFraction ?? 1;\n // Money guard: an out-of-range or non-finite fraction would persist\n // overpayment, negative, or NaN commissions — reject before any\n // component calculates.\n if (\n !Number.isFinite(shareFraction) ||\n shareFraction < 0 ||\n shareFraction > 1\n ) {\n throw new Error(\n `CommissionCalculationService.calculateForEvent: shareFraction must be a finite number in [0, 1], got ${String(shareFraction)}`,\n );\n }\n // Money guard: direct callers can hand in arbitrary component arrays\n // (snapshots and plans validate on write, but nothing forces callers\n // through them) — malformed rates/amounts must never reach the math.\n validateCommissionPlanComponents(input.components);\n\n // Tenant-lane guard: an earner from a different lane than the event\n // would receive a commission payable to another tenant's account\n // (reachable via any surface that lets an earnerId be assigned).\n if (this.earners) {\n const earner = await this.earners.get({ id: input.earnerId });\n if (\n earner &&\n earner.tenantId !== null &&\n (event.tenantId ?? null) !== null &&\n earner.tenantId !== event.tenantId\n ) {\n throw new Error(\n `CommissionCalculationService.calculateForEvent: earner '${input.earnerId}' ` +\n `belongs to tenant '${earner.tenantId}' but the event belongs to ` +\n `tenant '${event.tenantId}' — cross-tenant commissions are refused.`,\n );\n }\n }\n const termsRef =\n input.termsSnapshotId || `${input.planKey}@${input.planVersion}`;\n\n const created: Commission[] = [];\n const skipped: CommissionComponentSkip[] = [];\n const existing: Commission[] = [];\n\n for (const component of input.components) {\n // Trigger filter: only components listening for this event kind (or\n // everything via '*') participate at all.\n if (component.trigger !== '*' && component.trigger !== event.eventKind) {\n continue;\n }\n\n // Idempotent replay: the same event can only ever earn once per\n // component per earner under the same terms, regardless of what the\n // occurrence resolver would report on a re-run.\n const priorForEvent = await this.commissions.list({\n where: {\n earningEventId: event.id,\n earnerId: input.earnerId,\n componentKey: component.key,\n planKey: input.planKey,\n planVersion: input.planVersion,\n termsSnapshotId: input.termsSnapshotId ?? '',\n },\n limit: 1,\n });\n if (priorForEvent[0]) {\n existing.push(priorForEvent[0]);\n continue;\n }\n\n // Currency: terms and event must agree — this module performs no FX.\n if (input.currency !== undefined && input.currency !== event.currency) {\n skipped.push({\n componentKey: component.key,\n reason: 'currency_mismatch',\n });\n continue;\n }\n\n // Recurrence limits.\n const occurrenceCount = input.occurrenceCountResolver\n ? await input.occurrenceCountResolver(component.key)\n : 0;\n const recurrence = component.recurrence;\n if (recurrence) {\n if (recurrence.kind === 'one_time' && occurrenceCount >= 1) {\n skipped.push({\n componentKey: component.key,\n reason: 'occurrence_limit_reached',\n });\n continue;\n }\n if (\n recurrence.kind === 'recurring' &&\n recurrence.maxOccurrences !== undefined &&\n occurrenceCount >= recurrence.maxOccurrences\n ) {\n skipped.push({\n componentKey: component.key,\n reason: 'occurrence_limit_reached',\n });\n continue;\n }\n if (\n recurrence.windowMonths !== undefined &&\n input.anchorAt !== undefined &&\n event.occurredAt.getTime() >\n CommissionCalculationService.addMonths(\n input.anchorAt,\n recurrence.windowMonths,\n ).getTime()\n ) {\n skipped.push({\n componentKey: component.key,\n reason: 'outside_recurrence_window',\n });\n continue;\n }\n }\n\n // Resolve the base amount for the component's basis.\n let baseAmountCents: number;\n switch (component.basis) {\n case 'gross':\n baseAmountCents = event.grossAmountCents;\n break;\n case 'net':\n if (event.netAmountCents === null) {\n // Net must be explicitly defined by the ingesting system —\n // it is NEVER derived from gross.\n skipped.push({\n componentKey: component.key,\n reason: 'net_basis_undefined',\n });\n continue;\n }\n baseAmountCents = event.netAmountCents;\n break;\n case 'margin':\n if (event.marginCents === null) {\n skipped.push({\n componentKey: component.key,\n reason: 'margin_basis_undefined',\n });\n continue;\n }\n baseAmountCents = event.marginCents;\n break;\n case 'fixed':\n if (typeof component.fixedAmountCents !== 'number') {\n skipped.push({\n componentKey: component.key,\n reason: 'fixed_amount_missing',\n });\n continue;\n }\n baseAmountCents = component.fixedAmountCents;\n break;\n case 'custom': {\n const bases = event.getCustomBases();\n const key = component.customBasisKey ?? '';\n const value = key ? bases[key] : undefined;\n if (typeof value !== 'number') {\n skipped.push({\n componentKey: component.key,\n reason: 'custom_basis_missing',\n });\n continue;\n }\n baseAmountCents = value;\n break;\n }\n }\n\n // Resolve rate + amount. Fixed components record rate 0 and apply\n // only the share fraction; everything else applies rate × share.\n let rate: number;\n let amountCents: number;\n if (component.basis === 'fixed') {\n rate = 0;\n amountCents = roundCents(baseAmountCents * shareFraction);\n } else {\n if (typeof component.rate !== 'number') {\n skipped.push({\n componentKey: component.key,\n reason: 'rate_missing',\n });\n continue;\n }\n rate = component.rate;\n amountCents = calculateCommissionAmountCents(\n baseAmountCents,\n rate,\n shareFraction,\n );\n }\n\n const occurrenceIndex = occurrenceCount;\n const dedupeKey = `${event.dedupeKey}:${termsRef}:${component.key}:${input.earnerId}:${occurrenceIndex}`;\n\n // Second idempotency belt: an exact dedupe-key hit (however it came\n // to exist) is returned rather than re-created — creating through the\n // conflictColumns upsert would otherwise UPDATE the existing row.\n const priorByKey = await this.commissions.findByDedupeKey(dedupeKey);\n if (priorByKey) {\n existing.push(priorByKey);\n continue;\n }\n\n const trace: CommissionCalculationTrace = {\n planKey: input.planKey,\n planVersion: input.planVersion,\n componentKey: component.key,\n basis: component.basis,\n baseAmountCents,\n rate,\n shareFraction,\n occurrenceIndex,\n earningEventId: event.id,\n roundingMode: 'half_away_from_zero',\n };\n\n let commission: Commission;\n try {\n commission = await this.commissions.create({\n // Commissions inherit the event's tenancy so background\n // calculation (no active tenant context) still lands rows in the\n // right tenant.\n tenantId: event.tenantId,\n earnerId: input.earnerId,\n earningEventId: event.id,\n planKey: input.planKey,\n planVersion: input.planVersion,\n componentKey: component.key,\n termsSnapshotKind: input.termsSnapshotKind ?? '',\n termsSnapshotId: input.termsSnapshotId ?? '',\n basis: component.basis,\n baseAmountCents,\n rate,\n shareFraction,\n splitGroupId: input.splitGroupId ?? '',\n amountCents,\n currency: event.currency,\n status: 'pending',\n clearingEndsAt:\n input.clearingDays !== undefined\n ? new Date(\n event.occurredAt.getTime() + input.clearingDays * MS_PER_DAY,\n )\n : null,\n sourceKind: event.sourceKind,\n sourceId: event.sourceId,\n calculationTrace: JSON.stringify(trace),\n dedupeKey,\n });\n } catch (error) {\n // The Commission dedupe-key guard refuses to overwrite an existing\n // row — under a calculation race the loser lands here. That IS the\n // idempotent outcome: hand back the row that won.\n if (\n error instanceof Error &&\n error.message.includes('immutable audit rows')\n ) {\n const winner = await this.commissions.list({\n where: { dedupeKey },\n limit: 1,\n });\n if (winner[0]) {\n existing.push(winner[0]);\n continue;\n }\n }\n throw error;\n }\n created.push(commission);\n }\n\n return { created, skipped, existing };\n }\n\n /**\n * Calendar-month addition (UTC). JS `setUTCMonth` semantics: day-of-month\n * overflow rolls into the next month (Jan 31 + 1 month → Mar 2/3), which\n * is acceptable for coarse recurrence windows.\n */\n private static addMonths(date: Date, months: number): Date {\n const result = new Date(date.getTime());\n result.setUTCMonth(result.getUTCMonth() + months);\n return result;\n }\n}\n\nexport default CommissionCalculationService;\n","/**\n * CommissionPayoutService — mints and drives {@link CommissionPayout}\n * settlement batches.\n *\n * The service is the ONLY sanctioned creation path for payouts (their\n * generated surface is fully read-only): it gathers the exact payable\n * unsettled Commissions and eligible unsettled Adjustments, refuses\n * below-threshold / non-positive batches, stamps `payoutId` on the exact\n * gathered rows, and later flips the batch's commissions to `paid` when the\n * payout completes.\n *\n * Batch creation is deliberately non-transactional (conditional claims +\n * disjoint scopes — see {@link createPayoutBatch}). Two surfaces layered on\n * top serve per-source consumers:\n *\n * - {@link getSourcePayoutHistory} — the source-scoped, paginated,\n * membership-verified payout history (#1985), indexed by the derived\n * single-source stamp each batch/repair pass maintains.\n * - {@link transitionPayoutForSource} — atomic source-authorized lifecycle\n * transitions (#1987): payout row locked, membership re-verified and\n * totals recomputed under the lock, transition + member writes committed\n * together on the same transaction database.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { CommissionAdjustmentCollection } from '../collections/CommissionAdjustmentCollection.js';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport { CommissionPayoutCollection } from '../collections/CommissionPayoutCollection.js';\nimport { EarnerCollection } from '../collections/EarnerCollection.js';\nimport type { Commission } from '../models/Commission.js';\nimport type { CommissionAdjustment } from '../models/CommissionAdjustment.js';\nimport type { CommissionPayout } from '../models/CommissionPayout.js';\nimport {\n ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES,\n type CommissionPayoutStatus,\n type CommissionStatus,\n type PayoutMethod,\n} from '../types.js';\n\n/** The subset of adapter capabilities the transactional transition uses. */\ntype TransactionCapableDatabase = DatabaseInterface & {\n transaction?: <T>(\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n acquireSession?: unknown;\n};\n\n/**\n * Per-database promise chain serializing transactional transitions on\n * engines that multiplex every transaction over ONE shared connection\n * (SQLite, DuckDB, JSON) — concurrent `BEGIN`/`COMMIT` pairs would\n * interleave there. PostgreSQL (pooled per-transaction connections) skips\n * the chain entirely. WeakMap so the tail GCs with the database instance.\n */\nconst singleConnectionTransitionTails = new WeakMap<object, Promise<unknown>>();\n\n/**\n * What the transactional transition callback reports back across the\n * commit boundary — ids only, so the public result can rehydrate on the\n * service's own connection.\n */\ninterface TxTransitionOutcome {\n outcome: 'transitioned' | 'already_applied' | 'refused';\n payoutId: string | null;\n refusal?: { reason: PayoutTransitionRefusalReason; detail: string };\n /** Prevent payout rehydration when source ownership was not proven. */\n authorizationFailed?: true;\n releasedCommissionIds?: string[];\n releasedAdjustmentIds?: string[];\n}\n\n/** Collaborators for {@link CommissionPayoutService}. */\nexport interface CommissionPayoutServiceDeps {\n earners: EarnerCollection;\n commissions: CommissionCollection;\n adjustments: CommissionAdjustmentCollection;\n payouts: CommissionPayoutCollection;\n}\n\n/** Input for {@link CommissionPayoutService.createPayoutBatch}. */\nexport interface CreatePayoutBatchInput {\n earnerId: string;\n currency: string;\n /** Informational period bounds recorded on the payout. */\n periodStart?: Date;\n periodEnd?: Date;\n /**\n * Idempotency natural key. Defaults to\n * `` `${earnerId}:${currency}:${periodEnd ISO date}` ``, or\n * `` `${earnerId}:${currency}:${sourceKind}:${sourceId}:${periodEnd ISO date}` ``\n * when scoped by source (so a per-network batch and the earner-wide batch\n * on the same day don't collide). REQUIRED when {@link commissionIds} is\n * given — an explicit set has no natural default key. Callers running more\n * than one batch per key/day must supply their own key.\n */\n idempotencyKey?: string;\n /**\n * Restrict the batch to commissions from ONE earning source (e.g. a\n * single ad network). Both `sourceKind` and `sourceId` must be set\n * together. Only payable, unsettled commissions matching this source are\n * gathered, and eligible adjustments are narrowed to those whose parent\n * commission shares the source. Mutually exclusive with\n * {@link commissionIds}. Omit both to settle the whole earner+currency\n * (the original behavior).\n */\n sourceKind?: string;\n sourceId?: string;\n /**\n * Restrict the batch to EXACTLY these commissions. Each id is included\n * only when it is payable, unsettled, and belongs to this earner+currency\n * — ineligible ids are ignored (inspect `settledCommissionIds` for what\n * was actually claimed). Adjustments whose parent commission is in this\n * set come along. Mutually exclusive with {@link sourceKind}/\n * {@link sourceId}; requires an explicit {@link idempotencyKey}.\n */\n commissionIds?: string[];\n /** Overrides the earner's `payoutThresholdCents`. */\n minimumThresholdCents?: number;\n /** Overrides the earner's `payoutMethod`. */\n payoutMethod?: PayoutMethod;\n /** Clock override for deterministic tests. */\n now?: Date;\n}\n\n/** Result of {@link CommissionPayoutService.createPayoutBatch}. */\nexport interface CreatePayoutBatchResult {\n /** The created (or, on an idempotent replay, existing) payout — `null` on refusal. */\n payout: CommissionPayout | null;\n /** `true` only when THIS call minted the payout. */\n created: boolean;\n /** Why no payout was created, when refused. */\n reason?: 'below_threshold' | 'nothing_payable';\n /** Ids of the commissions THIS call stamped onto the payout. */\n settledCommissionIds: string[];\n /** Ids of the adjustments THIS call stamped onto the payout. */\n settledAdjustmentIds: string[];\n}\n\n/**\n * Why a payout's membership failed source verification. Every reason is\n * fail-closed: the payout is excluded from source-scoped listings and\n * refused source-authorized transitions until repaired.\n *\n * - `membership_empty` — no rows are stamped with the payout's id (nothing\n * proves ownership; e.g. a raced-away batch artifact or a rejected batch\n * whose rows were released).\n * - `source_mismatch` — a member commission (or an adjustment's parent\n * commission) carries a different or empty `(sourceKind, sourceId)`.\n * - `adjustment_parent_missing` — a member adjustment's parent commission\n * cannot be loaded, so its ownership cannot be proven.\n * - `earner_mismatch` / `currency_mismatch` / `tenant_mismatch` — a member\n * row disagrees with the payout on that axis.\n */\nexport type PayoutMembershipRefusalReason =\n | 'membership_empty'\n | 'source_mismatch'\n | 'adjustment_parent_missing'\n | 'earner_mismatch'\n | 'currency_mismatch'\n | 'tenant_mismatch';\n\n/** Why {@link CommissionPayoutService.transitionPayoutForSource} refused. */\nexport type PayoutTransitionRefusalReason =\n | PayoutMembershipRefusalReason\n | 'payout_not_found'\n | 'status_conflict'\n | 'totals_drift'\n | 'non_positive_total';\n\n/**\n * Source-authorized lifecycle actions. Targets:\n * `approve` (pending → approved), `mark_processing` (approved →\n * processing), `complete` (processing → completed, requires\n * `paymentReference`), `fail` (approved|processing → failed, requires\n * `reason`), `reject` (pending|approved → rejected, requires `reason`;\n * releases the batch's membership).\n */\nexport type PayoutSourceTransitionAction =\n | 'approve'\n | 'mark_processing'\n | 'complete'\n | 'fail'\n | 'reject';\n\n/** Input for {@link CommissionPayoutService.transitionPayoutForSource}. */\nexport interface TransitionPayoutForSourceInput {\n payoutId: string;\n /**\n * The earning source this transition is authorized against. EVERY member\n * commission — and every member adjustment through its parent commission\n * — must belong to exactly this source or the call is refused.\n */\n sourceKind: string;\n sourceId: string;\n action: PayoutSourceTransitionAction;\n /**\n * Optimistic concurrency guard: refuse with `status_conflict` when the\n * LOCKED payout's status differs, after source membership is authorized.\n * Omit to let the action's own from-status rule arbitrate (concurrent\n * duplicate calls for actions that retain membership then resolve as one\n * `transitioned` + one `already_applied`; `reject` replays fail\n * `membership_empty` after releasing that evidence).\n */\n expectedStatus?: CommissionPayoutStatus;\n /** Required for `complete`. */\n paymentReference?: string;\n /** Required for `fail` and `reject`; appended to the payout's notes. */\n reason?: string;\n /** Clock override for deterministic tests. */\n now?: Date;\n}\n\n/** Result of {@link CommissionPayoutService.transitionPayoutForSource}. */\nexport interface TransitionPayoutForSourceResult {\n /**\n * `transitioned` — THIS call performed the transition.\n * `already_applied` — the payout was already in the action's target\n * status; nothing was written (terminal completion metadata —\n * `paymentReference`, `providerRef`, `paidAt` — is never overwritten by\n * a replay).\n * `refused` — fail-closed; see {@link refusal}.\n */\n outcome: 'transitioned' | 'already_applied' | 'refused';\n /**\n * The payout re-read AFTER the transaction (bound to the service's own\n * connection). `null` for `payout_not_found` and every membership\n * authorization refusal, so an unverified caller receives no payout\n * lifecycle or settlement data.\n */\n payout: CommissionPayout | null;\n /** Set exactly when {@link outcome} is `refused`. */\n refusal?: { reason: PayoutTransitionRefusalReason; detail: string };\n /** Commissions a `reject` released back to unsettled. */\n releasedCommissionIds?: string[];\n /** Adjustments a `reject` released back to unsettled. */\n releasedAdjustmentIds?: string[];\n}\n\n/** Input for {@link CommissionPayoutService.getSourcePayoutHistory}. */\nexport interface SourcePayoutHistoryInput {\n sourceKind: string;\n sourceId: string;\n /** Page size, 1–100. Default 25. */\n limit?: number;\n /** Rows to skip (offset pagination). Default 0. */\n offset?: number;\n}\n\n/** One page of {@link CommissionPayoutService.getSourcePayoutHistory}. */\nexport interface SourcePayoutHistoryPage {\n /**\n * The page's VERIFIED payouts, newest first (`created_at DESC, id DESC`).\n * May hold fewer than `limit` rows even when {@link nextOffset} is set —\n * rows that failed verification are in {@link excluded} instead.\n */\n payouts: CommissionPayout[];\n /** Stamped rows on this page excluded fail-closed, with reasons. */\n excluded: {\n payoutId: string;\n reason: PayoutMembershipRefusalReason;\n detail: string;\n }[];\n /** Echo of the requested offset. */\n offset: number;\n /** Echo of the effective page size. */\n limit: number;\n /**\n * Offset of the next page (advances by the SCANNED count, so excluded\n * rows never cause skips), or `null` when the history is exhausted.\n */\n nextOffset: number | null;\n}\n\nexport class CommissionPayoutService {\n /**\n * Canonical UUID shape. Explicit `commissionIds` are filtered against this\n * before hitting the native-`uuid` `id` column so a malformed external id\n * can't abort the batch on Postgres/DuckDB.\n */\n private static readonly UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n constructor(private readonly deps: CommissionPayoutServiceDeps) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CommissionPayoutService> {\n return new CommissionPayoutService({\n earners: await EarnerCollection.create(classOptions),\n commissions: await CommissionCollection.create(classOptions),\n adjustments: await CommissionAdjustmentCollection.create(classOptions),\n payouts: await CommissionPayoutCollection.create(classOptions),\n });\n }\n\n /**\n * Create a settlement batch for one earner in one currency.\n *\n * Flow:\n * 1. **Idempotency + repair** — an existing payout with the (defaulted)\n * key is returned as `{ payout, created: false }`. A clean replay\n * touches nothing (new payable work is never swept into an existing\n * batch). A PENDING payout whose stored totals disagree with the rows\n * stamped with its id — the signature of an interrupted claim pass —\n * is repaired: the claim pass re-runs and the totals are reconciled\n * from the verified membership. Past `pending` the batch is frozen.\n * 2. **Gather** — payable unsettled commissions for the earner/currency,\n * plus unsettled adjustments whose parent commission is\n * earned/approved/payable/paid (same eligibility as the balance\n * service, so the batch settles exactly what the balance reports). Pass\n * `sourceKind`/`sourceId` to gather only ONE source's commissions, or\n * `commissionIds` to gather an explicit set; in both scoped modes the\n * eligible adjustments are narrowed to the same scope.\n * 3. **Refuse** — `netTotal <= 0` → `'nothing_payable'`;\n * `netTotal < threshold` (earner default, overridable) →\n * `'below_threshold'`. Nothing is minted or stamped on refusal.\n * 4. **Mint, claim, reconcile** — create the `pending` payout, then\n * CLAIM the gathered rows through the collections' conditional\n * `claimForPayout` (rows grabbed by another batch in the interim are\n * skipped, never double-claimed), and finally store totals computed\n * from the rows that were VERIFIABLY claimed — the payout's totals\n * are always reproducible from its member rows.\n *\n * Concurrency: claims are conditional with post-save verification, which\n * narrows but does not eliminate races, and a batch is NOT wrapped in one\n * DB transaction (the collection layer exposes none). Safe concurrent\n * settlement therefore relies on SCOPING to DISJOINT sets: two batches\n * scoped to different sources (or non-overlapping `commissionIds`) gather\n * disjoint rows and never contend — this is the intended multi-source\n * (e.g. per-ad-network) settlement pattern. Running OVERLAPPING scopes\n * concurrently (a source batch and the earner-wide batch, or intersecting\n * id sets) is the caller's responsibility to serialize: `claimForPayout`\n * still won't double-own a single row, but a shared commission and its\n * negative adjustment could split across the two batches, so neither\n * payout's net would be authoritative. An interrupted claim pass within a\n * single scope is healed by the repair-on-replay above.\n */\n async createPayoutBatch(\n input: CreatePayoutBatchInput,\n ): Promise<CreatePayoutBatchResult> {\n const now = input.now ?? new Date();\n CommissionPayoutService.assertScope(input);\n const earner = await this.deps.earners.get({ id: input.earnerId });\n if (!earner) {\n throw new Error(\n `CommissionPayoutService: earner '${input.earnerId}' not found`,\n );\n }\n\n const idempotencyKey =\n input.idempotencyKey ??\n CommissionPayoutService.defaultIdempotencyKey(\n input.earnerId,\n input.currency,\n input.periodEnd ?? now,\n input.sourceKind && input.sourceId\n ? { sourceKind: input.sourceKind, sourceId: input.sourceId }\n : undefined,\n );\n\n // Idempotent replay: same key → same payout. A CLEAN replay (stored\n // totals match the stamped membership) returns without touching rows —\n // new payable work is never swept into an existing batch. A pending\n // payout whose stored totals DISAGREE with its membership is the\n // signature of an interrupted claim pass: repair it (re-claim +\n // reconcile totals) instead of returning totals its rows can't\n // reproduce. Anything past pending is frozen.\n const existingPayout =\n await this.deps.payouts.findByIdempotencyKey(idempotencyKey);\n if (existingPayout) {\n if (\n existingPayout.isPending() &&\n !(await this.membershipConsistent(existingPayout))\n ) {\n const repaired = await this.claimAndReconcile(existingPayout, input);\n return { ...repaired, created: false };\n }\n return {\n payout: existingPayout,\n created: false,\n settledCommissionIds: [],\n settledAdjustmentIds: [],\n };\n }\n\n // Gather the exact rows this batch would settle (honoring any source /\n // explicit-id scope).\n const commissions = await this.gatherBatchCommissions(input);\n const eligibleAdjustments = await this.findEligibleUnsettledAdjustments(\n input.earnerId,\n input.currency,\n CommissionPayoutService.adjustmentParentPredicate(input),\n );\n\n const commissionTotalCents = commissions.reduce(\n (sum, c) => sum + c.amountCents,\n 0,\n );\n const adjustmentTotalCents = eligibleAdjustments.reduce(\n (sum, a) => sum + a.amountCents,\n 0,\n );\n const netTotalCents = commissionTotalCents + adjustmentTotalCents;\n\n if (netTotalCents <= 0) {\n return {\n payout: null,\n created: false,\n reason: 'nothing_payable',\n settledCommissionIds: [],\n settledAdjustmentIds: [],\n };\n }\n\n const thresholdCents =\n input.minimumThresholdCents ?? earner.payoutThresholdCents;\n if (netTotalCents < thresholdCents) {\n return {\n payout: null,\n created: false,\n reason: 'below_threshold',\n settledCommissionIds: [],\n settledAdjustmentIds: [],\n };\n }\n\n const minted = await this.deps.payouts.create({\n // Payouts inherit the earner's tenancy so scheduled batch runs (no\n // active tenant context) still land in the right tenant.\n tenantId: earner.tenantId,\n earnerId: input.earnerId,\n currency: input.currency,\n periodStart: input.periodStart ?? null,\n periodEnd: input.periodEnd ?? null,\n payoutMethod: input.payoutMethod ?? earner.payoutMethod,\n status: 'pending',\n commissionTotalCents,\n adjustmentTotalCents,\n totalAmountCents: netTotalCents,\n idempotencyKey,\n });\n\n // Adopt the PERSISTED row for the idempotency key before claiming\n // anything: two workers racing past the earlier lookup both reach the\n // natural-key upsert, and the loser's in-memory instance carries an id\n // the database no longer holds. Claiming with that orphaned id would\n // stamp rows onto a payout that doesn't exist — so both workers\n // converge on whichever row actually won the key.\n const payout =\n (await this.deps.payouts.findByIdempotencyKey(idempotencyKey)) ?? minted;\n\n // Claim the gathered rows conditionally and reconcile the stored\n // totals from what was VERIFIABLY claimed — a row grabbed by another\n // batch between gather and claim is skipped, never double-claimed, and\n // never counted.\n const result = await this.claimAndReconcile(payout, input);\n return { ...result, created: true };\n }\n\n /**\n * Whether a payout's stored totals are reproducible from the rows\n * actually stamped with its id — the invariant an interrupted claim pass\n * breaks. Clean replays short-circuit on this; repair runs only when it\n * fails.\n */\n private async membershipConsistent(\n payout: CommissionPayout,\n ): Promise<boolean> {\n const payoutId = payout.id ?? '';\n const members = await this.deps.commissions.findByPayout(payoutId);\n const memberAdjustments =\n await this.deps.adjustments.findByPayout(payoutId);\n const commissionTotalCents = members.reduce(\n (sum, c) => sum + c.amountCents,\n 0,\n );\n const adjustmentTotalCents = memberAdjustments.reduce(\n (sum, a) => sum + a.amountCents,\n 0,\n );\n return (\n payout.commissionTotalCents === commissionTotalCents &&\n payout.adjustmentTotalCents === adjustmentTotalCents &&\n payout.totalAmountCents === commissionTotalCents + adjustmentTotalCents\n );\n }\n\n /**\n * Claim pass + totals reconciliation for a PENDING payout.\n *\n * The claim set is the union of rows already stamped with this payout\n * (an interrupted earlier pass) and the currently gathered eligible\n * rows. Claims go through the collections' conditional `claimForPayout`\n * (rows owned by another batch are skipped); totals are then recomputed\n * from the claimed rows and saved when they drift from what the payout\n * carries. In the pathological all-rows-raced-away case the payout keeps\n * zero totals and a note — auditable, never double-paid.\n *\n * The pass also derives the payout's single-source stamp\n * (`sourceKind`/`sourceId`) from the verified claimed membership — set\n * when every claimed commission and every claimed adjustment's parent\n * shares exactly one non-empty source, empty otherwise — which is what\n * the source-scoped history listing indexes on.\n *\n * A fresh status re-read gates the pass: only a payout that is STILL\n * pending claims rows. This narrows (but, like every claim here, does not\n * transactionally eliminate) the race against a concurrent lifecycle\n * transition of the same payout — replaying a batch while its payout is\n * being approved/rejected is an overlapping concurrent scope the caller\n * must serialize, same as the documented batch-scope contract.\n */\n private async claimAndReconcile(\n stalePayout: CommissionPayout,\n input: CreatePayoutBatchInput,\n ): Promise<Omit<CreatePayoutBatchResult, 'created'>> {\n const payoutId = stalePayout.id ?? '';\n\n // Authoritative re-read: claims may only land on a payout that is\n // still pending. (The stale instance is the pre-lookup snapshot.)\n const payout = (await this.deps.payouts.get({ id: payoutId })) ?? null;\n if (!payout?.isPending()) {\n return {\n payout: payout ?? stalePayout,\n settledCommissionIds: [],\n settledAdjustmentIds: [],\n };\n }\n\n const previouslyClaimed =\n await this.deps.commissions.findByPayout(payoutId);\n // Re-gather with the SAME scope as the initial pass so a repair never\n // pulls out-of-scope rows into a scoped batch.\n const gathered = await this.gatherBatchCommissions(input);\n const commissionIds = [\n ...new Set(\n [...previouslyClaimed, ...gathered]\n .map((c) => c.id)\n .filter((id): id is string => !!id),\n ),\n ];\n const claimedCommissions = await this.deps.commissions.claimForPayout(\n commissionIds,\n payoutId,\n );\n\n const previouslyClaimedAdjustments =\n await this.deps.adjustments.findByPayout(payoutId);\n const gatheredAdjustments = await this.findEligibleUnsettledAdjustments(\n input.earnerId,\n input.currency,\n CommissionPayoutService.adjustmentParentPredicate(input),\n );\n const adjustmentIds = [\n ...new Set(\n [...previouslyClaimedAdjustments, ...gatheredAdjustments]\n .map((a) => a.id)\n .filter((id): id is string => !!id),\n ),\n ];\n const claimedAdjustments = await this.deps.adjustments.claimForPayout(\n adjustmentIds,\n payoutId,\n );\n\n const commissionTotalCents = claimedCommissions.reduce(\n (sum, c) => sum + c.amountCents,\n 0,\n );\n const adjustmentTotalCents = claimedAdjustments.reduce(\n (sum, a) => sum + a.amountCents,\n 0,\n );\n const totalAmountCents = commissionTotalCents + adjustmentTotalCents;\n\n // Derive the single-source stamp from the VERIFIED claimed membership\n // (adjustments prove their source through their parent commission).\n const parentById = await this.loadAdjustmentParents(\n this.deps.commissions,\n claimedCommissions,\n claimedAdjustments,\n );\n const derivedSource = CommissionPayoutService.deriveMembershipSource(\n claimedCommissions,\n claimedAdjustments,\n parentById,\n );\n\n if (\n payout.commissionTotalCents !== commissionTotalCents ||\n payout.adjustmentTotalCents !== adjustmentTotalCents ||\n payout.totalAmountCents !== totalAmountCents ||\n payout.sourceKind !== derivedSource.sourceKind ||\n payout.sourceId !== derivedSource.sourceId\n ) {\n payout.commissionTotalCents = commissionTotalCents;\n payout.adjustmentTotalCents = adjustmentTotalCents;\n payout.totalAmountCents = totalAmountCents;\n payout.sourceKind = derivedSource.sourceKind;\n payout.sourceId = derivedSource.sourceId;\n if (claimedCommissions.length === 0 && claimedAdjustments.length === 0) {\n payout.notes =\n 'no rows claimed (raced by a concurrent batch); nothing will be paid';\n }\n await payout.save();\n }\n\n return {\n payout,\n settledCommissionIds: claimedCommissions\n .map((c) => c.id)\n .filter((id): id is string => !!id),\n settledAdjustmentIds: claimedAdjustments\n .map((a) => a.id)\n .filter((id): id is string => !!id),\n };\n }\n\n /**\n * Complete a payout: flip the batch's settled commissions\n * `payable → paid` FIRST, then `payout.complete(paymentReference)`\n * (requires status `processing`). Ordering matters for recoverability —\n * if a member save fails mid-loop the payout is still `processing`, so a\n * retry finishes the remaining members (already-paid ones are skipped)\n * and then finalizes; the terminal transition never strands `payable`\n * members behind a `completed` payout. Adjustments carry no status —\n * stamping `payoutId` at batch time already settled them.\n */\n async completePayout(\n payoutId: string,\n paymentReference: string,\n now: Date = new Date(),\n ): Promise<CommissionPayout> {\n const payout = await this.requirePayout(payoutId);\n if (!payout.isProcessing()) {\n throw new Error(\n `CommissionPayout ${payout.id ?? '<new>'}: cannot complete from status '${payout.status}'`,\n );\n }\n if (!paymentReference) {\n throw new Error(\n `CommissionPayout ${payout.id ?? '<new>'}: complete() requires a paymentReference`,\n );\n }\n\n const members = await this.deps.commissions.findByPayout(payoutId);\n for (const commission of members) {\n if (commission.isPayable()) {\n commission.markPaid(now);\n await commission.save();\n }\n }\n\n payout.complete(paymentReference, now);\n await payout.save();\n return payout;\n }\n\n /**\n * Fail a payout (`approved | processing → failed`). The batch's rows stay\n * stamped — after `resetFromFailed()` the SAME payout retries the SAME\n * rows; releasing the rows to a different batch would double-pay them if\n * the failed remittance later settled.\n */\n async failPayout(\n payoutId: string,\n reason: string,\n ): Promise<CommissionPayout> {\n const payout = await this.requirePayout(payoutId);\n payout.fail(reason);\n await payout.save();\n return payout;\n }\n\n /**\n * One page of the payout history belonging to ONE earning source,\n * newest first (`created_at DESC, id DESC` — deterministic across ties).\n *\n * Candidates come from the indexed single-source stamp\n * (`CommissionPayout.sourceKind`/`sourceId`, maintained from verified\n * claimed membership at batch/repair time), so database work is bounded\n * by the page size — a sparse source never forces a scan of the global\n * payout history. Each page is then RE-VERIFIED against its actual\n * membership in three batched queries (commissions, adjustments,\n * adjustment parents — no per-payout N+1): every member commission and\n * every adjustment's parent must carry exactly the requested source and\n * agree with the payout on earner/currency/tenant. Rows the stamp alone\n * cannot prove are excluded fail-closed and reported in `excluded`\n * (mixed-source membership, missing adjustment parents, memberless\n * artifacts, released/rejected batches).\n *\n * Adjustment-only payouts are first-class: a batch that settled only\n * CommissionAdjustment rows proves its source through each adjustment's\n * parent commission and lists normally.\n *\n * Payouts minted before the stamp existed carry an empty stamp and are\n * invisible here until backfilled — see {@link restampPayoutSource}.\n *\n * Offset pagination contract: `nextOffset` advances by the SCANNED count\n * (verified + excluded), so pages never skip rows; a page may hold fewer\n * than `limit` verified payouts. Newly minted payouts prepend to the\n * history between calls, as with any offset listing. Tenant interception\n * applies to every query (candidates, membership, parents), so a tenant\n * context sees only its own history.\n */\n async getSourcePayoutHistory(\n input: SourcePayoutHistoryInput,\n ): Promise<SourcePayoutHistoryPage> {\n if (!input.sourceKind || !input.sourceId) {\n throw new Error(\n 'CommissionPayoutService.getSourcePayoutHistory: sourceKind and sourceId are required',\n );\n }\n const limit = Math.max(1, Math.min(100, Math.trunc(input.limit ?? 25)));\n const offset = Math.max(0, Math.trunc(input.offset ?? 0));\n\n // limit + 1 probes for a further page without a COUNT query.\n const probed = await this.deps.payouts.findBySource(\n input.sourceKind,\n input.sourceId,\n { limit: limit + 1, offset },\n );\n const hasMore = probed.length > limit;\n const candidates = hasMore ? probed.slice(0, limit) : probed;\n\n const payoutIds = candidates\n .map((p) => p.id)\n .filter((id): id is string => !!id);\n const members = await this.deps.commissions.findByPayouts(payoutIds);\n const memberAdjustments =\n await this.deps.adjustments.findByPayouts(payoutIds);\n const parentById = await this.loadAdjustmentParents(\n this.deps.commissions,\n members,\n memberAdjustments,\n );\n // Cardinality guard (see countStampedRows): rows a tenant scope cannot\n // see must still fail the page's membership proof, not silently thin\n // it out.\n const db = this.resolveDatabase();\n const rawCommissionCounts = await CommissionPayoutService.countStampedRows(\n db,\n this.deps.commissions.tableName,\n payoutIds,\n );\n const rawAdjustmentCounts = await CommissionPayoutService.countStampedRows(\n db,\n this.deps.adjustments.tableName,\n payoutIds,\n );\n\n const membersByPayout = new Map<string, Commission[]>();\n for (const commission of members) {\n const bucket = membersByPayout.get(commission.payoutId);\n if (bucket) bucket.push(commission);\n else membersByPayout.set(commission.payoutId, [commission]);\n }\n const adjustmentsByPayout = new Map<string, CommissionAdjustment[]>();\n for (const adjustment of memberAdjustments) {\n const bucket = adjustmentsByPayout.get(adjustment.payoutId);\n if (bucket) bucket.push(adjustment);\n else adjustmentsByPayout.set(adjustment.payoutId, [adjustment]);\n }\n\n const page: SourcePayoutHistoryPage = {\n payouts: [],\n excluded: [],\n offset,\n limit,\n nextOffset: hasMore ? offset + candidates.length : null,\n };\n for (const payout of candidates) {\n const id = payout.id ?? '';\n const visibleMembers = membersByPayout.get(id) ?? [];\n const visibleAdjustments = adjustmentsByPayout.get(id) ?? [];\n const rawCommissionCount = rawCommissionCounts.get(id) ?? 0;\n const rawAdjustmentCount = rawAdjustmentCounts.get(id) ?? 0;\n if (\n rawCommissionCount !== visibleMembers.length ||\n rawAdjustmentCount !== visibleAdjustments.length\n ) {\n page.excluded.push({\n payoutId: id,\n reason: 'tenant_mismatch',\n detail:\n `payout has ${rawCommissionCount} commission and ${rawAdjustmentCount} adjustment rows stamped, ` +\n `but only ${visibleMembers.length} and ${visibleAdjustments.length} are visible in the current tenant scope`,\n });\n continue;\n }\n const verdict = CommissionPayoutService.verifySourceMembership({\n payout,\n commissions: visibleMembers,\n adjustments: visibleAdjustments,\n parentById,\n sourceKind: input.sourceKind,\n sourceId: input.sourceId,\n });\n if (verdict.ok) {\n page.payouts.push(payout);\n } else {\n page.excluded.push({\n payoutId: id,\n reason: verdict.reason,\n detail: verdict.detail,\n });\n }\n }\n return page;\n }\n\n /**\n * Backfill/repair the derived single-source stamp of ONE payout from its\n * actual membership — the documented migration path for payouts minted\n * before the stamp existed (they carry `''`/`''` and are invisible to\n * {@link getSourcePayoutHistory} until restamped). Safe on any status:\n * the stamp is derived data, and a payout whose membership is mixed or\n * unprovable derives back to the empty stamp.\n *\n * One-time migration loop: page through `payouts.list({})` and call this\n * per row (idempotent — an already-correct stamp saves nothing).\n */\n async restampPayoutSource(payoutId: string): Promise<{\n payout: CommissionPayout | null;\n sourceKind: string;\n sourceId: string;\n changed: boolean;\n }> {\n const payout = await this.deps.payouts.get({ id: payoutId });\n if (!payout) {\n return { payout: null, sourceKind: '', sourceId: '', changed: false };\n }\n const members = await this.deps.commissions.findByPayout(payoutId);\n const memberAdjustments =\n await this.deps.adjustments.findByPayout(payoutId);\n const parentById = await this.loadAdjustmentParents(\n this.deps.commissions,\n members,\n memberAdjustments,\n );\n const derived = CommissionPayoutService.deriveMembershipSource(\n members,\n memberAdjustments,\n parentById,\n );\n if (\n payout.sourceKind === derived.sourceKind &&\n payout.sourceId === derived.sourceId\n ) {\n return { payout, ...derived, changed: false };\n }\n payout.sourceKind = derived.sourceKind;\n payout.sourceId = derived.sourceId;\n await payout.save();\n return { payout, ...derived, changed: true };\n }\n\n /**\n * Atomically authorize ONE payout against ONE earning source and perform\n * a lifecycle transition — the multi-replica-safe alternative to loading\n * the payout, querying membership, and calling the model transitions\n * yourself (which leaves a TOCTOU window between authorization and\n * transition).\n *\n * Everything runs on the SAME transaction database: the payout row is\n * locked (PostgreSQL `SELECT … FOR UPDATE`, so concurrent calls across\n * app replicas serialize on the row; single-connection engines get\n * equivalent behavior from the transaction plus an in-process\n * per-database queue), membership is re-read and re-verified under the\n * lock, totals are recomputed under the lock, and the transition + every\n * member write commit or roll back together.\n *\n * Under the lock, in order:\n *\n * 1. **Hydrate** — load the locked payout. A missing/cross-tenant id\n * refuses `payout_not_found`.\n * 2. **Source authorization** — every member commission, and every\n * member adjustment through its parent commission, must carry exactly\n * the requested `(sourceKind, sourceId)` and agree with the payout on\n * earner/currency/tenant; anything unprovable refuses fail-closed\n * (`source_mismatch`, `adjustment_parent_missing`, mismatches,\n * `membership_empty` — note this means memberless raced-away batch\n * artifacts cannot receive any status-derived outcome through this\n * source-authorized door).\n * 3. **Status outcome** — after authorization, a payout already in the\n * action's target status returns `already_applied` WITHOUT writing\n * (terminal completion metadata — `paymentReference`, `providerRef`,\n * `paidAt` — is never overwritten by a replay). `expectedStatus`, when\n * given, must then match the locked status or the call refuses\n * `status_conflict`; the action's own from-status rule applies last.\n * Concurrent duplicate calls for actions that RETAIN membership\n * therefore resolve deterministically: one `transitioned`, the rest\n * `already_applied` (or `status_conflict` when they raced a DIFFERENT\n * action). `reject` releases the membership evidence, so a serialized\n * replay fails closed as `membership_empty` rather than exposing the\n * rejected status.\n * 4. **Totals recompute** — commission/adjustment/total amounts are\n * recomputed from the locked membership; drift refuses `totals_drift`\n * for the money-forward actions (`approve`, `mark_processing`,\n * `complete` — repair via a `createPayoutBatch` replay while the\n * payout is pending). The defensive actions (`fail`, `reject`)\n * proceed despite drift — rejecting a drifted batch IS the remedy. A\n * non-positive recomputed total refuses `approve` with\n * `non_positive_total`.\n * 5. **Apply** — `complete` flips the batch's payable commissions to\n * `paid` and completes the payout in the same transaction (no more\n * retryable-but-partial completion); `reject` RELEASES the batch's\n * membership (clears `payoutId` on every member commission and\n * adjustment, model-layer per row) so the rows settle through a\n * future batch, then marks the payout rejected — terminal.\n *\n * Replaying a `createPayoutBatch` for the same payout concurrently with\n * a transition is an overlapping concurrent scope (same contract as\n * overlapping batch scopes): the batch side re-checks pending before\n * claiming, which narrows but does not transactionally close that race —\n * serialize those two call sites per payout.\n */\n async transitionPayoutForSource(\n input: TransitionPayoutForSourceInput,\n ): Promise<TransitionPayoutForSourceResult> {\n const now = input.now ?? new Date();\n if (!input.sourceKind || !input.sourceId) {\n throw new Error(\n 'CommissionPayoutService.transitionPayoutForSource: sourceKind and sourceId are required',\n );\n }\n const targetStatus =\n CommissionPayoutService.TRANSITION_TARGET[input.action];\n if (!targetStatus) {\n throw new Error(\n `CommissionPayoutService.transitionPayoutForSource: unknown action '${String(input.action)}'`,\n );\n }\n if (input.action === 'complete' && !input.paymentReference) {\n throw new Error(\n \"CommissionPayoutService.transitionPayoutForSource: action 'complete' requires a paymentReference\",\n );\n }\n if (\n (input.action === 'fail' || input.action === 'reject') &&\n !input.reason\n ) {\n throw new Error(\n `CommissionPayoutService.transitionPayoutForSource: action '${input.action}' requires a reason`,\n );\n }\n // A malformed id can't match a payout, and would abort the whole query\n // as an invalid cast on native-uuid columns — refuse it as not-found.\n if (!CommissionPayoutService.UUID_RE.test(input.payoutId)) {\n return {\n outcome: 'refused',\n payout: null,\n refusal: {\n reason: 'payout_not_found',\n detail: `payout id '${input.payoutId}' is not a valid id`,\n },\n };\n }\n\n const db = this.resolveDatabase();\n const outcome = await this.runSerializedTransaction<TxTransitionOutcome>(\n db,\n async (txDb): Promise<TxTransitionOutcome> => {\n const tx = {\n payouts: await CommissionPayoutCollection.create(\n CommissionPayoutService.txOptions(txDb),\n ),\n commissions: await CommissionCollection.create(\n CommissionPayoutService.txOptions(txDb),\n ),\n adjustments: await CommissionAdjustmentCollection.create(\n CommissionPayoutService.txOptions(txDb),\n ),\n };\n\n // Row lock: PostgreSQL serializes concurrent transitions of one\n // payout across replicas here. Single-connection engines (SQLite,\n // DuckDB) don't support FOR UPDATE and don't need it — their whole\n // transaction is serialized by runSerializedTransaction.\n if (\n typeof (db as TransactionCapableDatabase).acquireSession ===\n 'function'\n ) {\n await txDb.query(\n `SELECT id FROM ${tx.payouts.tableName} WHERE id = $1 FOR UPDATE`,\n input.payoutId,\n );\n }\n\n const payout = await tx.payouts.get({ id: input.payoutId });\n if (!payout) {\n return CommissionPayoutService.txRefusal(\n null,\n 'payout_not_found',\n `payout '${input.payoutId}' not found`,\n );\n }\n const payoutId = payout.id ?? '';\n\n const members = await tx.commissions.findByPayout(payoutId);\n const memberAdjustments = await tx.adjustments.findByPayout(payoutId);\n\n // Cardinality guard: the reads above are tenant-scoped, so a\n // foreign tenant's row stamped onto this payout would be invisible\n // — and verification over the visible subset would authorize\n // incomplete membership. Compare against RAW counts (count-only,\n // no row data crosses the tenant boundary) and fail closed on any\n // excess.\n const rawCommissionCount =\n (\n await CommissionPayoutService.countStampedRows(\n txDb,\n tx.commissions.tableName,\n [input.payoutId],\n )\n ).get(input.payoutId) ?? 0;\n const rawAdjustmentCount =\n (\n await CommissionPayoutService.countStampedRows(\n txDb,\n tx.adjustments.tableName,\n [input.payoutId],\n )\n ).get(input.payoutId) ?? 0;\n if (\n rawCommissionCount !== members.length ||\n rawAdjustmentCount !== memberAdjustments.length\n ) {\n return CommissionPayoutService.txAuthorizationRefusal(\n payoutId,\n 'tenant_mismatch',\n );\n }\n\n const parentById = await this.loadAdjustmentParents(\n tx.commissions,\n members,\n memberAdjustments,\n );\n const verdict = CommissionPayoutService.verifySourceMembership({\n payout,\n commissions: members,\n adjustments: memberAdjustments,\n parentById,\n sourceKind: input.sourceKind,\n sourceId: input.sourceId,\n });\n if (!verdict.ok) {\n return CommissionPayoutService.txAuthorizationRefusal(\n payoutId,\n verdict.reason,\n );\n }\n\n if (payout.status === targetStatus) {\n return { outcome: 'already_applied' as const, payoutId };\n }\n if (input.expectedStatus && payout.status !== input.expectedStatus) {\n return CommissionPayoutService.txRefusal(\n payoutId,\n 'status_conflict',\n `expected status '${input.expectedStatus}' but payout is '${payout.status}'`,\n );\n }\n const legalFrom = CommissionPayoutService.TRANSITION_FROM[input.action];\n if (!legalFrom.includes(payout.status)) {\n return CommissionPayoutService.txRefusal(\n payoutId,\n 'status_conflict',\n `cannot ${input.action} from status '${payout.status}'`,\n );\n }\n\n const commissionTotalCents = members.reduce(\n (sum, c) => sum + c.amountCents,\n 0,\n );\n const adjustmentTotalCents = memberAdjustments.reduce(\n (sum, a) => sum + a.amountCents,\n 0,\n );\n const totalAmountCents = commissionTotalCents + adjustmentTotalCents;\n const drifted =\n payout.commissionTotalCents !== commissionTotalCents ||\n payout.adjustmentTotalCents !== adjustmentTotalCents ||\n payout.totalAmountCents !== totalAmountCents;\n const moneyForward =\n input.action === 'approve' ||\n input.action === 'mark_processing' ||\n input.action === 'complete';\n if (drifted && moneyForward) {\n return CommissionPayoutService.txRefusal(\n payoutId,\n 'totals_drift',\n `persisted totals (commission=${payout.commissionTotalCents} adjustment=${payout.adjustmentTotalCents} total=${payout.totalAmountCents}) ` +\n `do not match membership (commission=${commissionTotalCents} adjustment=${adjustmentTotalCents} total=${totalAmountCents}) — ` +\n 'repair via a createPayoutBatch replay while pending',\n );\n }\n if (input.action === 'approve' && payout.totalAmountCents <= 0) {\n return CommissionPayoutService.txRefusal(\n payoutId,\n 'non_positive_total',\n `cannot approve a batch with non-positive total (${payout.totalAmountCents} cents)`,\n );\n }\n\n const releasedCommissionIds: string[] = [];\n const releasedAdjustmentIds: string[] = [];\n switch (input.action) {\n case 'approve':\n payout.approve();\n break;\n case 'mark_processing':\n payout.markProcessing();\n break;\n case 'fail':\n payout.fail(input.reason ?? '');\n break;\n case 'reject': {\n // Release the membership FIRST (model-layer per row — tenancy-\n // and dialect-safe), then the terminal decline; the transaction\n // makes the pair atomic. Stranded stamped rows would otherwise\n // be unsettleable forever.\n for (const commission of members) {\n commission.payoutId = '';\n await commission.save();\n if (commission.id) releasedCommissionIds.push(commission.id);\n }\n for (const adjustment of memberAdjustments) {\n adjustment.payoutId = '';\n await adjustment.save();\n if (adjustment.id) releasedAdjustmentIds.push(adjustment.id);\n }\n payout.reject(input.reason ?? '');\n break;\n }\n case 'complete': {\n // Members flip to paid in the SAME transaction as the terminal\n // payout write — a mid-loop failure rolls everything back\n // instead of leaving a partially-paid batch.\n for (const commission of members) {\n if (commission.isPayable()) {\n commission.markPaid(now);\n await commission.save();\n }\n }\n payout.complete(input.paymentReference ?? '', now);\n break;\n }\n }\n await payout.save();\n return {\n outcome: 'transitioned' as const,\n payoutId,\n releasedCommissionIds,\n releasedAdjustmentIds,\n };\n },\n );\n\n // Rehydrate OUTSIDE the transaction so the returned instance is bound\n // to the service's own connection, not the released transaction.\n const payout =\n outcome.payoutId && !outcome.authorizationFailed\n ? await this.deps.payouts.get({ id: outcome.payoutId })\n : null;\n const result: TransitionPayoutForSourceResult = {\n outcome: outcome.outcome,\n payout,\n };\n if (outcome.refusal) result.refusal = outcome.refusal;\n if (outcome.releasedCommissionIds?.length) {\n result.releasedCommissionIds = outcome.releasedCommissionIds;\n }\n if (outcome.releasedAdjustmentIds?.length) {\n result.releasedAdjustmentIds = outcome.releasedAdjustmentIds;\n }\n return result;\n }\n\n // -------- Source membership verification internals --------\n\n /** Target status per action — also the `already_applied` echo test. */\n private static readonly TRANSITION_TARGET: Record<\n PayoutSourceTransitionAction,\n CommissionPayoutStatus\n > = {\n approve: 'approved',\n mark_processing: 'processing',\n complete: 'completed',\n fail: 'failed',\n reject: 'rejected',\n };\n\n /** Legal from-statuses per action (mirrors the model transition guards). */\n private static readonly TRANSITION_FROM: Record<\n PayoutSourceTransitionAction,\n CommissionPayoutStatus[]\n > = {\n approve: ['pending'],\n mark_processing: ['approved'],\n complete: ['processing'],\n fail: ['approved', 'processing'],\n reject: ['pending', 'approved'],\n };\n\n private static txOptions(txDb: DatabaseInterface): SmrtClassOptions {\n return {\n db: txDb,\n // The transaction database is the SAME initialized database on a\n // pinned connection — skip system-table bootstrap and runtime\n // service setup (signals/AI) for these short-lived bindings.\n _reuseInitializedDb: true,\n _deferRuntimeInitialization: true,\n };\n }\n\n private static txRefusal(\n payoutId: string | null,\n reason: PayoutTransitionRefusalReason,\n detail: string,\n ): {\n outcome: 'refused';\n payoutId: string | null;\n refusal: { reason: PayoutTransitionRefusalReason; detail: string };\n } {\n return { outcome: 'refused', payoutId, refusal: { reason, detail } };\n }\n\n /**\n * A membership refusal means the requested source was never authorized.\n * Keep the typed reason for callers while withholding both the payout and\n * member-specific detail (actual source, ids, account, tenant, or money).\n */\n private static txAuthorizationRefusal(\n payoutId: string,\n reason: PayoutMembershipRefusalReason,\n ): TxTransitionOutcome {\n return {\n outcome: 'refused',\n payoutId,\n authorizationFailed: true,\n refusal: {\n reason,\n detail: 'requested source is not authorized for this payout membership',\n },\n };\n }\n\n /**\n * RAW stamped-row counts per payout id — deliberately UNSCOPED\n * (count-only, reviewed): tenant-scoped reads cannot see a foreign\n * tenant's row stamped onto a payout, so membership verification that\n * trusted only the visible subset would authorize (or list) incomplete\n * membership. No row data crosses the tenant boundary — only per-payout\n * counts, compared against the visible membership; any excess fails\n * closed as `tenant_mismatch`. Ids must be UUID-shaped (callers pass\n * validated payout ids), and the payout-id predicate never touches the\n * empty-FK encoding, so the query is dialect-safe.\n */\n private static async countStampedRows(\n db: DatabaseInterface,\n table: string,\n payoutIds: string[],\n ): Promise<Map<string, number>> {\n const counts = new Map<string, number>();\n const ids = payoutIds.filter((id) =>\n CommissionPayoutService.UUID_RE.test(id),\n );\n if (ids.length === 0) return counts;\n const placeholders = ids.map((_, i) => `$${i + 1}`).join(', ');\n const res = await db.query(\n `SELECT payout_id, COUNT(*) AS row_count FROM ${table} WHERE payout_id IN (${placeholders}) GROUP BY payout_id`,\n ...ids,\n );\n const rows = Array.isArray(res)\n ? (res as Record<string, unknown>[])\n : ((res as { rows?: Record<string, unknown>[] }).rows ?? []);\n for (const row of rows) {\n counts.set(String(row.payout_id), Number(row.row_count));\n }\n return counts;\n }\n\n /**\n * Parents of the given adjustments, keyed by commission id — member\n * commissions are reused, only the rest are fetched (one `IN` query).\n */\n private async loadAdjustmentParents(\n commissions: CommissionCollection,\n memberCommissions: Commission[],\n adjustments: CommissionAdjustment[],\n ): Promise<Map<string, Commission>> {\n const parentById = new Map<string, Commission>();\n for (const commission of memberCommissions) {\n if (commission.id) parentById.set(commission.id, commission);\n }\n const missingIds = [\n ...new Set(\n adjustments\n .map((a) => a.commissionId)\n .filter((id) => !!id && !parentById.has(id)),\n ),\n ];\n if (missingIds.length > 0) {\n const fetched = await commissions.listByIds(missingIds);\n for (const parent of fetched) {\n if (parent.id) parentById.set(parent.id, parent);\n }\n }\n return parentById;\n }\n\n /**\n * The single `(sourceKind, sourceId)` a payout's membership provably\n * belongs to — or the empty stamp when membership is empty, any member's\n * source is missing, an adjustment parent is unloadable, or more than\n * one source appears.\n */\n private static deriveMembershipSource(\n memberCommissions: Commission[],\n adjustments: CommissionAdjustment[],\n parentById: Map<string, Commission>,\n ): { sourceKind: string; sourceId: string } {\n const empty = { sourceKind: '', sourceId: '' };\n if (memberCommissions.length === 0 && adjustments.length === 0) {\n return empty;\n }\n const sources = new Map<string, { sourceKind: string; sourceId: string }>();\n const add = (sourceKind: string, sourceId: string) => {\n sources.set(`${sourceKind.length}:${sourceKind}:${sourceId}`, {\n sourceKind,\n sourceId,\n });\n };\n for (const commission of memberCommissions) {\n if (!commission.sourceKind || !commission.sourceId) return empty;\n add(commission.sourceKind, commission.sourceId);\n }\n for (const adjustment of adjustments) {\n const parent = parentById.get(adjustment.commissionId);\n if (!parent?.sourceKind || !parent.sourceId) return empty;\n add(parent.sourceKind, parent.sourceId);\n }\n if (sources.size !== 1) return empty;\n const [only] = sources.values();\n return only;\n }\n\n /**\n * Prove that EVERY member of a payout belongs to the requested source\n * and agrees with the payout on earner/currency/tenant. Adjustments\n * prove their source through their parent commission. Fail-closed: the\n * first unprovable member decides the verdict.\n */\n private static verifySourceMembership(input: {\n payout: CommissionPayout;\n commissions: Commission[];\n adjustments: CommissionAdjustment[];\n parentById: Map<string, Commission>;\n sourceKind: string;\n sourceId: string;\n }):\n | { ok: true }\n | { ok: false; reason: PayoutMembershipRefusalReason; detail: string } {\n const { payout } = input;\n // '' and NULL both mean \"no tenant\" depending on dialect — normalize.\n const tenantOf = (value: string | null | undefined) => value || null;\n if (input.commissions.length === 0 && input.adjustments.length === 0) {\n return {\n ok: false,\n reason: 'membership_empty',\n detail: 'no commissions or adjustments are stamped with this payout',\n };\n }\n for (const commission of input.commissions) {\n if (commission.earnerId !== payout.earnerId) {\n return {\n ok: false,\n reason: 'earner_mismatch',\n detail: `commission ${commission.id} belongs to earner '${commission.earnerId}', payout to '${payout.earnerId}'`,\n };\n }\n if (commission.currency !== payout.currency) {\n return {\n ok: false,\n reason: 'currency_mismatch',\n detail: `commission ${commission.id} is ${commission.currency}, payout is ${payout.currency}`,\n };\n }\n if (tenantOf(commission.tenantId) !== tenantOf(payout.tenantId)) {\n return {\n ok: false,\n reason: 'tenant_mismatch',\n detail: `commission ${commission.id} and the payout disagree on tenant`,\n };\n }\n if (\n commission.sourceKind !== input.sourceKind ||\n commission.sourceId !== input.sourceId\n ) {\n return {\n ok: false,\n reason: 'source_mismatch',\n detail: `commission ${commission.id} belongs to source '${commission.sourceKind}:${commission.sourceId}', not '${input.sourceKind}:${input.sourceId}'`,\n };\n }\n }\n for (const adjustment of input.adjustments) {\n if (adjustment.earnerId !== payout.earnerId) {\n return {\n ok: false,\n reason: 'earner_mismatch',\n detail: `adjustment ${adjustment.id} belongs to earner '${adjustment.earnerId}', payout to '${payout.earnerId}'`,\n };\n }\n if (adjustment.currency !== payout.currency) {\n return {\n ok: false,\n reason: 'currency_mismatch',\n detail: `adjustment ${adjustment.id} is ${adjustment.currency}, payout is ${payout.currency}`,\n };\n }\n if (tenantOf(adjustment.tenantId) !== tenantOf(payout.tenantId)) {\n return {\n ok: false,\n reason: 'tenant_mismatch',\n detail: `adjustment ${adjustment.id} and the payout disagree on tenant`,\n };\n }\n const parent = input.parentById.get(adjustment.commissionId);\n if (!parent) {\n return {\n ok: false,\n reason: 'adjustment_parent_missing',\n detail: `adjustment ${adjustment.id} parent commission '${adjustment.commissionId}' cannot be loaded to prove source ownership`,\n };\n }\n // The PARENT must agree with the payout on account axes too — an\n // adjustment's earner/currency/tenant are denormalized by\n // convention, not enforced, so a coherent-looking adjustment can\n // still hang off another account's commission.\n if (parent.earnerId !== payout.earnerId) {\n return {\n ok: false,\n reason: 'earner_mismatch',\n detail: `adjustment ${adjustment.id} parent commission belongs to earner '${parent.earnerId}', payout to '${payout.earnerId}'`,\n };\n }\n if (parent.currency !== payout.currency) {\n return {\n ok: false,\n reason: 'currency_mismatch',\n detail: `adjustment ${adjustment.id} parent commission is ${parent.currency}, payout is ${payout.currency}`,\n };\n }\n if (tenantOf(parent.tenantId) !== tenantOf(payout.tenantId)) {\n return {\n ok: false,\n reason: 'tenant_mismatch',\n detail: `adjustment ${adjustment.id} parent commission and the payout disagree on tenant`,\n };\n }\n if (\n parent.sourceKind !== input.sourceKind ||\n parent.sourceId !== input.sourceId\n ) {\n return {\n ok: false,\n reason: 'source_mismatch',\n detail: `adjustment ${adjustment.id} parent commission belongs to source '${parent.sourceKind}:${parent.sourceId}', not '${input.sourceKind}:${input.sourceId}'`,\n };\n }\n }\n return { ok: true };\n }\n\n /** The initialized database behind the payout collection. */\n private resolveDatabase(): DatabaseInterface {\n const db = this.deps.payouts.options.db;\n if (!db || typeof db === 'string' || !('query' in db)) {\n throw new Error(\n 'CommissionPayoutService: the payout collection has no initialized database',\n );\n }\n return db as DatabaseInterface;\n }\n\n /**\n * Run `fn` inside a database transaction. PostgreSQL transactions get\n * their own pooled connection, so they run concurrently (the FOR UPDATE\n * row lock inside `fn` provides the per-payout serialization, replica-\n * safe). Single-connection engines (SQLite, DuckDB, JSON) multiplex\n * every transaction over one connection where concurrent BEGIN/COMMIT\n * pairs would interleave — their transitions chain per database\n * instance, giving equivalent serialized behavior in-process. An engine\n * with no transaction support at all still gets the serialized chain.\n */\n private async runSerializedTransaction<T>(\n db: DatabaseInterface,\n fn: (txDb: DatabaseInterface) => Promise<T>,\n ): Promise<T> {\n const capable = db as TransactionCapableDatabase;\n const runTx = () =>\n typeof capable.transaction === 'function'\n ? capable.transaction(fn)\n : fn(db);\n if (typeof capable.acquireSession === 'function') {\n return await runTx();\n }\n const previous =\n singleConnectionTransitionTails.get(db) ?? Promise.resolve();\n // Chain regardless of the predecessor's outcome — a failed transition\n // must not poison the queue behind it.\n const turn = previous.then(runTx, runTx);\n singleConnectionTransitionTails.set(\n db,\n turn.then(\n () => undefined,\n () => undefined,\n ),\n );\n return await turn;\n }\n\n /**\n * Unsettled adjustments for the earner/currency whose parent commission\n * is earned/approved/payable/paid — the same eligibility rule the balance\n * service applies, so batches settle exactly what balances report.\n *\n * When `parentPredicate` is given (a scoped batch), an adjustment is also\n * kept only when its parent commission satisfies the predicate — so a\n * source-scoped or explicit-id batch settles only its own adjustments.\n */\n private async findEligibleUnsettledAdjustments(\n earnerId: string,\n currency: string,\n parentPredicate?: (parent: Commission) => boolean,\n ) {\n const unsettled = await this.deps.adjustments.findUnsettledByEarner(\n earnerId,\n currency,\n );\n if (unsettled.length === 0) return unsettled;\n\n const parentIds = [\n ...new Set(unsettled.map((a) => a.commissionId).filter(Boolean)),\n ];\n const parents = await this.deps.commissions.listByIds(parentIds);\n const parentById = new Map<string, Commission>();\n for (const parent of parents) {\n if (parent.id) parentById.set(parent.id, parent);\n }\n const settleable =\n ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES as readonly CommissionStatus[];\n return unsettled.filter((adjustment) => {\n const parent = parentById.get(adjustment.commissionId);\n if (parent === undefined) return false;\n if (!settleable.includes(parent.status)) return false;\n if (parentPredicate && !parentPredicate(parent)) return false;\n return true;\n });\n }\n\n /**\n * The payable, unsettled commissions this batch would settle, honoring\n * the input scope: an explicit `commissionIds` set (each validated\n * payable + unsettled + belonging to this earner/currency), a single\n * `(sourceKind, sourceId)`, or — unscoped — the whole earner/currency.\n */\n private async gatherBatchCommissions(\n input: CreatePayoutBatchInput,\n ): Promise<Commission[]> {\n // A PRESENT `commissionIds` (even `[]`) is an explicit scope — an empty\n // list settles nothing, it must never fall through to the earner-wide\n // gather.\n if (input.commissionIds !== undefined) {\n // Drop empty / non-UUID ids before querying: `id` is a native `uuid`\n // column on Postgres/DuckDB, so a malformed value would abort the\n // whole `listByIds` query there (SQLite silently misses it). Every\n // real smrt id is a UUID, so a non-UUID id can't match a commission\n // anyway — filtering it here is exactly the documented \"ineligible\n // ids are ignored\" behavior, and stops one bad id failing the batch.\n const validIds = input.commissionIds.filter((id) =>\n CommissionPayoutService.UUID_RE.test(id),\n );\n if (validIds.length === 0) return [];\n const rows = await this.deps.commissions.listByIds(validIds);\n return rows.filter(\n (c) =>\n c.earnerId === input.earnerId &&\n c.currency === input.currency &&\n c.status === 'payable' &&\n !c.payoutId,\n );\n }\n const scope =\n input.sourceKind && input.sourceId\n ? { sourceKind: input.sourceKind, sourceId: input.sourceId }\n : undefined;\n return await this.deps.commissions.findPayableUnsettled(\n input.earnerId,\n input.currency,\n scope,\n );\n }\n\n /**\n * Parent-commission predicate that narrows eligible adjustments to the\n * batch scope: explicit-id batches keep adjustments whose parent is in\n * the requested id set (even a now-paid parent — a clawback is still\n * owed); source-scoped batches keep adjustments whose parent shares the\n * source; unscoped batches keep all (predicate `undefined`).\n */\n private static adjustmentParentPredicate(\n input: CreatePayoutBatchInput,\n ): ((parent: Commission) => boolean) | undefined {\n // A present `commissionIds` (even `[]`) scopes adjustments to that set;\n // an empty set matches nothing.\n if (input.commissionIds !== undefined) {\n const ids = new Set(input.commissionIds);\n return (parent) => !!parent.id && ids.has(parent.id);\n }\n if (input.sourceKind && input.sourceId) {\n const { sourceKind, sourceId } = input;\n return (parent) =>\n parent.sourceKind === sourceKind && parent.sourceId === sourceId;\n }\n return undefined;\n }\n\n /**\n * Validate the batch scope: `sourceKind`/`sourceId` are all-or-nothing\n * and mutually exclusive with `commissionIds`; an explicit `commissionIds`\n * batch requires its own `idempotencyKey` (no natural default exists).\n */\n private static assertScope(input: CreatePayoutBatchInput): void {\n // A PRESENT `commissionIds` is an explicit scope regardless of length —\n // an empty list is a valid \"settle nothing\" request, not an unscoped\n // batch. Guarding on presence (not length) is what makes a\n // dynamically-computed `[]` fail closed instead of settling the whole\n // earner.\n const hasIds = input.commissionIds !== undefined;\n // A source scope is INTENDED when either property is present. Detecting\n // presence (not truthiness) is what stops `{ sourceKind: '', sourceId:\n // '' }` from failing open into an earner-wide settlement — a\n // present-but-empty (or half-set) source is malformed, not \"unscoped\".\n const hasSource =\n input.sourceKind !== undefined || input.sourceId !== undefined;\n if (hasSource && (!input.sourceKind || !input.sourceId)) {\n throw new Error(\n 'CommissionPayoutService.createPayoutBatch: sourceKind and sourceId must both be set and non-empty to scope by source',\n );\n }\n if (hasIds && hasSource) {\n throw new Error(\n 'CommissionPayoutService.createPayoutBatch: commissionIds and sourceKind/sourceId are mutually exclusive',\n );\n }\n if (hasIds && !input.idempotencyKey) {\n throw new Error(\n 'CommissionPayoutService.createPayoutBatch: an explicit commissionIds batch requires an idempotencyKey',\n );\n }\n }\n\n private async requirePayout(payoutId: string): Promise<CommissionPayout> {\n const payout = await this.deps.payouts.get({ id: payoutId });\n if (!payout) {\n throw new Error(\n `CommissionPayoutService: payout '${payoutId}' not found`,\n );\n }\n return payout;\n }\n\n /**\n * `${earnerId}:${currency}:${YYYY-MM-DD}` — or, when scoped by source, a\n * key that folds the source in so a per-network batch and the earner-wide\n * batch on the same day get distinct keys. `sourceKind`/`sourceId` are\n * unconstrained generic strings, so each is LENGTH-PREFIXED (`len:value`)\n * to keep the encoding unambiguous: a literal `:` inside a source string\n * can't make two different `(sourceKind, sourceId)` pairs collide (e.g.\n * `('a:b','c')` → `…:src:3:a:b:1:c:…` vs `('a','b:c')` → `…:src:1:a:3:b:c:…`).\n * See the input doc.\n */\n private static defaultIdempotencyKey(\n earnerId: string,\n currency: string,\n periodEnd: Date,\n scope?: { sourceKind: string; sourceId: string },\n ): string {\n const date = periodEnd.toISOString().slice(0, 10);\n if (scope) {\n const enc = (s: string) => `${s.length}:${s}`;\n return `${earnerId}:${currency}:src:${enc(scope.sourceKind)}:${enc(scope.sourceId)}:${date}`;\n }\n return `${earnerId}:${currency}:${date}`;\n }\n}\n\nexport default CommissionPayoutService;\n","/**\n * CommissionSettlementService — advances Commissions along the strict\n * `pending → earned → approved → payable` chain (the final `payable → paid`\n * step belongs to `CommissionPayoutService.completePayout`, which flips a\n * batch's rows when the money actually moves).\n *\n * All methods persist the rows they advance (transition method + save per\n * row) and return the updated instances.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport type { Commission } from '../models/Commission.js';\n\nexport class CommissionSettlementService {\n constructor(private readonly commissions: CommissionCollection) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CommissionSettlementService> {\n return new CommissionSettlementService(\n await CommissionCollection.create(classOptions),\n );\n }\n\n /**\n * Sweep the clearing window: every `pending` commission whose\n * `clearingEndsAt` is `<= now` — or whose `clearingEndsAt` is `null`\n * (null means NO clearing window applies, so the row is immediately\n * sweepable) — transitions to `earned` and is saved.\n *\n * @returns The commissions that were marked earned by this sweep.\n */\n async sweepClearing(now: Date = new Date()): Promise<Commission[]> {\n const pending = await this.commissions.findByStatus('pending');\n const swept: Commission[] = [];\n for (const commission of pending) {\n const clearingEndsAt = commission.clearingEndsAt;\n if (clearingEndsAt !== null && clearingEndsAt.getTime() > now.getTime()) {\n continue; // still clearing\n }\n commission.markEarned(now);\n await commission.save();\n swept.push(commission);\n }\n return swept;\n }\n\n /**\n * Approve `earned` commissions by id (`earned → approved`). Strict: a\n * missing id or a commission in any other status throws — the caller\n * names exact rows, so a mismatch is a bug worth surfacing, not skipping.\n */\n async approveCommissions(\n ids: string[],\n now: Date = new Date(),\n ): Promise<Commission[]> {\n return await this.transitionByIds(ids, (commission) => {\n commission.approve(now);\n });\n }\n\n /**\n * Release `approved` commissions to `payable` by id. Strict — see\n * {@link approveCommissions}.\n */\n async markPayable(\n ids: string[],\n now: Date = new Date(),\n ): Promise<Commission[]> {\n return await this.transitionByIds(ids, (commission) => {\n commission.markPayable(now);\n });\n }\n\n /**\n * Convenience chain: advance each commission from wherever it currently\n * sits up to `payable` (`pending → earned → approved → payable`), saving\n * after EACH step — the save-time guard only admits single-step edges, so\n * every intermediate state is persisted (each with its timestamp). Rows\n * already `payable` or `paid` are returned untouched (idempotent). Note\n * this deliberately bypasses the clearing window — it's the \"operator\n * says pay these now\" path.\n */\n async settleUpToPayable(\n ids: string[],\n now: Date = new Date(),\n ): Promise<Commission[]> {\n const updated: Commission[] = [];\n for (const id of ids) {\n const commission = await this.requireCommission(id);\n if (commission.isPending()) {\n commission.markEarned(now);\n await commission.save();\n }\n if (commission.isEarned()) {\n commission.approve(now);\n await commission.save();\n }\n if (commission.isApproved()) {\n commission.markPayable(now);\n await commission.save();\n }\n updated.push(commission);\n }\n return updated;\n }\n\n private async transitionByIds(\n ids: string[],\n transition: (commission: Commission) => void,\n ): Promise<Commission[]> {\n const updated: Commission[] = [];\n for (const id of ids) {\n const commission = await this.requireCommission(id);\n const statusBefore = commission.status;\n transition(commission);\n if (commission.status !== statusBefore) {\n await commission.save();\n }\n updated.push(commission);\n }\n return updated;\n }\n\n private async requireCommission(id: string): Promise<Commission> {\n const commission = await this.commissions.get({ id });\n if (!commission) {\n throw new Error(\n `CommissionSettlementService: commission '${id}' not found`,\n );\n }\n return commission;\n }\n}\n\nexport default CommissionSettlementService;\n","/**\n * EarnerAttributionService — registration and indexed resolution of\n * {@link EarnerSourceAttribution} mappings.\n *\n * High-volume ingestion (e.g. billing events that must credit an earner per\n * ad-network property) resolves earners here: one indexed attribution query\n * plus one earner load per call, bounded by the REQUESTED keys — never a\n * scan of all active earners. Resolution is fail-closed: a key that cannot\n * be proven to map to exactly one active mapping and one active earner\n * resolves to nothing, with a typed reason.\n *\n * Registration is the idempotent write path (and the documented\n * metadata-migration backfill primitive — see the model doc): re-registering\n * a key updates the existing mapping in place and reports the displaced\n * earner instead of silently duplicating.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { EarnerCollection } from '../collections/EarnerCollection.js';\nimport { EarnerSourceAttributionCollection } from '../collections/EarnerSourceAttributionCollection.js';\nimport type { Earner } from '../models/Earner.js';\nimport type { EarnerSourceAttribution } from '../models/EarnerSourceAttribution.js';\nimport type { EarnerSourceAttributionStatus } from '../types.js';\n\n/** Collaborators for {@link EarnerAttributionService}. */\nexport interface EarnerAttributionServiceDeps {\n earners: EarnerCollection;\n attributions: EarnerSourceAttributionCollection;\n}\n\n/**\n * Why a source key did not resolve to an active earner. Every reason is\n * fail-closed — the key resolves to nothing rather than to a guess.\n *\n * - `no_mapping` — no attribution row for the key.\n * - `mapping_inactive` — row(s) exist but none is `active`.\n * - `ambiguous_mapping` — more than one ACTIVE row for the key (resolving\n * without tenant context across tenants, or duplicate global rows minted\n * outside the model layer — the unique index treats NULL tenants as\n * distinct). Repair by deactivating/deleting the extras, then re-resolve.\n * - `earner_not_found` — the mapping's earner does not exist or is not\n * visible in the current tenant scope.\n * - `earner_not_active` — the earner exists but is `pending`/`suspended`.\n */\nexport type EarnerSourceResolutionRefusal =\n | 'no_mapping'\n | 'mapping_inactive'\n | 'ambiguous_mapping'\n | 'earner_not_found'\n | 'earner_not_active';\n\n/** Result of {@link EarnerAttributionService.resolveActiveEarnerBySource}. */\nexport interface ResolveActiveEarnerResult {\n /** The resolved ACTIVE earner, or `null` with a {@link reason}. */\n earner: Earner | null;\n /** The active mapping that resolved, when {@link earner} is set. */\n attribution: EarnerSourceAttribution | null;\n /** Why resolution failed — set exactly when {@link earner} is `null`. */\n reason?: EarnerSourceResolutionRefusal;\n}\n\n/** Result of {@link EarnerAttributionService.resolveActiveEarnersBySources}. */\nexport interface ResolveActiveEarnersBySourcesResult {\n /** Requested sourceId → resolved active earner (resolved keys only). */\n earnersBySourceId: Map<string, Earner>;\n /** Requested sourceId → the active mapping that resolved it. */\n attributionsBySourceId: Map<string, EarnerSourceAttribution>;\n /** Keys that did not resolve, each with its fail-closed reason. */\n unresolved: { sourceId: string; reason: EarnerSourceResolutionRefusal }[];\n}\n\n/** Input for {@link EarnerAttributionService.registerAttribution}. */\nexport interface RegisterAttributionInput {\n earnerId: string;\n sourceKind: string;\n sourceId: string;\n /**\n * Tenant for the mapping. Defaults to the earner's own `tenantId` so\n * registrations from operator/scheduled contexts land in the earner's\n * tenant. When given explicitly it MUST equal the earner's tenant — a\n * mapping lives in its earner's tenant (model save guard).\n */\n tenantId?: string | null;\n status?: EarnerSourceAttributionStatus;\n metadata?: string;\n}\n\n/** Result of {@link EarnerAttributionService.registerAttribution}. */\nexport interface RegisterAttributionResult {\n attribution: EarnerSourceAttribution;\n /** `true` when THIS call created the mapping (vs updating in place). */\n created: boolean;\n /** The earner the key previously mapped to, when re-pointed. */\n previousEarnerId: string | null;\n}\n\nexport class EarnerAttributionService {\n constructor(private readonly deps: EarnerAttributionServiceDeps) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<EarnerAttributionService> {\n return new EarnerAttributionService({\n earners: await EarnerCollection.create(classOptions),\n attributions:\n await EarnerSourceAttributionCollection.create(classOptions),\n });\n }\n\n /**\n * Register (or re-point) the mapping for one external key WITHIN ONE\n * TENANT. The registration's target tenant is `input.tenantId` when\n * given, else the earner's own `tenantId` — and the update-vs-create\n * decision considers only that tenant's rows, so an operator/scheduled\n * registration (no tenant context) can never re-point or re-tenant\n * ANOTHER tenant's mapping for the same key, and legitimate per-tenant\n * mappings of one key are never mistaken for duplicates. A mapping's\n * tenant is fixed at registration (re-tenanting is not supported).\n *\n * Idempotent: an existing target-tenant mapping is updated in place —\n * never duplicated — and the displaced earner is reported. Throws when\n * the earner does not exist, when the key is incomplete, or when the key\n * already holds MULTIPLE rows within the target tenant (duplicates\n * minted outside the model layer — repair before registering again).\n *\n * Two concurrent first registrations of the same key converge through the\n * natural-key upsert (the adapters' null-aware upsert covers NULL-tenant\n * keys too — last write wins). Should a duplicate global row still arrive\n * outside the model layer, the lookups fail closed on it until repaired.\n */\n async registerAttribution(\n input: RegisterAttributionInput,\n ): Promise<RegisterAttributionResult> {\n if (!input.earnerId || !input.sourceKind || !input.sourceId) {\n throw new Error(\n 'EarnerAttributionService.registerAttribution: earnerId, sourceKind, and sourceId are required',\n );\n }\n const earner = await this.deps.earners.get({ id: input.earnerId });\n if (!earner) {\n throw new Error(\n `EarnerAttributionService: earner '${input.earnerId}' not found`,\n );\n }\n\n // '' and NULL both mean \"no tenant\" depending on dialect — normalize.\n const tenantOf = (value: string | null | undefined) => value || null;\n const targetTenant = tenantOf(\n input.tenantId !== undefined ? input.tenantId : earner.tenantId,\n );\n // Only the TARGET tenant's rows participate in the update-vs-create\n // decision. In tenant context the interceptor already narrows the\n // query; without context (operator/scheduled) this filter is what\n // keeps another tenant's mapping for the same key untouchable.\n const scoped = (\n await this.deps.attributions.findBySource(\n input.sourceKind,\n input.sourceId,\n )\n ).filter((row) => tenantOf(row.tenantId) === targetTenant);\n // Ambiguity means more than one ACTIVE row — inactive duplicates are\n // exactly what the documented repair (deactivation) produces, and they\n // must not keep blocking registration afterwards.\n const active = scoped.filter((row) => row.isActive());\n if (active.length > 1) {\n throw new Error(\n `EarnerAttributionService: source '${input.sourceKind}:${input.sourceId}' ` +\n `holds ${active.length} active mappings in the target tenant scope — deactivate the duplicates before registering`,\n );\n }\n\n // Prefer the single active row; with only inactive rows (a repaired\n // duplicate set), reuse the OLDEST deterministically instead of\n // inserting a sibling.\n const current = active[0] ?? scoped[0];\n if (current) {\n const previousEarnerId =\n current.earnerId !== input.earnerId ? current.earnerId : null;\n current.earnerId = input.earnerId;\n current.status = input.status ?? 'active';\n if (input.metadata !== undefined) current.metadata = input.metadata;\n await current.save();\n return { attribution: current, created: false, previousEarnerId };\n }\n\n const minted = await this.deps.attributions.create({\n tenantId: targetTenant,\n earnerId: input.earnerId,\n sourceKind: input.sourceKind,\n sourceId: input.sourceId,\n status: input.status ?? 'active',\n metadata: input.metadata ?? '{}',\n });\n // Adopt the PERSISTED row: two workers racing the first registration\n // of one key both reach the natural-key upsert, and the loser's\n // in-memory instance carries an id the database no longer holds\n // (same convergence pattern as createPayoutBatch).\n const persisted = (\n await this.deps.attributions.findBySource(\n input.sourceKind,\n input.sourceId,\n )\n ).filter((row) => tenantOf(row.tenantId) === targetTenant);\n const attribution =\n persisted.length === 1 ? persisted[0] : (persisted[0] ?? minted);\n return { attribution, created: true, previousEarnerId: null };\n }\n\n /**\n * Resolve the ACTIVE earner for one external key. Exactly two queries\n * regardless of how many earners exist. Fail-closed: `earner` is `null`\n * with a typed {@link ResolveActiveEarnerResult.reason} unless the key\n * maps unambiguously to one active mapping whose earner is active.\n */\n async resolveActiveEarnerBySource(input: {\n sourceKind: string;\n sourceId: string;\n }): Promise<ResolveActiveEarnerResult> {\n const batched = await this.resolveActiveEarnersBySources({\n sourceKind: input.sourceKind,\n sourceIds: [input.sourceId],\n });\n const earner = batched.earnersBySourceId.get(input.sourceId) ?? null;\n if (earner) {\n return {\n earner,\n attribution: batched.attributionsBySourceId.get(input.sourceId) ?? null,\n };\n }\n return {\n earner: null,\n attribution: null,\n reason: batched.unresolved[0]?.reason ?? 'no_mapping',\n };\n }\n\n /**\n * Resolve the ACTIVE earners for a batch of external keys sharing one\n * kind. Query work is bounded by the REQUESTED ids — one indexed\n * attribution `IN` query plus one earner load for the mapped ids — never\n * a scan of all active earners. Duplicate/empty requested ids are\n * deduped; every requested id comes back either in\n * `earnersBySourceId` or in `unresolved` with its fail-closed reason.\n */\n async resolveActiveEarnersBySources(input: {\n sourceKind: string;\n sourceIds: string[];\n }): Promise<ResolveActiveEarnersBySourcesResult> {\n if (!input.sourceKind) {\n throw new Error(\n 'EarnerAttributionService.resolveActiveEarnersBySources: sourceKind is required',\n );\n }\n const requested = [...new Set(input.sourceIds.filter(Boolean))];\n const result: ResolveActiveEarnersBySourcesResult = {\n earnersBySourceId: new Map(),\n attributionsBySourceId: new Map(),\n unresolved: [],\n };\n if (requested.length === 0) return result;\n\n const rows = await this.deps.attributions.findBySources(\n input.sourceKind,\n requested,\n );\n const rowsBySourceId = new Map<string, EarnerSourceAttribution[]>();\n for (const row of rows) {\n const bucket = rowsBySourceId.get(row.sourceId);\n if (bucket) {\n bucket.push(row);\n } else {\n rowsBySourceId.set(row.sourceId, [row]);\n }\n }\n\n // Classify each requested key down to its single active mapping (or a\n // fail-closed reason) before touching the earners table.\n const activeBySourceId = new Map<string, EarnerSourceAttribution>();\n for (const sourceId of requested) {\n const bucket = rowsBySourceId.get(sourceId) ?? [];\n if (bucket.length === 0) {\n result.unresolved.push({ sourceId, reason: 'no_mapping' });\n continue;\n }\n const active = bucket.filter((row) => row.isActive());\n if (active.length === 0) {\n result.unresolved.push({ sourceId, reason: 'mapping_inactive' });\n continue;\n }\n if (active.length > 1) {\n result.unresolved.push({ sourceId, reason: 'ambiguous_mapping' });\n continue;\n }\n activeBySourceId.set(sourceId, active[0]);\n }\n if (activeBySourceId.size === 0) return result;\n\n const earnerIds = [\n ...new Set(\n [...activeBySourceId.values()]\n .map((row) => row.earnerId)\n .filter(Boolean),\n ),\n ];\n const earners = await this.deps.earners.listByIds(earnerIds);\n const earnerById = new Map<string, Earner>();\n for (const earner of earners) {\n if (earner.id) earnerById.set(earner.id, earner);\n }\n\n for (const [sourceId, attribution] of activeBySourceId) {\n const earner = earnerById.get(attribution.earnerId);\n if (!earner) {\n result.unresolved.push({ sourceId, reason: 'earner_not_found' });\n continue;\n }\n if (!earner.isActive()) {\n result.unresolved.push({ sourceId, reason: 'earner_not_active' });\n continue;\n }\n result.earnersBySourceId.set(sourceId, earner);\n result.attributionsBySourceId.set(sourceId, attribution);\n }\n return result;\n }\n}\n\nexport default EarnerAttributionService;\n"],"mappings":"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"}
1
+ {"version":3,"file":"commissions-BAsJWmKg.js","names":["results","payout"],"sources":["../../src/commissions/models/CommissionAdjustment.ts","../../src/commissions/collections/CommissionAdjustmentCollection.ts","../../src/commissions/models/CommissionAdjustmentOperation.ts","../../src/commissions/collections/CommissionAdjustmentOperationCollection.ts","../../src/commissions/models/Commission.ts","../../src/commissions/collections/CommissionCollection.ts","../../src/commissions/models/CommissionPayout.ts","../../src/commissions/collections/CommissionPayoutCollection.ts","../../src/commissions/types.ts","../../src/commissions/models/CommissionPlan.ts","../../src/commissions/collections/CommissionPlanCollection.ts","../../src/commissions/models/Earner.ts","../../src/commissions/collections/EarnerCollection.ts","../../src/commissions/models/EarnerSourceAttribution.ts","../../src/commissions/collections/EarnerSourceAttributionCollection.ts","../../src/commissions/models/EarningEvent.ts","../../src/commissions/collections/EarningEventCollection.ts","../../src/commissions/money.ts","../../src/commissions/services/CommissionAdjustmentService.ts","../../src/commissions/services/CommissionBalanceService.ts","../../src/commissions/services/CommissionCalculationService.ts","../../src/commissions/services/CommissionPayoutService.ts","../../src/commissions/services/CommissionSettlementService.ts","../../src/commissions/services/EarnerAttributionService.ts"],"sourcesContent":["/**\n * CommissionAdjustment — append-only correction against a Commission.\n *\n * Earned/paid Commissions are NEVER rewritten. A refund, credit,\n * chargeback, dispute outcome, or manual correction appends one of these\n * rows instead. `amountCents` is SIGNED — negative amounts claw earnings\n * back; positive amounts credit extra.\n *\n * Immutability contract: once persisted, an adjustment's substance\n * (`commissionId`, `earnerId`, `adjustmentKind`, `amountCents`, `currency`,\n * `reason`, `createdByProfileId`, `metadata`, `tenantId`) is frozen — the\n * save-time guard rejects any change via a WeakMap snapshot compare (the\n * commerce LicenseSale pattern). The ONLY post-create mutation allowed is\n * stamping/clearing `payoutId` when a settlement batch picks the row up.\n * A wrong adjustment is corrected by appending a counter-adjustment.\n *\n * @packageDocumentation\n */\n\nimport {\n crossPackageRef,\n field,\n foreignKey,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n CommissionAdjustmentKind,\n CommissionAdjustmentOptions,\n} from '../types.js';\n\n/**\n * Module-scoped record of the frozen-fields snapshot each persisted\n * adjustment was loaded with (or first saved as). WeakMap keeps it out of\n * the schema and GCs with the instance — commerce LicenseSale pattern.\n */\nconst frozenAdjustmentSnapshot = new WeakMap<CommissionAdjustment, string>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // Append-only audit rows: create/list/get only — no generated update or\n // delete on any surface.\n api: { include: ['create', 'list', 'get'] },\n mcp: { include: ['list', 'create'] },\n cli: false,\n})\nexport class CommissionAdjustment extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global rows). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The {@link Commission} this adjustment corrects. Required. */\n @foreignKey('Commission', { required: true })\n commissionId: string = '';\n\n /**\n * The {@link Earner} the adjustment applies to — denormalized from the\n * parent commission so balance queries never need a join. Required.\n */\n @foreignKey('Earner', { required: true })\n earnerId: string = '';\n\n /** What kind of correction this is. */\n adjustmentKind: CommissionAdjustmentKind = 'correction';\n\n /**\n * SIGNED amount in integer cents. Negative claws earnings back (refund,\n * chargeback); positive credits extra.\n */\n amountCents: number = 0;\n\n /** ISO 4217 currency — must match the parent commission's. */\n currency: string = 'USD';\n\n /** Human-readable justification. Required — audit rows explain themselves. */\n @field({ required: true })\n reason: string = '';\n\n /**\n * Profile of the operator/automation that created the adjustment\n * (cross-package string reference to smrt-profiles).\n */\n @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n createdByProfileId: string = '';\n\n /**\n * The {@link CommissionPayout} batch that settled this adjustment. Empty\n * until stamped. This is the ONLY field mutable after creation.\n */\n @foreignKey('CommissionPayout')\n payoutId: string = '';\n\n /** Additional metadata as a JSON string. Frozen once persisted. */\n metadata: string = '{}';\n\n constructor(options: CommissionAdjustmentOptions = {}) {\n super(options);\n if ('operationId' in (options as unknown as Record<string, unknown>)) {\n throw new Error(\n 'CommissionAdjustment has no public operationId field; use ' +\n 'CommissionAdjustmentService.createAdjustment()',\n );\n }\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.commissionId !== undefined)\n this.commissionId = options.commissionId;\n if (options.earnerId !== undefined) this.earnerId = options.earnerId;\n if (options.adjustmentKind !== undefined)\n this.adjustmentKind = options.adjustmentKind;\n if (options.amountCents !== undefined)\n this.amountCents = options.amountCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.reason !== undefined) this.reason = options.reason;\n if (options.createdByProfileId !== undefined)\n this.createdByProfileId = options.createdByProfileId;\n if (options.payoutId !== undefined) this.payoutId = options.payoutId;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Capture the frozen-fields snapshot when the row was loaded from the\n * database — from that moment on, only {@link payoutId} may change.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n if (await this.isSaved()) {\n frozenAdjustmentSnapshot.set(this, this.serializeFrozenSnapshot());\n }\n return this;\n }\n\n /** `true` once a payout batch has stamped {@link payoutId}. */\n isSettled(): boolean {\n return !!this.payoutId;\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n /**\n * Save with the append-only guard: once the row has been persisted, every\n * field except `payoutId` must match the captured snapshot. Corrections\n * to a wrong adjustment are new counter-adjustments, never edits.\n */\n override async save(): Promise<this> {\n this.assertImmutableOncePersisted();\n const result = (await super.save()) as this;\n if (!frozenAdjustmentSnapshot.has(this)) {\n frozenAdjustmentSnapshot.set(this, this.serializeFrozenSnapshot());\n }\n return result;\n }\n\n private assertImmutableOncePersisted(): void {\n const captured = frozenAdjustmentSnapshot.get(this);\n if (!captured) return; // brand-new row — first save captures below\n const current = this.serializeFrozenSnapshot();\n if (captured !== current) {\n throw new Error(\n `CommissionAdjustment ${this.id ?? '<new>'}: adjustments are ` +\n 'append-only — only payoutId may change after creation. Append a ' +\n 'counter-adjustment instead of editing this one.',\n );\n }\n }\n\n /**\n * Serialize every field EXCEPT `payoutId` (the sole post-create mutable\n * field) with stable key ordering.\n */\n private serializeFrozenSnapshot(): string {\n return JSON.stringify({\n tenantId: this.tenantId,\n commissionId: this.commissionId,\n earnerId: this.earnerId,\n adjustmentKind: this.adjustmentKind,\n amountCents: this.amountCents,\n currency: this.currency,\n reason: this.reason,\n createdByProfileId: this.createdByProfileId,\n metadata: this.metadata,\n });\n }\n}\n\nexport default CommissionAdjustment;\n","/**\n * CommissionAdjustmentCollection — collection manager for\n * {@link CommissionAdjustment}.\n *\n * \"Unsettled\" means `payoutId` is empty (checked in memory so `''`/`NULL`\n * storage differences don't matter). NOTE: the collection-level queries do\n * NOT apply the parent-commission-status eligibility rule — that lives in\n * `CommissionBalanceService` / `CommissionPayoutService`, which filter\n * unsettled adjustments to those whose parent commission is\n * earned/approved/payable/paid.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { CommissionAdjustment } from '../models/CommissionAdjustment.js';\n\nexport class CommissionAdjustmentCollection extends SmrtCollection<CommissionAdjustment> {\n static readonly _itemClass = CommissionAdjustment;\n\n /** All adjustments appended to one commission, oldest first. */\n async findByCommission(\n commissionId: string,\n ): Promise<CommissionAdjustment[]> {\n return await this.list({\n where: { commissionId },\n orderBy: 'created_at ASC',\n });\n }\n\n /** Unsettled adjustments for an earner+currency, oldest first. */\n async findUnsettledByEarner(\n earnerId: string,\n currency: string,\n ): Promise<CommissionAdjustment[]> {\n const rows = await this.list({\n where: { earnerId, currency },\n orderBy: 'created_at ASC',\n });\n return rows.filter((a) => !a.payoutId);\n }\n\n /**\n * Σ signed amountCents of {@link findUnsettledByEarner} rows (integer\n * cents; clawbacks make it negative).\n */\n async sumUnsettledByEarner(\n earnerId: string,\n currency: string,\n ): Promise<number> {\n const rows = await this.findUnsettledByEarner(earnerId, currency);\n return rows.reduce((sum, a) => sum + a.amountCents, 0);\n }\n\n /** Adjustments settled by one payout batch. */\n async findByPayout(payoutId: string): Promise<CommissionAdjustment[]> {\n return await this.list({\n where: { payoutId },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Adjustments settled by ANY of the given payout batches, in one `IN`\n * query — the adjustment twin of `CommissionCollection.findByPayouts`.\n * Empty input returns `[]` without querying.\n */\n async findByPayouts(payoutIds: string[]): Promise<CommissionAdjustment[]> {\n const ids = [...new Set(payoutIds.filter(Boolean))];\n if (ids.length === 0) return [];\n return await this.list({\n where: { payoutId: ids },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Conditionally claim adjustment rows for a payout batch — the adjustment\n * twin of `CommissionCollection.claimForPayout`. Rows already claimed by a\n * DIFFERENT payout are skipped; rows already claimed by THIS payout pass\n * through (idempotent retry / repair); every claim is verified by a\n * post-save re-read. Reads/writes go through the model layer, so this\n * respects the tenancy interceptor and the dialect's empty-FK encoding.\n * Not a cross-row transaction — safe concurrency relies on disjoint batch\n * scopes (see `CommissionPayoutService`). Returns the claimed rows.\n */\n async claimForPayout(\n adjustmentIds: string[],\n payoutId: string,\n ): Promise<CommissionAdjustment[]> {\n const claimed: CommissionAdjustment[] = [];\n for (const id of adjustmentIds) {\n const row = await this.get({ id });\n if (!row) continue;\n if (row.payoutId && row.payoutId !== payoutId) continue; // other batch\n if (!row.payoutId) {\n row.payoutId = payoutId;\n await row.save();\n }\n const verified = await this.get({ id });\n if (verified && verified.payoutId === payoutId) {\n claimed.push(verified);\n }\n }\n return claimed;\n }\n}\n\nexport default CommissionAdjustmentCollection;\n","/** Persisted serialization fence for idempotent CommissionAdjustment writes. */\n\nimport {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\n\ninterface CommissionAdjustmentOperationOptions extends SmrtObjectOptions {\n tenantId?: string;\n adjustmentId?: string;\n}\n\n/**\n * One globally unique adjustment operation UUID mapped to its adjustment.\n *\n * This is package-owned infrastructure for `CommissionAdjustmentService`, not\n * a second financial record. The operation UUID is stored as the table's\n * primary `id`, which gives every supported database a persisted uniqueness\n * fence without adding a constrained column to the existing adjustments\n * table. The service inserts this fence and the adjustment in one transaction.\n */\n@TenantScoped({ mode: 'required' })\n@smrt({\n api: false,\n mcp: false,\n cli: false,\n})\nexport class CommissionAdjustmentOperation extends SmrtObject {\n /** Owning tenant; the operation UUID itself remains globally unique. */\n @tenantId()\n tenantId: string = '';\n\n /** Adjustment that the operation creates in the same transaction. */\n // Deliberately not a database foreign key: the fence is inserted first in\n // the transaction, then its adjustment. Atomic commit plus replay\n // verification preserve integrity without requiring deferred constraints.\n @field({ sqlType: 'UUID', required: true, readonly: true, indexed: true })\n adjustmentId!: string;\n\n constructor(options: CommissionAdjustmentOperationOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.adjustmentId !== undefined)\n this.adjustmentId = options.adjustmentId;\n }\n}\n\nexport default CommissionAdjustmentOperation;\n","/** Database serialization primitive for adjustment operation UUIDs. */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { requireTenantId } from '@happyvertical/smrt-tenancy';\nimport { CommissionAdjustmentOperation } from '../models/CommissionAdjustmentOperation.js';\n\nexport interface ClaimCommissionAdjustmentOperationInput {\n operationId: string;\n tenantId: string;\n adjustmentId: string;\n}\n\nexport interface ClaimCommissionAdjustmentOperationResult {\n operation: CommissionAdjustmentOperation | null;\n claimed: boolean;\n}\n\nexport class CommissionAdjustmentOperationCollection extends SmrtCollection<CommissionAdjustmentOperation> {\n static readonly _itemClass = CommissionAdjustmentOperation;\n\n /** Tenant-scoped lookup; foreign-tenant operation payloads stay invisible. */\n async findByOperationId(\n operationId: string,\n ): Promise<CommissionAdjustmentOperation | null> {\n const tenantId = requireTenantId();\n const [operation] = await this.query(\n `SELECT\n id, slug, context, created_at, updated_at, tenant_id,\n CAST(adjustment_id AS TEXT) AS adjustment_id\n FROM ${this.tableName}\n WHERE id = ? AND tenant_id = ?\n LIMIT 1`,\n [operationId, tenantId],\n { allowRawOnTenantScoped: true },\n );\n return operation ?? null;\n }\n\n /**\n * Claim the globally unique operation UUID without changing an existing\n * winner. This must run inside the same transaction that creates the\n * corresponding adjustment.\n */\n async claim(\n input: ClaimCommissionAdjustmentOperationInput,\n ): Promise<ClaimCommissionAdjustmentOperationResult> {\n if (requireTenantId().toLowerCase() !== input.tenantId.toLowerCase()) {\n throw new Error('CommissionAdjustment operation tenant mismatch');\n }\n\n const inserted = await this.query(\n `INSERT INTO ${this.tableName} (\n id, slug, context, tenant_id, adjustment_id\n ) VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO NOTHING\n RETURNING id`,\n [\n input.operationId,\n input.operationId,\n '',\n input.tenantId,\n input.adjustmentId,\n ],\n { allowRawOnTenantScoped: true },\n );\n\n const operation = await this.findByOperationId(input.operationId);\n return { operation, claimed: inserted.length === 1 };\n }\n}\n\nexport default CommissionAdjustmentOperationCollection;\n","/**\n * Commission — one earning record for one earner, one plan component, one\n * earning-event occurrence.\n *\n * Amounts are integer cents; `rate`/`shareFraction` are decimals in 0–1.\n * Every row stores snapshot references (`planKey`/`planVersion` plus a\n * generic polymorphic `termsSnapshotKind`/`termsSnapshotId` — the referrals\n * module points the latter at its ReferralTermSnapshot) and a JSON\n * `calculationTrace` sufficient to reproduce `amountCents`, so earnings stay\n * auditable after plans are superseded.\n *\n * Lifecycle is a STRICT forward chain — `pending → earned → approved →\n * payable → paid` — enforced at save time against the AUTHORITATIVE prior\n * persisted status (re-read from the database, commerce pattern), so neither\n * raw mass-assignment nor a `create({ id, _skipLoad: true })` upsert can skip\n * steps or roll back. Use the transition methods ({@link markEarned} /\n * {@link approve} / {@link markPayable} / {@link markPaid}); they mutate and\n * stamp timestamps but DO NOT save — the caller saves (one explicit\n * persistence point per mutation, matching commerce's markSent/markConfirmed\n * convention).\n *\n * Commissions are audit rows: the generated surface has no update or delete.\n * Corrections append {@link CommissionAdjustment} rows instead of editing.\n *\n * @packageDocumentation\n */\n\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n CommissionBasis,\n CommissionCalculationTrace,\n CommissionOptions,\n CommissionStatus,\n} from '../types.js';\n\n/**\n * Legal status transitions — the strict chain, keyed by prior persisted\n * status. No-op re-saves and brand-new rows are always permitted (imports /\n * fixtures may seed any status); this map governs *changes* to persisted\n * rows only.\n */\nconst COMMISSION_STATUS_TRANSITIONS: Record<\n CommissionStatus,\n CommissionStatus[]\n> = {\n pending: ['earned'],\n earned: ['approved'],\n approved: ['payable'],\n payable: ['paid'],\n paid: [],\n};\n\n/**\n * Module-scoped record of the status each Commission instance was loaded\n * with — fallback for the save-time guard when the DB re-read is\n * unavailable. WeakMap keeps it out of the schema (commerce pattern).\n */\nconst loadedCommissionStatus = new WeakMap<Commission, CommissionStatus>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // Idempotent creation: dedupeKey is the natural key so a retried\n // calculation upserts instead of duplicating.\n conflictColumns: ['dedupe_key'],\n // Audit rows: create/list/get only — no generated update or delete.\n // Lifecycle mutations happen through the guarded transition methods and\n // the settlement/payout services.\n api: { include: ['list', 'get', 'create'] },\n mcp: { include: ['list', 'get'] },\n // High volume and mutation-sensitive — no CLI surface.\n cli: false,\n})\nexport class Commission extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global rows). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The {@link Earner} this commission belongs to. Required. */\n @foreignKey('Earner', { required: true })\n earnerId: string = '';\n\n /** The {@link EarningEvent} evidence row this commission derives from. */\n @foreignKey('EarningEvent')\n earningEventId: string = '';\n\n /** Snapshot reference: plan key at calculation time. */\n planKey: string = '';\n\n /** Snapshot reference: plan version at calculation time. */\n planVersion: number = 0;\n\n /** Which plan component produced this commission. */\n componentKey: string = '';\n\n /**\n * Generic polymorphic reference to the terms snapshot that governed the\n * calculation (e.g. the referrals module sets\n * `('referral_term_snapshot', <id>)`). Free-form; this module attaches no\n * semantics beyond recording it in the dedupe key and trace.\n */\n termsSnapshotKind: string = '';\n\n /** Id of the terms snapshot named by {@link termsSnapshotKind}. */\n termsSnapshotId: string = '';\n\n /** How {@link baseAmountCents} was resolved from the event. */\n basis: CommissionBasis = 'gross';\n\n /** Base amount the rate was applied to, in integer cents. */\n baseAmountCents: number = 0;\n\n /** Rate applied (0–1). Recorded as `0` for `fixed`-basis commissions. */\n rate: number = 0.0;\n\n /** Split share applied (0–1). `1.0` for unsplit commissions. */\n shareFraction: number = 1.0;\n\n /**\n * Groups the sibling commissions of one split — every earner sharing an\n * event/component carries the same `splitGroupId`. Empty for unsplit rows.\n */\n splitGroupId: string = '';\n\n /** The earned amount in integer cents. */\n amountCents: number = 0;\n\n /** ISO 4217 currency (copied from the earning event). */\n currency: string = 'USD';\n\n /**\n * Lifecycle status — strict chain `pending → earned → approved → payable\n * → paid`. Mutate via the transition methods; the save-time guard rejects\n * illegal edges.\n */\n status: CommissionStatus = 'pending';\n\n /**\n * End of the clearing window (refund/chargeback holdback). `null` means\n * no clearing applies — the commission is immediately sweepable to\n * `earned` (see `CommissionSettlementService.sweepClearing`).\n */\n clearingEndsAt: Date | null = null;\n\n /** When the commission transitioned to `earned`. */\n earnedAt: Date | null = null;\n\n /** When the commission transitioned to `approved`. */\n approvedAt: Date | null = null;\n\n /** When the commission transitioned to `payable`. */\n payableAt: Date | null = null;\n\n /** When the commission transitioned to `paid`. */\n paidAt: Date | null = null;\n\n /**\n * The {@link CommissionPayout} batch that settled this commission. Empty\n * until a payout batch stamps it.\n */\n @foreignKey('CommissionPayout')\n payoutId: string = '';\n\n /** Copied from the earning event for reporting (generic source pair). */\n sourceKind: string = '';\n\n /** Copied from the earning event for reporting. */\n sourceId: string = '';\n\n /**\n * JSON-string {@link CommissionCalculationTrace} — everything needed to\n * reproduce {@link amountCents}. Use {@link getCalculationTrace} /\n * {@link setCalculationTrace}.\n */\n calculationTrace: string = '{}';\n\n /**\n * Idempotency natural key —\n * `` `${event.dedupeKey}:${terms}:${componentKey}:${earnerId}:${occurrenceIndex}` ``\n * (see `CommissionCalculationService`). Required.\n */\n @field({ required: true })\n dedupeKey: string = '';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: CommissionOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.earnerId !== undefined) this.earnerId = options.earnerId;\n if (options.earningEventId !== undefined)\n this.earningEventId = options.earningEventId;\n if (options.planKey !== undefined) this.planKey = options.planKey;\n if (options.planVersion !== undefined)\n this.planVersion = options.planVersion;\n if (options.componentKey !== undefined)\n this.componentKey = options.componentKey;\n if (options.termsSnapshotKind !== undefined)\n this.termsSnapshotKind = options.termsSnapshotKind;\n if (options.termsSnapshotId !== undefined)\n this.termsSnapshotId = options.termsSnapshotId;\n if (options.basis !== undefined) this.basis = options.basis;\n if (options.baseAmountCents !== undefined)\n this.baseAmountCents = options.baseAmountCents;\n if (options.rate !== undefined) this.rate = options.rate;\n if (options.shareFraction !== undefined)\n this.shareFraction = options.shareFraction;\n if (options.splitGroupId !== undefined)\n this.splitGroupId = options.splitGroupId;\n if (options.amountCents !== undefined)\n this.amountCents = options.amountCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.status !== undefined) this.status = options.status;\n if (options.clearingEndsAt !== undefined)\n this.clearingEndsAt = Commission.coerceDate(options.clearingEndsAt);\n if (options.earnedAt !== undefined)\n this.earnedAt = Commission.coerceDate(options.earnedAt);\n if (options.approvedAt !== undefined)\n this.approvedAt = Commission.coerceDate(options.approvedAt);\n if (options.payableAt !== undefined)\n this.payableAt = Commission.coerceDate(options.payableAt);\n if (options.paidAt !== undefined)\n this.paidAt = Commission.coerceDate(options.paidAt);\n if (options.payoutId !== undefined) this.payoutId = options.payoutId;\n if (options.sourceKind !== undefined) this.sourceKind = options.sourceKind;\n if (options.sourceId !== undefined) this.sourceId = options.sourceId;\n if (options.calculationTrace !== undefined)\n this.calculationTrace = options.calculationTrace;\n if (options.dedupeKey !== undefined) this.dedupeKey = options.dedupeKey;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Re-coerce timestamp fields after the framework reapplies raw option /\n * hydrated row values, and record the loaded status for the save guard.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n this.clearingEndsAt = Commission.coerceDate(this.clearingEndsAt);\n this.earnedAt = Commission.coerceDate(this.earnedAt);\n this.approvedAt = Commission.coerceDate(this.approvedAt);\n this.payableAt = Commission.coerceDate(this.payableAt);\n this.paidAt = Commission.coerceDate(this.paidAt);\n if (await this.isSaved()) {\n loadedCommissionStatus.set(this, this.status);\n }\n return this;\n }\n\n // -------- Status predicates --------\n\n isPending(): boolean {\n return this.status === 'pending';\n }\n\n isEarned(): boolean {\n return this.status === 'earned';\n }\n\n isApproved(): boolean {\n return this.status === 'approved';\n }\n\n isPayable(): boolean {\n return this.status === 'payable';\n }\n\n isPaid(): boolean {\n return this.status === 'paid';\n }\n\n /** `true` once a payout batch has stamped {@link payoutId}. */\n isSettled(): boolean {\n return !!this.payoutId;\n }\n\n // -------- Transition methods (mutate only — caller saves) --------\n\n /**\n * `pending → earned` (clearing window passed). Stamps {@link earnedAt}.\n * Does NOT save — the caller saves.\n */\n markEarned(now: Date = new Date()): void {\n this.assertTransitionFrom('pending', 'earned');\n this.status = 'earned';\n this.earnedAt = now;\n }\n\n /**\n * `earned → approved` (operator/automation approved the earning).\n * Stamps {@link approvedAt}. Does NOT save — the caller saves.\n */\n approve(now: Date = new Date()): void {\n this.assertTransitionFrom('earned', 'approved');\n this.status = 'approved';\n this.approvedAt = now;\n }\n\n /**\n * `approved → payable` (released for the next payout batch).\n * Stamps {@link payableAt}. Does NOT save — the caller saves.\n */\n markPayable(now: Date = new Date()): void {\n this.assertTransitionFrom('approved', 'payable');\n this.status = 'payable';\n this.payableAt = now;\n }\n\n /**\n * `payable → paid` (its payout batch completed). Stamps {@link paidAt}.\n * Does NOT save — the caller saves.\n */\n markPaid(now: Date = new Date()): void {\n this.assertTransitionFrom('payable', 'paid');\n this.status = 'paid';\n this.paidAt = now;\n }\n\n private assertTransitionFrom(\n expected: CommissionStatus,\n next: CommissionStatus,\n ): void {\n if (this.status !== expected) {\n throw new Error(\n `Commission ${this.id ?? '<new>'}: cannot transition to '${next}' ` +\n `from status '${this.status}' (chain is pending → earned → ` +\n 'approved → payable → paid)',\n );\n }\n }\n\n // -------- Trace / metadata helpers --------\n\n /** Parse {@link calculationTrace}; returns `null` on empty/invalid JSON. */\n getCalculationTrace(): CommissionCalculationTrace | null {\n if (!this.calculationTrace) return null;\n try {\n const parsed = JSON.parse(this.calculationTrace) as unknown;\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return null;\n }\n const trace = parsed as CommissionCalculationTrace;\n return typeof trace.componentKey === 'string' &&\n typeof trace.baseAmountCents === 'number'\n ? trace\n : null;\n } catch {\n return null;\n }\n }\n\n /** Serialize and store {@link calculationTrace}. */\n setCalculationTrace(trace: CommissionCalculationTrace): void {\n this.calculationTrace = JSON.stringify(trace);\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n // -------- Save-time guard --------\n\n /**\n * Save-time state-machine guard (commerce pattern). Validates the status\n * transition against the AUTHORITATIVE prior persisted status — re-read\n * from the database so a `create({ id: <existing>, _skipLoad: true })`\n * upsert is correctly treated as an update rather than a guard-free new\n * row. Brand-new rows may start in any status (fixtures/imports); a\n * persisted row may only advance one legal step.\n */\n override async save(): Promise<this> {\n const prior = await this.resolvePriorStatus();\n this.assertStatusTransition(prior);\n await this.assertDedupeKeyNotTaken();\n const result = (await super.save()) as this;\n loadedCommissionStatus.set(this, this.status);\n return result;\n }\n\n /**\n * Refuse a save whose `dedupeKey` already belongs to a DIFFERENT row —\n * commissions are audit rows, and the natural-key upsert would let a\n * fresh instance (generated `create`, or the loser of a calculation\n * race) overwrite the persisted amount/status and rotate the row id.\n * `CommissionCalculationService` treats this refusal as \"someone else\n * already earned it\" and returns the existing row.\n */\n private async assertDedupeKeyNotTaken(): Promise<void> {\n if (!this.dedupeKey) return;\n try {\n const res = await this.db.query(\n `SELECT id FROM ${this.tableName} WHERE dedupe_key = $1`,\n this.dedupeKey,\n );\n const rows = Array.isArray(res)\n ? (res as Record<string, unknown>[])\n : ((res as { rows?: Record<string, unknown>[] }).rows ?? []);\n const taken = rows.find((row) => row.id !== this.id);\n if (taken) {\n throw new Error(\n `Commission (dedupeKey '${this.dedupeKey}'): a commission with ` +\n 'this dedupe key already exists — commissions are immutable ' +\n 'audit rows; corrections append CommissionAdjustments.',\n );\n }\n } catch (error) {\n if (error instanceof Error && error.message.includes('immutable')) {\n throw error;\n }\n // DB not ready / table absent — nothing persisted to collide with.\n }\n }\n\n private async resolvePriorStatus(): Promise<CommissionStatus | undefined> {\n if (this.id) {\n try {\n const row = await this.db.get(this.tableName, { id: this.id });\n if (row && row.status != null) {\n return row.status as CommissionStatus;\n }\n } catch {\n // DB not ready — fall through to the in-memory record.\n }\n }\n return loadedCommissionStatus.get(this);\n }\n\n private assertStatusTransition(prior: CommissionStatus | undefined): void {\n if (prior === undefined) return; // new row\n if (prior === this.status) return; // no-op re-save\n const allowed = COMMISSION_STATUS_TRANSITIONS[prior] ?? [];\n if (!allowed.includes(this.status)) {\n throw new Error(\n `Commission ${this.id}: illegal status transition '${prior}' → ` +\n `'${this.status}'. Use markEarned() / approve() / markPayable() ` +\n '/ markPaid().',\n );\n }\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n return null;\n }\n}\n\nexport default Commission;\n","/**\n * CommissionCollection — collection manager for {@link Commission}.\n *\n * \"Unsettled\" throughout means `payoutId` is empty — the row has not been\n * gathered into a {@link CommissionPayout} batch yet. The emptiness check is\n * applied in memory (`!c.payoutId`) rather than as a `WHERE payout_id = ''`\n * filter so the semantics hold whether an adapter stores the empty\n * reference as `''` or `NULL`.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Commission } from '../models/Commission.js';\nimport type { CommissionStatus } from '../types.js';\n\nexport class CommissionCollection extends SmrtCollection<Commission> {\n static readonly _itemClass = Commission;\n\n /** All commissions for an earner, newest first. */\n async findByEarner(earnerId: string): Promise<Commission[]> {\n return await this.list({\n where: { earnerId },\n orderBy: 'created_at DESC',\n });\n }\n\n /** All commissions derived from one earning event. */\n async findByEvent(earningEventId: string): Promise<Commission[]> {\n return await this.list({\n where: { earningEventId },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Commissions by lifecycle status, newest first. */\n async findByStatus(status: CommissionStatus): Promise<Commission[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Look up a commission by its idempotency natural key. */\n async findByDedupeKey(dedupeKey: string): Promise<Commission | null> {\n if (!dedupeKey) return null;\n const results = await this.list({ where: { dedupeKey }, limit: 1 });\n return results[0] ?? null;\n }\n\n /**\n * Payable commissions for an earner+currency that no payout batch has\n * settled yet — the rows `CommissionPayoutService.createPayoutBatch`\n * gathers.\n *\n * Pass `scope` to narrow the gather to one earning source (e.g. a single\n * ad network): only commissions whose `(sourceKind, sourceId)` match are\n * returned. This lets a caller cut a payout batch that claims *only* its\n * network's commissions, so concurrent per-network batches settle\n * disjoint sets instead of one sweeping the other's rows.\n */\n async findPayableUnsettled(\n earnerId: string,\n currency: string,\n scope?: { sourceKind: string; sourceId: string },\n ): Promise<Commission[]> {\n const where: Record<string, unknown> = {\n earnerId,\n currency,\n status: 'payable',\n };\n if (scope) {\n where.sourceKind = scope.sourceKind;\n where.sourceId = scope.sourceId;\n }\n const payable = await this.list({ where, orderBy: 'created_at ASC' });\n return payable.filter((c) => !c.payoutId);\n }\n\n /** Σ amountCents of {@link findPayableUnsettled} rows (integer cents). */\n async sumPayableByEarner(\n earnerId: string,\n currency: string,\n ): Promise<number> {\n const payable = await this.findPayableUnsettled(earnerId, currency);\n return payable.reduce((sum, c) => sum + c.amountCents, 0);\n }\n\n /** Commissions settled by one payout batch. */\n async findByPayout(payoutId: string): Promise<Commission[]> {\n return await this.list({\n where: { payoutId },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Commissions settled by ANY of the given payout batches, in one `IN`\n * query — the batched-membership primitive behind the source-scoped\n * payout-history verification (one query per page instead of one per\n * payout). Empty input returns `[]` without querying.\n */\n async findByPayouts(payoutIds: string[]): Promise<Commission[]> {\n const ids = [...new Set(payoutIds.filter(Boolean))];\n if (ids.length === 0) return [];\n return await this.list({\n where: { payoutId: ids },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Conditionally claim rows for a payout batch: each row is re-loaded\n * fresh and stamped with `payoutId` only when it is still payable and\n * unclaimed (or already claimed by THIS payout — the idempotent-retry /\n * repair case). Rows claimed by a DIFFERENT payout are skipped, and every\n * claim is verified by a post-save re-read so a lost race never counts\n * toward the caller's totals.\n *\n * Reads and writes go through the model layer (`get` / `save`), so this\n * respects the tenancy interceptor (a cross-tenant id resolves to `null`\n * and is skipped, never mutated) and the DB dialect (an empty FK is `''`\n * on SQLite / `NULL` on the native-`uuid` Postgres/DuckDB columns — the\n * model normalizes both).\n *\n * This is the single place claim semantics live. It narrows the\n * concurrent-batch window to the re-read granularity but is NOT a\n * cross-row transaction — safe concurrent settlement relies on batches\n * using DISJOINT scopes (see `CommissionPayoutService.createPayoutBatch`);\n * overlapping concurrent scopes must be serialized by the caller.\n *\n * Returns the claimed rows (freshly loaded, `payoutId` verified).\n */\n async claimForPayout(\n commissionIds: string[],\n payoutId: string,\n ): Promise<Commission[]> {\n const claimed: Commission[] = [];\n for (const id of commissionIds) {\n const row = await this.get({ id });\n if (!row) continue;\n if (row.payoutId && row.payoutId !== payoutId) continue; // other batch\n if (!row.payoutId) {\n if (!row.isPayable()) continue; // no longer eligible\n row.payoutId = payoutId;\n await row.save();\n }\n const verified = await this.get({ id });\n if (verified && verified.payoutId === payoutId) {\n claimed.push(verified);\n }\n }\n return claimed;\n }\n}\n\nexport default CommissionCollection;\n","/**\n * CommissionPayout — settlement batch for one earner in one currency.\n *\n * A payout gathers an earner's payable unsettled Commissions plus the\n * unsettled Adjustments of eligible commissions, stamps `payoutId` on those\n * EXACT rows, and records the totals it settled\n * (`totalAmountCents = commissionTotalCents + adjustmentTotalCents` —\n * enforced at save time). Batches are minted by\n * `CommissionPayoutService.createPayoutBatch`, idempotently via the\n * `idempotencyKey` natural key.\n *\n * The model is named CommissionPayout (table `commission_payouts`) to avoid\n * the global table-name collision with commerce `Payout` / legacy affiliates\n * `Payout` (`payouts`).\n *\n * Generated surface is FULLY read-only — list/get on api, mcp, AND cli.\n * Commerce Payout precedent (\"a payout has no safe generated write\"): the\n * status drives an outgoing remittance and the totals are the integrity\n * core, so the only legitimate writes are the service's creation path and\n * the guarded transition helpers below. The CLI is an independently\n * configured write surface — `cli: true` would regenerate the exact\n * create/update vector closed on api/mcp, so it is locked to list/get too.\n *\n * Lifecycle: `pending → approved → processing → completed | failed`, with\n * `failed` reachable from approved/processing and resettable to `pending`\n * only via {@link resetFromFailed}, and `rejected` the terminal\n * operator-decline exit from pending/approved (see {@link reject} — the\n * membership release lives in\n * `CommissionPayoutService.transitionPayoutForSource`). Transition helpers\n * mutate and stamp but DO NOT save — the caller saves (same convention as\n * Commission).\n *\n * @packageDocumentation\n */\n\nimport {\n crossPackageRef,\n field,\n foreignKey,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type {\n CommissionPayoutOptions,\n CommissionPayoutStatus,\n PayoutMethod,\n} from '../types.js';\n\n/**\n * Legal status transitions, keyed by the prior persisted status.\n * `failed → pending` exists only for {@link resetFromFailed}. `completed`\n * is terminal. No-op re-saves and brand-new rows are always permitted;\n * this map governs *changes* to persisted rows only (commerce pattern).\n */\nconst PAYOUT_STATUS_TRANSITIONS: Record<\n CommissionPayoutStatus,\n CommissionPayoutStatus[]\n> = {\n pending: ['approved', 'rejected'],\n approved: ['processing', 'failed', 'rejected'],\n processing: ['completed', 'failed'],\n completed: [],\n // FAILED is resettable to PENDING via resetFromFailed().\n failed: ['pending'],\n // REJECTED is terminal — the batch was declined and its membership\n // released; the released rows settle through a FUTURE batch instead.\n rejected: [],\n};\n\n/**\n * Module-scoped record of the status each payout instance was loaded with —\n * fallback for the save-time guard when the DB re-read is unavailable.\n */\nconst loadedPayoutStatus = new WeakMap<\n CommissionPayout,\n CommissionPayoutStatus\n>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // Idempotent settlement: a retried batch with the same idempotencyKey\n // resolves to the existing payout instead of double-paying.\n conflictColumns: ['idempotency_key'],\n // Fully read-only generated surface on ALL THREE surfaces — see the\n // class doc. Writes go through CommissionPayoutService and the guarded\n // transition helpers only.\n api: { include: ['list', 'get'] },\n mcp: { include: ['list', 'get'] },\n cli: { include: ['list', 'get'] },\n})\nexport class CommissionPayout extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global payouts). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The {@link Earner} being paid. Required. */\n @foreignKey('Earner', { required: true })\n earnerId: string = '';\n\n /** Start of the settlement period this batch covers (informational). */\n periodStart: Date | null = null;\n\n /** End of the settlement period this batch covers (informational). */\n periodEnd: Date | null = null;\n\n /** Σ amountCents of the Commissions this batch settled (integer cents). */\n commissionTotalCents: number = 0;\n\n /**\n * Σ signed amountCents of the Adjustments this batch settled (integer\n * cents; clawbacks make it negative).\n */\n adjustmentTotalCents: number = 0;\n\n /**\n * Net amount remitted — must equal\n * `commissionTotalCents + adjustmentTotalCents` (enforced on save).\n */\n totalAmountCents: number = 0;\n\n /** ISO 4217 currency of the batch. */\n currency: string = 'USD';\n\n /** Delivery method for this batch (defaulted from the Earner). */\n payoutMethod: PayoutMethod = 'bank_transfer';\n\n /**\n * Lifecycle status — see the class doc. Mutate via {@link approve} /\n * {@link markProcessing} / {@link complete} / {@link fail} /\n * {@link resetFromFailed}.\n */\n status: CommissionPayoutStatus = 'pending';\n\n /**\n * Payment reference recorded at completion (check number, transfer id,\n * …). Cleared by {@link resetFromFailed}.\n */\n paymentReference: string = '';\n\n /**\n * Opaque payout-provider reference (processor batch id, remittance file\n * id, …). Retained across failure/reset for audit.\n */\n providerRef: string = '';\n\n /** When the payout completed. */\n paidAt: Date | null = null;\n\n /**\n * Optional link to the commerce Invoice that papers this payout\n * (cross-package string reference — never a DDL foreign key).\n */\n @crossPackageRef('@happyvertical/smrt-commerce:Invoice')\n invoiceId: string = '';\n\n /** Operator notes — approval memos, failure reasons (append-only). */\n notes: string = '';\n\n /**\n * Idempotency natural key. Required. The payout service defaults it to\n * `` `${earnerId}:${currency}:${periodEnd ISO date}` `` when the caller\n * doesn't supply one.\n */\n @field({ required: true })\n idempotencyKey: string = '';\n\n /**\n * DERIVED single-source stamp: when every member commission — and every\n * member adjustment's parent commission — shares exactly one non-empty\n * `(sourceKind, sourceId)`, that source is stamped here; otherwise both\n * stay `''` (mixed-source, unknown-source, or empty membership). The\n * payout service maintains the stamp from VERIFIED claimed membership at\n * batch/repair time (`restampPayoutSource` is the backfill for payouts\n * minted before the stamp existed). It is the index behind the\n * source-scoped payout-history listing, which still re-verifies\n * membership per page — never an authorization input by itself.\n */\n sourceKind: string = '';\n\n /** Id half of the derived single-source stamp — see {@link sourceKind}. */\n @field({ indexed: true })\n sourceId: string = '';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: CommissionPayoutOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.earnerId !== undefined) this.earnerId = options.earnerId;\n if (options.periodStart !== undefined)\n this.periodStart = CommissionPayout.coerceDate(options.periodStart);\n if (options.periodEnd !== undefined)\n this.periodEnd = CommissionPayout.coerceDate(options.periodEnd);\n if (options.commissionTotalCents !== undefined)\n this.commissionTotalCents = options.commissionTotalCents;\n if (options.adjustmentTotalCents !== undefined)\n this.adjustmentTotalCents = options.adjustmentTotalCents;\n if (options.totalAmountCents !== undefined)\n this.totalAmountCents = options.totalAmountCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.payoutMethod !== undefined)\n this.payoutMethod = options.payoutMethod;\n if (options.status !== undefined) this.status = options.status;\n if (options.paymentReference !== undefined)\n this.paymentReference = options.paymentReference;\n if (options.providerRef !== undefined)\n this.providerRef = options.providerRef;\n if (options.paidAt !== undefined)\n this.paidAt = CommissionPayout.coerceDate(options.paidAt);\n if (options.invoiceId !== undefined) this.invoiceId = options.invoiceId;\n if (options.notes !== undefined) this.notes = options.notes;\n if (options.idempotencyKey !== undefined)\n this.idempotencyKey = options.idempotencyKey;\n if (options.sourceKind !== undefined) this.sourceKind = options.sourceKind;\n if (options.sourceId !== undefined) this.sourceId = options.sourceId;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Re-coerce timestamp fields after the framework reapplies raw option /\n * hydrated row values, and record the loaded status for the save guard.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n this.periodStart = CommissionPayout.coerceDate(this.periodStart);\n this.periodEnd = CommissionPayout.coerceDate(this.periodEnd);\n this.paidAt = CommissionPayout.coerceDate(this.paidAt);\n if (await this.isSaved()) {\n loadedPayoutStatus.set(this, this.status);\n }\n return this;\n }\n\n // -------- Status predicates --------\n\n isPending(): boolean {\n return this.status === 'pending';\n }\n\n isApproved(): boolean {\n return this.status === 'approved';\n }\n\n isProcessing(): boolean {\n return this.status === 'processing';\n }\n\n isCompleted(): boolean {\n return this.status === 'completed';\n }\n\n isFailed(): boolean {\n return this.status === 'failed';\n }\n\n isRejected(): boolean {\n return this.status === 'rejected';\n }\n\n // -------- Transition methods (mutate only — caller saves) --------\n\n /** `pending → approved`. Does NOT save — the caller saves. */\n approve(): void {\n if (this.status !== 'pending') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot approve from status '${this.status}'`,\n );\n }\n // A batch whose reconciled membership nets to nothing (or a clawback\n // surplus) must never move toward remittance — such payouts exist only\n // as audit artifacts of a raced/interrupted claim pass.\n if (this.totalAmountCents <= 0) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot approve a batch with ` +\n `non-positive total (${this.totalAmountCents} cents)`,\n );\n }\n this.status = 'approved';\n }\n\n /** `approved → processing`. Does NOT save — the caller saves. */\n markProcessing(): void {\n if (this.status !== 'approved') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot mark processing from status '${this.status}'`,\n );\n }\n this.status = 'processing';\n }\n\n /**\n * `processing → completed`. Requires a payment reference — a completed\n * payout with no reference is untraceable. Stamps {@link paidAt}.\n * Does NOT save — the caller saves.\n */\n complete(paymentReference: string, now: Date = new Date()): void {\n if (this.status !== 'processing') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot complete from status '${this.status}'`,\n );\n }\n if (!paymentReference) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: complete() requires a paymentReference`,\n );\n }\n this.status = 'completed';\n this.paymentReference = paymentReference;\n this.paidAt = now;\n }\n\n /**\n * `pending | approved → rejected` (operator declined the batch before\n * remittance started). Terminal — there is no reset from rejected; the\n * released membership settles through a future batch. Requires a reason,\n * appended to {@link notes}. This mutates the payout only: use\n * `CommissionPayoutService.transitionPayoutForSource` to reject, which\n * also RELEASES the batch's membership (clears `payoutId` on its\n * commissions and adjustments) in the same operation — a rejected payout\n * that kept its rows stamped would strand them unsettleable forever.\n * Does NOT save — the caller saves.\n */\n reject(reason: string): void {\n if (this.status !== 'pending' && this.status !== 'approved') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot reject from status '${this.status}' — processing/terminal batches use fail()/resetFromFailed()`,\n );\n }\n if (!reason) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: reject() requires a reason`,\n );\n }\n this.status = 'rejected';\n const memo = `Rejected: ${reason}`;\n this.notes = this.notes ? `${this.notes}\\n${memo}` : memo;\n }\n\n /**\n * `approved | processing → failed`. Appends the reason to {@link notes}.\n * Does NOT save — the caller saves.\n */\n fail(reason: string): void {\n if (this.status !== 'approved' && this.status !== 'processing') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot fail from status '${this.status}'`,\n );\n }\n this.status = 'failed';\n const memo = `Failed: ${reason ?? ''}`;\n this.notes = this.notes ? `${this.notes}\\n${memo}` : memo;\n }\n\n /**\n * Operator-driven reset: `failed → pending` after fixing whatever broke.\n * Clears {@link paymentReference} and {@link paidAt} (the next attempt\n * gets fresh ones) but RETAINS {@link providerRef} and {@link notes} for\n * audit. The only path out of `failed`. Does NOT save — the caller saves.\n */\n resetFromFailed(): void {\n if (this.status !== 'failed') {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: cannot reset from status '${this.status}' — only failed payouts are resettable`,\n );\n }\n this.status = 'pending';\n this.paymentReference = '';\n this.paidAt = null;\n }\n\n // -------- Metadata helpers --------\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n // -------- Save-time guards --------\n\n /**\n * Save with two guards (commerce pattern):\n *\n * 1. **Totals invariant** — `totalAmountCents` must equal\n * `commissionTotalCents + adjustmentTotalCents` (exact integer\n * arithmetic, no epsilon).\n * 2. **Status transition** — validated against the AUTHORITATIVE prior\n * persisted status (re-read from the database so a\n * `create({ id, _skipLoad: true })` upsert can't sidestep the guard).\n * A `completed` payout additionally requires a payment reference,\n * matching {@link complete}'s invariant, regardless of how the status\n * was set.\n */\n override async save(): Promise<this> {\n this.validateTotals();\n const prior = await this.resolvePriorStatus();\n this.assertStatusTransition(prior);\n if (this.status === 'completed' && !this.paymentReference) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: a completed payout requires a paymentReference (use complete()).`,\n );\n }\n const result = (await super.save()) as this;\n loadedPayoutStatus.set(this, this.status);\n return result;\n }\n\n /** Throws when the totals invariant doesn't hold. */\n validateTotals(): void {\n for (const [name, value] of [\n ['commissionTotalCents', this.commissionTotalCents],\n ['adjustmentTotalCents', this.adjustmentTotalCents],\n ['totalAmountCents', this.totalAmountCents],\n ] as const) {\n if (!Number.isInteger(value)) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: ${name} must be integer cents (got ${value}).`,\n );\n }\n }\n const expected = this.commissionTotalCents + this.adjustmentTotalCents;\n if (this.totalAmountCents !== expected) {\n throw new Error(\n `CommissionPayout ${this.id ?? '<new>'}: totals invariant violated — ` +\n `commission=${this.commissionTotalCents} adjustment=${this.adjustmentTotalCents} ` +\n `total=${this.totalAmountCents} (expected total=${expected}).`,\n );\n }\n }\n\n private async resolvePriorStatus(): Promise<\n CommissionPayoutStatus | undefined\n > {\n if (this.id) {\n try {\n const row = await this.db.get(this.tableName, { id: this.id });\n if (row && row.status != null) {\n return row.status as CommissionPayoutStatus;\n }\n } catch {\n // DB not ready — fall through to the in-memory record.\n }\n }\n return loadedPayoutStatus.get(this);\n }\n\n private assertStatusTransition(\n prior: CommissionPayoutStatus | undefined,\n ): void {\n if (prior === undefined) return; // new row\n if (prior === this.status) return; // no-op re-save\n const allowed = PAYOUT_STATUS_TRANSITIONS[prior] ?? [];\n if (!allowed.includes(this.status)) {\n throw new Error(\n `CommissionPayout ${this.id}: illegal status transition '${prior}' ` +\n `→ '${this.status}'. Use approve() / markProcessing() / ` +\n 'complete() / fail() / reject() / resetFromFailed().',\n );\n }\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n return null;\n }\n}\n\nexport default CommissionPayout;\n","/**\n * CommissionPayoutCollection — collection manager for\n * {@link CommissionPayout}.\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { CommissionPayout } from '../models/CommissionPayout.js';\nimport type { CommissionPayoutStatus } from '../types.js';\n\nexport class CommissionPayoutCollection extends SmrtCollection<CommissionPayout> {\n static readonly _itemClass = CommissionPayout;\n\n /** All payout batches for an earner, newest first. */\n async findByEarner(earnerId: string): Promise<CommissionPayout[]> {\n return await this.list({\n where: { earnerId },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Payout batches by status, newest first. */\n async findByStatus(\n status: CommissionPayoutStatus,\n ): Promise<CommissionPayout[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Look up a payout by its idempotency natural key. */\n async findByIdempotencyKey(\n idempotencyKey: string,\n ): Promise<CommissionPayout | null> {\n if (!idempotencyKey) return null;\n const results = await this.list({ where: { idempotencyKey }, limit: 1 });\n return results[0] ?? null;\n }\n\n /**\n * One page of payouts carrying the derived single-source stamp for\n * `(sourceKind, sourceId)`, newest first with a deterministic id\n * tiebreak. This is the RAW indexed page — stamped rows only, membership\n * unverified. Consumers want\n * `CommissionPayoutService.getSourcePayoutHistory`, which re-verifies\n * each page's membership and fails closed on rows the stamp alone cannot\n * prove.\n */\n async findBySource(\n sourceKind: string,\n sourceId: string,\n page: { limit: number; offset: number },\n ): Promise<CommissionPayout[]> {\n if (!sourceKind || !sourceId) return [];\n return await this.list({\n where: { sourceKind, sourceId },\n orderBy: ['created_at DESC', 'id DESC'],\n limit: page.limit,\n offset: page.offset,\n });\n }\n\n /**\n * Σ totalAmountCents of COMPLETED payouts for an earner+currency —\n * lifetime settled earnings (integer cents).\n */\n async sumPaidByEarner(earnerId: string, currency: string): Promise<number> {\n const completed = await this.list({\n where: { earnerId, currency, status: 'completed' },\n });\n return completed.reduce((sum, p) => sum + p.totalAmountCents, 0);\n }\n}\n\nexport default CommissionPayoutCollection;\n","/**\n * Shared types for the neutral commissions financial core.\n *\n * Statuses are string-literal unions derived from `as const` arrays (no TS\n * enums) so downstream code can iterate the legal values and the types stay\n * erasable. All monetary fields across the module are integer cents with a\n * `*Cents` suffix; rates/fractions are decimals in the range 0–1.\n *\n * This module is the neutral financial core of `@happyvertical/smrt-sales`:\n * it never imports from the `crm` or `referrals` modules and never assumes\n * advertising, Referral, Lead, or Opportunity semantics. Earning sources are\n * generic `(sourceKind, sourceId)` string pairs.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtObjectOptions } from '@happyvertical/smrt-core';\n\n// ---------------------------------------------------------------------------\n// Status / kind vocabularies\n// ---------------------------------------------------------------------------\n\n/** Lifecycle of an {@link Earner} payout account. */\nexport const EARNER_STATUSES = ['pending', 'active', 'suspended'] as const;\nexport type EarnerStatus = (typeof EARNER_STATUSES)[number];\n\n/**\n * Lifecycle of an {@link EarnerSourceAttribution} mapping row. `inactive`\n * rows are retained for audit but never resolve through the attribution\n * lookups.\n */\nexport const EARNER_SOURCE_ATTRIBUTION_STATUSES = [\n 'active',\n 'inactive',\n] as const;\nexport type EarnerSourceAttributionStatus =\n (typeof EARNER_SOURCE_ATTRIBUTION_STATUSES)[number];\n\n/**\n * How a payout is delivered. Shared between {@link Earner} (preference) and\n * {@link CommissionPayout} (what a specific batch will use).\n */\nexport const PAYOUT_METHODS = [\n 'bank_transfer',\n 'check',\n 'paypal',\n 'credit',\n 'other',\n] as const;\nexport type PayoutMethod = (typeof PAYOUT_METHODS)[number];\n\n/**\n * Lifecycle of a versioned {@link CommissionPlan} row.\n *\n * `draft → active | retired`; `active → superseded | retired`;\n * `superseded` / `retired` are terminal. Amendments never mutate an active\n * row — they insert a new `(planKey, version + 1)` draft.\n */\nexport const COMMISSION_PLAN_STATUSES = [\n 'draft',\n 'active',\n 'superseded',\n 'retired',\n] as const;\nexport type CommissionPlanStatus = (typeof COMMISSION_PLAN_STATUSES)[number];\n\n/**\n * Lifecycle of a {@link Commission} earning record. STRICT forward chain:\n * `pending → earned → approved → payable → paid` — no skips, no reversals.\n * Corrections to earned/paid commissions are appended as\n * {@link CommissionAdjustment} rows, never edits.\n */\nexport const COMMISSION_STATUSES = [\n 'pending',\n 'earned',\n 'approved',\n 'payable',\n 'paid',\n] as const;\nexport type CommissionStatus = (typeof COMMISSION_STATUSES)[number];\n\n/** How a commission amount is derived from its earning event. */\nexport const COMMISSION_BASES = [\n 'fixed',\n 'gross',\n 'net',\n 'margin',\n 'custom',\n] as const;\nexport type CommissionBasis = (typeof COMMISSION_BASES)[number];\n\n/** Kinds of append-only {@link CommissionAdjustment} corrections. */\nexport const COMMISSION_ADJUSTMENT_KINDS = [\n 'refund',\n 'credit',\n 'chargeback',\n 'dispute',\n 'correction',\n] as const;\nexport type CommissionAdjustmentKind =\n (typeof COMMISSION_ADJUSTMENT_KINDS)[number];\n\n/**\n * Lifecycle of a {@link CommissionPayout} settlement batch.\n *\n * `pending → approved → processing → completed | failed`, with `failed`\n * reachable from `approved`/`processing` and resettable to `pending` only via\n * the dedicated `resetFromFailed()` helper. `rejected` is the terminal\n * operator-decline exit from `pending`/`approved` — rejecting releases the\n * batch's membership back to unsettled so a future batch can re-gather it\n * (`reject()` on the model mutates status only; the release lives in\n * `CommissionPayoutService.transitionPayoutForSource`).\n */\nexport const COMMISSION_PAYOUT_STATUSES = [\n 'pending',\n 'approved',\n 'processing',\n 'completed',\n 'failed',\n 'rejected',\n] as const;\nexport type CommissionPayoutStatus =\n (typeof COMMISSION_PAYOUT_STATUSES)[number];\n\n/**\n * Recommended earning-event kinds. The `EarningEvent.eventKind` field stays an\n * open string so applications can define their own commercial vocabulary —\n * these are the kinds the framework's own modules emit and recognize.\n */\nexport const EARNING_EVENT_KINDS = [\n 'conversion',\n 'agreement_execution',\n 'invoice_payment',\n 'collected_revenue',\n 'recognized_margin',\n 'milestone',\n] as const;\nexport type EarningEventKind = (typeof EARNING_EVENT_KINDS)[number];\n\n/**\n * Commission statuses whose unsettled adjustments count toward an earner's\n * net payable balance (and are gathered into payout batches). Adjustments\n * against a still-`pending` commission stay out of settlement until the\n * underlying earning clears.\n */\nexport const ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES = [\n 'earned',\n 'approved',\n 'payable',\n 'paid',\n] as const satisfies readonly CommissionStatus[];\n\n// ---------------------------------------------------------------------------\n// Plan components\n// ---------------------------------------------------------------------------\n\n/** Recurrence contract for a {@link CommissionPlanComponent}. */\nexport interface CommissionPlanComponentRecurrence {\n /** `one_time` fires at most once per earner+terms; `recurring` repeats. */\n kind: 'one_time' | 'recurring';\n /** Maximum number of occurrences for `recurring` components. */\n maxOccurrences?: number;\n /**\n * Only events whose `occurredAt` falls within `anchorAt + windowMonths`\n * qualify (the anchor — e.g. an agreement's effective date — is supplied by\n * the caller at calculation time).\n */\n windowMonths?: number;\n}\n\n/**\n * One calculation term inside a {@link CommissionPlan}'s `components` JSON\n * array. Each earning event is matched against every component whose\n * `trigger` equals the event's `eventKind` (or `'*'`).\n */\nexport interface CommissionPlanComponent {\n /** Unique key within the plan (stable across versions by convention). */\n key: string;\n /** Earning-event kind this component fires on, or `'*'` for any kind. */\n trigger: string;\n /** How the commission base amount is resolved from the event. */\n basis: CommissionBasis;\n /** Rate in the range 0–1. Required for every basis except `fixed`. */\n rate?: number;\n /** Flat amount in integer cents. Required for basis `fixed`. */\n fixedAmountCents?: number;\n /** Optional recurrence limits; omitted means unlimited. */\n recurrence?: CommissionPlanComponentRecurrence;\n /**\n * For basis `custom`: the key into the earning event's `customBases`\n * JSON map (`basisKey → cents`) that supplies the base amount.\n */\n customBasisKey?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Calculation trace\n// ---------------------------------------------------------------------------\n\n/**\n * Everything needed to reproduce a Commission's `amountCents` from first\n * principles. Persisted as a JSON string on every Commission so amounts stay\n * auditable even after plans are superseded.\n */\nexport interface CommissionCalculationTrace {\n planKey: string;\n planVersion: number;\n componentKey: string;\n basis: CommissionBasis;\n /** Base amount the rate was applied to, in integer cents. */\n baseAmountCents: number;\n /** Rate applied (0–1). Recorded as `0` for `fixed`-basis components. */\n rate: number;\n /** Split share applied (0–1; `1` for unsplit commissions). */\n shareFraction: number;\n /** Zero-based occurrence index within the component's recurrence. */\n occurrenceIndex: number;\n /** Id of the {@link EarningEvent} evidence row. */\n earningEventId: string;\n /** Rounding contract used by `roundCents()`. */\n roundingMode: 'half_away_from_zero';\n}\n\n// ---------------------------------------------------------------------------\n// Balances\n// ---------------------------------------------------------------------------\n\n/**\n * Computed (never stored) per-earner, per-currency balance snapshot.\n * All figures in integer cents.\n */\nexport interface EarnerBalance {\n earnerId: string;\n currency: string;\n /** Σ unsettled `payable` commissions. */\n payableCents: number;\n /** Σ `pending` commissions (still clearing). */\n pendingCents: number;\n /** Σ `earned` commissions (cleared, awaiting approval). */\n earnedCents: number;\n /** Σ `approved` commissions (awaiting payable release). */\n approvedCents: number;\n /**\n * Σ unsettled adjustments whose parent commission is\n * earned/approved/payable/paid (signed — clawbacks are negative).\n */\n unsettledAdjustmentCents: number;\n /** `payableCents + unsettledAdjustmentCents`. May be negative. */\n netPayableCents: number;\n}\n\n// ---------------------------------------------------------------------------\n// Model constructor options\n// ---------------------------------------------------------------------------\n\n/** Options for constructing an {@link Earner}. */\nexport interface EarnerOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n profileId?: string;\n displayName?: string;\n status?: EarnerStatus;\n payoutMethod?: PayoutMethod;\n payoutThresholdCents?: number;\n payoutScheduleKey?: string;\n currency?: string;\n metadata?: string;\n}\n\n/** Options for constructing an {@link EarnerSourceAttribution}. */\nexport interface EarnerSourceAttributionOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n earnerId?: string;\n sourceKind?: string;\n sourceId?: string;\n status?: EarnerSourceAttributionStatus;\n metadata?: string;\n}\n\n/** Options for constructing a {@link CommissionPlan}. */\nexport interface CommissionPlanOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n planKey?: string;\n version?: number;\n name?: string;\n description?: string;\n status?: CommissionPlanStatus;\n effectiveFrom?: Date | string | number | null;\n currency?: string;\n components?: string;\n metadata?: string;\n}\n\n/** Options for constructing an {@link EarningEvent}. */\nexport interface EarningEventOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n eventKind?: string;\n occurredAt?: Date | string | number;\n sourceKind?: string;\n sourceId?: string;\n grossAmountCents?: number;\n netAmountCents?: number | null;\n marginCents?: number | null;\n currency?: string;\n customBases?: string;\n dedupeKey?: string;\n metadata?: string;\n}\n\n/** Options for constructing a {@link Commission}. */\nexport interface CommissionOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n earnerId?: string;\n earningEventId?: string;\n planKey?: string;\n planVersion?: number;\n componentKey?: string;\n termsSnapshotKind?: string;\n termsSnapshotId?: string;\n basis?: CommissionBasis;\n baseAmountCents?: number;\n rate?: number;\n shareFraction?: number;\n splitGroupId?: string;\n amountCents?: number;\n currency?: string;\n status?: CommissionStatus;\n clearingEndsAt?: Date | string | number | null;\n earnedAt?: Date | string | number | null;\n approvedAt?: Date | string | number | null;\n payableAt?: Date | string | number | null;\n paidAt?: Date | string | number | null;\n payoutId?: string;\n sourceKind?: string;\n sourceId?: string;\n calculationTrace?: string;\n dedupeKey?: string;\n metadata?: string;\n}\n\n/** Options for constructing a {@link CommissionAdjustment}. */\nexport interface CommissionAdjustmentOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n commissionId?: string;\n earnerId?: string;\n adjustmentKind?: CommissionAdjustmentKind;\n amountCents?: number;\n currency?: string;\n reason?: string;\n createdByProfileId?: string;\n payoutId?: string;\n metadata?: string;\n}\n\n/** Options for constructing a {@link CommissionPayout}. */\nexport interface CommissionPayoutOptions extends SmrtObjectOptions {\n tenantId?: string | null;\n earnerId?: string;\n periodStart?: Date | string | number | null;\n periodEnd?: Date | string | number | null;\n commissionTotalCents?: number;\n adjustmentTotalCents?: number;\n totalAmountCents?: number;\n currency?: string;\n payoutMethod?: PayoutMethod;\n status?: CommissionPayoutStatus;\n paymentReference?: string;\n providerRef?: string;\n paidAt?: Date | string | number | null;\n invoiceId?: string;\n notes?: string;\n idempotencyKey?: string;\n sourceKind?: string;\n sourceId?: string;\n metadata?: string;\n}\n","/**\n * CommissionPlan — versioned commission calculation terms.\n *\n * A plan is identified by its `(planKey, version)` natural key. Versions are\n * ROWS, not edits: amending a plan inserts a `version + 1` draft (see\n * `CommissionPlanCollection.createAmendment`) and the prior version is never\n * rewritten. Once a row has been saved with status `active`, its calculation\n * identity (`components`, `currency`, `planKey`, `version`, `effectiveFrom`)\n * is frozen — re-saving with any of them changed throws (same\n * WeakMap-serialize pattern as commerce's `LicenseSale` rights snapshot).\n * Status transitions remain allowed on frozen rows.\n *\n * Generated write surface is deliberately narrow: `create` only (drafts),\n * NO generated update route — amendments are new rows, and status moves go\n * through the guarded transition methods / legal save-time edges.\n *\n * @packageDocumentation\n */\n\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport {\n COMMISSION_BASES,\n type CommissionPlanComponent,\n type CommissionPlanOptions,\n type CommissionPlanStatus,\n} from '../types.js';\n\n/**\n * Legal status transitions, keyed by the prior persisted status.\n * `draft → active | retired`; `active → superseded | retired`;\n * `superseded` / `retired` are terminal. No-op re-saves and brand-new rows\n * are always permitted; this map governs *changes* to persisted rows only\n * (commerce pattern, S5 audit #1390 lineage).\n */\nconst PLAN_STATUS_TRANSITIONS: Record<\n CommissionPlanStatus,\n CommissionPlanStatus[]\n> = {\n draft: ['active', 'retired'],\n active: ['superseded', 'retired'],\n superseded: [],\n retired: [],\n};\n\n/**\n * Module-scoped record of the status each plan instance was loaded with —\n * fallback for the save-time transition guard when the DB re-read is\n * unavailable. WeakMap keeps it out of the schema and GCs with the instance.\n */\nconst loadedPlanStatus = new WeakMap<CommissionPlan, CommissionPlanStatus>();\n\n/**\n * Module-scoped record of the frozen calculation-identity snapshot captured\n * when a plan row is (or becomes) non-draft. Same rationale as commerce\n * `LicenseSale`: an instance field would become a persisted column, a\n * `Meta<T>` would round-trip and tautologically match — a WeakMap keyed by\n * the instance has no schema interaction and GCs with the instance.\n */\nconst frozenPlanSnapshot = new WeakMap<CommissionPlan, string>();\n\n/**\n * Validate a components array. Throws a descriptive error on the first\n * violation. Exported for reuse by the calculation service's input guards\n * and by referral-terms builders that assemble component arrays.\n *\n * Rules:\n * - component keys are non-empty and unique within the plan\n * - `trigger` is a non-empty string (`'*'` matches every event kind)\n * - `basis` is one of {@link COMMISSION_BASES}\n * - basis `fixed` requires an integer `fixedAmountCents`\n * - every other basis requires `rate` in `[0, 1]`\n * - basis `custom` additionally requires a non-empty `customBasisKey`\n * - `recurrence.kind` (when present) is `one_time` or `recurring`;\n * `maxOccurrences` / `windowMonths` (when present) are positive integers\n */\nexport function validateCommissionPlanComponents(\n components: CommissionPlanComponent[],\n): void {\n if (!Array.isArray(components)) {\n throw new Error('CommissionPlan components must be an array');\n }\n const seen = new Set<string>();\n for (const component of components) {\n const label = component?.key || '<missing key>';\n if (!component || typeof component !== 'object') {\n throw new Error('CommissionPlan component must be an object');\n }\n if (!component.key || typeof component.key !== 'string') {\n throw new Error('CommissionPlan component requires a non-empty key');\n }\n if (seen.has(component.key)) {\n throw new Error(\n `CommissionPlan component keys must be unique — duplicate '${component.key}'`,\n );\n }\n seen.add(component.key);\n if (!component.trigger || typeof component.trigger !== 'string') {\n throw new Error(\n `CommissionPlan component '${label}' requires a non-empty trigger ('*' matches all kinds)`,\n );\n }\n if (!COMMISSION_BASES.includes(component.basis)) {\n throw new Error(\n `CommissionPlan component '${label}' has invalid basis '${component.basis}'`,\n );\n }\n if (component.basis === 'fixed') {\n if (\n typeof component.fixedAmountCents !== 'number' ||\n !Number.isInteger(component.fixedAmountCents)\n ) {\n throw new Error(\n `CommissionPlan component '${label}' with basis 'fixed' requires an integer fixedAmountCents`,\n );\n }\n } else {\n if (\n typeof component.rate !== 'number' ||\n !Number.isFinite(component.rate) ||\n component.rate < 0 ||\n component.rate > 1\n ) {\n throw new Error(\n `CommissionPlan component '${label}' with basis '${component.basis}' requires a rate in [0, 1]`,\n );\n }\n }\n if (component.basis === 'custom' && !component.customBasisKey) {\n throw new Error(\n `CommissionPlan component '${label}' with basis 'custom' requires a customBasisKey`,\n );\n }\n const recurrence = component.recurrence;\n if (recurrence !== undefined) {\n if (recurrence.kind !== 'one_time' && recurrence.kind !== 'recurring') {\n throw new Error(\n `CommissionPlan component '${label}' recurrence.kind must be 'one_time' or 'recurring'`,\n );\n }\n for (const [name, value] of [\n ['maxOccurrences', recurrence.maxOccurrences],\n ['windowMonths', recurrence.windowMonths],\n ] as const) {\n if (\n value !== undefined &&\n (!Number.isInteger(value) || (value as number) <= 0)\n ) {\n throw new Error(\n `CommissionPlan component '${label}' recurrence.${name} must be a positive integer`,\n );\n }\n }\n }\n }\n}\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // (tenantId, planKey, version) is the natural key — a retried create of\n // the same version upserts instead of duplicating, and two tenants can\n // both own a plan key like 'default' without colliding. NULL-tenant\n // (global) rows opt out of upsert dedup on adapters where NULLs compare\n // distinct — the PaymentIntent natural-key convention.\n conflictColumns: ['tenant_id', 'plan_key', 'version'],\n // NO generated update route: amendments are new rows\n // (CommissionPlanCollection.createAmendment) and status transitions go\n // through the guarded methods. The save-time guards below still protect\n // any server-side write path.\n api: { include: ['list', 'get', 'create'] },\n mcp: { include: ['list', 'get'] },\n cli: true,\n})\nexport class CommissionPlan extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global plans). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** Stable plan identity shared by every version of the plan. */\n @field({ required: true })\n planKey: string = '';\n\n /** Monotonic version within `planKey`. Amendments insert `max + 1`. */\n version: number = 1;\n\n /** Human-readable plan name. */\n name: string = '';\n\n /** Longer human-readable description of the terms. */\n description: string = '';\n\n /**\n * Lifecycle status — see {@link PLAN_STATUS_TRANSITIONS}. Mutate via\n * {@link activate} / {@link supersede} / {@link retire} (or a legal\n * single-step assignment; the save-time guard rejects illegal edges).\n */\n status: CommissionPlanStatus = 'draft';\n\n /** When this version takes effect. Frozen once the plan activates. */\n effectiveFrom: Date | null = null;\n\n /** ISO 4217 currency the plan's terms are denominated in. */\n currency: string = 'USD';\n\n /**\n * Calculation components as a JSON-string array — see\n * {@link CommissionPlanComponent}. Use {@link getComponents} /\n * {@link setComponents} (the setter validates).\n */\n components: string = '[]';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: CommissionPlanOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.planKey !== undefined) this.planKey = options.planKey;\n if (options.version !== undefined) this.version = options.version;\n if (options.name !== undefined) this.name = options.name;\n if (options.description !== undefined)\n this.description = options.description;\n if (options.status !== undefined) this.status = options.status;\n if (options.effectiveFrom !== undefined)\n this.effectiveFrom = CommissionPlan.coerceDate(options.effectiveFrom);\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.components !== undefined) this.components = options.components;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Re-coerce date fields after the framework reapplies raw option values,\n * record the loaded status for the transition guard, and capture the\n * frozen snapshot when the row arrived already activated. The snapshot is\n * captured for every non-draft status (not just `active`) so a superseded\n * or retired version — history — can't be rewritten either.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n this.effectiveFrom = CommissionPlan.coerceDate(this.effectiveFrom);\n if (await this.isSaved()) {\n loadedPlanStatus.set(this, this.status);\n if (this.status !== 'draft') {\n frozenPlanSnapshot.set(this, this.serializeFrozenSnapshot());\n }\n }\n return this;\n }\n\n // -------- Status predicates --------\n\n isDraft(): boolean {\n return this.status === 'draft';\n }\n\n isActive(): boolean {\n return this.status === 'active';\n }\n\n // -------- Components / metadata helpers --------\n\n /** Parse {@link components}; returns `[]` on empty/invalid JSON. */\n getComponents(): CommissionPlanComponent[] {\n if (!this.components) return [];\n try {\n const parsed = JSON.parse(this.components) as unknown;\n return Array.isArray(parsed) ? (parsed as CommissionPlanComponent[]) : [];\n } catch {\n return [];\n }\n }\n\n /**\n * Validate and store the components array. Throws on invalid components —\n * see {@link validateCommissionPlanComponents} for the rules.\n */\n setComponents(components: CommissionPlanComponent[]): void {\n validateCommissionPlanComponents(components);\n this.components = JSON.stringify(components);\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n // -------- Status transitions --------\n\n /**\n * Transition `draft → active`. Validates components first so no active\n * plan can carry malformed terms.\n */\n activate(): void {\n if (this.status !== 'draft') {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: cannot activate from status '${this.status}'`,\n );\n }\n validateCommissionPlanComponents(this.getComponents());\n this.status = 'active';\n }\n\n /** Transition `active → superseded` (a newer version took over). */\n supersede(): void {\n if (this.status !== 'active') {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: cannot supersede from status '${this.status}'`,\n );\n }\n this.status = 'superseded';\n }\n\n /** Transition `draft | active → retired` (terminal). */\n retire(): void {\n if (this.status !== 'draft' && this.status !== 'active') {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: cannot retire from status '${this.status}'`,\n );\n }\n this.status = 'retired';\n }\n\n // -------- Save-time guards --------\n\n /**\n * Save with two guards:\n *\n * 1. **Status transition** — the about-to-be-written status must be a\n * legal edge from the authoritative prior persisted status (re-read\n * from the DB so a `create({ id: <existing>, _skipLoad: true })` upsert\n * can't sidestep the guard — commerce pattern).\n * 2. **Frozen calculation identity** — once the row has been saved\n * non-draft, `components` / `currency` / `planKey` / `version` /\n * `effectiveFrom` must match the captured snapshot. Amend by inserting\n * a new version instead.\n *\n * Activating saves also re-validate components, so an `active` row always\n * carries well-formed terms regardless of which write path set them.\n */\n override async save(): Promise<this> {\n const prior = await this.resolvePriorStatus();\n this.assertStatusTransition(prior);\n this.assertFrozenIdentityUnchanged();\n await this.assertNaturalKeyNotTaken();\n if (this.status === 'active') {\n validateCommissionPlanComponents(this.getComponents());\n }\n const result = (await super.save()) as this;\n loadedPlanStatus.set(this, this.status);\n if (this.status !== 'draft' && !frozenPlanSnapshot.has(this)) {\n frozenPlanSnapshot.set(this, this.serializeFrozenSnapshot());\n }\n return result;\n }\n\n /**\n * Refuse a save whose `(tenantId, planKey, version)` natural key already\n * belongs to a DIFFERENT row. The frozen-identity guard above is\n * instance-local (WeakMap), so a FRESH instance carrying an existing\n * natural key would otherwise sail through and the conflict-column\n * upsert would rewrite the persisted terms (and rotate the row id).\n * Edit drafts by hydrating them; change terms with\n * `CommissionPlanCollection.createAmendment()`.\n */\n private async assertNaturalKeyNotTaken(): Promise<void> {\n if (!this.planKey) return;\n try {\n const res = await this.db.query(\n `SELECT id, tenant_id FROM ${this.tableName} WHERE plan_key = $1 AND version = $2`,\n this.planKey,\n this.version,\n );\n const rows = Array.isArray(res)\n ? (res as Record<string, unknown>[])\n : ((res as { rows?: Record<string, unknown>[] }).rows ?? []);\n const taken = rows.find(\n (row) =>\n (row.tenant_id ?? null) === (this.tenantId ?? null) &&\n row.id !== this.id,\n );\n if (taken) {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: this version ` +\n 'already exists for the tenant — plan versions are immutable ' +\n 'records. Hydrate the existing row to edit a draft, or create ' +\n 'new terms with CommissionPlanCollection.createAmendment().',\n );\n }\n } catch (error) {\n if (error instanceof Error && error.message.includes('immutable')) {\n throw error;\n }\n // DB not ready / table absent — nothing persisted to collide with.\n }\n }\n\n /**\n * Resolve the AUTHORITATIVE prior status from the database; fall back to\n * the loaded-status WeakMap only when the DB is unavailable. `undefined`\n * means no persisted row exists (genuinely new).\n */\n private async resolvePriorStatus(): Promise<\n CommissionPlanStatus | undefined\n > {\n if (this.id) {\n try {\n const row = await this.db.get(this.tableName, { id: this.id });\n if (row && row.status != null) {\n return row.status as CommissionPlanStatus;\n }\n } catch {\n // DB not ready — fall through to the in-memory record.\n }\n }\n return loadedPlanStatus.get(this);\n }\n\n private assertStatusTransition(\n prior: CommissionPlanStatus | undefined,\n ): void {\n if (prior === undefined) return; // new row — any starting status\n if (prior === this.status) return; // no-op re-save\n const allowed = PLAN_STATUS_TRANSITIONS[prior] ?? [];\n if (!allowed.includes(this.status)) {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: illegal status ` +\n `transition '${prior}' → '${this.status}'. Use activate() / ` +\n 'supersede() / retire().',\n );\n }\n }\n\n private assertFrozenIdentityUnchanged(): void {\n const captured = frozenPlanSnapshot.get(this);\n if (!captured) return;\n const current = this.serializeFrozenSnapshot();\n if (captured !== current) {\n throw new Error(\n `CommissionPlan ${this.planKey}@${this.version}: components, ` +\n 'currency, planKey, version, and effectiveFrom are immutable once ' +\n 'the plan has been active. Create an amendment ' +\n '(CommissionPlanCollection.createAmendment) instead of editing ' +\n 'this version.',\n );\n }\n }\n\n private serializeFrozenSnapshot(): string {\n // Stable key ordering so a no-op re-serialization matches.\n return JSON.stringify({\n planKey: this.planKey,\n version: this.version,\n currency: this.currency,\n components: this.components,\n effectiveFrom: this.effectiveFrom\n ? this.effectiveFrom.toISOString()\n : null,\n });\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n return null;\n }\n}\n\nexport default CommissionPlan;\n","/**\n * CommissionPlanCollection — collection manager for {@link CommissionPlan}.\n *\n * Plans are versioned rows: amendments insert `(planKey, maxVersion + 1)`\n * drafts via {@link createAmendment}; existing versions are never rewritten.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { assertTenantReadAllowed } from '@happyvertical/smrt-tenancy';\nimport {\n CommissionPlan,\n validateCommissionPlanComponents,\n} from '../models/CommissionPlan.js';\nimport type {\n CommissionPlanComponent,\n CommissionPlanStatus,\n} from '../types.js';\n\n/**\n * Fields an amendment may change relative to the version it copies.\n * `planKey` is fixed (it identifies the plan), `version` is computed, and\n * `status` is always `draft` — a caller cannot mint a pre-activated\n * amendment.\n */\nexport interface CommissionPlanAmendmentChanges {\n name?: string;\n description?: string;\n components?: CommissionPlanComponent[];\n currency?: string;\n effectiveFrom?: Date | null;\n metadata?: Record<string, unknown>;\n}\n\nexport class CommissionPlanCollection extends SmrtCollection<CommissionPlan> {\n static readonly _itemClass = CommissionPlan;\n\n /** Every version of a plan, newest version first. */\n async findByPlanKey(planKey: string): Promise<CommissionPlan[]> {\n return await this.list({\n where: { planKey },\n orderBy: 'version DESC',\n });\n }\n\n /** Plans by status. */\n async findByStatus(status: CommissionPlanStatus): Promise<CommissionPlan[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /**\n * The highest ACTIVE version of a plan already IN EFFECT at `at`, or\n * `null` when none is. This is what calculation callers resolve terms\n * from when no frozen snapshot pins a specific version. A future-dated\n * amendment can be activated ahead of its effective date without\n * governing earlier qualifications (`effectiveFrom: null` = effective\n * immediately).\n */\n async latestActiveByKey(\n planKey: string,\n at: Date = new Date(),\n tenantId?: string | null,\n ): Promise<CommissionPlan | null> {\n if (tenantId === undefined) {\n // No explicit scope: ambient tenant scoping (when present) applies.\n const results = await this.list({\n where: { planKey, status: 'active' },\n orderBy: 'version DESC',\n });\n return (\n results.find(\n (plan) => plan.effectiveFrom === null || plan.effectiveFrom <= at,\n ) ?? null\n );\n }\n\n // Explicit scope (system/background paths run without ambient tenant\n // context): the tenant's own versions form their own key-space; global\n // (NULL-tenant) versions are the fallback. Never another tenant's.\n const atIso = at.toISOString();\n if (tenantId === null) {\n const results = await this.query(\n `SELECT * FROM ${this.tableName}\n WHERE tenant_id IS NULL\n AND plan_key = ?\n AND status = ?\n AND (effective_from IS NULL OR effective_from <= ?)\n ORDER BY version DESC\n LIMIT 1`,\n [planKey, 'active', atIso],\n { allowRawOnTenantScoped: true },\n );\n return results[0] ?? null;\n }\n\n assertTenantReadAllowed(\n tenantId,\n 'CommissionPlanCollection.latestActiveByKey',\n );\n const results = await this.query(\n `SELECT * FROM ${this.tableName}\n WHERE (tenant_id = ? OR tenant_id IS NULL)\n AND plan_key = ?\n AND status = ?\n AND (effective_from IS NULL OR effective_from <= ?)\n ORDER BY CASE WHEN tenant_id = ? THEN 0 ELSE 1 END, version DESC\n LIMIT 1`,\n [tenantId, planKey, 'active', atIso, tenantId],\n { allowRawOnTenantScoped: true },\n );\n return results[0] ?? null;\n }\n\n /**\n * Create an amendment: insert a new DRAFT row with\n * `version = max(existing versions) + 1`, copying the latest existing\n * version's fields and then applying `changes`. The source version is not\n * touched — activate the draft (and supersede the prior active version)\n * as a separate, explicit step.\n *\n * Throws when no version of `planKey` exists (nothing to amend — use\n * `create` for a brand-new plan).\n */\n async createAmendment(\n planKey: string,\n changes: CommissionPlanAmendmentChanges = {},\n ): Promise<CommissionPlan> {\n const versions = await this.findByPlanKey(planKey);\n const latest = versions[0];\n if (!latest) {\n throw new Error(\n `CommissionPlanCollection.createAmendment: no versions exist for plan key '${planKey}' — create the plan first`,\n );\n }\n // Validate amended components BEFORE persisting anything, so a bad\n // amendment fails cleanly instead of leaving a malformed draft row.\n if (changes.components !== undefined) {\n validateCommissionPlanComponents(changes.components);\n }\n\n const draft = await this.create({\n tenantId: latest.tenantId,\n planKey,\n version: latest.version + 1,\n status: 'draft',\n name: changes.name ?? latest.name,\n description: changes.description ?? latest.description,\n currency: changes.currency ?? latest.currency,\n effectiveFrom:\n changes.effectiveFrom !== undefined\n ? changes.effectiveFrom\n : latest.effectiveFrom,\n components:\n changes.components !== undefined\n ? JSON.stringify(changes.components)\n : latest.components,\n metadata:\n changes.metadata !== undefined\n ? JSON.stringify(changes.metadata)\n : latest.metadata,\n });\n return draft;\n }\n}\n\nexport default CommissionPlanCollection;\n","/**\n * Earner — neutral financial payout account.\n *\n * Replaces the financial half of legacy smrt-affiliates' `Partner`: it holds\n * everything money-related about a party that earns commissions (payout\n * method, threshold, schedule, currency, status) and NOTHING role-related.\n * Role models (a CRM `SalesRepresentative`, a referrals `Referrer`, or any\n * application-defined role) each hold their own `earnerId` pointing here, so\n * one person acting in several roles still settles through a single account.\n *\n * @packageDocumentation\n */\n\nimport { crossPackageRef, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { EarnerOptions, EarnerStatus, PayoutMethod } from '../types.js';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n api: { include: ['list', 'get', 'create', 'update'] },\n mcp: { include: ['list', 'get', 'create'] },\n cli: true,\n})\nexport class Earner extends SmrtObject {\n /**\n * Tenant ID for multi-tenant isolation. Nullable so global/operator-level\n * earners remain possible; unlike legacy affiliates, sales earners are\n * tenant-owned by default.\n */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * Identity link to a smrt-profiles Profile (cross-package string\n * reference — never a DDL foreign key).\n */\n @crossPackageRef('@happyvertical/smrt-profiles:Profile')\n profileId: string = '';\n\n /** Human-readable display name for portals and operator views. */\n displayName: string = '';\n\n /** Account lifecycle: `pending` (default) → `active` / `suspended`. */\n status: EarnerStatus = 'pending';\n\n /** Preferred payout delivery method. */\n payoutMethod: PayoutMethod = 'bank_transfer';\n\n /**\n * Minimum unsettled balance (integer cents) before a payout batch is\n * created. Default $50.00 = 5000 cents.\n */\n payoutThresholdCents: number = 5000;\n\n /**\n * Payout cadence key. Open string so applications can define their own\n * schedules (`manual`, `monthly`, `weekly`, `net_30`, …); `manual` means\n * an operator triggers batches explicitly.\n */\n payoutScheduleKey: string = 'manual';\n\n /** ISO 4217 currency all of this earner's balances settle in. */\n currency: string = 'USD';\n\n /**\n * Additional metadata as a JSON string (tax info, payout-rail details,\n * …). Use {@link getMetadata}/{@link setMetadata}.\n */\n metadata: string = '{}';\n\n constructor(options: EarnerOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.profileId !== undefined) this.profileId = options.profileId;\n if (options.displayName !== undefined)\n this.displayName = options.displayName;\n if (options.status !== undefined) this.status = options.status;\n if (options.payoutMethod !== undefined)\n this.payoutMethod = options.payoutMethod;\n if (options.payoutThresholdCents !== undefined)\n this.payoutThresholdCents = options.payoutThresholdCents;\n if (options.payoutScheduleKey !== undefined)\n this.payoutScheduleKey = options.payoutScheduleKey;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n isActive(): boolean {\n return this.status === 'active';\n }\n\n isPending(): boolean {\n return this.status === 'pending';\n }\n\n isSuspended(): boolean {\n return this.status === 'suspended';\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n}\n\nexport default Earner;\n","/**\n * EarnerCollection — collection manager for {@link Earner}.\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { Earner } from '../models/Earner.js';\nimport type { EarnerStatus } from '../types.js';\n\nexport class EarnerCollection extends SmrtCollection<Earner> {\n static readonly _itemClass = Earner;\n\n /** Earners linked to a smrt-profiles Profile. */\n async findByProfile(profileId: string): Promise<Earner[]> {\n return await this.list({\n where: { profileId },\n orderBy: 'created_at DESC',\n });\n }\n\n /** Earners by status. */\n async findByStatus(status: EarnerStatus): Promise<Earner[]> {\n return await this.list({\n where: { status },\n orderBy: 'created_at DESC',\n });\n }\n\n /** All active earners. */\n async findActive(): Promise<Earner[]> {\n return await this.findByStatus('active');\n }\n}\n\nexport default EarnerCollection;\n","/**\n * EarnerSourceAttribution — indexed external attribution mapping for an\n * {@link Earner}.\n *\n * Maps a generic external key `(sourceKind, sourceId)` — an ad-network\n * property, a marketplace storefront, a partner account, any\n * application-defined attribution surface — to the Earner credited for it.\n * High-volume ingestion resolves earners through the indexed lookups on\n * `EarnerSourceAttributionCollection` / `EarnerAttributionService` instead of\n * scanning every active earner's JSON metadata.\n *\n * The kind space is the CONSUMER's to define. It may — but need not —\n * coincide with the `(sourceKind, sourceId)` earning-source pairs recorded on\n * EarningEvents and Commissions: an application can attribute earners by\n * property while its earning events carry the network as their source.\n *\n * ## Uniqueness and tenancy\n *\n * Natural key `(tenant_id, source_kind, source_id)` (`conflictColumns`): one\n * mapping per external key per tenant. A `create` for an existing key\n * UPSERTS — it re-points the mapping to the new `earnerId` (idempotent\n * registration; use `EarnerAttributionService.registerAttribution` to observe\n * whether a call created or re-pointed). The adapters' null-aware upsert\n * dedups NULL-tenant (global) keys too, but the unique INDEX itself treats\n * NULLs as distinct, so duplicate global rows can still arrive outside the\n * model layer (raw-SQL imports, pre-null-aware data) — the lookups treat\n * more than one ACTIVE row for a key as ambiguous and fail closed instead\n * of picking one.\n *\n * With an active tenant context, lookups resolve within that tenant only\n * (global rows are invisible). Without tenant context (`optional` mode)\n * lookups see every row, so operator-level resolution across tenants can\n * surface an ambiguity that per-tenant resolution would not — tenant-scoped\n * applications should resolve inside `withTenant()`.\n *\n * ## Migrating metadata-based associations\n *\n * Consumers that previously stashed the association in `Earner.metadata`\n * migrate with a one-time loop — `registerAttribution` is the idempotent\n * backfill primitive (re-running the loop upserts, never duplicates):\n *\n * ```typescript\n * const service = await EarnerAttributionService.create({ db });\n * for (const earner of await earners.list({})) {\n * const propertyIds = (earner.getMetadata().propertyIds ?? []) as string[];\n * for (const propertyId of propertyIds) {\n * await service.registerAttribution({\n * earnerId: earner.id!,\n * sourceKind: 'ad_network_property',\n * sourceId: propertyId,\n * tenantId: earner.tenantId,\n * });\n * }\n * }\n * // Verify via resolveActiveEarnersBySources(), then drop the metadata key.\n * ```\n *\n * @packageDocumentation\n */\n\nimport { field, foreignKey, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport {\n getCurrentTenant,\n TenantScoped,\n tenantId,\n} from '@happyvertical/smrt-tenancy';\nimport type {\n EarnerSourceAttributionOptions,\n EarnerSourceAttributionStatus,\n} from '../types.js';\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // One mapping per external key per tenant — a retried registration\n // upserts (re-points) instead of duplicating. NULL-tenant rows opt out of\n // dedup (see the class doc); the lookups fail closed on the resulting\n // ambiguity.\n conflictColumns: ['tenant_id', 'source_kind', 'source_id'],\n // Configuration rows: full read plus create/update (deactivate via\n // status). No generated delete on ANY surface — deactivation preserves\n // the audit trail of who was credited for a surface (a bare `cli: true`\n // would regenerate the delete verb this contract closes).\n api: { include: ['list', 'get', 'create', 'update'] },\n mcp: { include: ['list', 'get', 'create'] },\n cli: { include: ['list', 'get', 'create', 'update'] },\n})\nexport class EarnerSourceAttribution extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global mappings). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /** The {@link Earner} credited for this external key. Required. */\n @foreignKey('Earner', { required: true })\n earnerId: string = '';\n\n /**\n * Consumer-defined attribution kind (`ad_network_property`,\n * `marketplace_storefront`, …). Required.\n */\n @field({ required: true })\n sourceKind: string = '';\n\n /**\n * External identifier within {@link sourceKind}. Required. Indexed so\n * batched ingestion lookups stay bounded by the requested ids even\n * without a tenant predicate (the natural-key index is led by\n * `tenant_id`, which tenant-context lookups use).\n */\n @field({ required: true, indexed: true })\n sourceId: string = '';\n\n /**\n * Mapping lifecycle: only `active` rows resolve through the lookups.\n * `inactive` retains the row for audit.\n */\n status: EarnerSourceAttributionStatus = 'active';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: EarnerSourceAttributionOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.earnerId !== undefined) this.earnerId = options.earnerId;\n if (options.sourceKind !== undefined) this.sourceKind = options.sourceKind;\n if (options.sourceId !== undefined) this.sourceId = options.sourceId;\n if (options.status !== undefined) this.status = options.status;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n isActive(): boolean {\n return this.status === 'active';\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n /**\n * Save with two guards:\n *\n * 1. **Completeness** — a mapping without an earner or a full external\n * key can never resolve, so it must never persist.\n * 2. **Tenant coherence** — the mapping's tenant must equal its earner's\n * tenant (both normalized; `''` and `NULL` mean \"no tenant\"). A tenant\n * A mapping crediting a tenant B earner would be unresolvable in\n * tenant scope yet credit across tenants in operator scope — fail\n * closed at the model boundary, for the generated create/update\n * surface as much as the service. The earner row is read RAW (no\n * tenant interception) because the guard must see the earner's true\n * tenant even when saving from another tenant's context.\n */\n override async save(): Promise<this> {\n if (!this.earnerId || !this.sourceKind || !this.sourceId) {\n throw new Error(\n `EarnerSourceAttribution ${this.id ?? '<new>'}: earnerId, ` +\n 'sourceKind, and sourceId are all required.',\n );\n }\n await this.assertEarnerTenantCoherence();\n return (await super.save()) as this;\n }\n\n private async assertEarnerTenantCoherence(): Promise<void> {\n let earnerRow: Record<string, unknown> | null = null;\n try {\n earnerRow = await this.db.get('earners', { id: this.earnerId });\n } catch {\n // DB not ready / earners table absent — nothing to compare against\n // (the FK layer owns pure existence).\n return;\n }\n if (!earnerRow) {\n throw new Error(\n `EarnerSourceAttribution ${this.id ?? '<new>'}: earner ` +\n `'${this.earnerId}' does not exist.`,\n );\n }\n const tenantOf = (value: unknown) => (value ? String(value) : null);\n const earnerTenant = tenantOf(earnerRow.tenant_id);\n // Compare against the EFFECTIVE tenant: an unset tenantId is\n // auto-stamped from the active tenant context by the tenancy\n // interceptor during save, after this guard runs.\n const mappingTenant =\n tenantOf(this.tenantId) ?? tenantOf(getCurrentTenant()?.tenantId);\n if (earnerTenant !== mappingTenant) {\n throw new Error(\n `EarnerSourceAttribution ${this.id ?? '<new>'}: mapping tenant ` +\n `'${mappingTenant ?? 'global'}' does not match earner tenant ` +\n `'${earnerTenant ?? 'global'}' — a mapping must live in its ` +\n \"earner's tenant.\",\n );\n }\n }\n}\n\nexport default EarnerSourceAttribution;\n","/**\n * EarnerSourceAttributionCollection — collection manager for\n * {@link EarnerSourceAttribution}.\n *\n * The queries here are the indexed primitives; the earner-resolving lookups\n * (single + batched, active-earner filtered, ambiguity fail-closed) live on\n * `EarnerAttributionService`.\n *\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { EarnerSourceAttribution } from '../models/EarnerSourceAttribution.js';\n\nexport class EarnerSourceAttributionCollection extends SmrtCollection<EarnerSourceAttribution> {\n static readonly _itemClass = EarnerSourceAttribution;\n\n /** Every mapping for one external key (any status), oldest first. */\n async findBySource(\n sourceKind: string,\n sourceId: string,\n ): Promise<EarnerSourceAttribution[]> {\n if (!sourceKind || !sourceId) return [];\n return await this.list({\n where: { sourceKind, sourceId },\n orderBy: 'created_at ASC',\n });\n }\n\n /**\n * Every mapping for a batch of external keys sharing one kind (any\n * status), in one indexed `IN` query. Empty/duplicate ids are dropped;\n * an empty batch returns `[]` without querying.\n */\n async findBySources(\n sourceKind: string,\n sourceIds: string[],\n ): Promise<EarnerSourceAttribution[]> {\n if (!sourceKind) return [];\n const ids = [...new Set(sourceIds.filter(Boolean))];\n if (ids.length === 0) return [];\n return await this.list({\n where: { sourceKind, sourceId: ids },\n orderBy: 'created_at ASC',\n });\n }\n\n /** All mappings held by one earner (any status), oldest first. */\n async findByEarner(earnerId: string): Promise<EarnerSourceAttribution[]> {\n return await this.list({\n where: { earnerId },\n orderBy: 'created_at ASC',\n });\n }\n}\n\nexport default EarnerSourceAttributionCollection;\n","/**\n * EarningEvent — immutable commercial-event evidence.\n *\n * An EarningEvent records that something commission-worthy happened: a\n * conversion, an agreement execution, an invoice payment, collected revenue,\n * recognized margin, a milestone — or any application-defined kind\n * (`eventKind` is an open string; see `EARNING_EVENT_KINDS` for the\n * recommended vocabulary). The source is a generic `(sourceKind, sourceId)`\n * string pair — this module never assumes advertising, Referral, Lead, or\n * Opportunity semantics.\n *\n * Immutability contract: events are evidence. The generated surface exposes\n * `create`/`list`/`get` only (no update, no delete), and application code\n * must treat persisted rows as append-only — commissions reference an\n * event's amounts in their calculation traces, so rewriting an event would\n * silently orphan the audit trail. Corrections are modelled as NEW events\n * (e.g. a `refund`-kind event) or as `CommissionAdjustment` rows.\n *\n * Idempotent ingestion: `dedupeKey` is the natural key\n * (`conflictColumns: ['dedupe_key']`). Callers embed tenant and source\n * identity in the key — e.g.\n * `` `${tenantId}:${sourceKind}:${sourceId}:${eventKind}:${occurrence}` `` —\n * so a retried ingest resolves to the existing row (see\n * `EarningEventCollection.getOrCreateByDedupeKey`).\n *\n * @packageDocumentation\n */\n\nimport { field, SmrtObject, smrt } from '@happyvertical/smrt-core';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport type { EarningEventOptions } from '../types.js';\n\n/**\n * Serialized state of each persisted instance, for the save-time\n * immutability guard (WeakMap keeps it out of the schema and GCs with the\n * instance — the ReferralTermSnapshot pattern).\n */\nconst persistedEventState = new WeakMap<EarningEvent, string>();\n\n@TenantScoped({ mode: 'optional' })\n@smrt({\n // Natural key for idempotent ingestion — a retried create with the same\n // dedupeKey upserts instead of duplicating.\n conflictColumns: ['dedupe_key'],\n // Immutable evidence: create/list/get only — no update or delete on any\n // generated surface.\n api: { include: ['create', 'list', 'get'] },\n mcp: { include: ['list', 'create'] },\n // High-volume evidence rows are not useful from the CLI.\n cli: false,\n})\nexport class EarningEvent extends SmrtObject {\n /** Tenant ID for multi-tenant isolation (nullable → global events). */\n @tenantId({ nullable: true })\n tenantId: string | null = null;\n\n /**\n * What kind of commercial event this is. Open string — see\n * `EARNING_EVENT_KINDS` for the recommended vocabulary. Plan components\n * match on this via their `trigger`.\n */\n @field({ required: true })\n eventKind: string = '';\n\n /** When the commercial event occurred (not when it was ingested). */\n occurredAt: Date = new Date();\n\n /**\n * Generic earning-source discriminator (`referral`, `opportunity`,\n * `subscription`, `ad_event`, …). Free-form; this module attaches no\n * semantics to it.\n */\n sourceKind: string = '';\n\n /** Id of the source record named by {@link sourceKind}. */\n sourceId: string = '';\n\n /** Gross amount of the event in integer cents. */\n grossAmountCents: number = 0;\n\n /**\n * Net amount in integer cents, when the ingesting system defines one.\n * `null` means \"net is not defined for this event\" — `net`-basis\n * components then SKIP rather than falling back to gross (net is never\n * derived).\n */\n @field({ type: 'integer', nullable: true })\n netAmountCents: number | null = null;\n\n /**\n * Recognized margin in integer cents, when defined. `null` skips\n * `margin`-basis components — margin is never derived.\n */\n @field({ type: 'integer', nullable: true })\n marginCents: number | null = null;\n\n /** ISO 4217 currency of the event's amounts. */\n currency: string = 'USD';\n\n /**\n * JSON map of `basisKey → integer cents` for `custom`-basis plan\n * components. Use {@link getCustomBases}/{@link setCustomBases}.\n */\n customBases: string = '{}';\n\n /**\n * Idempotency natural key. Required. Callers embed tenant/source identity\n * (see the class doc) — the framework does not synthesize it.\n */\n @field({ required: true })\n dedupeKey: string = '';\n\n /** Additional metadata as a JSON string. */\n metadata: string = '{}';\n\n constructor(options: EarningEventOptions = {}) {\n super(options);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.eventKind !== undefined) this.eventKind = options.eventKind;\n if (options.occurredAt !== undefined)\n this.occurredAt =\n EarningEvent.coerceDate(options.occurredAt) ?? new Date();\n if (options.sourceKind !== undefined) this.sourceKind = options.sourceKind;\n if (options.sourceId !== undefined) this.sourceId = options.sourceId;\n if (options.grossAmountCents !== undefined)\n this.grossAmountCents = options.grossAmountCents;\n if (options.netAmountCents !== undefined)\n this.netAmountCents = options.netAmountCents;\n if (options.marginCents !== undefined)\n this.marginCents = options.marginCents;\n if (options.currency !== undefined) this.currency = options.currency;\n if (options.customBases !== undefined)\n this.customBases = options.customBases;\n if (options.dedupeKey !== undefined) this.dedupeKey = options.dedupeKey;\n if (options.metadata !== undefined) this.metadata = options.metadata;\n }\n\n /**\n * Re-coerce {@link occurredAt} after the framework reapplies raw option /\n * hydrated row values (SQLite hands back ISO strings), and capture the\n * persisted state for the immutability guard when this instance hydrated\n * an existing row.\n */\n override async initialize(): Promise<this> {\n await super.initialize();\n this.occurredAt = EarningEvent.coerceDate(this.occurredAt) ?? new Date();\n if (await this.isSaved()) {\n persistedEventState.set(this, this.serializeState());\n }\n return this;\n }\n\n /**\n * Save with the evidence-immutability guard. EarningEvents are immutable\n * commercial evidence; three write vectors are closed:\n *\n * - a HYDRATED persisted row must serialize identically to its captured\n * state (no-op re-saves pass, any change throws);\n * - an instance carrying an existing id WITHOUT having hydrated it\n * (`create({ id, _skipLoad: true })`) is rejected outright;\n * - a NEW instance whose `dedupeKey` already belongs to another row is\n * refused outright: the natural-key upsert would not only rewrite the\n * evidence values but ROTATE the row's id (orphaning any Commission\n * whose `earningEventId` points at it). Idempotent ingestion goes\n * through `EarningEventCollection.getOrCreateByDedupeKey()`, which\n * finds first and never upserts.\n */\n override async save(): Promise<this> {\n const captured = persistedEventState.get(this);\n if (captured !== undefined) {\n if (captured !== this.serializeState()) {\n throw new Error(\n `EarningEvent ${this.id ?? '<new>'}: earning events are immutable ` +\n 'evidence — record a correcting event (or a CommissionAdjustment ' +\n 'downstream) instead of editing this row.',\n );\n }\n } else if (this.id && (await this.isSaved())) {\n throw new Error(\n `EarningEvent ${this.id}: refusing to overwrite an existing event ` +\n 'row from a non-hydrated instance — earning events are immutable ' +\n 'evidence.',\n );\n } else if (this.dedupeKey) {\n // Fresh instance (create() pre-assigns an id, so key off \"no captured\n // state and not a persisted id\" rather than a missing id): its\n // natural key may collide with existing evidence via the upsert.\n try {\n const row = await this.db.get(this.tableName, {\n dedupe_key: this.dedupeKey,\n });\n if (row && row.id !== this.id) {\n throw new Error(\n `EarningEvent (dedupeKey '${this.dedupeKey}'): an event with ` +\n 'this dedupe key already exists — earning events are ' +\n 'immutable evidence, and the natural-key upsert would rotate ' +\n \"the existing row's id (orphaning commissions that reference \" +\n 'it). Use EarningEventCollection.getOrCreateByDedupeKey() ' +\n 'for idempotent ingestion, or record a new event under its ' +\n 'own dedupe key.',\n );\n }\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes('immutable evidence')\n ) {\n throw error;\n }\n // DB not ready / table absent — nothing persisted to protect yet.\n }\n }\n const result = (await super.save()) as this;\n persistedEventState.set(this, this.serializeState());\n return result;\n }\n\n private serializeState(): string {\n // Stable key ordering so a no-op re-serialization matches.\n return JSON.stringify({\n tenantId: this.tenantId,\n eventKind: this.eventKind,\n occurredAt: this.occurredAt.toISOString(),\n sourceKind: this.sourceKind,\n sourceId: this.sourceId,\n grossAmountCents: this.grossAmountCents,\n netAmountCents: this.netAmountCents,\n marginCents: this.marginCents,\n currency: this.currency,\n customBases: this.customBases,\n dedupeKey: this.dedupeKey,\n metadata: this.metadata,\n });\n }\n\n /**\n * Parse {@link customBases} into a `basisKey → cents` map; non-numeric\n * values are dropped. Returns `{}` on empty/invalid JSON.\n */\n getCustomBases(): Record<string, number> {\n if (!this.customBases) return {};\n try {\n const parsed = JSON.parse(this.customBases) as unknown;\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return {};\n }\n const out: Record<string, number> = {};\n for (const [key, value] of Object.entries(\n parsed as Record<string, unknown>,\n )) {\n if (typeof value === 'number' && Number.isFinite(value)) {\n out[key] = value;\n }\n }\n return out;\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link customBases}. */\n setCustomBases(bases: Record<string, number>): void {\n this.customBases = JSON.stringify(bases ?? {});\n }\n\n /** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */\n getMetadata(): Record<string, unknown> {\n if (!this.metadata) return {};\n try {\n const parsed = JSON.parse(this.metadata) as unknown;\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n }\n\n /** Serialize and store {@link metadata}. */\n setMetadata(data: Record<string, unknown>): void {\n this.metadata = JSON.stringify(data ?? {});\n }\n\n private static coerceDate(value: unknown): Date | null {\n if (value == null) return null;\n if (value instanceof Date) return value;\n if (typeof value === 'number' || typeof value === 'string') {\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n }\n return null;\n }\n}\n\nexport default EarningEvent;\n","/**\n * EarningEventCollection — collection manager for {@link EarningEvent}.\n * @packageDocumentation\n */\n\nimport { SmrtCollection } from '@happyvertical/smrt-core';\nimport { EarningEvent } from '../models/EarningEvent.js';\nimport type { EarningEventOptions } from '../types.js';\n\nexport class EarningEventCollection extends SmrtCollection<EarningEvent> {\n static readonly _itemClass = EarningEvent;\n\n /** Look up an event by its idempotency natural key. */\n async findByDedupeKey(dedupeKey: string): Promise<EarningEvent | null> {\n if (!dedupeKey) return null;\n const results = await this.list({ where: { dedupeKey }, limit: 1 });\n return results[0] ?? null;\n }\n\n /**\n * Idempotent ingestion: if an event with `options.dedupeKey` already\n * exists, return it untouched (`created: false`) — evidence is immutable,\n * so a replay never updates the stored row. Otherwise create the event.\n *\n * `dedupeKey` is required — callers embed tenant/source identity in it\n * (e.g. `` `${tenantId}:${sourceKind}:${sourceId}:${eventKind}` ``);\n * an empty key would silently disable idempotency, so it throws instead.\n */\n async getOrCreateByDedupeKey(\n options: EarningEventOptions,\n ): Promise<{ event: EarningEvent; created: boolean }> {\n const dedupeKey = options.dedupeKey ?? '';\n if (!dedupeKey) {\n throw new Error(\n 'EarningEventCollection.getOrCreateByDedupeKey requires a dedupeKey',\n );\n }\n const existing = await this.findByDedupeKey(dedupeKey);\n if (existing) {\n return { event: existing, created: false };\n }\n // Coerce the Date-ish option here so the create input is a real Date\n // (the model would coerce at initialize anyway; this keeps types exact).\n const { occurredAt, ...rest } = options;\n const event = await this.create({\n ...rest,\n ...(occurredAt !== undefined ? { occurredAt: new Date(occurredAt) } : {}),\n });\n return { event, created: true };\n }\n\n /** Events for one generic earning source, newest occurrence first. */\n async findBySource(\n sourceKind: string,\n sourceId: string,\n ): Promise<EarningEvent[]> {\n return await this.list({\n where: { sourceKind, sourceId },\n orderBy: 'occurred_at DESC',\n });\n }\n\n /** Events by kind, newest occurrence first. */\n async findByKind(eventKind: string): Promise<EarningEvent[]> {\n return await this.list({\n where: { eventKind },\n orderBy: 'occurred_at DESC',\n });\n }\n}\n\nexport default EarningEventCollection;\n","/**\n * Integer-cents money helpers for the commissions module.\n *\n * Every monetary field in this module is stored as integer cents (`*Cents`\n * suffix). Rounding happens exactly once per calculation step via\n * {@link roundCents} using half-away-from-zero semantics, and every\n * Commission persists a `calculationTrace` naming that rounding mode so\n * amounts stay reproducible.\n *\n * @packageDocumentation\n */\n\n/**\n * Round to the nearest integer cent, half away from zero.\n *\n * `Math.round` alone rounds -2.5 to -2 (half toward +∞); financial\n * conventions want symmetric behaviour, so the sign is factored out first:\n * `Math.sign(v) * Math.round(Math.abs(v))` → `roundCents(2.5) === 3` and\n * `roundCents(-2.5) === -3`.\n */\nexport function roundCents(value: number): number {\n const rounded = Math.sign(value) * Math.round(Math.abs(value));\n // Normalize -0 to 0 so strict equality (Object.is) comparisons behave.\n return rounded === 0 ? 0 : rounded;\n}\n\n/** Convert integer cents to a decimal major-unit amount (`/ 100`). */\nexport function centsToAmount(cents: number): number {\n return cents / 100;\n}\n\n/**\n * Convert a decimal major-unit amount to integer cents, rounding half away\n * from zero (`roundCents(amount * 100)`).\n */\nexport function amountToCents(amount: number): number {\n return roundCents(amount * 100);\n}\n\n/**\n * Calculate a commission amount in integer cents:\n * `roundCents(baseCents * rate * (shareFraction ?? 1))`.\n *\n * Rounding is applied once, on the final product, so split siblings each\n * round independently and their sum can differ from the unsplit amount by at\n * most one cent per sibling — the calculation trace records the inputs so any\n * such drift is auditable.\n *\n * @param baseCents - Base amount in integer cents\n * @param rate - Commission rate (0–1)\n * @param shareFraction - Optional split share (0–1); defaults to 1\n */\nexport function calculateCommissionAmountCents(\n baseCents: number,\n rate: number,\n shareFraction?: number,\n): number {\n return roundCents(baseCents * rate * (shareFraction ?? 1));\n}\n","/** Tenant-safe, idempotent creation of immutable commission adjustments. */\n\nimport { randomUUID } from 'node:crypto';\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport {\n requireTenantId,\n TenantContextError,\n} from '@happyvertical/smrt-tenancy';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { CommissionAdjustmentCollection } from '../collections/CommissionAdjustmentCollection.js';\nimport { CommissionAdjustmentOperationCollection } from '../collections/CommissionAdjustmentOperationCollection.js';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport { EarnerCollection } from '../collections/EarnerCollection.js';\nimport type { CommissionAdjustment } from '../models/CommissionAdjustment.js';\nimport {\n COMMISSION_ADJUSTMENT_KINDS,\n type CommissionAdjustmentKind,\n} from '../types.js';\n\nconst UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\ninterface CommissionAdjustmentServiceDeps {\n commissions: CommissionCollection;\n adjustments: CommissionAdjustmentCollection;\n operations: CommissionAdjustmentOperationCollection;\n earners: EarnerCollection;\n}\n\ninterface TransactionCapableDatabase extends DatabaseInterface {\n transaction?<T>(fn: (tx: DatabaseInterface) => Promise<T>): Promise<T>;\n}\n\n/** One immutable operator correction intent. */\nexport interface CreateCommissionAdjustmentInput {\n /** Stable caller-supplied UUID; retries MUST reuse it. */\n operationId: string;\n tenantId: string;\n commissionId: string;\n /** Denormalized value, validated against the parent Commission. */\n earnerId: string;\n adjustmentKind: CommissionAdjustmentKind;\n /** Signed integer cents; zero is not an adjustment. */\n amountCents: number;\n /** ISO currency, validated against the parent Commission. */\n currency: string;\n reason: string;\n /** Cross-package Profile UUID identifying the operator/automation. */\n createdByProfileId: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface CreateCommissionAdjustmentResult {\n adjustment: CommissionAdjustment;\n /** `true` only when this invocation persisted the row. */\n created: boolean;\n}\n\nexport type CommissionAdjustmentReplayMismatchField =\n | 'tenantId'\n | 'commissionId'\n | 'earnerId'\n | 'adjustmentKind'\n | 'amountCents'\n | 'currency'\n | 'reason'\n | 'createdByProfileId'\n | 'metadata';\n\n/** Typed fail-closed result for a reused operation UUID with another intent. */\nexport class CommissionAdjustmentReplayConflictError extends Error {\n readonly code = 'COMMISSION_ADJUSTMENT_REPLAY_CONFLICT' as const;\n\n constructor(\n readonly operationId: string,\n readonly mismatches: readonly CommissionAdjustmentReplayMismatchField[],\n ) {\n super(\n `Commission adjustment operation '${operationId}' was already used with ` +\n `different immutable ${mismatches.length === 1 ? 'field' : 'fields'}: ` +\n mismatches.join(', '),\n );\n this.name = 'CommissionAdjustmentReplayConflictError';\n }\n}\n\nexport type CommissionAdjustmentValidationReason =\n | 'tenant_context_mismatch'\n | 'invalid_operation_id'\n | 'invalid_tenant_id'\n | 'invalid_commission_id'\n | 'invalid_earner_id'\n | 'invalid_operator_profile_id'\n | 'invalid_adjustment_kind'\n | 'invalid_amount'\n | 'invalid_currency'\n | 'invalid_metadata'\n | 'reason_required'\n | 'commission_not_found'\n | 'commission_tenant_mismatch'\n | 'earner_not_found'\n | 'earner_tenant_mismatch'\n | 'earner_mismatch'\n | 'currency_mismatch'\n | 'transaction_unavailable'\n | 'operation_adjustment_missing';\n\n/** Actionable validation refusal raised before any adjustment is persisted. */\nexport class CommissionAdjustmentValidationError extends Error {\n readonly code = 'COMMISSION_ADJUSTMENT_VALIDATION_ERROR' as const;\n\n constructor(\n readonly reason: CommissionAdjustmentValidationReason,\n message: string,\n ) {\n super(message);\n this.name = 'CommissionAdjustmentValidationError';\n }\n}\n\ninterface CanonicalAdjustmentIntent {\n operationId: string;\n tenantId: string;\n commissionId: string;\n earnerId: string;\n adjustmentKind: CommissionAdjustmentKind;\n amountCents: number;\n currency: string;\n reason: string;\n createdByProfileId: string;\n metadata: string;\n}\n\nexport class CommissionAdjustmentService {\n private constructor(private readonly deps: CommissionAdjustmentServiceDeps) {}\n\n static async create(\n options: SmrtClassOptions = {},\n ): Promise<CommissionAdjustmentService> {\n return new CommissionAdjustmentService({\n commissions: await CommissionCollection.create(options),\n adjustments: await CommissionAdjustmentCollection.create(options),\n operations: await CommissionAdjustmentOperationCollection.create(options),\n earners: await EarnerCollection.create(options),\n });\n }\n\n /**\n * Create exactly one immutable adjustment for `operationId`.\n *\n * The operation fence table's primary UUID is the serialization point. The\n * fence and adjustment are committed in one transaction. An exact replay\n * returns the persisted row; any changed immutable input raises\n * {@link CommissionAdjustmentReplayConflictError}. The claim primitive uses\n * `ON CONFLICT DO NOTHING`, so a losing PostgreSQL transaction remains\n * usable and can read/verify the committed winner rather than entering an\n * aborted transaction state.\n */\n async createAdjustment(\n input: CreateCommissionAdjustmentInput,\n ): Promise<CreateCommissionAdjustmentResult> {\n const intent = this.canonicalize(input);\n this.assertTenant(intent.tenantId);\n return await this.runTransaction(async (deps) => {\n const existingOperation = await deps.operations.findByOperationId(\n intent.operationId,\n );\n if (existingOperation) {\n return await this.replayFromOperation(deps, existingOperation, intent);\n }\n\n await this.assertParentAndEarner(deps, intent);\n const adjustmentId = randomUUID();\n const claimed = await deps.operations.claim({\n operationId: intent.operationId,\n tenantId: intent.tenantId,\n adjustmentId,\n });\n\n if (!claimed.operation) {\n // The operation UUID exists outside this tenant. Tenant-scoped reads\n // deliberately reveal no foreign row or payload; report only that the\n // caller's tenant is not the owner of the globally unique operation.\n throw new CommissionAdjustmentReplayConflictError(intent.operationId, [\n 'tenantId',\n ]);\n }\n if (!claimed.claimed) {\n return await this.replayFromOperation(deps, claimed.operation, intent);\n }\n\n const { operationId: _operationId, ...adjustmentIntent } = intent;\n const adjustment = await deps.adjustments.create({\n id: adjustmentId,\n ...adjustmentIntent,\n });\n this.assertExactReplay(adjustment, intent);\n return { adjustment, created: true };\n });\n }\n\n private canonicalize(\n input: CreateCommissionAdjustmentInput,\n ): CanonicalAdjustmentIntent {\n this.assertUuid(input.operationId, 'invalid_operation_id', 'operationId');\n this.assertUuid(input.tenantId, 'invalid_tenant_id', 'tenantId');\n if (input.tenantId !== input.tenantId.toLowerCase()) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_tenant_id',\n 'Commission adjustment tenantId must use canonical lowercase UUID casing',\n );\n }\n this.assertUuid(\n input.commissionId,\n 'invalid_commission_id',\n 'commissionId',\n );\n this.assertUuid(input.earnerId, 'invalid_earner_id', 'earnerId');\n this.assertUuid(\n input.createdByProfileId,\n 'invalid_operator_profile_id',\n 'createdByProfileId',\n );\n if (!COMMISSION_ADJUSTMENT_KINDS.includes(input.adjustmentKind)) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_adjustment_kind',\n `Unknown commission adjustment kind '${String(input.adjustmentKind)}'`,\n );\n }\n if (!Number.isSafeInteger(input.amountCents) || input.amountCents === 0) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_amount',\n 'Commission adjustment amountCents must be a non-zero safe integer',\n );\n }\n const currency = input.currency.trim().toUpperCase();\n if (!/^[A-Z]{3}$/.test(currency)) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_currency',\n 'Commission adjustment currency must be a three-letter ISO code',\n );\n }\n const reason = input.reason.trim();\n if (!reason) {\n throw new CommissionAdjustmentValidationError(\n 'reason_required',\n 'Commission adjustment reason is required',\n );\n }\n\n return {\n operationId: input.operationId.toLowerCase(),\n tenantId: input.tenantId.toLowerCase(),\n commissionId: input.commissionId.toLowerCase(),\n earnerId: input.earnerId.toLowerCase(),\n adjustmentKind: input.adjustmentKind,\n amountCents: input.amountCents,\n currency,\n reason,\n createdByProfileId: input.createdByProfileId.toLowerCase(),\n metadata: this.canonicalMetadata(\n input.metadata === undefined ? {} : input.metadata,\n ),\n };\n }\n\n private assertTenant(tenantId: string): void {\n let activeTenantId: string;\n try {\n activeTenantId = requireTenantId();\n } catch (error) {\n if (!(error instanceof TenantContextError)) throw error;\n throw new CommissionAdjustmentValidationError(\n 'tenant_context_mismatch',\n 'Commission adjustment creation requires an active tenant context',\n );\n }\n if (\n activeTenantId !== activeTenantId.toLowerCase() ||\n activeTenantId !== tenantId\n ) {\n throw new CommissionAdjustmentValidationError(\n 'tenant_context_mismatch',\n 'Commission adjustment tenant must exactly match the canonical lowercase active tenant',\n );\n }\n }\n\n private async assertParentAndEarner(\n deps: CommissionAdjustmentServiceDeps,\n intent: CanonicalAdjustmentIntent,\n ): Promise<void> {\n const commission = await deps.commissions.get({\n id: intent.commissionId,\n });\n if (!commission) {\n throw new CommissionAdjustmentValidationError(\n 'commission_not_found',\n `Commission '${intent.commissionId}' was not found in the active tenant`,\n );\n }\n if (commission.tenantId?.toLowerCase() !== intent.tenantId) {\n throw new CommissionAdjustmentValidationError(\n 'commission_tenant_mismatch',\n 'Commission does not belong to the adjustment tenant',\n );\n }\n if (commission.earnerId.toLowerCase() !== intent.earnerId) {\n throw new CommissionAdjustmentValidationError(\n 'earner_mismatch',\n `Adjustment earner '${intent.earnerId}' does not match Commission earner '${commission.earnerId}'`,\n );\n }\n if (commission.currency.toUpperCase() !== intent.currency) {\n throw new CommissionAdjustmentValidationError(\n 'currency_mismatch',\n `Adjustment currency '${intent.currency}' does not match Commission currency '${commission.currency}'`,\n );\n }\n\n const earner = await deps.earners.get({ id: intent.earnerId });\n if (!earner) {\n throw new CommissionAdjustmentValidationError(\n 'earner_not_found',\n `Earner '${intent.earnerId}' was not found in the active tenant`,\n );\n }\n if (earner.tenantId?.toLowerCase() !== intent.tenantId) {\n throw new CommissionAdjustmentValidationError(\n 'earner_tenant_mismatch',\n 'Earner does not belong to the adjustment tenant',\n );\n }\n }\n\n private assertExactReplay(\n existing: CommissionAdjustment,\n intent: CanonicalAdjustmentIntent,\n ): void {\n const mismatches: CommissionAdjustmentReplayMismatchField[] = [];\n if (existing.tenantId?.toLowerCase() !== intent.tenantId)\n mismatches.push('tenantId');\n if (existing.commissionId.toLowerCase() !== intent.commissionId)\n mismatches.push('commissionId');\n if (existing.earnerId.toLowerCase() !== intent.earnerId)\n mismatches.push('earnerId');\n if (existing.adjustmentKind !== intent.adjustmentKind)\n mismatches.push('adjustmentKind');\n if (existing.amountCents !== intent.amountCents)\n mismatches.push('amountCents');\n if (existing.currency.toUpperCase() !== intent.currency)\n mismatches.push('currency');\n if (existing.reason !== intent.reason) mismatches.push('reason');\n if (existing.createdByProfileId.toLowerCase() !== intent.createdByProfileId)\n mismatches.push('createdByProfileId');\n if (canonicalizePersistedJson(existing.metadata) !== intent.metadata)\n mismatches.push('metadata');\n if (mismatches.length > 0) {\n throw new CommissionAdjustmentReplayConflictError(\n intent.operationId,\n mismatches,\n );\n }\n }\n\n private assertUuid(\n value: string,\n reason: CommissionAdjustmentValidationReason,\n field: string,\n ): void {\n if (typeof value !== 'string' || !UUID_RE.test(value)) {\n throw new CommissionAdjustmentValidationError(\n reason,\n `Commission adjustment ${field} must be a UUID`,\n );\n }\n }\n\n private canonicalMetadata(metadata: Record<string, unknown>): string {\n try {\n if (!isPlainJsonObject(metadata)) {\n throw new TypeError('metadata must be a plain JSON object');\n }\n return stableJson(metadata);\n } catch (error) {\n throw new CommissionAdjustmentValidationError(\n 'invalid_metadata',\n `Commission adjustment metadata must be JSON-serializable: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n }\n\n private async replayFromOperation(\n deps: CommissionAdjustmentServiceDeps,\n operation: { adjustmentId: string },\n intent: CanonicalAdjustmentIntent,\n ): Promise<CreateCommissionAdjustmentResult> {\n const adjustment = await deps.adjustments.get({\n id: operation.adjustmentId,\n });\n if (!adjustment) {\n throw new CommissionAdjustmentValidationError(\n 'operation_adjustment_missing',\n `Commission adjustment operation '${intent.operationId}' has no visible adjustment`,\n );\n }\n this.assertExactReplay(adjustment, intent);\n return { adjustment, created: false };\n }\n\n private async runTransaction<T>(\n fn: (deps: CommissionAdjustmentServiceDeps) => Promise<T>,\n ): Promise<T> {\n const db = this.deps.adjustments.db as TransactionCapableDatabase;\n if (typeof db.transaction !== 'function') {\n throw new CommissionAdjustmentValidationError(\n 'transaction_unavailable',\n 'Commission adjustment creation requires a transaction-capable database adapter',\n );\n }\n return await db.transaction(async (tx) =>\n fn({\n commissions: await CommissionCollection.create({ db: tx }),\n adjustments: await CommissionAdjustmentCollection.create({ db: tx }),\n operations: await CommissionAdjustmentOperationCollection.create({\n db: tx,\n }),\n earners: await EarnerCollection.create({ db: tx }),\n }),\n );\n }\n}\n\nfunction canonicalizePersistedJson(value: string): string {\n try {\n return stableJson(JSON.parse(value) as unknown);\n } catch {\n return value;\n }\n}\n\nfunction stableJson(value: unknown): string {\n return JSON.stringify(sortJsonValue(value, new Set<object>()));\n}\n\nfunction isPlainJsonObject(value: unknown): value is Record<string, unknown> {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\nfunction sortJsonValue(value: unknown, ancestors: Set<object>): unknown {\n if (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'boolean' ||\n (typeof value === 'number' && Number.isFinite(value))\n ) {\n return value;\n }\n if (Array.isArray(value)) {\n if (ancestors.has(value)) throw new TypeError('metadata must be acyclic');\n ancestors.add(value);\n try {\n return value.map((item) => sortJsonValue(item, ancestors));\n } finally {\n ancestors.delete(value);\n }\n }\n if (value && typeof value === 'object') {\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new TypeError('metadata objects must be plain JSON objects');\n }\n if (ancestors.has(value)) throw new TypeError('metadata must be acyclic');\n ancestors.add(value);\n try {\n const sorted = Object.create(null) as Record<string, unknown>;\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n const item = (value as Record<string, unknown>)[key];\n if (item !== undefined) sorted[key] = sortJsonValue(item, ancestors);\n }\n return sorted;\n } finally {\n ancestors.delete(value);\n }\n }\n throw new TypeError('metadata must contain only JSON values');\n}\n\nexport default CommissionAdjustmentService;\n","/**\n * CommissionBalanceService — computed (never stored) per-earner balances.\n *\n * `payableCents` is the sum of unsettled `payable` commissions;\n * `unsettledAdjustmentCents` is the signed sum of unsettled adjustments\n * whose parent commission is earned/approved/payable/paid (adjustments\n * against still-`pending` commissions wait for the earning to clear);\n * `netPayableCents = payableCents + unsettledAdjustmentCents` and can go\n * NEGATIVE when clawbacks against already-paid commissions exceed what is\n * currently payable. The pending/earned/approved breakdowns feed portal\n * views.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CommissionAdjustmentCollection } from '../collections/CommissionAdjustmentCollection.js';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport type { Commission } from '../models/Commission.js';\nimport {\n ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES,\n type CommissionStatus,\n type EarnerBalance,\n} from '../types.js';\n\nexport class CommissionBalanceService {\n constructor(\n private readonly commissions: CommissionCollection,\n private readonly adjustments: CommissionAdjustmentCollection,\n ) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CommissionBalanceService> {\n return new CommissionBalanceService(\n await CommissionCollection.create(classOptions),\n await CommissionAdjustmentCollection.create(classOptions),\n );\n }\n\n /** Compute the {@link EarnerBalance} for one earner in one currency. */\n async getBalance(earnerId: string, currency: string): Promise<EarnerBalance> {\n const rows = await this.commissions.list({\n where: { earnerId, currency },\n });\n\n const sumByStatus = (status: CommissionStatus): number =>\n rows\n .filter((c: Commission) => c.status === status)\n .reduce((sum: number, c: Commission) => sum + c.amountCents, 0);\n\n const pendingCents = sumByStatus('pending');\n const earnedCents = sumByStatus('earned');\n const approvedCents = sumByStatus('approved');\n const payableCents = rows\n .filter((c: Commission) => c.status === 'payable' && !c.payoutId)\n .reduce((sum: number, c: Commission) => sum + c.amountCents, 0);\n\n const statusById = new Map<string, CommissionStatus>();\n for (const c of rows) {\n if (c.id) statusById.set(c.id, c.status);\n }\n\n const unsettled = await this.adjustments.findUnsettledByEarner(\n earnerId,\n currency,\n );\n let unsettledAdjustmentCents = 0;\n for (const adjustment of unsettled) {\n const parentStatus = statusById.get(adjustment.commissionId);\n if (\n parentStatus !== undefined &&\n (\n ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES as readonly CommissionStatus[]\n ).includes(parentStatus)\n ) {\n unsettledAdjustmentCents += adjustment.amountCents;\n }\n }\n\n return {\n earnerId,\n currency,\n payableCents,\n pendingCents,\n earnedCents,\n approvedCents,\n unsettledAdjustmentCents,\n netPayableCents: payableCents + unsettledAdjustmentCents,\n };\n }\n}\n\nexport default CommissionBalanceService;\n","/**\n * CommissionCalculationService — turns one {@link EarningEvent} into\n * Commission rows for one earner, driven by a set of plan components.\n *\n * The service deliberately takes COMPONENTS (plus `planKey`/`planVersion`\n * snapshot refs), not a live plan: callers that calculate from frozen terms\n * — e.g. the referrals module's term snapshots — pass the components they\n * froze, so a later plan amendment can never leak into an already-agreed\n * calculation.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport { EarnerCollection } from '../collections/EarnerCollection.js';\nimport type { Commission } from '../models/Commission.js';\nimport { validateCommissionPlanComponents } from '../models/CommissionPlan.js';\nimport type { EarningEvent } from '../models/EarningEvent.js';\nimport { calculateCommissionAmountCents, roundCents } from '../money.js';\nimport type {\n CommissionCalculationTrace,\n CommissionPlanComponent,\n} from '../types.js';\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\n\n/** Input for {@link CommissionCalculationService.calculateForEvent}. */\nexport interface CommissionCalculationInput {\n /** The (persisted) earning event to calculate from. */\n event: EarningEvent;\n /** Snapshot reference recorded on every created Commission. */\n planKey: string;\n /** Snapshot reference recorded on every created Commission. */\n planVersion: number;\n /** The calculation terms to apply (typically from a frozen snapshot). */\n components: CommissionPlanComponent[];\n /** The earner the commissions belong to. */\n earnerId: string;\n /**\n * Split share (0–1) this earner receives; defaults to 1. Callers running\n * a split invoke the service once per earner with the shares and a shared\n * `splitGroupId`.\n */\n shareFraction?: number;\n /** Groups the sibling commissions of one split. */\n splitGroupId?: string;\n /** Generic polymorphic terms-snapshot reference (kind). */\n termsSnapshotKind?: string;\n /** Generic polymorphic terms-snapshot reference (id). */\n termsSnapshotId?: string;\n /**\n * Clearing window in days: created commissions get\n * `clearingEndsAt = event.occurredAt + clearingDays`. Omitted → no\n * clearing (`clearingEndsAt: null`, immediately sweepable).\n */\n clearingDays?: number;\n /**\n * Currency the plan/terms are denominated in. When provided and different\n * from `event.currency`, every matching component skips with reason\n * `'currency_mismatch'` (this module performs no FX). Omitted → the event\n * currency is taken as authoritative and no mismatch is possible.\n */\n currency?: string;\n /**\n * Resolves how many occurrences of a component this earner has already\n * consumed under these terms (commissions from PRIOR events — the current\n * event must not be counted). Drives `one_time` / `maxOccurrences` limits\n * and the `occurrenceIndex` in the dedupe key and trace. Omitted →\n * occurrence count `0`, i.e. recurrence limits are NOT enforced.\n */\n occurrenceCountResolver?: (componentKey: string) => Promise<number>;\n /**\n * Anchor for `windowMonths` recurrence checks (e.g. an agreement's\n * effective date). Omitted → window checks are skipped.\n */\n anchorAt?: Date;\n}\n\n/** One component the calculation declined, and why. */\nexport interface CommissionComponentSkip {\n componentKey: string;\n /**\n * `'net_basis_undefined'` | `'margin_basis_undefined'` |\n * `'fixed_amount_missing'` | `'rate_missing'` | `'custom_basis_missing'`\n * | `'currency_mismatch'` | `'occurrence_limit_reached'` |\n * `'outside_recurrence_window'`\n */\n reason: string;\n}\n\n/** Result of {@link CommissionCalculationService.calculateForEvent}. */\nexport interface CommissionCalculationResult {\n /** Commissions newly created by THIS call. */\n created: Commission[];\n /** Components that produced nothing, with reasons. */\n skipped: CommissionComponentSkip[];\n /**\n * Idempotent replays: commissions that already existed for this\n * (event, terms, component, earner) tuple. Never re-created, never\n * mutated, and never in `created`.\n */\n existing: Commission[];\n}\n\nexport class CommissionCalculationService {\n constructor(\n private readonly commissions: CommissionCollection,\n /**\n * Optional earner lookup for the tenant-lane guard. When provided,\n * `calculateForEvent` refuses an earner from a different tenant lane\n * than the event (a cross-tenant `earnerId` would create a commission\n * payable to another tenant's account). `static create()` always wires\n * it; direct constructors may omit it for narrow test fixtures.\n */\n private readonly earners?: EarnerCollection,\n ) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CommissionCalculationService> {\n return new CommissionCalculationService(\n await CommissionCollection.create(classOptions),\n await EarnerCollection.create(classOptions),\n );\n }\n\n /**\n * Calculate commissions for one event × one earner × a component set.\n *\n * For each component whose `trigger` matches `event.eventKind` (or `'*'`\n * — non-matching components are silently filtered, not \"skipped\"):\n *\n * 1. **Idempotency** — if a Commission already exists for this\n * (event, terms, component, earner) tuple, it is returned in\n * `existing` and nothing else runs for the component.\n * 2. **Currency** — `input.currency` (when given) must equal the event's;\n * otherwise skip `'currency_mismatch'`.\n * 3. **Recurrence** — `one_time` components skip\n * `'occurrence_limit_reached'` once the resolver reports ≥ 1 prior\n * occurrence; `recurring` components honor `maxOccurrences` and\n * `windowMonths` (events after `anchorAt + windowMonths` skip\n * `'outside_recurrence_window'`).\n * 4. **Basis** — gross → `grossAmountCents`; net → `netAmountCents`\n * (skip `'net_basis_undefined'` when null — net is explicit, NEVER\n * derived from gross); margin → `marginCents` (skip\n * `'margin_basis_undefined'` when null); fixed → `fixedAmountCents`;\n * custom → `getCustomBases()[customBasisKey]` (skip\n * `'custom_basis_missing'`).\n * 5. **Amount** — `roundCents(base * rate * shareFraction)`; for `fixed`,\n * `roundCents(fixedAmountCents * shareFraction)` with `rate` recorded\n * as `0`. Rounding happens exactly once, on the final product.\n *\n * Every created Commission is persisted `pending`, carries the event's\n * tenant/currency/source, a complete {@link CommissionCalculationTrace},\n * `clearingEndsAt` when `clearingDays` was given, and the dedupe key\n * `` `${event.dedupeKey}:${termsSnapshotId || planKey + '@' + planVersion}:${componentKey}:${earnerId}:${occurrenceIndex}` ``.\n */\n async calculateForEvent(\n input: CommissionCalculationInput,\n ): Promise<CommissionCalculationResult> {\n const { event } = input;\n if (!event.id) {\n throw new Error(\n 'CommissionCalculationService.calculateForEvent requires a persisted event (missing id)',\n );\n }\n if (!input.earnerId) {\n throw new Error(\n 'CommissionCalculationService.calculateForEvent requires an earnerId',\n );\n }\n\n const shareFraction = input.shareFraction ?? 1;\n // Money guard: an out-of-range or non-finite fraction would persist\n // overpayment, negative, or NaN commissions — reject before any\n // component calculates.\n if (\n !Number.isFinite(shareFraction) ||\n shareFraction < 0 ||\n shareFraction > 1\n ) {\n throw new Error(\n `CommissionCalculationService.calculateForEvent: shareFraction must be a finite number in [0, 1], got ${String(shareFraction)}`,\n );\n }\n // Money guard: direct callers can hand in arbitrary component arrays\n // (snapshots and plans validate on write, but nothing forces callers\n // through them) — malformed rates/amounts must never reach the math.\n validateCommissionPlanComponents(input.components);\n\n // Tenant-lane guard: an earner from a different lane than the event\n // would receive a commission payable to another tenant's account\n // (reachable via any surface that lets an earnerId be assigned).\n if (this.earners) {\n const earner = await this.earners.get({ id: input.earnerId });\n if (\n earner &&\n earner.tenantId !== null &&\n (event.tenantId ?? null) !== null &&\n earner.tenantId !== event.tenantId\n ) {\n throw new Error(\n `CommissionCalculationService.calculateForEvent: earner '${input.earnerId}' ` +\n `belongs to tenant '${earner.tenantId}' but the event belongs to ` +\n `tenant '${event.tenantId}' — cross-tenant commissions are refused.`,\n );\n }\n }\n const termsRef =\n input.termsSnapshotId || `${input.planKey}@${input.planVersion}`;\n\n const created: Commission[] = [];\n const skipped: CommissionComponentSkip[] = [];\n const existing: Commission[] = [];\n\n for (const component of input.components) {\n // Trigger filter: only components listening for this event kind (or\n // everything via '*') participate at all.\n if (component.trigger !== '*' && component.trigger !== event.eventKind) {\n continue;\n }\n\n // Idempotent replay: the same event can only ever earn once per\n // component per earner under the same terms, regardless of what the\n // occurrence resolver would report on a re-run.\n const priorForEvent = await this.commissions.list({\n where: {\n earningEventId: event.id,\n earnerId: input.earnerId,\n componentKey: component.key,\n planKey: input.planKey,\n planVersion: input.planVersion,\n termsSnapshotId: input.termsSnapshotId ?? '',\n },\n limit: 1,\n });\n if (priorForEvent[0]) {\n existing.push(priorForEvent[0]);\n continue;\n }\n\n // Currency: terms and event must agree — this module performs no FX.\n if (input.currency !== undefined && input.currency !== event.currency) {\n skipped.push({\n componentKey: component.key,\n reason: 'currency_mismatch',\n });\n continue;\n }\n\n // Recurrence limits.\n const occurrenceCount = input.occurrenceCountResolver\n ? await input.occurrenceCountResolver(component.key)\n : 0;\n const recurrence = component.recurrence;\n if (recurrence) {\n if (recurrence.kind === 'one_time' && occurrenceCount >= 1) {\n skipped.push({\n componentKey: component.key,\n reason: 'occurrence_limit_reached',\n });\n continue;\n }\n if (\n recurrence.kind === 'recurring' &&\n recurrence.maxOccurrences !== undefined &&\n occurrenceCount >= recurrence.maxOccurrences\n ) {\n skipped.push({\n componentKey: component.key,\n reason: 'occurrence_limit_reached',\n });\n continue;\n }\n if (\n recurrence.windowMonths !== undefined &&\n input.anchorAt !== undefined &&\n event.occurredAt.getTime() >\n CommissionCalculationService.addMonths(\n input.anchorAt,\n recurrence.windowMonths,\n ).getTime()\n ) {\n skipped.push({\n componentKey: component.key,\n reason: 'outside_recurrence_window',\n });\n continue;\n }\n }\n\n // Resolve the base amount for the component's basis.\n let baseAmountCents: number;\n switch (component.basis) {\n case 'gross':\n baseAmountCents = event.grossAmountCents;\n break;\n case 'net':\n if (event.netAmountCents === null) {\n // Net must be explicitly defined by the ingesting system —\n // it is NEVER derived from gross.\n skipped.push({\n componentKey: component.key,\n reason: 'net_basis_undefined',\n });\n continue;\n }\n baseAmountCents = event.netAmountCents;\n break;\n case 'margin':\n if (event.marginCents === null) {\n skipped.push({\n componentKey: component.key,\n reason: 'margin_basis_undefined',\n });\n continue;\n }\n baseAmountCents = event.marginCents;\n break;\n case 'fixed':\n if (typeof component.fixedAmountCents !== 'number') {\n skipped.push({\n componentKey: component.key,\n reason: 'fixed_amount_missing',\n });\n continue;\n }\n baseAmountCents = component.fixedAmountCents;\n break;\n case 'custom': {\n const bases = event.getCustomBases();\n const key = component.customBasisKey ?? '';\n const value = key ? bases[key] : undefined;\n if (typeof value !== 'number') {\n skipped.push({\n componentKey: component.key,\n reason: 'custom_basis_missing',\n });\n continue;\n }\n baseAmountCents = value;\n break;\n }\n }\n\n // Resolve rate + amount. Fixed components record rate 0 and apply\n // only the share fraction; everything else applies rate × share.\n let rate: number;\n let amountCents: number;\n if (component.basis === 'fixed') {\n rate = 0;\n amountCents = roundCents(baseAmountCents * shareFraction);\n } else {\n if (typeof component.rate !== 'number') {\n skipped.push({\n componentKey: component.key,\n reason: 'rate_missing',\n });\n continue;\n }\n rate = component.rate;\n amountCents = calculateCommissionAmountCents(\n baseAmountCents,\n rate,\n shareFraction,\n );\n }\n\n const occurrenceIndex = occurrenceCount;\n const dedupeKey = `${event.dedupeKey}:${termsRef}:${component.key}:${input.earnerId}:${occurrenceIndex}`;\n\n // Second idempotency belt: an exact dedupe-key hit (however it came\n // to exist) is returned rather than re-created — creating through the\n // conflictColumns upsert would otherwise UPDATE the existing row.\n const priorByKey = await this.commissions.findByDedupeKey(dedupeKey);\n if (priorByKey) {\n existing.push(priorByKey);\n continue;\n }\n\n const trace: CommissionCalculationTrace = {\n planKey: input.planKey,\n planVersion: input.planVersion,\n componentKey: component.key,\n basis: component.basis,\n baseAmountCents,\n rate,\n shareFraction,\n occurrenceIndex,\n earningEventId: event.id,\n roundingMode: 'half_away_from_zero',\n };\n\n let commission: Commission;\n try {\n commission = await this.commissions.create({\n // Commissions inherit the event's tenancy so background\n // calculation (no active tenant context) still lands rows in the\n // right tenant.\n tenantId: event.tenantId,\n earnerId: input.earnerId,\n earningEventId: event.id,\n planKey: input.planKey,\n planVersion: input.planVersion,\n componentKey: component.key,\n termsSnapshotKind: input.termsSnapshotKind ?? '',\n termsSnapshotId: input.termsSnapshotId ?? '',\n basis: component.basis,\n baseAmountCents,\n rate,\n shareFraction,\n splitGroupId: input.splitGroupId ?? '',\n amountCents,\n currency: event.currency,\n status: 'pending',\n clearingEndsAt:\n input.clearingDays !== undefined\n ? new Date(\n event.occurredAt.getTime() + input.clearingDays * MS_PER_DAY,\n )\n : null,\n sourceKind: event.sourceKind,\n sourceId: event.sourceId,\n calculationTrace: JSON.stringify(trace),\n dedupeKey,\n });\n } catch (error) {\n // The Commission dedupe-key guard refuses to overwrite an existing\n // row — under a calculation race the loser lands here. That IS the\n // idempotent outcome: hand back the row that won.\n if (\n error instanceof Error &&\n error.message.includes('immutable audit rows')\n ) {\n const winner = await this.commissions.list({\n where: { dedupeKey },\n limit: 1,\n });\n if (winner[0]) {\n existing.push(winner[0]);\n continue;\n }\n }\n throw error;\n }\n created.push(commission);\n }\n\n return { created, skipped, existing };\n }\n\n /**\n * Calendar-month addition (UTC). JS `setUTCMonth` semantics: day-of-month\n * overflow rolls into the next month (Jan 31 + 1 month → Mar 2/3), which\n * is acceptable for coarse recurrence windows.\n */\n private static addMonths(date: Date, months: number): Date {\n const result = new Date(date.getTime());\n result.setUTCMonth(result.getUTCMonth() + months);\n return result;\n }\n}\n\nexport default CommissionCalculationService;\n","/**\n * CommissionPayoutService — mints and drives {@link CommissionPayout}\n * settlement batches.\n *\n * The service is the ONLY sanctioned creation path for payouts (their\n * generated surface is fully read-only): it gathers the exact payable\n * unsettled Commissions and eligible unsettled Adjustments, refuses\n * below-threshold / non-positive batches, stamps `payoutId` on the exact\n * gathered rows, and later flips the batch's commissions to `paid` when the\n * payout completes.\n *\n * Batch creation is deliberately non-transactional (conditional claims +\n * disjoint scopes — see {@link createPayoutBatch}). Two surfaces layered on\n * top serve per-source consumers:\n *\n * - {@link getSourcePayoutHistory} — the source-scoped, paginated,\n * membership-verified payout history (#1985), indexed by the derived\n * single-source stamp each batch/repair pass maintains.\n * - {@link transitionPayoutForSource} — atomic source-authorized lifecycle\n * transitions (#1987): payout row locked, membership re-verified and\n * totals recomputed under the lock, transition + member writes committed\n * together on the same transaction database.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { CommissionAdjustmentCollection } from '../collections/CommissionAdjustmentCollection.js';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport { CommissionPayoutCollection } from '../collections/CommissionPayoutCollection.js';\nimport { EarnerCollection } from '../collections/EarnerCollection.js';\nimport type { Commission } from '../models/Commission.js';\nimport type { CommissionAdjustment } from '../models/CommissionAdjustment.js';\nimport type { CommissionPayout } from '../models/CommissionPayout.js';\nimport {\n ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES,\n type CommissionPayoutStatus,\n type CommissionStatus,\n type PayoutMethod,\n} from '../types.js';\n\n/** The subset of adapter capabilities the transactional transition uses. */\ntype TransactionCapableDatabase = DatabaseInterface & {\n transaction?: <T>(\n callback: (tx: DatabaseInterface) => Promise<T>,\n ) => Promise<T>;\n acquireSession?: unknown;\n};\n\n/**\n * Per-database promise chain serializing transactional transitions on\n * engines that multiplex every transaction over ONE shared connection\n * (SQLite, DuckDB, JSON) — concurrent `BEGIN`/`COMMIT` pairs would\n * interleave there. PostgreSQL (pooled per-transaction connections) skips\n * the chain entirely. WeakMap so the tail GCs with the database instance.\n */\nconst singleConnectionTransitionTails = new WeakMap<object, Promise<unknown>>();\n\n/**\n * What the transactional transition callback reports back across the\n * commit boundary — ids only, so the public result can rehydrate on the\n * service's own connection.\n */\ninterface TxTransitionOutcome {\n outcome: 'transitioned' | 'already_applied' | 'refused';\n payoutId: string | null;\n refusal?: { reason: PayoutTransitionRefusalReason; detail: string };\n /** Prevent payout rehydration when source ownership was not proven. */\n authorizationFailed?: true;\n releasedCommissionIds?: string[];\n releasedAdjustmentIds?: string[];\n}\n\n/** Collaborators for {@link CommissionPayoutService}. */\nexport interface CommissionPayoutServiceDeps {\n earners: EarnerCollection;\n commissions: CommissionCollection;\n adjustments: CommissionAdjustmentCollection;\n payouts: CommissionPayoutCollection;\n}\n\n/** Input for {@link CommissionPayoutService.createPayoutBatch}. */\nexport interface CreatePayoutBatchInput {\n earnerId: string;\n currency: string;\n /** Informational period bounds recorded on the payout. */\n periodStart?: Date;\n periodEnd?: Date;\n /**\n * Idempotency natural key. Defaults to\n * `` `${earnerId}:${currency}:${periodEnd ISO date}` ``, or\n * `` `${earnerId}:${currency}:${sourceKind}:${sourceId}:${periodEnd ISO date}` ``\n * when scoped by source (so a per-network batch and the earner-wide batch\n * on the same day don't collide). REQUIRED when {@link commissionIds} is\n * given — an explicit set has no natural default key. Callers running more\n * than one batch per key/day must supply their own key.\n */\n idempotencyKey?: string;\n /**\n * Restrict the batch to commissions from ONE earning source (e.g. a\n * single ad network). Both `sourceKind` and `sourceId` must be set\n * together. Only payable, unsettled commissions matching this source are\n * gathered, and eligible adjustments are narrowed to those whose parent\n * commission shares the source. Mutually exclusive with\n * {@link commissionIds}. Omit both to settle the whole earner+currency\n * (the original behavior).\n */\n sourceKind?: string;\n sourceId?: string;\n /**\n * Restrict the batch to EXACTLY these commissions. Each id is included\n * only when it is payable, unsettled, and belongs to this earner+currency\n * — ineligible ids are ignored (inspect `settledCommissionIds` for what\n * was actually claimed). Adjustments whose parent commission is in this\n * set come along. Mutually exclusive with {@link sourceKind}/\n * {@link sourceId}; requires an explicit {@link idempotencyKey}.\n */\n commissionIds?: string[];\n /** Overrides the earner's `payoutThresholdCents`. */\n minimumThresholdCents?: number;\n /** Overrides the earner's `payoutMethod`. */\n payoutMethod?: PayoutMethod;\n /** Clock override for deterministic tests. */\n now?: Date;\n}\n\n/** Result of {@link CommissionPayoutService.createPayoutBatch}. */\nexport interface CreatePayoutBatchResult {\n /** The created (or, on an idempotent replay, existing) payout — `null` on refusal. */\n payout: CommissionPayout | null;\n /** `true` only when THIS call minted the payout. */\n created: boolean;\n /** Why no payout was created, when refused. */\n reason?: 'below_threshold' | 'nothing_payable';\n /** Ids of the commissions THIS call stamped onto the payout. */\n settledCommissionIds: string[];\n /** Ids of the adjustments THIS call stamped onto the payout. */\n settledAdjustmentIds: string[];\n}\n\n/**\n * Why a payout's membership failed source verification. Every reason is\n * fail-closed: the payout is excluded from source-scoped listings and\n * refused source-authorized transitions until repaired.\n *\n * - `membership_empty` — no rows are stamped with the payout's id (nothing\n * proves ownership; e.g. a raced-away batch artifact or a rejected batch\n * whose rows were released).\n * - `source_mismatch` — a member commission (or an adjustment's parent\n * commission) carries a different or empty `(sourceKind, sourceId)`.\n * - `adjustment_parent_missing` — a member adjustment's parent commission\n * cannot be loaded, so its ownership cannot be proven.\n * - `earner_mismatch` / `currency_mismatch` / `tenant_mismatch` — a member\n * row disagrees with the payout on that axis.\n */\nexport type PayoutMembershipRefusalReason =\n | 'membership_empty'\n | 'source_mismatch'\n | 'adjustment_parent_missing'\n | 'earner_mismatch'\n | 'currency_mismatch'\n | 'tenant_mismatch';\n\n/** Why {@link CommissionPayoutService.transitionPayoutForSource} refused. */\nexport type PayoutTransitionRefusalReason =\n | PayoutMembershipRefusalReason\n | 'payout_not_found'\n | 'status_conflict'\n | 'totals_drift'\n | 'non_positive_total';\n\n/**\n * Source-authorized lifecycle actions. Targets:\n * `approve` (pending → approved), `mark_processing` (approved →\n * processing), `complete` (processing → completed, requires\n * `paymentReference`), `fail` (approved|processing → failed, requires\n * `reason`), `reject` (pending|approved → rejected, requires `reason`;\n * releases the batch's membership).\n */\nexport type PayoutSourceTransitionAction =\n | 'approve'\n | 'mark_processing'\n | 'complete'\n | 'fail'\n | 'reject';\n\n/** Input for {@link CommissionPayoutService.transitionPayoutForSource}. */\nexport interface TransitionPayoutForSourceInput {\n payoutId: string;\n /**\n * The earning source this transition is authorized against. EVERY member\n * commission — and every member adjustment through its parent commission\n * — must belong to exactly this source or the call is refused.\n */\n sourceKind: string;\n sourceId: string;\n action: PayoutSourceTransitionAction;\n /**\n * Optimistic concurrency guard: refuse with `status_conflict` when the\n * LOCKED payout's status differs, after source membership is authorized.\n * Omit to let the action's own from-status rule arbitrate (concurrent\n * duplicate calls for actions that retain membership then resolve as one\n * `transitioned` + one `already_applied`; `reject` replays fail\n * `membership_empty` after releasing that evidence).\n */\n expectedStatus?: CommissionPayoutStatus;\n /** Required for `complete`. */\n paymentReference?: string;\n /** Required for `fail` and `reject`; appended to the payout's notes. */\n reason?: string;\n /** Clock override for deterministic tests. */\n now?: Date;\n}\n\n/** Result of {@link CommissionPayoutService.transitionPayoutForSource}. */\nexport interface TransitionPayoutForSourceResult {\n /**\n * `transitioned` — THIS call performed the transition.\n * `already_applied` — the payout was already in the action's target\n * status; nothing was written (terminal completion metadata —\n * `paymentReference`, `providerRef`, `paidAt` — is never overwritten by\n * a replay).\n * `refused` — fail-closed; see {@link refusal}.\n */\n outcome: 'transitioned' | 'already_applied' | 'refused';\n /**\n * The payout re-read AFTER the transaction (bound to the service's own\n * connection). `null` for `payout_not_found` and every membership\n * authorization refusal, so an unverified caller receives no payout\n * lifecycle or settlement data.\n */\n payout: CommissionPayout | null;\n /** Set exactly when {@link outcome} is `refused`. */\n refusal?: { reason: PayoutTransitionRefusalReason; detail: string };\n /** Commissions a `reject` released back to unsettled. */\n releasedCommissionIds?: string[];\n /** Adjustments a `reject` released back to unsettled. */\n releasedAdjustmentIds?: string[];\n}\n\n/** Input for {@link CommissionPayoutService.getSourcePayoutHistory}. */\nexport interface SourcePayoutHistoryInput {\n sourceKind: string;\n sourceId: string;\n /** Page size, 1–100. Default 25. */\n limit?: number;\n /** Rows to skip (offset pagination). Default 0. */\n offset?: number;\n}\n\n/** One page of {@link CommissionPayoutService.getSourcePayoutHistory}. */\nexport interface SourcePayoutHistoryPage {\n /**\n * The page's VERIFIED payouts, newest first (`created_at DESC, id DESC`).\n * May hold fewer than `limit` rows even when {@link nextOffset} is set —\n * rows that failed verification are in {@link excluded} instead.\n */\n payouts: CommissionPayout[];\n /** Stamped rows on this page excluded fail-closed, with reasons. */\n excluded: {\n payoutId: string;\n reason: PayoutMembershipRefusalReason;\n detail: string;\n }[];\n /** Echo of the requested offset. */\n offset: number;\n /** Echo of the effective page size. */\n limit: number;\n /**\n * Offset of the next page (advances by the SCANNED count, so excluded\n * rows never cause skips), or `null` when the history is exhausted.\n */\n nextOffset: number | null;\n}\n\nexport class CommissionPayoutService {\n /**\n * Canonical UUID shape. Explicit `commissionIds` are filtered against this\n * before hitting the native-`uuid` `id` column so a malformed external id\n * can't abort the batch on Postgres/DuckDB.\n */\n private static readonly UUID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n constructor(private readonly deps: CommissionPayoutServiceDeps) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CommissionPayoutService> {\n return new CommissionPayoutService({\n earners: await EarnerCollection.create(classOptions),\n commissions: await CommissionCollection.create(classOptions),\n adjustments: await CommissionAdjustmentCollection.create(classOptions),\n payouts: await CommissionPayoutCollection.create(classOptions),\n });\n }\n\n /**\n * Create a settlement batch for one earner in one currency.\n *\n * Flow:\n * 1. **Idempotency + repair** — an existing payout with the (defaulted)\n * key is returned as `{ payout, created: false }`. A clean replay\n * touches nothing (new payable work is never swept into an existing\n * batch). A PENDING payout whose stored totals disagree with the rows\n * stamped with its id — the signature of an interrupted claim pass —\n * is repaired: the claim pass re-runs and the totals are reconciled\n * from the verified membership. Past `pending` the batch is frozen.\n * 2. **Gather** — payable unsettled commissions for the earner/currency,\n * plus unsettled adjustments whose parent commission is\n * earned/approved/payable/paid (same eligibility as the balance\n * service, so the batch settles exactly what the balance reports). Pass\n * `sourceKind`/`sourceId` to gather only ONE source's commissions, or\n * `commissionIds` to gather an explicit set; in both scoped modes the\n * eligible adjustments are narrowed to the same scope.\n * 3. **Refuse** — `netTotal <= 0` → `'nothing_payable'`;\n * `netTotal < threshold` (earner default, overridable) →\n * `'below_threshold'`. Nothing is minted or stamped on refusal.\n * 4. **Mint, claim, reconcile** — create the `pending` payout, then\n * CLAIM the gathered rows through the collections' conditional\n * `claimForPayout` (rows grabbed by another batch in the interim are\n * skipped, never double-claimed), and finally store totals computed\n * from the rows that were VERIFIABLY claimed — the payout's totals\n * are always reproducible from its member rows.\n *\n * Concurrency: claims are conditional with post-save verification, which\n * narrows but does not eliminate races, and a batch is NOT wrapped in one\n * DB transaction (the collection layer exposes none). Safe concurrent\n * settlement therefore relies on SCOPING to DISJOINT sets: two batches\n * scoped to different sources (or non-overlapping `commissionIds`) gather\n * disjoint rows and never contend — this is the intended multi-source\n * (e.g. per-ad-network) settlement pattern. Running OVERLAPPING scopes\n * concurrently (a source batch and the earner-wide batch, or intersecting\n * id sets) is the caller's responsibility to serialize: `claimForPayout`\n * still won't double-own a single row, but a shared commission and its\n * negative adjustment could split across the two batches, so neither\n * payout's net would be authoritative. An interrupted claim pass within a\n * single scope is healed by the repair-on-replay above.\n */\n async createPayoutBatch(\n input: CreatePayoutBatchInput,\n ): Promise<CreatePayoutBatchResult> {\n const now = input.now ?? new Date();\n CommissionPayoutService.assertScope(input);\n const earner = await this.deps.earners.get({ id: input.earnerId });\n if (!earner) {\n throw new Error(\n `CommissionPayoutService: earner '${input.earnerId}' not found`,\n );\n }\n\n const idempotencyKey =\n input.idempotencyKey ??\n CommissionPayoutService.defaultIdempotencyKey(\n input.earnerId,\n input.currency,\n input.periodEnd ?? now,\n input.sourceKind && input.sourceId\n ? { sourceKind: input.sourceKind, sourceId: input.sourceId }\n : undefined,\n );\n\n // Idempotent replay: same key → same payout. A CLEAN replay (stored\n // totals match the stamped membership) returns without touching rows —\n // new payable work is never swept into an existing batch. A pending\n // payout whose stored totals DISAGREE with its membership is the\n // signature of an interrupted claim pass: repair it (re-claim +\n // reconcile totals) instead of returning totals its rows can't\n // reproduce. Anything past pending is frozen.\n const existingPayout =\n await this.deps.payouts.findByIdempotencyKey(idempotencyKey);\n if (existingPayout) {\n if (\n existingPayout.isPending() &&\n !(await this.membershipConsistent(existingPayout))\n ) {\n const repaired = await this.claimAndReconcile(existingPayout, input);\n return { ...repaired, created: false };\n }\n return {\n payout: existingPayout,\n created: false,\n settledCommissionIds: [],\n settledAdjustmentIds: [],\n };\n }\n\n // Gather the exact rows this batch would settle (honoring any source /\n // explicit-id scope).\n const commissions = await this.gatherBatchCommissions(input);\n const eligibleAdjustments = await this.findEligibleUnsettledAdjustments(\n input.earnerId,\n input.currency,\n CommissionPayoutService.adjustmentParentPredicate(input),\n );\n\n const commissionTotalCents = commissions.reduce(\n (sum, c) => sum + c.amountCents,\n 0,\n );\n const adjustmentTotalCents = eligibleAdjustments.reduce(\n (sum, a) => sum + a.amountCents,\n 0,\n );\n const netTotalCents = commissionTotalCents + adjustmentTotalCents;\n\n if (netTotalCents <= 0) {\n return {\n payout: null,\n created: false,\n reason: 'nothing_payable',\n settledCommissionIds: [],\n settledAdjustmentIds: [],\n };\n }\n\n const thresholdCents =\n input.minimumThresholdCents ?? earner.payoutThresholdCents;\n if (netTotalCents < thresholdCents) {\n return {\n payout: null,\n created: false,\n reason: 'below_threshold',\n settledCommissionIds: [],\n settledAdjustmentIds: [],\n };\n }\n\n const minted = await this.deps.payouts.create({\n // Payouts inherit the earner's tenancy so scheduled batch runs (no\n // active tenant context) still land in the right tenant.\n tenantId: earner.tenantId,\n earnerId: input.earnerId,\n currency: input.currency,\n periodStart: input.periodStart ?? null,\n periodEnd: input.periodEnd ?? null,\n payoutMethod: input.payoutMethod ?? earner.payoutMethod,\n status: 'pending',\n commissionTotalCents,\n adjustmentTotalCents,\n totalAmountCents: netTotalCents,\n idempotencyKey,\n });\n\n // Adopt the PERSISTED row for the idempotency key before claiming\n // anything: two workers racing past the earlier lookup both reach the\n // natural-key upsert, and the loser's in-memory instance carries an id\n // the database no longer holds. Claiming with that orphaned id would\n // stamp rows onto a payout that doesn't exist — so both workers\n // converge on whichever row actually won the key.\n const payout =\n (await this.deps.payouts.findByIdempotencyKey(idempotencyKey)) ?? minted;\n\n // Claim the gathered rows conditionally and reconcile the stored\n // totals from what was VERIFIABLY claimed — a row grabbed by another\n // batch between gather and claim is skipped, never double-claimed, and\n // never counted.\n const result = await this.claimAndReconcile(payout, input);\n return { ...result, created: true };\n }\n\n /**\n * Whether a payout's stored totals are reproducible from the rows\n * actually stamped with its id — the invariant an interrupted claim pass\n * breaks. Clean replays short-circuit on this; repair runs only when it\n * fails.\n */\n private async membershipConsistent(\n payout: CommissionPayout,\n ): Promise<boolean> {\n const payoutId = payout.id ?? '';\n const members = await this.deps.commissions.findByPayout(payoutId);\n const memberAdjustments =\n await this.deps.adjustments.findByPayout(payoutId);\n const commissionTotalCents = members.reduce(\n (sum, c) => sum + c.amountCents,\n 0,\n );\n const adjustmentTotalCents = memberAdjustments.reduce(\n (sum, a) => sum + a.amountCents,\n 0,\n );\n return (\n payout.commissionTotalCents === commissionTotalCents &&\n payout.adjustmentTotalCents === adjustmentTotalCents &&\n payout.totalAmountCents === commissionTotalCents + adjustmentTotalCents\n );\n }\n\n /**\n * Claim pass + totals reconciliation for a PENDING payout.\n *\n * The claim set is the union of rows already stamped with this payout\n * (an interrupted earlier pass) and the currently gathered eligible\n * rows. Claims go through the collections' conditional `claimForPayout`\n * (rows owned by another batch are skipped); totals are then recomputed\n * from the claimed rows and saved when they drift from what the payout\n * carries. In the pathological all-rows-raced-away case the payout keeps\n * zero totals and a note — auditable, never double-paid.\n *\n * The pass also derives the payout's single-source stamp\n * (`sourceKind`/`sourceId`) from the verified claimed membership — set\n * when every claimed commission and every claimed adjustment's parent\n * shares exactly one non-empty source, empty otherwise — which is what\n * the source-scoped history listing indexes on.\n *\n * A fresh status re-read gates the pass: only a payout that is STILL\n * pending claims rows. This narrows (but, like every claim here, does not\n * transactionally eliminate) the race against a concurrent lifecycle\n * transition of the same payout — replaying a batch while its payout is\n * being approved/rejected is an overlapping concurrent scope the caller\n * must serialize, same as the documented batch-scope contract.\n */\n private async claimAndReconcile(\n stalePayout: CommissionPayout,\n input: CreatePayoutBatchInput,\n ): Promise<Omit<CreatePayoutBatchResult, 'created'>> {\n const payoutId = stalePayout.id ?? '';\n\n // Authoritative re-read: claims may only land on a payout that is\n // still pending. (The stale instance is the pre-lookup snapshot.)\n const payout = (await this.deps.payouts.get({ id: payoutId })) ?? null;\n if (!payout?.isPending()) {\n return {\n payout: payout ?? stalePayout,\n settledCommissionIds: [],\n settledAdjustmentIds: [],\n };\n }\n\n const previouslyClaimed =\n await this.deps.commissions.findByPayout(payoutId);\n // Re-gather with the SAME scope as the initial pass so a repair never\n // pulls out-of-scope rows into a scoped batch.\n const gathered = await this.gatherBatchCommissions(input);\n const commissionIds = [\n ...new Set(\n [...previouslyClaimed, ...gathered]\n .map((c) => c.id)\n .filter((id): id is string => !!id),\n ),\n ];\n const claimedCommissions = await this.deps.commissions.claimForPayout(\n commissionIds,\n payoutId,\n );\n\n const previouslyClaimedAdjustments =\n await this.deps.adjustments.findByPayout(payoutId);\n const gatheredAdjustments = await this.findEligibleUnsettledAdjustments(\n input.earnerId,\n input.currency,\n CommissionPayoutService.adjustmentParentPredicate(input),\n );\n const adjustmentIds = [\n ...new Set(\n [...previouslyClaimedAdjustments, ...gatheredAdjustments]\n .map((a) => a.id)\n .filter((id): id is string => !!id),\n ),\n ];\n const claimedAdjustments = await this.deps.adjustments.claimForPayout(\n adjustmentIds,\n payoutId,\n );\n\n const commissionTotalCents = claimedCommissions.reduce(\n (sum, c) => sum + c.amountCents,\n 0,\n );\n const adjustmentTotalCents = claimedAdjustments.reduce(\n (sum, a) => sum + a.amountCents,\n 0,\n );\n const totalAmountCents = commissionTotalCents + adjustmentTotalCents;\n\n // Derive the single-source stamp from the VERIFIED claimed membership\n // (adjustments prove their source through their parent commission).\n const parentById = await this.loadAdjustmentParents(\n this.deps.commissions,\n claimedCommissions,\n claimedAdjustments,\n );\n const derivedSource = CommissionPayoutService.deriveMembershipSource(\n claimedCommissions,\n claimedAdjustments,\n parentById,\n );\n\n if (\n payout.commissionTotalCents !== commissionTotalCents ||\n payout.adjustmentTotalCents !== adjustmentTotalCents ||\n payout.totalAmountCents !== totalAmountCents ||\n payout.sourceKind !== derivedSource.sourceKind ||\n payout.sourceId !== derivedSource.sourceId\n ) {\n payout.commissionTotalCents = commissionTotalCents;\n payout.adjustmentTotalCents = adjustmentTotalCents;\n payout.totalAmountCents = totalAmountCents;\n payout.sourceKind = derivedSource.sourceKind;\n payout.sourceId = derivedSource.sourceId;\n if (claimedCommissions.length === 0 && claimedAdjustments.length === 0) {\n payout.notes =\n 'no rows claimed (raced by a concurrent batch); nothing will be paid';\n }\n await payout.save();\n }\n\n return {\n payout,\n settledCommissionIds: claimedCommissions\n .map((c) => c.id)\n .filter((id): id is string => !!id),\n settledAdjustmentIds: claimedAdjustments\n .map((a) => a.id)\n .filter((id): id is string => !!id),\n };\n }\n\n /**\n * Complete a payout: flip the batch's settled commissions\n * `payable → paid` FIRST, then `payout.complete(paymentReference)`\n * (requires status `processing`). Ordering matters for recoverability —\n * if a member save fails mid-loop the payout is still `processing`, so a\n * retry finishes the remaining members (already-paid ones are skipped)\n * and then finalizes; the terminal transition never strands `payable`\n * members behind a `completed` payout. Adjustments carry no status —\n * stamping `payoutId` at batch time already settled them.\n */\n async completePayout(\n payoutId: string,\n paymentReference: string,\n now: Date = new Date(),\n ): Promise<CommissionPayout> {\n const payout = await this.requirePayout(payoutId);\n if (!payout.isProcessing()) {\n throw new Error(\n `CommissionPayout ${payout.id ?? '<new>'}: cannot complete from status '${payout.status}'`,\n );\n }\n if (!paymentReference) {\n throw new Error(\n `CommissionPayout ${payout.id ?? '<new>'}: complete() requires a paymentReference`,\n );\n }\n\n const members = await this.deps.commissions.findByPayout(payoutId);\n for (const commission of members) {\n if (commission.isPayable()) {\n commission.markPaid(now);\n await commission.save();\n }\n }\n\n payout.complete(paymentReference, now);\n await payout.save();\n return payout;\n }\n\n /**\n * Fail a payout (`approved | processing → failed`). The batch's rows stay\n * stamped — after `resetFromFailed()` the SAME payout retries the SAME\n * rows; releasing the rows to a different batch would double-pay them if\n * the failed remittance later settled.\n */\n async failPayout(\n payoutId: string,\n reason: string,\n ): Promise<CommissionPayout> {\n const payout = await this.requirePayout(payoutId);\n payout.fail(reason);\n await payout.save();\n return payout;\n }\n\n /**\n * One page of the payout history belonging to ONE earning source,\n * newest first (`created_at DESC, id DESC` — deterministic across ties).\n *\n * Candidates come from the indexed single-source stamp\n * (`CommissionPayout.sourceKind`/`sourceId`, maintained from verified\n * claimed membership at batch/repair time), so database work is bounded\n * by the page size — a sparse source never forces a scan of the global\n * payout history. Each page is then RE-VERIFIED against its actual\n * membership in three batched queries (commissions, adjustments,\n * adjustment parents — no per-payout N+1): every member commission and\n * every adjustment's parent must carry exactly the requested source and\n * agree with the payout on earner/currency/tenant. Rows the stamp alone\n * cannot prove are excluded fail-closed and reported in `excluded`\n * (mixed-source membership, missing adjustment parents, memberless\n * artifacts, released/rejected batches).\n *\n * Adjustment-only payouts are first-class: a batch that settled only\n * CommissionAdjustment rows proves its source through each adjustment's\n * parent commission and lists normally.\n *\n * Payouts minted before the stamp existed carry an empty stamp and are\n * invisible here until backfilled — see {@link restampPayoutSource}.\n *\n * Offset pagination contract: `nextOffset` advances by the SCANNED count\n * (verified + excluded), so pages never skip rows; a page may hold fewer\n * than `limit` verified payouts. Newly minted payouts prepend to the\n * history between calls, as with any offset listing. Tenant interception\n * applies to every query (candidates, membership, parents), so a tenant\n * context sees only its own history.\n */\n async getSourcePayoutHistory(\n input: SourcePayoutHistoryInput,\n ): Promise<SourcePayoutHistoryPage> {\n if (!input.sourceKind || !input.sourceId) {\n throw new Error(\n 'CommissionPayoutService.getSourcePayoutHistory: sourceKind and sourceId are required',\n );\n }\n const limit = Math.max(1, Math.min(100, Math.trunc(input.limit ?? 25)));\n const offset = Math.max(0, Math.trunc(input.offset ?? 0));\n\n // limit + 1 probes for a further page without a COUNT query.\n const probed = await this.deps.payouts.findBySource(\n input.sourceKind,\n input.sourceId,\n { limit: limit + 1, offset },\n );\n const hasMore = probed.length > limit;\n const candidates = hasMore ? probed.slice(0, limit) : probed;\n\n const payoutIds = candidates\n .map((p) => p.id)\n .filter((id): id is string => !!id);\n const members = await this.deps.commissions.findByPayouts(payoutIds);\n const memberAdjustments =\n await this.deps.adjustments.findByPayouts(payoutIds);\n const parentById = await this.loadAdjustmentParents(\n this.deps.commissions,\n members,\n memberAdjustments,\n );\n // Cardinality guard (see countStampedRows): rows a tenant scope cannot\n // see must still fail the page's membership proof, not silently thin\n // it out.\n const db = this.resolveDatabase();\n const rawCommissionCounts = await CommissionPayoutService.countStampedRows(\n db,\n this.deps.commissions.tableName,\n payoutIds,\n );\n const rawAdjustmentCounts = await CommissionPayoutService.countStampedRows(\n db,\n this.deps.adjustments.tableName,\n payoutIds,\n );\n\n const membersByPayout = new Map<string, Commission[]>();\n for (const commission of members) {\n const bucket = membersByPayout.get(commission.payoutId);\n if (bucket) bucket.push(commission);\n else membersByPayout.set(commission.payoutId, [commission]);\n }\n const adjustmentsByPayout = new Map<string, CommissionAdjustment[]>();\n for (const adjustment of memberAdjustments) {\n const bucket = adjustmentsByPayout.get(adjustment.payoutId);\n if (bucket) bucket.push(adjustment);\n else adjustmentsByPayout.set(adjustment.payoutId, [adjustment]);\n }\n\n const page: SourcePayoutHistoryPage = {\n payouts: [],\n excluded: [],\n offset,\n limit,\n nextOffset: hasMore ? offset + candidates.length : null,\n };\n for (const payout of candidates) {\n const id = payout.id ?? '';\n const visibleMembers = membersByPayout.get(id) ?? [];\n const visibleAdjustments = adjustmentsByPayout.get(id) ?? [];\n const rawCommissionCount = rawCommissionCounts.get(id) ?? 0;\n const rawAdjustmentCount = rawAdjustmentCounts.get(id) ?? 0;\n if (\n rawCommissionCount !== visibleMembers.length ||\n rawAdjustmentCount !== visibleAdjustments.length\n ) {\n page.excluded.push({\n payoutId: id,\n reason: 'tenant_mismatch',\n detail:\n `payout has ${rawCommissionCount} commission and ${rawAdjustmentCount} adjustment rows stamped, ` +\n `but only ${visibleMembers.length} and ${visibleAdjustments.length} are visible in the current tenant scope`,\n });\n continue;\n }\n const verdict = CommissionPayoutService.verifySourceMembership({\n payout,\n commissions: visibleMembers,\n adjustments: visibleAdjustments,\n parentById,\n sourceKind: input.sourceKind,\n sourceId: input.sourceId,\n });\n if (verdict.ok) {\n page.payouts.push(payout);\n } else {\n page.excluded.push({\n payoutId: id,\n reason: verdict.reason,\n detail: verdict.detail,\n });\n }\n }\n return page;\n }\n\n /**\n * Backfill/repair the derived single-source stamp of ONE payout from its\n * actual membership — the documented migration path for payouts minted\n * before the stamp existed (they carry `''`/`''` and are invisible to\n * {@link getSourcePayoutHistory} until restamped). Safe on any status:\n * the stamp is derived data, and a payout whose membership is mixed or\n * unprovable derives back to the empty stamp.\n *\n * One-time migration loop: page through `payouts.list({})` and call this\n * per row (idempotent — an already-correct stamp saves nothing).\n */\n async restampPayoutSource(payoutId: string): Promise<{\n payout: CommissionPayout | null;\n sourceKind: string;\n sourceId: string;\n changed: boolean;\n }> {\n const payout = await this.deps.payouts.get({ id: payoutId });\n if (!payout) {\n return { payout: null, sourceKind: '', sourceId: '', changed: false };\n }\n const members = await this.deps.commissions.findByPayout(payoutId);\n const memberAdjustments =\n await this.deps.adjustments.findByPayout(payoutId);\n const parentById = await this.loadAdjustmentParents(\n this.deps.commissions,\n members,\n memberAdjustments,\n );\n const derived = CommissionPayoutService.deriveMembershipSource(\n members,\n memberAdjustments,\n parentById,\n );\n if (\n payout.sourceKind === derived.sourceKind &&\n payout.sourceId === derived.sourceId\n ) {\n return { payout, ...derived, changed: false };\n }\n payout.sourceKind = derived.sourceKind;\n payout.sourceId = derived.sourceId;\n await payout.save();\n return { payout, ...derived, changed: true };\n }\n\n /**\n * Atomically authorize ONE payout against ONE earning source and perform\n * a lifecycle transition — the multi-replica-safe alternative to loading\n * the payout, querying membership, and calling the model transitions\n * yourself (which leaves a TOCTOU window between authorization and\n * transition).\n *\n * Everything runs on the SAME transaction database: the payout row is\n * locked (PostgreSQL `SELECT … FOR UPDATE`, so concurrent calls across\n * app replicas serialize on the row; single-connection engines get\n * equivalent behavior from the transaction plus an in-process\n * per-database queue), membership is re-read and re-verified under the\n * lock, totals are recomputed under the lock, and the transition + every\n * member write commit or roll back together.\n *\n * Under the lock, in order:\n *\n * 1. **Hydrate** — load the locked payout. A missing/cross-tenant id\n * refuses `payout_not_found`.\n * 2. **Source authorization** — every member commission, and every\n * member adjustment through its parent commission, must carry exactly\n * the requested `(sourceKind, sourceId)` and agree with the payout on\n * earner/currency/tenant; anything unprovable refuses fail-closed\n * (`source_mismatch`, `adjustment_parent_missing`, mismatches,\n * `membership_empty` — note this means memberless raced-away batch\n * artifacts cannot receive any status-derived outcome through this\n * source-authorized door).\n * 3. **Status outcome** — after authorization, a payout already in the\n * action's target status returns `already_applied` WITHOUT writing\n * (terminal completion metadata — `paymentReference`, `providerRef`,\n * `paidAt` — is never overwritten by a replay). `expectedStatus`, when\n * given, must then match the locked status or the call refuses\n * `status_conflict`; the action's own from-status rule applies last.\n * Concurrent duplicate calls for actions that RETAIN membership\n * therefore resolve deterministically: one `transitioned`, the rest\n * `already_applied` (or `status_conflict` when they raced a DIFFERENT\n * action). `reject` releases the membership evidence, so a serialized\n * replay fails closed as `membership_empty` rather than exposing the\n * rejected status.\n * 4. **Totals recompute** — commission/adjustment/total amounts are\n * recomputed from the locked membership; drift refuses `totals_drift`\n * for the money-forward actions (`approve`, `mark_processing`,\n * `complete` — repair via a `createPayoutBatch` replay while the\n * payout is pending). The defensive actions (`fail`, `reject`)\n * proceed despite drift — rejecting a drifted batch IS the remedy. A\n * non-positive recomputed total refuses `approve` with\n * `non_positive_total`.\n * 5. **Apply** — `complete` flips the batch's payable commissions to\n * `paid` and completes the payout in the same transaction (no more\n * retryable-but-partial completion); `reject` RELEASES the batch's\n * membership (clears `payoutId` on every member commission and\n * adjustment, model-layer per row) so the rows settle through a\n * future batch, then marks the payout rejected — terminal.\n *\n * Replaying a `createPayoutBatch` for the same payout concurrently with\n * a transition is an overlapping concurrent scope (same contract as\n * overlapping batch scopes): the batch side re-checks pending before\n * claiming, which narrows but does not transactionally close that race —\n * serialize those two call sites per payout.\n */\n async transitionPayoutForSource(\n input: TransitionPayoutForSourceInput,\n ): Promise<TransitionPayoutForSourceResult> {\n const now = input.now ?? new Date();\n if (!input.sourceKind || !input.sourceId) {\n throw new Error(\n 'CommissionPayoutService.transitionPayoutForSource: sourceKind and sourceId are required',\n );\n }\n const targetStatus =\n CommissionPayoutService.TRANSITION_TARGET[input.action];\n if (!targetStatus) {\n throw new Error(\n `CommissionPayoutService.transitionPayoutForSource: unknown action '${String(input.action)}'`,\n );\n }\n if (input.action === 'complete' && !input.paymentReference) {\n throw new Error(\n \"CommissionPayoutService.transitionPayoutForSource: action 'complete' requires a paymentReference\",\n );\n }\n if (\n (input.action === 'fail' || input.action === 'reject') &&\n !input.reason\n ) {\n throw new Error(\n `CommissionPayoutService.transitionPayoutForSource: action '${input.action}' requires a reason`,\n );\n }\n // A malformed id can't match a payout, and would abort the whole query\n // as an invalid cast on native-uuid columns — refuse it as not-found.\n if (!CommissionPayoutService.UUID_RE.test(input.payoutId)) {\n return {\n outcome: 'refused',\n payout: null,\n refusal: {\n reason: 'payout_not_found',\n detail: `payout id '${input.payoutId}' is not a valid id`,\n },\n };\n }\n\n const db = this.resolveDatabase();\n const outcome = await this.runSerializedTransaction<TxTransitionOutcome>(\n db,\n async (txDb): Promise<TxTransitionOutcome> => {\n const tx = {\n payouts: await CommissionPayoutCollection.create(\n CommissionPayoutService.txOptions(txDb),\n ),\n commissions: await CommissionCollection.create(\n CommissionPayoutService.txOptions(txDb),\n ),\n adjustments: await CommissionAdjustmentCollection.create(\n CommissionPayoutService.txOptions(txDb),\n ),\n };\n\n // Row lock: PostgreSQL serializes concurrent transitions of one\n // payout across replicas here. Single-connection engines (SQLite,\n // DuckDB) don't support FOR UPDATE and don't need it — their whole\n // transaction is serialized by runSerializedTransaction.\n if (\n typeof (db as TransactionCapableDatabase).acquireSession ===\n 'function'\n ) {\n await txDb.query(\n `SELECT id FROM ${tx.payouts.tableName} WHERE id = $1 FOR UPDATE`,\n input.payoutId,\n );\n }\n\n const payout = await tx.payouts.get({ id: input.payoutId });\n if (!payout) {\n return CommissionPayoutService.txRefusal(\n null,\n 'payout_not_found',\n `payout '${input.payoutId}' not found`,\n );\n }\n const payoutId = payout.id ?? '';\n\n const members = await tx.commissions.findByPayout(payoutId);\n const memberAdjustments = await tx.adjustments.findByPayout(payoutId);\n\n // Cardinality guard: the reads above are tenant-scoped, so a\n // foreign tenant's row stamped onto this payout would be invisible\n // — and verification over the visible subset would authorize\n // incomplete membership. Compare against RAW counts (count-only,\n // no row data crosses the tenant boundary) and fail closed on any\n // excess.\n const rawCommissionCount =\n (\n await CommissionPayoutService.countStampedRows(\n txDb,\n tx.commissions.tableName,\n [input.payoutId],\n )\n ).get(input.payoutId) ?? 0;\n const rawAdjustmentCount =\n (\n await CommissionPayoutService.countStampedRows(\n txDb,\n tx.adjustments.tableName,\n [input.payoutId],\n )\n ).get(input.payoutId) ?? 0;\n if (\n rawCommissionCount !== members.length ||\n rawAdjustmentCount !== memberAdjustments.length\n ) {\n return CommissionPayoutService.txAuthorizationRefusal(\n payoutId,\n 'tenant_mismatch',\n );\n }\n\n const parentById = await this.loadAdjustmentParents(\n tx.commissions,\n members,\n memberAdjustments,\n );\n const verdict = CommissionPayoutService.verifySourceMembership({\n payout,\n commissions: members,\n adjustments: memberAdjustments,\n parentById,\n sourceKind: input.sourceKind,\n sourceId: input.sourceId,\n });\n if (!verdict.ok) {\n return CommissionPayoutService.txAuthorizationRefusal(\n payoutId,\n verdict.reason,\n );\n }\n\n if (payout.status === targetStatus) {\n return { outcome: 'already_applied' as const, payoutId };\n }\n if (input.expectedStatus && payout.status !== input.expectedStatus) {\n return CommissionPayoutService.txRefusal(\n payoutId,\n 'status_conflict',\n `expected status '${input.expectedStatus}' but payout is '${payout.status}'`,\n );\n }\n const legalFrom = CommissionPayoutService.TRANSITION_FROM[input.action];\n if (!legalFrom.includes(payout.status)) {\n return CommissionPayoutService.txRefusal(\n payoutId,\n 'status_conflict',\n `cannot ${input.action} from status '${payout.status}'`,\n );\n }\n\n const commissionTotalCents = members.reduce(\n (sum, c) => sum + c.amountCents,\n 0,\n );\n const adjustmentTotalCents = memberAdjustments.reduce(\n (sum, a) => sum + a.amountCents,\n 0,\n );\n const totalAmountCents = commissionTotalCents + adjustmentTotalCents;\n const drifted =\n payout.commissionTotalCents !== commissionTotalCents ||\n payout.adjustmentTotalCents !== adjustmentTotalCents ||\n payout.totalAmountCents !== totalAmountCents;\n const moneyForward =\n input.action === 'approve' ||\n input.action === 'mark_processing' ||\n input.action === 'complete';\n if (drifted && moneyForward) {\n return CommissionPayoutService.txRefusal(\n payoutId,\n 'totals_drift',\n `persisted totals (commission=${payout.commissionTotalCents} adjustment=${payout.adjustmentTotalCents} total=${payout.totalAmountCents}) ` +\n `do not match membership (commission=${commissionTotalCents} adjustment=${adjustmentTotalCents} total=${totalAmountCents}) — ` +\n 'repair via a createPayoutBatch replay while pending',\n );\n }\n if (input.action === 'approve' && payout.totalAmountCents <= 0) {\n return CommissionPayoutService.txRefusal(\n payoutId,\n 'non_positive_total',\n `cannot approve a batch with non-positive total (${payout.totalAmountCents} cents)`,\n );\n }\n\n const releasedCommissionIds: string[] = [];\n const releasedAdjustmentIds: string[] = [];\n switch (input.action) {\n case 'approve':\n payout.approve();\n break;\n case 'mark_processing':\n payout.markProcessing();\n break;\n case 'fail':\n payout.fail(input.reason ?? '');\n break;\n case 'reject': {\n // Release the membership FIRST (model-layer per row — tenancy-\n // and dialect-safe), then the terminal decline; the transaction\n // makes the pair atomic. Stranded stamped rows would otherwise\n // be unsettleable forever.\n for (const commission of members) {\n commission.payoutId = '';\n await commission.save();\n if (commission.id) releasedCommissionIds.push(commission.id);\n }\n for (const adjustment of memberAdjustments) {\n adjustment.payoutId = '';\n await adjustment.save();\n if (adjustment.id) releasedAdjustmentIds.push(adjustment.id);\n }\n payout.reject(input.reason ?? '');\n break;\n }\n case 'complete': {\n // Members flip to paid in the SAME transaction as the terminal\n // payout write — a mid-loop failure rolls everything back\n // instead of leaving a partially-paid batch.\n for (const commission of members) {\n if (commission.isPayable()) {\n commission.markPaid(now);\n await commission.save();\n }\n }\n payout.complete(input.paymentReference ?? '', now);\n break;\n }\n }\n await payout.save();\n return {\n outcome: 'transitioned' as const,\n payoutId,\n releasedCommissionIds,\n releasedAdjustmentIds,\n };\n },\n );\n\n // Rehydrate OUTSIDE the transaction so the returned instance is bound\n // to the service's own connection, not the released transaction.\n const payout =\n outcome.payoutId && !outcome.authorizationFailed\n ? await this.deps.payouts.get({ id: outcome.payoutId })\n : null;\n const result: TransitionPayoutForSourceResult = {\n outcome: outcome.outcome,\n payout,\n };\n if (outcome.refusal) result.refusal = outcome.refusal;\n if (outcome.releasedCommissionIds?.length) {\n result.releasedCommissionIds = outcome.releasedCommissionIds;\n }\n if (outcome.releasedAdjustmentIds?.length) {\n result.releasedAdjustmentIds = outcome.releasedAdjustmentIds;\n }\n return result;\n }\n\n // -------- Source membership verification internals --------\n\n /** Target status per action — also the `already_applied` echo test. */\n private static readonly TRANSITION_TARGET: Record<\n PayoutSourceTransitionAction,\n CommissionPayoutStatus\n > = {\n approve: 'approved',\n mark_processing: 'processing',\n complete: 'completed',\n fail: 'failed',\n reject: 'rejected',\n };\n\n /** Legal from-statuses per action (mirrors the model transition guards). */\n private static readonly TRANSITION_FROM: Record<\n PayoutSourceTransitionAction,\n CommissionPayoutStatus[]\n > = {\n approve: ['pending'],\n mark_processing: ['approved'],\n complete: ['processing'],\n fail: ['approved', 'processing'],\n reject: ['pending', 'approved'],\n };\n\n private static txOptions(txDb: DatabaseInterface): SmrtClassOptions {\n return {\n db: txDb,\n // The transaction database is the SAME initialized database on a\n // pinned connection — skip system-table bootstrap and runtime\n // service setup (signals/AI) for these short-lived bindings.\n _reuseInitializedDb: true,\n _deferRuntimeInitialization: true,\n };\n }\n\n private static txRefusal(\n payoutId: string | null,\n reason: PayoutTransitionRefusalReason,\n detail: string,\n ): {\n outcome: 'refused';\n payoutId: string | null;\n refusal: { reason: PayoutTransitionRefusalReason; detail: string };\n } {\n return { outcome: 'refused', payoutId, refusal: { reason, detail } };\n }\n\n /**\n * A membership refusal means the requested source was never authorized.\n * Keep the typed reason for callers while withholding both the payout and\n * member-specific detail (actual source, ids, account, tenant, or money).\n */\n private static txAuthorizationRefusal(\n payoutId: string,\n reason: PayoutMembershipRefusalReason,\n ): TxTransitionOutcome {\n return {\n outcome: 'refused',\n payoutId,\n authorizationFailed: true,\n refusal: {\n reason,\n detail: 'requested source is not authorized for this payout membership',\n },\n };\n }\n\n /**\n * RAW stamped-row counts per payout id — deliberately UNSCOPED\n * (count-only, reviewed): tenant-scoped reads cannot see a foreign\n * tenant's row stamped onto a payout, so membership verification that\n * trusted only the visible subset would authorize (or list) incomplete\n * membership. No row data crosses the tenant boundary — only per-payout\n * counts, compared against the visible membership; any excess fails\n * closed as `tenant_mismatch`. Ids must be UUID-shaped (callers pass\n * validated payout ids), and the payout-id predicate never touches the\n * empty-FK encoding, so the query is dialect-safe.\n */\n private static async countStampedRows(\n db: DatabaseInterface,\n table: string,\n payoutIds: string[],\n ): Promise<Map<string, number>> {\n const counts = new Map<string, number>();\n const ids = payoutIds.filter((id) =>\n CommissionPayoutService.UUID_RE.test(id),\n );\n if (ids.length === 0) return counts;\n const placeholders = ids.map((_, i) => `$${i + 1}`).join(', ');\n const res = await db.query(\n `SELECT payout_id, COUNT(*) AS row_count FROM ${table} WHERE payout_id IN (${placeholders}) GROUP BY payout_id`,\n ...ids,\n );\n const rows = Array.isArray(res)\n ? (res as Record<string, unknown>[])\n : ((res as { rows?: Record<string, unknown>[] }).rows ?? []);\n for (const row of rows) {\n counts.set(String(row.payout_id), Number(row.row_count));\n }\n return counts;\n }\n\n /**\n * Parents of the given adjustments, keyed by commission id — member\n * commissions are reused, only the rest are fetched (one `IN` query).\n */\n private async loadAdjustmentParents(\n commissions: CommissionCollection,\n memberCommissions: Commission[],\n adjustments: CommissionAdjustment[],\n ): Promise<Map<string, Commission>> {\n const parentById = new Map<string, Commission>();\n for (const commission of memberCommissions) {\n if (commission.id) parentById.set(commission.id, commission);\n }\n const missingIds = [\n ...new Set(\n adjustments\n .map((a) => a.commissionId)\n .filter((id) => !!id && !parentById.has(id)),\n ),\n ];\n if (missingIds.length > 0) {\n const fetched = await commissions.listByIds(missingIds);\n for (const parent of fetched) {\n if (parent.id) parentById.set(parent.id, parent);\n }\n }\n return parentById;\n }\n\n /**\n * The single `(sourceKind, sourceId)` a payout's membership provably\n * belongs to — or the empty stamp when membership is empty, any member's\n * source is missing, an adjustment parent is unloadable, or more than\n * one source appears.\n */\n private static deriveMembershipSource(\n memberCommissions: Commission[],\n adjustments: CommissionAdjustment[],\n parentById: Map<string, Commission>,\n ): { sourceKind: string; sourceId: string } {\n const empty = { sourceKind: '', sourceId: '' };\n if (memberCommissions.length === 0 && adjustments.length === 0) {\n return empty;\n }\n const sources = new Map<string, { sourceKind: string; sourceId: string }>();\n const add = (sourceKind: string, sourceId: string) => {\n sources.set(`${sourceKind.length}:${sourceKind}:${sourceId}`, {\n sourceKind,\n sourceId,\n });\n };\n for (const commission of memberCommissions) {\n if (!commission.sourceKind || !commission.sourceId) return empty;\n add(commission.sourceKind, commission.sourceId);\n }\n for (const adjustment of adjustments) {\n const parent = parentById.get(adjustment.commissionId);\n if (!parent?.sourceKind || !parent.sourceId) return empty;\n add(parent.sourceKind, parent.sourceId);\n }\n if (sources.size !== 1) return empty;\n const [only] = sources.values();\n return only;\n }\n\n /**\n * Prove that EVERY member of a payout belongs to the requested source\n * and agrees with the payout on earner/currency/tenant. Adjustments\n * prove their source through their parent commission. Fail-closed: the\n * first unprovable member decides the verdict.\n */\n private static verifySourceMembership(input: {\n payout: CommissionPayout;\n commissions: Commission[];\n adjustments: CommissionAdjustment[];\n parentById: Map<string, Commission>;\n sourceKind: string;\n sourceId: string;\n }):\n | { ok: true }\n | { ok: false; reason: PayoutMembershipRefusalReason; detail: string } {\n const { payout } = input;\n // '' and NULL both mean \"no tenant\" depending on dialect — normalize.\n const tenantOf = (value: string | null | undefined) => value || null;\n if (input.commissions.length === 0 && input.adjustments.length === 0) {\n return {\n ok: false,\n reason: 'membership_empty',\n detail: 'no commissions or adjustments are stamped with this payout',\n };\n }\n for (const commission of input.commissions) {\n if (commission.earnerId !== payout.earnerId) {\n return {\n ok: false,\n reason: 'earner_mismatch',\n detail: `commission ${commission.id} belongs to earner '${commission.earnerId}', payout to '${payout.earnerId}'`,\n };\n }\n if (commission.currency !== payout.currency) {\n return {\n ok: false,\n reason: 'currency_mismatch',\n detail: `commission ${commission.id} is ${commission.currency}, payout is ${payout.currency}`,\n };\n }\n if (tenantOf(commission.tenantId) !== tenantOf(payout.tenantId)) {\n return {\n ok: false,\n reason: 'tenant_mismatch',\n detail: `commission ${commission.id} and the payout disagree on tenant`,\n };\n }\n if (\n commission.sourceKind !== input.sourceKind ||\n commission.sourceId !== input.sourceId\n ) {\n return {\n ok: false,\n reason: 'source_mismatch',\n detail: `commission ${commission.id} belongs to source '${commission.sourceKind}:${commission.sourceId}', not '${input.sourceKind}:${input.sourceId}'`,\n };\n }\n }\n for (const adjustment of input.adjustments) {\n if (adjustment.earnerId !== payout.earnerId) {\n return {\n ok: false,\n reason: 'earner_mismatch',\n detail: `adjustment ${adjustment.id} belongs to earner '${adjustment.earnerId}', payout to '${payout.earnerId}'`,\n };\n }\n if (adjustment.currency !== payout.currency) {\n return {\n ok: false,\n reason: 'currency_mismatch',\n detail: `adjustment ${adjustment.id} is ${adjustment.currency}, payout is ${payout.currency}`,\n };\n }\n if (tenantOf(adjustment.tenantId) !== tenantOf(payout.tenantId)) {\n return {\n ok: false,\n reason: 'tenant_mismatch',\n detail: `adjustment ${adjustment.id} and the payout disagree on tenant`,\n };\n }\n const parent = input.parentById.get(adjustment.commissionId);\n if (!parent) {\n return {\n ok: false,\n reason: 'adjustment_parent_missing',\n detail: `adjustment ${adjustment.id} parent commission '${adjustment.commissionId}' cannot be loaded to prove source ownership`,\n };\n }\n // The PARENT must agree with the payout on account axes too — an\n // adjustment's earner/currency/tenant are denormalized by\n // convention, not enforced, so a coherent-looking adjustment can\n // still hang off another account's commission.\n if (parent.earnerId !== payout.earnerId) {\n return {\n ok: false,\n reason: 'earner_mismatch',\n detail: `adjustment ${adjustment.id} parent commission belongs to earner '${parent.earnerId}', payout to '${payout.earnerId}'`,\n };\n }\n if (parent.currency !== payout.currency) {\n return {\n ok: false,\n reason: 'currency_mismatch',\n detail: `adjustment ${adjustment.id} parent commission is ${parent.currency}, payout is ${payout.currency}`,\n };\n }\n if (tenantOf(parent.tenantId) !== tenantOf(payout.tenantId)) {\n return {\n ok: false,\n reason: 'tenant_mismatch',\n detail: `adjustment ${adjustment.id} parent commission and the payout disagree on tenant`,\n };\n }\n if (\n parent.sourceKind !== input.sourceKind ||\n parent.sourceId !== input.sourceId\n ) {\n return {\n ok: false,\n reason: 'source_mismatch',\n detail: `adjustment ${adjustment.id} parent commission belongs to source '${parent.sourceKind}:${parent.sourceId}', not '${input.sourceKind}:${input.sourceId}'`,\n };\n }\n }\n return { ok: true };\n }\n\n /** The initialized database behind the payout collection. */\n private resolveDatabase(): DatabaseInterface {\n const db = this.deps.payouts.options.db;\n if (!db || typeof db === 'string' || !('query' in db)) {\n throw new Error(\n 'CommissionPayoutService: the payout collection has no initialized database',\n );\n }\n return db as DatabaseInterface;\n }\n\n /**\n * Run `fn` inside a database transaction. PostgreSQL transactions get\n * their own pooled connection, so they run concurrently (the FOR UPDATE\n * row lock inside `fn` provides the per-payout serialization, replica-\n * safe). Single-connection engines (SQLite, DuckDB, JSON) multiplex\n * every transaction over one connection where concurrent BEGIN/COMMIT\n * pairs would interleave — their transitions chain per database\n * instance, giving equivalent serialized behavior in-process. An engine\n * with no transaction support at all still gets the serialized chain.\n */\n private async runSerializedTransaction<T>(\n db: DatabaseInterface,\n fn: (txDb: DatabaseInterface) => Promise<T>,\n ): Promise<T> {\n const capable = db as TransactionCapableDatabase;\n const runTx = () =>\n typeof capable.transaction === 'function'\n ? capable.transaction(fn)\n : fn(db);\n if (typeof capable.acquireSession === 'function') {\n return await runTx();\n }\n const previous =\n singleConnectionTransitionTails.get(db) ?? Promise.resolve();\n // Chain regardless of the predecessor's outcome — a failed transition\n // must not poison the queue behind it.\n const turn = previous.then(runTx, runTx);\n singleConnectionTransitionTails.set(\n db,\n turn.then(\n () => undefined,\n () => undefined,\n ),\n );\n return await turn;\n }\n\n /**\n * Unsettled adjustments for the earner/currency whose parent commission\n * is earned/approved/payable/paid — the same eligibility rule the balance\n * service applies, so batches settle exactly what balances report.\n *\n * When `parentPredicate` is given (a scoped batch), an adjustment is also\n * kept only when its parent commission satisfies the predicate — so a\n * source-scoped or explicit-id batch settles only its own adjustments.\n */\n private async findEligibleUnsettledAdjustments(\n earnerId: string,\n currency: string,\n parentPredicate?: (parent: Commission) => boolean,\n ) {\n const unsettled = await this.deps.adjustments.findUnsettledByEarner(\n earnerId,\n currency,\n );\n if (unsettled.length === 0) return unsettled;\n\n const parentIds = [\n ...new Set(unsettled.map((a) => a.commissionId).filter(Boolean)),\n ];\n const parents = await this.deps.commissions.listByIds(parentIds);\n const parentById = new Map<string, Commission>();\n for (const parent of parents) {\n if (parent.id) parentById.set(parent.id, parent);\n }\n const settleable =\n ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES as readonly CommissionStatus[];\n return unsettled.filter((adjustment) => {\n const parent = parentById.get(adjustment.commissionId);\n if (parent === undefined) return false;\n if (!settleable.includes(parent.status)) return false;\n if (parentPredicate && !parentPredicate(parent)) return false;\n return true;\n });\n }\n\n /**\n * The payable, unsettled commissions this batch would settle, honoring\n * the input scope: an explicit `commissionIds` set (each validated\n * payable + unsettled + belonging to this earner/currency), a single\n * `(sourceKind, sourceId)`, or — unscoped — the whole earner/currency.\n */\n private async gatherBatchCommissions(\n input: CreatePayoutBatchInput,\n ): Promise<Commission[]> {\n // A PRESENT `commissionIds` (even `[]`) is an explicit scope — an empty\n // list settles nothing, it must never fall through to the earner-wide\n // gather.\n if (input.commissionIds !== undefined) {\n // Drop empty / non-UUID ids before querying: `id` is a native `uuid`\n // column on Postgres/DuckDB, so a malformed value would abort the\n // whole `listByIds` query there (SQLite silently misses it). Every\n // real smrt id is a UUID, so a non-UUID id can't match a commission\n // anyway — filtering it here is exactly the documented \"ineligible\n // ids are ignored\" behavior, and stops one bad id failing the batch.\n const validIds = input.commissionIds.filter((id) =>\n CommissionPayoutService.UUID_RE.test(id),\n );\n if (validIds.length === 0) return [];\n const rows = await this.deps.commissions.listByIds(validIds);\n return rows.filter(\n (c) =>\n c.earnerId === input.earnerId &&\n c.currency === input.currency &&\n c.status === 'payable' &&\n !c.payoutId,\n );\n }\n const scope =\n input.sourceKind && input.sourceId\n ? { sourceKind: input.sourceKind, sourceId: input.sourceId }\n : undefined;\n return await this.deps.commissions.findPayableUnsettled(\n input.earnerId,\n input.currency,\n scope,\n );\n }\n\n /**\n * Parent-commission predicate that narrows eligible adjustments to the\n * batch scope: explicit-id batches keep adjustments whose parent is in\n * the requested id set (even a now-paid parent — a clawback is still\n * owed); source-scoped batches keep adjustments whose parent shares the\n * source; unscoped batches keep all (predicate `undefined`).\n */\n private static adjustmentParentPredicate(\n input: CreatePayoutBatchInput,\n ): ((parent: Commission) => boolean) | undefined {\n // A present `commissionIds` (even `[]`) scopes adjustments to that set;\n // an empty set matches nothing.\n if (input.commissionIds !== undefined) {\n const ids = new Set(input.commissionIds);\n return (parent) => !!parent.id && ids.has(parent.id);\n }\n if (input.sourceKind && input.sourceId) {\n const { sourceKind, sourceId } = input;\n return (parent) =>\n parent.sourceKind === sourceKind && parent.sourceId === sourceId;\n }\n return undefined;\n }\n\n /**\n * Validate the batch scope: `sourceKind`/`sourceId` are all-or-nothing\n * and mutually exclusive with `commissionIds`; an explicit `commissionIds`\n * batch requires its own `idempotencyKey` (no natural default exists).\n */\n private static assertScope(input: CreatePayoutBatchInput): void {\n // A PRESENT `commissionIds` is an explicit scope regardless of length —\n // an empty list is a valid \"settle nothing\" request, not an unscoped\n // batch. Guarding on presence (not length) is what makes a\n // dynamically-computed `[]` fail closed instead of settling the whole\n // earner.\n const hasIds = input.commissionIds !== undefined;\n // A source scope is INTENDED when either property is present. Detecting\n // presence (not truthiness) is what stops `{ sourceKind: '', sourceId:\n // '' }` from failing open into an earner-wide settlement — a\n // present-but-empty (or half-set) source is malformed, not \"unscoped\".\n const hasSource =\n input.sourceKind !== undefined || input.sourceId !== undefined;\n if (hasSource && (!input.sourceKind || !input.sourceId)) {\n throw new Error(\n 'CommissionPayoutService.createPayoutBatch: sourceKind and sourceId must both be set and non-empty to scope by source',\n );\n }\n if (hasIds && hasSource) {\n throw new Error(\n 'CommissionPayoutService.createPayoutBatch: commissionIds and sourceKind/sourceId are mutually exclusive',\n );\n }\n if (hasIds && !input.idempotencyKey) {\n throw new Error(\n 'CommissionPayoutService.createPayoutBatch: an explicit commissionIds batch requires an idempotencyKey',\n );\n }\n }\n\n private async requirePayout(payoutId: string): Promise<CommissionPayout> {\n const payout = await this.deps.payouts.get({ id: payoutId });\n if (!payout) {\n throw new Error(\n `CommissionPayoutService: payout '${payoutId}' not found`,\n );\n }\n return payout;\n }\n\n /**\n * `${earnerId}:${currency}:${YYYY-MM-DD}` — or, when scoped by source, a\n * key that folds the source in so a per-network batch and the earner-wide\n * batch on the same day get distinct keys. `sourceKind`/`sourceId` are\n * unconstrained generic strings, so each is LENGTH-PREFIXED (`len:value`)\n * to keep the encoding unambiguous: a literal `:` inside a source string\n * can't make two different `(sourceKind, sourceId)` pairs collide (e.g.\n * `('a:b','c')` → `…:src:3:a:b:1:c:…` vs `('a','b:c')` → `…:src:1:a:3:b:c:…`).\n * See the input doc.\n */\n private static defaultIdempotencyKey(\n earnerId: string,\n currency: string,\n periodEnd: Date,\n scope?: { sourceKind: string; sourceId: string },\n ): string {\n const date = periodEnd.toISOString().slice(0, 10);\n if (scope) {\n const enc = (s: string) => `${s.length}:${s}`;\n return `${earnerId}:${currency}:src:${enc(scope.sourceKind)}:${enc(scope.sourceId)}:${date}`;\n }\n return `${earnerId}:${currency}:${date}`;\n }\n}\n\nexport default CommissionPayoutService;\n","/**\n * CommissionSettlementService — advances Commissions along the strict\n * `pending → earned → approved → payable` chain (the final `payable → paid`\n * step belongs to `CommissionPayoutService.completePayout`, which flips a\n * batch's rows when the money actually moves).\n *\n * All methods persist the rows they advance (transition method + save per\n * row) and return the updated instances.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { CommissionCollection } from '../collections/CommissionCollection.js';\nimport type { Commission } from '../models/Commission.js';\n\nexport class CommissionSettlementService {\n constructor(private readonly commissions: CommissionCollection) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<CommissionSettlementService> {\n return new CommissionSettlementService(\n await CommissionCollection.create(classOptions),\n );\n }\n\n /**\n * Sweep the clearing window: every `pending` commission whose\n * `clearingEndsAt` is `<= now` — or whose `clearingEndsAt` is `null`\n * (null means NO clearing window applies, so the row is immediately\n * sweepable) — transitions to `earned` and is saved.\n *\n * @returns The commissions that were marked earned by this sweep.\n */\n async sweepClearing(now: Date = new Date()): Promise<Commission[]> {\n const pending = await this.commissions.findByStatus('pending');\n const swept: Commission[] = [];\n for (const commission of pending) {\n const clearingEndsAt = commission.clearingEndsAt;\n if (clearingEndsAt !== null && clearingEndsAt.getTime() > now.getTime()) {\n continue; // still clearing\n }\n commission.markEarned(now);\n await commission.save();\n swept.push(commission);\n }\n return swept;\n }\n\n /**\n * Approve `earned` commissions by id (`earned → approved`). Strict: a\n * missing id or a commission in any other status throws — the caller\n * names exact rows, so a mismatch is a bug worth surfacing, not skipping.\n */\n async approveCommissions(\n ids: string[],\n now: Date = new Date(),\n ): Promise<Commission[]> {\n return await this.transitionByIds(ids, (commission) => {\n commission.approve(now);\n });\n }\n\n /**\n * Release `approved` commissions to `payable` by id. Strict — see\n * {@link approveCommissions}.\n */\n async markPayable(\n ids: string[],\n now: Date = new Date(),\n ): Promise<Commission[]> {\n return await this.transitionByIds(ids, (commission) => {\n commission.markPayable(now);\n });\n }\n\n /**\n * Convenience chain: advance each commission from wherever it currently\n * sits up to `payable` (`pending → earned → approved → payable`), saving\n * after EACH step — the save-time guard only admits single-step edges, so\n * every intermediate state is persisted (each with its timestamp). Rows\n * already `payable` or `paid` are returned untouched (idempotent). Note\n * this deliberately bypasses the clearing window — it's the \"operator\n * says pay these now\" path.\n */\n async settleUpToPayable(\n ids: string[],\n now: Date = new Date(),\n ): Promise<Commission[]> {\n const updated: Commission[] = [];\n for (const id of ids) {\n const commission = await this.requireCommission(id);\n if (commission.isPending()) {\n commission.markEarned(now);\n await commission.save();\n }\n if (commission.isEarned()) {\n commission.approve(now);\n await commission.save();\n }\n if (commission.isApproved()) {\n commission.markPayable(now);\n await commission.save();\n }\n updated.push(commission);\n }\n return updated;\n }\n\n private async transitionByIds(\n ids: string[],\n transition: (commission: Commission) => void,\n ): Promise<Commission[]> {\n const updated: Commission[] = [];\n for (const id of ids) {\n const commission = await this.requireCommission(id);\n const statusBefore = commission.status;\n transition(commission);\n if (commission.status !== statusBefore) {\n await commission.save();\n }\n updated.push(commission);\n }\n return updated;\n }\n\n private async requireCommission(id: string): Promise<Commission> {\n const commission = await this.commissions.get({ id });\n if (!commission) {\n throw new Error(\n `CommissionSettlementService: commission '${id}' not found`,\n );\n }\n return commission;\n }\n}\n\nexport default CommissionSettlementService;\n","/**\n * EarnerAttributionService — registration and indexed resolution of\n * {@link EarnerSourceAttribution} mappings.\n *\n * High-volume ingestion (e.g. billing events that must credit an earner per\n * ad-network property) resolves earners here: one indexed attribution query\n * plus one earner load per call, bounded by the REQUESTED keys — never a\n * scan of all active earners. Resolution is fail-closed: a key that cannot\n * be proven to map to exactly one active mapping and one active earner\n * resolves to nothing, with a typed reason.\n *\n * Registration is the idempotent write path (and the documented\n * metadata-migration backfill primitive — see the model doc): re-registering\n * a key updates the existing mapping in place and reports the displaced\n * earner instead of silently duplicating.\n *\n * @packageDocumentation\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { EarnerCollection } from '../collections/EarnerCollection.js';\nimport { EarnerSourceAttributionCollection } from '../collections/EarnerSourceAttributionCollection.js';\nimport type { Earner } from '../models/Earner.js';\nimport type { EarnerSourceAttribution } from '../models/EarnerSourceAttribution.js';\nimport type { EarnerSourceAttributionStatus } from '../types.js';\n\n/** Collaborators for {@link EarnerAttributionService}. */\nexport interface EarnerAttributionServiceDeps {\n earners: EarnerCollection;\n attributions: EarnerSourceAttributionCollection;\n}\n\n/**\n * Why a source key did not resolve to an active earner. Every reason is\n * fail-closed — the key resolves to nothing rather than to a guess.\n *\n * - `no_mapping` — no attribution row for the key.\n * - `mapping_inactive` — row(s) exist but none is `active`.\n * - `ambiguous_mapping` — more than one ACTIVE row for the key (resolving\n * without tenant context across tenants, or duplicate global rows minted\n * outside the model layer — the unique index treats NULL tenants as\n * distinct). Repair by deactivating/deleting the extras, then re-resolve.\n * - `earner_not_found` — the mapping's earner does not exist or is not\n * visible in the current tenant scope.\n * - `earner_not_active` — the earner exists but is `pending`/`suspended`.\n */\nexport type EarnerSourceResolutionRefusal =\n | 'no_mapping'\n | 'mapping_inactive'\n | 'ambiguous_mapping'\n | 'earner_not_found'\n | 'earner_not_active';\n\n/** Result of {@link EarnerAttributionService.resolveActiveEarnerBySource}. */\nexport interface ResolveActiveEarnerResult {\n /** The resolved ACTIVE earner, or `null` with a {@link reason}. */\n earner: Earner | null;\n /** The active mapping that resolved, when {@link earner} is set. */\n attribution: EarnerSourceAttribution | null;\n /** Why resolution failed — set exactly when {@link earner} is `null`. */\n reason?: EarnerSourceResolutionRefusal;\n}\n\n/** Result of {@link EarnerAttributionService.resolveActiveEarnersBySources}. */\nexport interface ResolveActiveEarnersBySourcesResult {\n /** Requested sourceId → resolved active earner (resolved keys only). */\n earnersBySourceId: Map<string, Earner>;\n /** Requested sourceId → the active mapping that resolved it. */\n attributionsBySourceId: Map<string, EarnerSourceAttribution>;\n /** Keys that did not resolve, each with its fail-closed reason. */\n unresolved: { sourceId: string; reason: EarnerSourceResolutionRefusal }[];\n}\n\n/** Input for {@link EarnerAttributionService.registerAttribution}. */\nexport interface RegisterAttributionInput {\n earnerId: string;\n sourceKind: string;\n sourceId: string;\n /**\n * Tenant for the mapping. Defaults to the earner's own `tenantId` so\n * registrations from operator/scheduled contexts land in the earner's\n * tenant. When given explicitly it MUST equal the earner's tenant — a\n * mapping lives in its earner's tenant (model save guard).\n */\n tenantId?: string | null;\n status?: EarnerSourceAttributionStatus;\n metadata?: string;\n}\n\n/** Result of {@link EarnerAttributionService.registerAttribution}. */\nexport interface RegisterAttributionResult {\n attribution: EarnerSourceAttribution;\n /** `true` when THIS call created the mapping (vs updating in place). */\n created: boolean;\n /** The earner the key previously mapped to, when re-pointed. */\n previousEarnerId: string | null;\n}\n\nexport class EarnerAttributionService {\n constructor(private readonly deps: EarnerAttributionServiceDeps) {}\n\n static async create(\n classOptions: SmrtClassOptions = {},\n ): Promise<EarnerAttributionService> {\n return new EarnerAttributionService({\n earners: await EarnerCollection.create(classOptions),\n attributions:\n await EarnerSourceAttributionCollection.create(classOptions),\n });\n }\n\n /**\n * Register (or re-point) the mapping for one external key WITHIN ONE\n * TENANT. The registration's target tenant is `input.tenantId` when\n * given, else the earner's own `tenantId` — and the update-vs-create\n * decision considers only that tenant's rows, so an operator/scheduled\n * registration (no tenant context) can never re-point or re-tenant\n * ANOTHER tenant's mapping for the same key, and legitimate per-tenant\n * mappings of one key are never mistaken for duplicates. A mapping's\n * tenant is fixed at registration (re-tenanting is not supported).\n *\n * Idempotent: an existing target-tenant mapping is updated in place —\n * never duplicated — and the displaced earner is reported. Throws when\n * the earner does not exist, when the key is incomplete, or when the key\n * already holds MULTIPLE rows within the target tenant (duplicates\n * minted outside the model layer — repair before registering again).\n *\n * Two concurrent first registrations of the same key converge through the\n * natural-key upsert (the adapters' null-aware upsert covers NULL-tenant\n * keys too — last write wins). Should a duplicate global row still arrive\n * outside the model layer, the lookups fail closed on it until repaired.\n */\n async registerAttribution(\n input: RegisterAttributionInput,\n ): Promise<RegisterAttributionResult> {\n if (!input.earnerId || !input.sourceKind || !input.sourceId) {\n throw new Error(\n 'EarnerAttributionService.registerAttribution: earnerId, sourceKind, and sourceId are required',\n );\n }\n const earner = await this.deps.earners.get({ id: input.earnerId });\n if (!earner) {\n throw new Error(\n `EarnerAttributionService: earner '${input.earnerId}' not found`,\n );\n }\n\n // '' and NULL both mean \"no tenant\" depending on dialect — normalize.\n const tenantOf = (value: string | null | undefined) => value || null;\n const targetTenant = tenantOf(\n input.tenantId !== undefined ? input.tenantId : earner.tenantId,\n );\n // Only the TARGET tenant's rows participate in the update-vs-create\n // decision. In tenant context the interceptor already narrows the\n // query; without context (operator/scheduled) this filter is what\n // keeps another tenant's mapping for the same key untouchable.\n const scoped = (\n await this.deps.attributions.findBySource(\n input.sourceKind,\n input.sourceId,\n )\n ).filter((row) => tenantOf(row.tenantId) === targetTenant);\n // Ambiguity means more than one ACTIVE row — inactive duplicates are\n // exactly what the documented repair (deactivation) produces, and they\n // must not keep blocking registration afterwards.\n const active = scoped.filter((row) => row.isActive());\n if (active.length > 1) {\n throw new Error(\n `EarnerAttributionService: source '${input.sourceKind}:${input.sourceId}' ` +\n `holds ${active.length} active mappings in the target tenant scope — deactivate the duplicates before registering`,\n );\n }\n\n // Prefer the single active row; with only inactive rows (a repaired\n // duplicate set), reuse the OLDEST deterministically instead of\n // inserting a sibling.\n const current = active[0] ?? scoped[0];\n if (current) {\n const previousEarnerId =\n current.earnerId !== input.earnerId ? current.earnerId : null;\n current.earnerId = input.earnerId;\n current.status = input.status ?? 'active';\n if (input.metadata !== undefined) current.metadata = input.metadata;\n await current.save();\n return { attribution: current, created: false, previousEarnerId };\n }\n\n const minted = await this.deps.attributions.create({\n tenantId: targetTenant,\n earnerId: input.earnerId,\n sourceKind: input.sourceKind,\n sourceId: input.sourceId,\n status: input.status ?? 'active',\n metadata: input.metadata ?? '{}',\n });\n // Adopt the PERSISTED row: two workers racing the first registration\n // of one key both reach the natural-key upsert, and the loser's\n // in-memory instance carries an id the database no longer holds\n // (same convergence pattern as createPayoutBatch).\n const persisted = (\n await this.deps.attributions.findBySource(\n input.sourceKind,\n input.sourceId,\n )\n ).filter((row) => tenantOf(row.tenantId) === targetTenant);\n const attribution =\n persisted.length === 1 ? persisted[0] : (persisted[0] ?? minted);\n return { attribution, created: true, previousEarnerId: null };\n }\n\n /**\n * Resolve the ACTIVE earner for one external key. Exactly two queries\n * regardless of how many earners exist. Fail-closed: `earner` is `null`\n * with a typed {@link ResolveActiveEarnerResult.reason} unless the key\n * maps unambiguously to one active mapping whose earner is active.\n */\n async resolveActiveEarnerBySource(input: {\n sourceKind: string;\n sourceId: string;\n }): Promise<ResolveActiveEarnerResult> {\n const batched = await this.resolveActiveEarnersBySources({\n sourceKind: input.sourceKind,\n sourceIds: [input.sourceId],\n });\n const earner = batched.earnersBySourceId.get(input.sourceId) ?? null;\n if (earner) {\n return {\n earner,\n attribution: batched.attributionsBySourceId.get(input.sourceId) ?? null,\n };\n }\n return {\n earner: null,\n attribution: null,\n reason: batched.unresolved[0]?.reason ?? 'no_mapping',\n };\n }\n\n /**\n * Resolve the ACTIVE earners for a batch of external keys sharing one\n * kind. Query work is bounded by the REQUESTED ids — one indexed\n * attribution `IN` query plus one earner load for the mapped ids — never\n * a scan of all active earners. Duplicate/empty requested ids are\n * deduped; every requested id comes back either in\n * `earnersBySourceId` or in `unresolved` with its fail-closed reason.\n */\n async resolveActiveEarnersBySources(input: {\n sourceKind: string;\n sourceIds: string[];\n }): Promise<ResolveActiveEarnersBySourcesResult> {\n if (!input.sourceKind) {\n throw new Error(\n 'EarnerAttributionService.resolveActiveEarnersBySources: sourceKind is required',\n );\n }\n const requested = [...new Set(input.sourceIds.filter(Boolean))];\n const result: ResolveActiveEarnersBySourcesResult = {\n earnersBySourceId: new Map(),\n attributionsBySourceId: new Map(),\n unresolved: [],\n };\n if (requested.length === 0) return result;\n\n const rows = await this.deps.attributions.findBySources(\n input.sourceKind,\n requested,\n );\n const rowsBySourceId = new Map<string, EarnerSourceAttribution[]>();\n for (const row of rows) {\n const bucket = rowsBySourceId.get(row.sourceId);\n if (bucket) {\n bucket.push(row);\n } else {\n rowsBySourceId.set(row.sourceId, [row]);\n }\n }\n\n // Classify each requested key down to its single active mapping (or a\n // fail-closed reason) before touching the earners table.\n const activeBySourceId = new Map<string, EarnerSourceAttribution>();\n for (const sourceId of requested) {\n const bucket = rowsBySourceId.get(sourceId) ?? [];\n if (bucket.length === 0) {\n result.unresolved.push({ sourceId, reason: 'no_mapping' });\n continue;\n }\n const active = bucket.filter((row) => row.isActive());\n if (active.length === 0) {\n result.unresolved.push({ sourceId, reason: 'mapping_inactive' });\n continue;\n }\n if (active.length > 1) {\n result.unresolved.push({ sourceId, reason: 'ambiguous_mapping' });\n continue;\n }\n activeBySourceId.set(sourceId, active[0]);\n }\n if (activeBySourceId.size === 0) return result;\n\n const earnerIds = [\n ...new Set(\n [...activeBySourceId.values()]\n .map((row) => row.earnerId)\n .filter(Boolean),\n ),\n ];\n const earners = await this.deps.earners.listByIds(earnerIds);\n const earnerById = new Map<string, Earner>();\n for (const earner of earners) {\n if (earner.id) earnerById.set(earner.id, earner);\n }\n\n for (const [sourceId, attribution] of activeBySourceId) {\n const earner = earnerById.get(attribution.earnerId);\n if (!earner) {\n result.unresolved.push({ sourceId, reason: 'earner_not_found' });\n continue;\n }\n if (!earner.isActive()) {\n result.unresolved.push({ sourceId, reason: 'earner_not_active' });\n continue;\n }\n result.earnersBySourceId.set(sourceId, earner);\n result.attributionsBySourceId.set(sourceId, attribution);\n }\n return result;\n }\n}\n\nexport default EarnerAttributionService;\n"],"mappings":"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"}