@happyvertical/smrt-sales 0.57.23 → 0.57.24

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.
package/README.md CHANGED
@@ -163,6 +163,23 @@ connected account id or the public receiving address, and an active earner
163
163
  using either method must have one. The package records where money should
164
164
  go and never calls a payment provider.
165
165
 
166
+ `CommissionPayoutService.createPayoutBatch()` captures the typed destination on
167
+ `CommissionPayout.payoutDestination`. Executors and approval screens must read
168
+ `payout.getPayoutDestination()` from the batch, never the current Earner.
169
+ The batch's method and destination are immutable from its first save, including
170
+ pending batches and failed batches reset for retry. Earner preference changes,
171
+ idempotent replay and interrupted membership repair never refresh routing.
172
+ Reject the batch through `transitionPayoutForSource` and create a new batch with
173
+ a new idempotency key to use another destination. This also preserves the route
174
+ that was reviewed before approval; no payment provider is called here.
175
+
176
+ Run the normal manifest migration before adopting the new field. Existing
177
+ Stripe Connect/crypto batches without a recorded descriptor remain readable,
178
+ but cannot be approved or advanced toward payment. Reject and recreate them;
179
+ never backfill a historical route from today's Earner. Manual methods continue
180
+ to use an empty descriptor. Unknown/malformed descriptors and extra fields
181
+ (including credentials) are rejected.
182
+
166
183
  ## Core model
167
184
 
168
185
  - **Roles are separate from money.** `SalesRepresentative` and `Referrer` are
@@ -13,6 +13,7 @@ money), and the Gotchas that apply before editing anything live in
13
13
  - **Commission**: one earning record per earner per plan component per event occurrence. Integer-cents amounts (`baseAmountCents`, `amountCents`), decimal `rate`, `basis`, snapshot references (`planKey`/`planVersion`, generic `termsSnapshotKind`/`termsSnapshotId`), a JSON `calculationTrace` sufficient to reproduce the amount, split support (`splitGroupId`, `shareFraction`), and a `dedupeKey` for idempotent creation. Lifecycle `pending → earned → approved → payable → paid`, enforced by a save-time transition guard with an authoritative prior-status re-read (commerce pattern).
14
14
  - **CommissionAdjustment**: append-only corrections (`refund|credit|chargeback|dispute|correction`), signed `amountCents`, required `reason`, immutable once created. `CommissionAdjustmentService.createAdjustment()` is the tenant-required idempotent creation door: callers provide a stable operation UUID; the database creates exactly one row, exact retries return it, and changed tenant/commission/earner/kind/cents/currency/reason/operator/metadata fail with a typed replay conflict. Parent Commission plus denormalized tenant/Earner/currency and operator UUID are validated before insert. The financial row deliberately exposes no operation field. A private, required-tenant `CommissionAdjustmentOperation` table uses the globally unique operation UUID as its primary id, maps it to the adjustment UUID, and is inserted in the same transaction as the adjustment. Claims tolerate either primary-key or tenant/slug/context uniqueness arbitration without aborting the transaction; only the tenant-scoped operation UUID lookup and immutable intent comparison authorize a replay. Existing adjustment schemas and legacy direct creates remain unchanged. Earned/paid Commissions are never rewritten — adjustments append to them.
15
15
  - **Payable balance**: computed, not stored — `CommissionBalanceService` sums payable Commissions plus unsettled Adjustments per Earner/currency.
