@happyvertical/smrt-sales 0.57.9 → 0.57.10
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 +65 -0
- package/agents/commissions.md +2 -1
- package/dist/agreements.js +1 -1
- package/dist/chunks/{__smrt-register__-Czzs3Vof.js → __smrt-register__-DE9qSi5T.js} +2 -2
- package/dist/chunks/__smrt-register__-DE9qSi5T.js.map +1 -0
- package/dist/chunks/{commissions-Cip6QZ9Q.js → commissions-D5x24d4o.js} +443 -67
- package/dist/chunks/commissions-D5x24d4o.js.map +1 -0
- package/dist/chunks/{referrals-CYgrGkEo.js → referrals-BRdTkEFt.js} +2 -2
- package/dist/chunks/{referrals-CYgrGkEo.js.map → referrals-BRdTkEFt.js.map} +1 -1
- package/dist/commissions/models/Earner.d.ts +41 -1
- package/dist/commissions/models/Earner.d.ts.map +1 -1
- package/dist/commissions/models/index.d.ts +1 -1
- package/dist/commissions/models/index.d.ts.map +1 -1
- package/dist/commissions/services/CommissionPayoutService.d.ts.map +1 -1
- package/dist/commissions/services/WaterfallCalculationService.d.ts +33 -1
- package/dist/commissions/services/WaterfallCalculationService.d.ts.map +1 -1
- package/dist/commissions/services/index.d.ts +1 -1
- package/dist/commissions/services/index.d.ts.map +1 -1
- package/dist/commissions/types.d.ts +169 -1
- package/dist/commissions/types.d.ts.map +1 -1
- package/dist/commissions.js +3 -3
- package/dist/crm.js +1 -1
- package/dist/index.js +4 -4
- package/dist/manifest.json +93 -5
- package/dist/referrals.js +2 -2
- package/dist/smrt-knowledge.json +132 -6
- package/package.json +8 -8
- package/dist/chunks/__smrt-register__-Czzs3Vof.js.map +0 -1
- package/dist/chunks/commissions-Cip6QZ9Q.js.map +0 -1
package/README.md
CHANGED
|
@@ -98,6 +98,71 @@ v1 snapshots keep governing earnings. To recover, activate and bind compatible
|
|
|
98
98
|
component terms, then retry qualification; use `calculateWaterfall()` directly
|
|
99
99
|
for v2 allocations. Switching a plan key's engine does not migrate referrals.
|
|
100
100
|
|
|
101
|
+
### Pool recipients
|
|
102
|
+
|
|
103
|
+
A step can share its amount across a pool of members, weighted by a metric
|
|
104
|
+
such as deliveries, items published, or referrals sent. The step's
|
|
105
|
+
`recipient` names the pool and resolves like any other recipient; the host
|
|
106
|
+
injects who belongs to it and how much each member contributed:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import {
|
|
110
|
+
calculateWaterfallWithPools,
|
|
111
|
+
WaterfallRecipientResolverRegistry,
|
|
112
|
+
} from '@happyvertical/smrt-sales/commissions';
|
|
113
|
+
|
|
114
|
+
const resolver = new WaterfallRecipientResolverRegistry();
|
|
115
|
+
resolver.register('treasury', () => ({ kind: 'treasury', id: 'valley-coop' }));
|
|
116
|
+
resolver.register('grower_pool', (d) => ({ kind: 'grower_pool', id: d.id ?? 'all' }));
|
|
117
|
+
|
|
118
|
+
const { trace } = await calculateWaterfallWithPools(
|
|
119
|
+
{
|
|
120
|
+
planKey: 'valley-coop-wholesale',
|
|
121
|
+
version: 1,
|
|
122
|
+
currency: 'USD',
|
|
123
|
+
steps: [
|
|
124
|
+
{ key: 'treasury', base: 'gross', amount: { kind: 'percent', rate: 0.1 },
|
|
125
|
+
recipient: { kind: 'treasury' } },
|
|
126
|
+
{ key: 'growers', base: 'previous_remainder', amount: { kind: 'percent', rate: 1 },
|
|
127
|
+
recipient: { kind: 'grower_pool', id: '2026-season' },
|
|
128
|
+
pool: { metric: 'kg_delivered' } },
|
|
129
|
+
],
|
|
130
|
+
remainderSink: { kind: 'treasury' },
|
|
131
|
+
},
|
|
132
|
+
{ grossAmountCents: 10_000, currency: 'USD' },
|
|
133
|
+
{
|
|
134
|
+
resolver,
|
|
135
|
+
poolMembers: async (pool) => loadGrowers(pool.id),
|
|
136
|
+
metricProvider: { getMetricValues: async (req) => loadKgDelivered(req) },
|
|
137
|
+
metricPeriod: { start: '2026-09-01', end: '2026-10-01' },
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Omit `metric` for equal shares. Weights are non-negative integers, and
|
|
143
|
+
members the provider leaves out weigh zero. Shares are exact integer minor
|
|
144
|
+
units. The cents that cannot be divided go to the remainder sink by default.
|
|
145
|
+
With `residual: 'largest_remainder'`, they go one at a time to the largest
|
|
146
|
+
fractional remainders. Ties go to the larger weight, then to member `kind`
|
|
147
|
+
and `id` order. An empty pool goes to the sink. So does a pool whose weights
|
|
148
|
+
are all zero, unless the step sets `zeroWeight: 'equal'`. The trace freezes
|
|
149
|
+
the members, weights, and metric period, so a replay with `frozenInputs` never
|
|
150
|
+
calls the providers again. A snapshot must also match the step's declared
|
|
151
|
+
`metric`. Replay trusts the frozen inputs it receives, so store them where
|
|
152
|
+
they cannot be edited, such as an immutable calculation trace; a consistent
|
|
153
|
+
edit to the stored evidence cannot be detected here. `calculateWaterfall()`
|
|
154
|
+
stays synchronous; pass it
|
|
155
|
+
`pools` from `prepareWaterfallPools()` when you need the two phases apart.
|
|
156
|
+
|
|
157
|
+
### Organisation and treasury earners
|
|
158
|
+
|
|
159
|
+
`Earner.kind` is `person` (the default), `organisation`, or `treasury`.
|
|
160
|
+
Organisation and treasury earners need no person Profile. Payout methods
|
|
161
|
+
include `stripe_connect` and `crypto`; `setPayoutDestination()` stores the
|
|
162
|
+
connected account id or the public receiving address, and an active earner
|
|
163
|
+
using either method must have one. The package records where money should
|
|
164
|
+
go and never calls a payment provider.
|
|
165
|
+
|
|
101
166
|
## Core model
|
|
102
167
|
|
|
103
168
|
- **Roles are separate from money.** `SalesRepresentative` and `Referrer` are
|
package/agents/commissions.md
CHANGED
|
@@ -5,9 +5,10 @@ cross-module invariants (currency, tenancy, cross-package refs, roles vs.
|
|
|
5
5
|
money), and the Gotchas that apply before editing anything live in
|
|
6
6
|
[../AGENTS.md](../AGENTS.md) — read that first.
|
|
7
7
|
|
|
8
|
-
- **Earner**: `profileId`, `status` (`pending|active|suspended`), `payoutMethod` (`bank_transfer|check|paypal|credit|other`), `payoutThresholdCents`, `payoutScheduleKey` (open string: `manual`, `monthly`, …), `currency`, `metadata`.
|
|
8
|
+
- **Earner**: `kind` (`person|organisation|treasury`, default `person`; organisation and treasury earners need no person Profile, so `profileId` stays empty unless the host links an organisation Profile), `profileId`, `status` (`pending|active|suspended`), `payoutMethod` (`bank_transfer|check|paypal|credit|other|stripe_connect|crypto`), `payoutDestination` (typed JSON descriptor via `getPayoutDestination()`/`setPayoutDestination()`: Stripe Connect `connectedAccountId` (`acct_…`) or crypto `network`/`asset`/`address`/`memo?`; unknown fields are refused so secrets cannot ride along; its `method` must equal `payoutMethod`, and an `active` Stripe Connect/crypto earner must have one — enforced at save; `createPayoutBatch` refuses to create a NEW Stripe Connect/crypto batch, including a `payoutMethod` override, unless the earner's destination has that method; an existing idempotency key still replays/repairs its batch after the preference changes), `payoutThresholdCents`, `payoutScheduleKey` (open string: `manual`, `monthly`, …), `currency`, `metadata`. Descriptors only: nothing in this package calls a payment provider. Map an organisation earner to the host's own organisation id through `EarnerSourceAttribution`.
|
|
9
9
|
- **EarnerSourceAttribution** (#1986): indexed external attribution mapping `(sourceKind, sourceId) → earnerId` with `status` (`active|inactive`) — the queryable replacement for stashing associations in `Earner.metadata`. Natural key `(tenant_id, source_kind, source_id)` (one mapping per key per tenant; a re-`create` upserts/re-points; the adapters' null-aware upsert dedups NULL-tenant keys too, but duplicate global rows minted outside the model layer remain representable). The attribution kind space is consumer-defined and need not match earning-event source kinds. `EarnerAttributionService` is the public API: `registerAttribution` (idempotent; reports `created`/`previousEarnerId`; also the documented metadata-migration backfill primitive — loop earners, register each association, verify, drop the metadata key) and `resolveActiveEarnerBySource`/`resolveActiveEarnersBySources` (single/batched; 2 queries bounded by the REQUESTED ids, never a scan of active earners; fail-closed typed reasons `no_mapping|mapping_inactive|ambiguous_mapping|earner_not_found|earner_not_active` — more than one ACTIVE row for a key refuses rather than guessing). In tenant context mappings resolve within that tenant only; without context, lookups span all rows and cross-tenant duplicates surface as `ambiguous_mapping`.
|
|
10
10
|
- **CommissionPlan**: versioned calculation terms. Natural key `(tenant_id, plan_key, version)` (per-tenant keys; the adapters' null-aware upsert dedups NULL-tenant keys through the model layer, though duplicate global rows minted via raw SQL remain representable); `status` (`draft|active|superseded|retired`); `effectiveFrom`. Components are a JSON-string array (`getComponents()`): each component has `key`, `trigger` (earning-event kind or `*`), `basis` (`fixed|gross|net|margin|custom`), `rate` or `fixedAmountCents`, `recurrence` (`one_time` or `recurring` with optional `maxOccurrences`/`windowMonths`). **Immutable once active** — amendments create a new row with `version + 1`; prior versions are never rewritten. `latestActiveByKey(key, at?)` resolves the highest active version **already in effect at `at`** — a future-dated amendment can be activated ahead of time without governing earlier qualifications (same rule on `AttributionPolicyCollection`).
|
|
11
|
+
- **Waterfall pool steps** (#3834): a v2 step with `pool: { metric?, residual?, zeroWeight? }` splits its amount across the members of the pool its `recipient` descriptor resolves to (through the normal recipient registry). Membership comes from an injected `WaterfallPoolMembershipResolver`, weights from an injected `MetricProvider` (omitted `metric` ⇒ equal weights, no provider call). `calculateWaterfall` stays synchronous and never calls providers: `prepareWaterfallPools()` (async) resolves pools at the authoritative event and the result is passed as `pools`; `calculateWaterfallWithPools()` does both. Weights must be non-negative safe integers (hosts scale fractional metrics); members are canonicalized to `(kind, id)` order and duplicates, non-member metric rows, and repeated rows are refused; members the provider omits weigh zero. Allocation is exact BigInt `floor(amount × weight ÷ total)`; the residual goes to the remainder sink (default) or, with `residual: 'largest_remainder'`, one unit at a time by largest fractional remainder, then larger weight, then `kind`, then `id` (UTF-16 code-unit order). An empty pool, or an all-zero pool under the default `zeroWeight: 'remainder_sink'`, sends the whole step to the sink; `zeroWeight: 'equal'` splits equally instead. The trace freezes `frozenInputs.poolsByStep` (pool identity, metric, ISO period, members, weights) so replay never re-reads metrics; frozen and prepared snapshots must match their frozen recipient and the step's declared `metric` (`null` for equal weights), and a prepared snapshot (or `null` unresolved marker) must equal what the step's descriptor resolves to at calculation, so pools prepared for another plan are refused. Replay trusts the evidence it is given — like frozen `recipientByStep`, an internally consistent edit to stored evidence is undetectable without keyed integrity, so hosts store it immutably. Pool-free plans emit no `poolsByStep` key or step `pool` field, so their traces are unchanged. A pool step cannot be the application-fee offset recipient. Each member trace carries `shareFraction` for hosts that persist member allocations as Commissions under one `splitGroupId` (for example `${earningEventId}:${stepKey}`).
|
|
11
12
|
- **EarningEvent**: immutable commercial-event evidence (`conversion`, `agreement_execution`, `invoice_payment`, `collected_revenue`, `recognized_margin`, or any extensible kind). Carries `sourceKind`/`sourceId` (generic earning source), `grossAmountCents`/`netAmountCents`/`marginCents`, `currency`, `occurredAt`, and a `dedupeKey` natural key for idempotent ingestion. No update/delete surface, plus a save-time immutability guard: hydrated edits, blind id overwrites, and fresh creates onto an existing dedupe key all throw (the upsert would rotate the row id and orphan referencing Commissions) — idempotent ingestion goes through `getOrCreateByDedupeKey()`.
|
|
12
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).
|
|
13
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.
|
package/dist/agreements.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./chunks/__smrt-register__-
|
|
1
|
+
import "./chunks/__smrt-register__-DE9qSi5T.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-jBiVmttD.js";
|
|
3
3
|
export { AGREEMENT_EXECUTION_STATUSES, AGREEMENT_STATUSES, Agreement, AgreementCollection, AgreementExecution, AgreementExecutionCollection, AgreementExecutionEvent, AgreementExecutionEventCollection, AgreementExecutionService, ExecutedAgreement, ExecutedAgreementCollection, coerceAgreementDate, parseAgreementParty, sanitizeSignerEvidence, sanitizeSignerIntent, serializeAgreementParty };
|