16
- - **CommissionPayout**: settlement batch per Earner. `pending → approved → processing → completed | failed` (`resetFromFailed()` is the only exit from failed), plus the terminal operator decline `rejected` reachable from pending/approved (#1987 — reject through `transitionPayoutForSource`, which also RELEASES the batch's membership so the rows settle through a future batch; `reject()` on the model mutates status only). Settles rows via the collections' **conditional `claimForPayout`** (model-layer get/save/re-read — tenancy- and dialect-safe; a row owned by another batch is never re-claimed) and stores totals recomputed from the VERIFIED claimed membership; idempotent via `idempotencyKey` natural key (default `${earnerId}:${currency}:${YYYY-MM-DD}`) — a clean replay touches nothing, while a pending payout whose totals disagree with its stamped rows (interrupted claim pass) is repaired on replay (the claim pass re-checks the payout is still pending first). Each batch/repair pass also derives the payout's **single-source stamp** (`sourceKind`/`sourceId`, indexed): set when every claimed commission — and every claimed adjustment through its parent commission — shares exactly one non-empty source; empty for mixed/unknown/empty membership. `completePayout` flips member commissions to `paid` BEFORE the terminal transition, so a mid-loop failure stays retryable. Enforces `totalAmountCents = commissionTotalCents + adjustmentTotalCents` at save; retains `paymentReference`/`providerRef`; optional `invoiceId` cross-package string ref to a commerce Invoice. Generated surface is read-only on ALL doors (api/mcp/cli `list`/`get`) — writes go through `CommissionPayoutService`. `createPayoutBatch` settles the whole earner+currency by default, or a **scoped** subset: pass `sourceKind`+`sourceId` to settle just one earning source (e.g. one ad network), or `commissionIds` for an explicit set (each validated payable+unsettled+earner+currency; requires its own `idempotencyKey`) — adjustments are narrowed to the same scope, and the default idempotency key folds the source in. Scoping is how concurrent per-source batches settle safely: batches scoped to different sources (or disjoint `commissionIds`) gather **disjoint** row sets and never contend. The collection layer has no cross-row transaction, so OVERLAPPING concurrent scopes (a source batch + the earner-wide batch, intersecting id sets, or a batch replay racing a lifecycle transition of the same payout) must be serialized by the caller — `claimForPayout` still won't double-own a row, but a shared commission and its adjustment could split across the two batches.
16
+ - **CommissionPayout**: settlement batch per Earner. `pending → approved → processing → completed | failed` (`resetFromFailed()` is the only exit from failed), plus the terminal operator decline `rejected` reachable from pending/approved (#1987 — reject through `transitionPayoutForSource`, which also RELEASES the batch's membership so the rows settle through a future batch; `reject()` on the model mutates status only). Settles rows via the collections' **conditional `claimForPayout`** (model-layer get/save/re-read — tenancy- and dialect-safe; a row owned by another batch is never re-claimed) and stores totals recomputed from the VERIFIED claimed membership; idempotent via `idempotencyKey` natural key (default `${earnerId}:${currency}:${YYYY-MM-DD}`) — initial creation uses a strict insert so concurrent same-key creators cannot overwrite the first routing snapshot; the loser receives a unique-constraint error and can retry the same key. A clean replay touches nothing, while a pending payout whose totals disagree with its stamped rows (interrupted claim pass) is repaired on replay (the claim pass re-checks the payout is still pending first). Each batch/repair pass also derives the payout's **single-source stamp** (`sourceKind`/`sourceId`, indexed): set when every claimed commission — and every claimed adjustment through its parent commission — shares exactly one non-empty source; empty for mixed/unknown/empty membership. `completePayout` flips member commissions to `paid` BEFORE the terminal transition, so a mid-loop failure stays retryable. Enforces `totalAmountCents = commissionTotalCents + adjustmentTotalCents` at save; retains `paymentReference`/`providerRef`; optional `invoiceId` cross-package string ref to a commerce Invoice. Generated surface is read-only on ALL doors (api/mcp/cli `list`/`get`) — writes go through `CommissionPayoutService`. `createPayoutBatch` settles the whole earner+currency by default, or a **scoped** subset: pass `sourceKind`+`sourceId` to settle just one earning source (e.g. one ad network), or `commissionIds` for an explicit set (each validated payable+unsettled+earner+currency; requires its own `idempotencyKey`) — adjustments are narrowed to the same scope, and the default idempotency key folds the source in. Scoping is how concurrent per-source batches settle safely: batches scoped to different sources (or disjoint `commissionIds`) gather **disjoint** row sets and never contend. The collection layer has no cross-row transaction, so OVERLAPPING concurrent scopes (a source batch + the earner-wide batch, intersecting id sets, or a batch replay racing a lifecycle transition of the same payout) must be serialized by the caller — `claimForPayout` still won't double-own a row, but a shared commission and its adjustment could split across the two batches.
17
+ - **Payout destination snapshot** (#4148): `CommissionPayout.payoutDestination` stores the typed Earner descriptor captured at batch creation. Use `getPayoutDestination()` on the batch for execution/review, never live Earner routing. Method and destination are immutable from first persistence (pending included); idempotency replay/repair and failed-reset preserve them. Reject through the source transition service and create a new key to change routing. Existing Connect/crypto batches with an empty migrated snapshot cannot approve/process/complete; reject/recreate rather than infer historical routing. Manual methods retain `{}`. Generated list/get surfaces expose the snapshot; no provider transfer is implemented.
17
18
  - **Source-scoped payout history** (#1985): `CommissionPayoutService.getSourcePayoutHistory({ sourceKind, sourceId, limit?, offset? })` — one page of the payouts belonging to ONE earning source, newest first (`created_at DESC, id DESC`), offset contract (`nextOffset` advances by the scanned count; `null` when exhausted). Candidates come from the indexed stamp, so work is bounded by page size (a sparse source never scans the global history); each page is re-verified against actual membership in three batched queries (no per-payout N+1) — every member commission and every adjustment's parent must carry exactly the requested source and agree on earner/currency/tenant; unprovable rows (mixed-source, missing parents, memberless artifacts, released/rejected batches) are excluded fail-closed and reported in `excluded` with reasons. Adjustment-only payouts prove ownership through parent commissions and list normally. Payouts minted before the stamp existed are invisible until backfilled with `restampPayoutSource(payoutId)` (idempotent, any status).
18
19
  - **Source-authorized lifecycle transitions** (#1987): `CommissionPayoutService.transitionPayoutForSource({ payoutId, sourceKind, sourceId, action, expectedStatus?, paymentReference?, reason? })` — atomic alternative to load-verify-transition. Actions `approve | mark_processing | complete | fail | reject`. Everything runs on one transaction database: the payout row is locked (PostgreSQL `SELECT … FOR UPDATE`, replica-safe; single-connection engines serialize transitions per database in-process), membership is re-verified under the lock (every commission and every adjustment through its parent must carry exactly the requested source and matching earner/currency/tenant — else typed fail-closed refusals with no payout/member data), and only authorized calls receive status-derived `already_applied` or `status_conflict` outcomes. Totals are then recomputed under the lock (drift refuses the money-forward actions with `totals_drift`; repair = `createPayoutBatch` replay while pending; `fail`/`reject` proceed despite drift — reject IS the remedy), and the transition plus member writes commit or roll back together (`complete` flips members to paid in the same transaction; `reject` releases membership then declines). Outcomes: `transitioned`, `already_applied` (idempotent echo — terminal completion NEVER rewrites `paymentReference`/`providerRef`/`paidAt`), or `refused` with `{ reason, detail }`. Concurrent duplicate calls for actions that retain membership resolve as one `transitioned` + `already_applied` echoes; `reject` releases its membership evidence, so serialized replays fail `membership_empty` rather than expose rejected state. `expectedStatus` adds optimistic-concurrency strictness after authorization. Memberless batch artifacts cannot pass the source-authorized door (`membership_empty`) — they stay operator-level.
@@ -1,3 +1,3 @@
1
- import "./chunks/__smrt-register__-BRrJGGpG.js";
1
+ import "./chunks/__smrt-register__-Yzz7_2xD.js";
2
2
  import { a as AgreementExecutionEvent, c as AgreementCollection, d as serializeAgreementParty, f as AGREEMENT_EXECUTION_STATUSES, g as sanitizeSignerIntent, h as sanitizeSignerEvidence, i as AgreementExecutionEventCollection, l as Agreement, m as coerceAgreementDate, n as ExecutedAgreementCollection, o as AgreementExecutionCollection, p as AGREEMENT_STATUSES, r as ExecutedAgreement, s as AgreementExecution, t as AgreementExecutionService, u as parseAgreementParty } from "./chunks/agreements-RsyRLeDn.js";
3
3
  export { AGREEMENT_EXECUTION_STATUSES, AGREEMENT_STATUSES, Agreement, AgreementCollection, AgreementExecution, AgreementExecutionCollection, AgreementExecutionEvent, AgreementExecutionEventCollection, AgreementExecutionService, ExecutedAgreement, ExecutedAgreementCollection, coerceAgreementDate, parseAgreementParty, sanitizeSignerEvidence, sanitizeSignerIntent, serializeAgreementParty };