@happyvertical/smrt-sales 0.57.33 → 0.57.35

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.
@@ -3,13 +3,13 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-sales",
6
- "packageVersion": "0.57.33",
6
+ "packageVersion": "0.57.35",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "f94bca5eba8b84a4bdf6dbdaba9273919adfe1a56de350c6b755c31f4cf9c7d9",
11
- "packageJson": "2b2a8e76f4195b2861ee028921211ef0c5be0eb631e37a273bdc3bef484c1e71",
12
- "agents": "561dfe5741f74b8f105617a907b5a17446d4546f6fdef7eb754956bfd09aedd9",
10
+ "manifest": "9b771789fb397beb6efb642625d6fbc9cb47631a6698e08c9db40c9c4ae9358a",
11
+ "packageJson": "ca43423af361e16fd040e21dabf206c66922ec6f4f4e23db84e812d2e65ce72c",
12
+ "agents": "a96547d3b8105fd1a2d767b3c05341cf9f8ea9dec56c3c212d6239d19fb689ea",
13
13
  "moduleDoc:agents/agreements.md": "86609271b56633c1b6a3d9b1905a200cad1cb15de54adb8ca9f1200f6af8b9ce",
14
14
  "moduleDoc:agents/commissions.md": "2f41ada34b583f037d9337f9ba3f4c93fd64d1cfcd0830cb7e1c260a6b339374",
15
15
  "moduleDoc:agents/crm.md": "074eff9e3ca7ae6d86e65a1d29ac7b9b555b719b3801918678b15df7538f99e0",
@@ -2744,7 +2744,8 @@
2744
2744
  "type": "foreignKey",
2745
2745
  "required": true,
2746
2746
  "related": "Earner",
2747
- "columnType": "UUID"
2747
+ "columnType": "UUID",
2748
+ "description": "The affiliate who earned this commission."
2748
2749
  },
2749
2750
  {
2750
2751
  "name": "earningEventId",
@@ -2828,21 +2829,24 @@
2828
2829
  "type": "integer",
2829
2830
  "required": false,
2830
2831
  "columnType": "INTEGER",
2831
- "default": 0
2832
+ "default": 0,
2833
+ "description": "How much was earned, in the smallest unit of the currency."
2832
2834
  },
2833
2835
  {
2834
2836
  "name": "currency",
2835
2837
  "type": "text",
2836
2838
  "required": false,
2837
2839
  "columnType": "TEXT",
2838
- "default": "USD"
2840
+ "default": "USD",
2841
+ "description": "The currency the commission is paid in."
2839
2842
  },
2840
2843
  {
2841
2844
  "name": "status",
2842
2845
  "type": "text",
2843
2846
  "required": false,
2844
2847
  "columnType": "TEXT",
2845
- "default": "pending"
2848
+ "default": "pending",
2849
+ "description": "Where the commission stands, from pending to paid."
2846
2850
  },
2847
2851
  {
2848
2852
  "name": "clearingEndsAt",
@@ -2907,7 +2911,8 @@
2907
2911
  "type": "text",
2908
2912
  "required": true,
2909
2913
  "columnType": "TEXT",
2910
- "default": ""
2914
+ "default": "",
2915
+ "description": "A reference that stops the same commission being recorded twice."
2911
2916
  },
2912
2917
  {
2913
2918
  "name": "metadata",
@@ -3507,7 +3512,8 @@
3507
3512
  "type": "foreignKey",
3508
3513
  "required": true,
3509
3514
  "related": "Earner",
3510
- "columnType": "UUID"
3515
+ "columnType": "UUID",
3516
+ "description": "The affiliate being paid."
3511
3517
  },
3512
3518
  {
3513
3519
  "name": "periodStart",
@@ -3540,14 +3546,16 @@
3540
3546
  "type": "integer",
3541
3547
  "required": false,
3542
3548
  "columnType": "INTEGER",
3543
- "default": 0
3549
+ "default": 0,
3550
+ "description": "The total being paid, in the smallest unit of the currency."
3544
3551
  },
3545
3552
  {
3546
3553
  "name": "currency",
3547
3554
  "type": "text",
3548
3555
  "required": false,
3549
3556
  "columnType": "TEXT",
3550
- "default": "USD"
3557
+ "default": "USD",
3558
+ "description": "The currency of the payment."
3551
3559
  },
3552
3560
  {
3553
3561
  "name": "payoutMethod",
@@ -3568,7 +3576,8 @@
3568
3576
  "type": "text",
3569
3577
  "required": false,
3570
3578
  "columnType": "TEXT",
3571
- "default": "pending"
3579
+ "default": "pending",
3580
+ "description": "Whether the payment is waiting, being sent, completed or failed."
3572
3581
  },
3573
3582
  {
3574
3583
  "name": "paymentReference",
@@ -3588,7 +3597,8 @@
3588
3597
  "name": "paidAt",
3589
3598
  "type": "datetime",
3590
3599
  "required": false,
3591
- "columnType": "TIMESTAMP"
3600
+ "columnType": "TIMESTAMP",
3601
+ "description": "When the payment went out."
3592
3602
  },
3593
3603
  {
3594
3604
  "name": "invoiceId",
@@ -3609,7 +3619,8 @@
3609
3619
  "type": "text",
3610
3620
  "required": true,
3611
3621
  "columnType": "TEXT",
3612
- "default": ""
3622
+ "default": "",
3623
+ "description": "A reference that stops the same payout being made twice."
3613
3624
  },
3614
3625
  {
3615
3626
  "name": "sourceKind",
@@ -4478,14 +4489,16 @@
4478
4489
  "type": "text",
4479
4490
  "required": false,
4480
4491
  "columnType": "TEXT",
4481
- "default": ""
4492
+ "default": "",
4493
+ "description": "The name this payee is paid under."
4482
4494
  },
4483
4495
  {
4484
4496
  "name": "status",
4485
4497
  "type": "text",
4486
4498
  "required": false,
4487
4499
  "columnType": "TEXT",
4488
- "default": "pending"
4500
+ "default": "pending",
4501
+ "description": "Whether this payee can be paid."
4489
4502
  },
4490
4503
  {
4491
4504
  "name": "payoutMethod",
@@ -9474,14 +9487,16 @@
9474
9487
  "type": "foreignKey",
9475
9488
  "required": true,
9476
9489
  "related": "Referrer",
9477
- "columnType": "UUID"
9490
+ "columnType": "UUID",
9491
+ "description": "The affiliate who sent this person."
9478
9492
  },
9479
9493
  {
9480
9494
  "name": "programId",
9481
9495
  "type": "foreignKey",
9482
9496
  "required": true,
9483
9497
  "related": "ReferralProgram",
9484
- "columnType": "UUID"
9498
+ "columnType": "UUID",
9499
+ "description": "The program this referral counts towards."
9485
9500
  },
9486
9501
  {
9487
9502
  "name": "policyKey",
@@ -9502,7 +9517,8 @@
9502
9517
  "type": "text",
9503
9518
  "required": false,
9504
9519
  "columnType": "TEXT",
9505
- "default": "pending"
9520
+ "default": "pending",
9521
+ "description": "Whether the referral is waiting, has led to a sale or has lapsed."
9506
9522
  },
9507
9523
  {
9508
9524
  "name": "targetKind",
@@ -10722,21 +10738,24 @@
10722
10738
  "type": "text",
10723
10739
  "required": true,
10724
10740
  "columnType": "TEXT",
10725
- "default": ""
10741
+ "default": "",
10742
+ "description": "A short name for the program, used in links and reports."
10726
10743
  },
10727
10744
  {
10728
10745
  "name": "name",
10729
10746
  "type": "text",
10730
10747
  "required": false,
10731
10748
  "columnType": "TEXT",
10732
- "default": ""
10749
+ "default": "",
10750
+ "description": "What you call this program."
10733
10751
  },
10734
10752
  {
10735
10753
  "name": "status",
10736
10754
  "type": "text",
10737
10755
  "required": false,
10738
10756
  "columnType": "TEXT",
10739
- "default": "draft"
10757
+ "default": "draft",
10758
+ "description": "Whether the program is in draft, running or finished."
10740
10759
  },
10741
10760
  {
10742
10761
  "name": "defaultCommissionPlanKey",
@@ -11396,14 +11415,16 @@
11396
11415
  "type": "text",
11397
11416
  "required": false,
11398
11417
  "columnType": "TEXT",
11399
- "default": ""
11418
+ "default": "",
11419
+ "description": "The name you know this affiliate by."
11400
11420
  },
11401
11421
  {
11402
11422
  "name": "status",
11403
11423
  "type": "text",
11404
11424
  "required": false,
11405
11425
  "columnType": "TEXT",
11406
- "default": "pending"
11426
+ "default": "pending",
11427
+ "description": "Whether this affiliate is approved to send you referrals."
11407
11428
  },
11408
11429
  {
11409
11430
  "name": "metadata",
@@ -13908,7 +13929,7 @@
13908
13929
  "polymorphicAssociations": 0,
13909
13930
  "uuidColumns": 232
13910
13931
  },
13911
- "agentDoc": "# @happyvertical/smrt-sales\n\nModular sales: provider-neutral agreement execution, CRM, referral intake, a neutral commissions financial core, and reusable Svelte surfaces. One installable package with distinct subpath exports:\n\n- `@happyvertical/smrt-sales/agreements`\n- `@happyvertical/smrt-sales/crm`\n- `@happyvertical/smrt-sales/referrals`\n- `@happyvertical/smrt-sales/commissions`\n- `@happyvertical/smrt-sales/svelte`\n\nThe root export re-exports every TS module. `agreements` depends on the provider-neutral `@happyvertical/signatures` contract and `smrt-assets`; provider credentials stay in the injected SDK adapter/secret store. `referrals` binds its versioned terms to `agreements`.\n\n## Validation\n\nRun `pnpm --filter @happyvertical/smrt-sales test` and `pnpm --filter @happyvertical/smrt-sales typecheck` for package changes. PostgreSQL-sensitive changes must also run `pnpm --filter @happyvertical/smrt-sales test:postgres`; the command uses the repository's disposable PostgreSQL harness and is registered in the PostgreSQL CI shard.\n\n## Roles vs. money\n\nReferrers and Sales Representatives are **distinct roles** and stay that way. Both connect to money through one neutral financial account:\n\n- **Earner** (commissions): payout identity — method, threshold, currency, status. Referenced by every Commission and CommissionPayout.\n- **SalesRepresentative** (crm) and **Referrer** (referrals): role models, each holding `profileId` (cross-package string ref to smrt-profiles) and `earnerId`.\n\n## Modules\n\nPer-module semantics live in sibling module docs — read the one for the module\nyou are editing. This file keeps what holds in every module.\n\n| Module | Scope | Module doc |\n|---|---|---|\n| `agreements` | verified execution evidence — private orchestration state, append-only provider events, immutable executed agreements, and the provider-neutral execution service | [agents/agreements.md](agents/agreements.md) |\n| `commissions` | the neutral financial core — earners, versioned plans, earning events, commissions, adjustments, balances, and payout settlement | [agents/commissions.md](agents/commissions.md) |\n| `crm` | leads, configurable pipelines, opportunities, tenant-safe Lead follow-up, activity trail, and idempotent conversion links | [agents/crm.md](agents/crm.md) |\n| `referrals` | referrers, programs, versioned attribution policies, links/touches, referrals, exception review, and term snapshots | [agents/referrals.md](agents/referrals.md) |\n| `svelte` | props-driven presentational surfaces for CRM, the referrer portal, and operator review | [agents/svelte.md](agents/svelte.md) |\n\n## Currency\n\n**All monetary fields are integer cents** with `*Cents` suffixes; that scale is a Sales contract, not an implicit framework currency rule. Fresh PostgreSQL/DuckDB INTEGER columns are BIGINT, while JavaScript hydration rejects values outside the safe-integer range. Rates are decimal (`0`–`1`). Rounding happens once per calculation step via `roundCents()` (half-away-from-zero) and every Commission stores its `calculationTrace` so amounts are reproducible. Convert at display/commerce boundaries with `centsToAmount()`/`amountToCents()`.\n\n## Tenancy\n\nBusiness/domain models are generally `@TenantScoped({ mode: 'optional' })` with `@tenantId({ nullable: true })`. This deliberately departs from legacy smrt-affiliates (which was cross-tenant by design for ad networks): sales programs, earners, and payouts are tenant-owned, while `tenantId: null` remains available for intentional global/operator-level rows. Immutable tenant-bound execution evidence (`AgreementExecution`, `AgreementExecutionEvent`, `ExecutedAgreement`) and private orchestration fences (`CommissionAdjustmentOperation`) instead use required tenancy with a non-null owner; never weaken those rows to optional tenancy.\n\n## Cross-package references (plain strings)\n\n`profileId` → smrt-profiles Profile; `invoiceId` (CommissionPayout) → smrt-commerce Invoice; agreement artifacts → smrt-assets Asset ids. No static imports of unrelated sibling domain packages.\n\n## smrt-affiliates migration\n\n`@happyvertical/smrt-affiliates` is now a deprecated compatibility package that re-exports this package's commissions core under the legacy names (`Partner` → `Earner`, `Payout` → `CommissionPayout`, `Commission`, collections, enums) with `@deprecated` guidance and **no duplicate persistence model**. The data/API mapping lives in `packages/affiliates/MIGRATION.md`.\n\n## Gotchas\n\n- **Cents vs. rates**: `*Cents` fields are INTEGER (`= 0` defaults); `rate`/`probability`/`shareFraction` are DECIMAL (`= 0.0` defaults). Don't mix the two conventions on one field.\n- **Optional same-package foreign keys use null**: execution, supersession, earning-event, and payout references that are absent persist as `null`, never `''`; their physical database constraints remain enabled when a real id is present.\n- **Versioned terms are rows, not edits**: CommissionPlan / AttributionPolicy / ReferralAgreement amendments insert `version + 1`; active versions are save-guarded immutable.\n- **Idempotency is dedupeKey-based**: EarningEvent, Commission, and CommissionPayout carry natural keys (`conflictColumns`) — retried ingestion/settlement upserts instead of duplicating.\n- **Adjustments never rewrite**: correcting an earned/paid Commission means appending a CommissionAdjustment, not editing the Commission.\n- **Adjustment retries use the service**: CommissionAdjustment has no public operation field and rejects an untyped `operationId` constructor option; ordinary legacy creates without one remain compatible. `CommissionAdjustmentService` transactionally claims the private operation fence with insert-on-conflict-no-op, creates the explicit mapped adjustment id only for the winner, then verifies the persisted intent; the same globally unique operation UUID cannot be reused by another tenant.\n- **Manifest objects must remain root-importable**: generated consumer registration imports every manifest-advertised model and collection from `@happyvertical/smrt-sales`. The `CommissionAdjustmentOperation` and `ReferralClickOperation` model/collection values are therefore root and owning-subpath exports for runtime loading, while their `api: false`, `mcp: false`, and `cli: false` decorators keep them off generated application surfaces. Publish-pack validation imports the actual CLI-generated register against the packed tarball.\n- **CommissionPayout, not Payout**: avoids the pre-existing global table-name collision between commerce `Payout` and legacy affiliates `Payout` (`payouts`).\n- **Table names are global**: new models were named to avoid collisions across packages (`commission_payouts`, `sales_activities`, `attribution_policies`, …).\n- **Svelte validation includes interaction contracts**: intake and inbox components have runtime component tests; `typecheck` also runs `svelte-check` via `scripts/svelte-check-a11y.mjs`. Ship raw `.svelte` via `svelte-package`.\n- **Lead workflows stay generic**: call `LeadWorkflowService` for tenant-safe intake, audited follow-up, qualification, nonterminal Opportunity stage movement, and terminal closure with optional conversion links. Optional intake `profileId` is stored and fingerprinted but remains host-authorized. The service delegates CRM lifecycles to their owning collections; hosts supply authorization and create downstream records. `LeadCreateForm`, `LeadList`, and `LeadDetail` are props/callback only. See [agents/crm.md](agents/crm.md) for dedupe, retry, and inbox contracts.\n- **Model transition methods don't save**: `markEarned()/approve()/markPayable()/markPaid()` (Commission) and the payout transitions mutate + stamp timestamps only — callers save; the settlement/payout services do both.\n- **`reject()` alone strands rows**: the model method only flips status — always reject through `transitionPayoutForSource`, which releases the batch's membership in the same transaction; stamped rows on a rejected payout would be unsettleable forever.\n- **The payout source stamp is derived data, never authorization**: `sourceKind`/`sourceId` on CommissionPayout index the history listing; both the listing and the lifecycle service re-verify actual membership and fail closed when the stamp cannot be proven.\n- **`sweepClearing` treats `clearingEndsAt: null` as immediately sweepable** (no clearing configured ⇒ nothing to wait for).\n- **Agreement freeze scope**: activating a ReferralAgreement requires and freezes its execution/evidence refs along with its commercial terms. `effectiveTo` remains writable for explicit end-dating; signed bytes, hashes, signer evidence, and audit trail exist only on immutable ExecutedAgreement/Asset records.\n- **Circular FK pair by design**: `Referral.snapshotId ↔ ReferralTermSnapshot.referralId`. Fine on SQLite (and no cross-table DDL constraints are emitted for these string FKs); keep an eye on strict-DDL environments.\n- **Attribution resolution is single-writer per (target, program)**: `resolve()` is idempotent against PERSISTED state, but two workers resolving the same target concurrently can both pass the existence check and create duplicate credit — the collection layer has no cross-row transaction (the same stance as payout batching, which relies on disjoint scopes for concurrency). Run intake resolution serially per target (it naturally is, in a request handler); duplicates are visible in the portal and correctable via `override()`.\n- **recordClick inside your transaction participates, never nests**: a nested adapter `transaction()` takes an independent pooled connection (happyvertical/sdk#1108) — it deadlocks undetectably on locks the caller's transaction holds (PostgreSQL sees a promise-wait, not a lock-wait) and refuses caller-created uncommitted links as `unknown_code`. Pass the caller's transaction database as `RecordClickInput.transaction` or bind the collection to it (`{ db: tx, _reuseInitializedDb: true, _deferRuntimeInitialization: true }` — detected and honored automatically); a pool-level database passed as `transaction` is refused with typed reason `invalid_transaction`. Participating results are bound to the caller's transaction — carry ids across the commit boundary; the pool-bound default still self-transacts and rehydrates after commit.\n- **Click retries reuse one key**: pass the exact same non-empty, well-formed-Unicode `recordClick.idempotencyKey` (maximum 256 UTF-8 bytes) on transport/database retry. Omitting it remains source-compatible but creates and returns a one-shot UUID; it cannot deduplicate a later independent invocation. `maxEvidenceBytes` guards creation; changing it cannot turn an already-committed exact replay into failure. Replay validation is against the immutable operation/touch snapshot, not mutable current link fields: later link edits preserve exact replay, the result link is current, and the touch evidence is the original snapshot. The private optional-tenant `ReferralClickOperation` table is the serialization fence and must be included in schema migration. Never pre-bound caller evidence: Sales owns the authoritative final-envelope byte check.\n- **`ObjectRegistry.getConfig(X)` needs the class module imported** (decorator side effects) — manifest-only registration doesn't carry api/mcp/conflictColumns; tests asserting surface configs need a side-effect import of the module barrel.\n\n- **Governed sales use the owned service factory**: `SalesGovernanceService`\n accepts allowlisted PostgreSQL connection configuration only, owns its\n adapter and transaction executors, and guards private state/model writes.\n DuckDB support remains blocked on SDK #1452; SQLite is also rejected until\n same-process BUSY recovery is proven. Quote issue/sign always recheck\n current seller, host policy/inventory and exact finance approval; see\n [agents/crm.md](agents/crm.md#governed-sellers-quotes-and-canonical-advertiser-registration).\n",
13932
+ "agentDoc": "# @happyvertical/smrt-sales\n\nModular sales: provider-neutral agreement execution, CRM, referral intake, a neutral commissions financial core, and reusable Svelte surfaces. One installable package with distinct subpath exports:\n\n- `@happyvertical/smrt-sales/agreements`\n- `@happyvertical/smrt-sales/crm`\n- `@happyvertical/smrt-sales/referrals`\n- `@happyvertical/smrt-sales/commissions`\n- `@happyvertical/smrt-sales/svelte`\n\nThe root export re-exports every TS module. `agreements` depends on the provider-neutral `@happyvertical/signatures` contract and `smrt-assets`; provider credentials stay in the injected SDK adapter/secret store. `referrals` binds its versioned terms to `agreements`.\n\n## Validation\n\nRun `pnpm --filter @happyvertical/smrt-sales test` and `pnpm --filter @happyvertical/smrt-sales typecheck` for package changes. PostgreSQL-sensitive changes must also run `pnpm --filter @happyvertical/smrt-sales test:postgres`; the command uses the repository's disposable PostgreSQL harness and is registered in the PostgreSQL CI shard.\n\n## Roles vs. money\n\nReferrers and Sales Representatives are **distinct roles** and stay that way. Both connect to money through one neutral financial account:\n\n- **Earner** (commissions): payout identity — method, threshold, currency, status. Referenced by every Commission and CommissionPayout.\n- **SalesRepresentative** (crm) and **Referrer** (referrals): role models, each holding `profileId` (cross-package string ref to smrt-profiles) and `earnerId`.\n\n## Modules\n\nPer-module semantics live in sibling module docs — read the one for the module\nyou are editing. This file keeps what holds in every module.\n\n| Module | Scope | Module doc |\n|---|---|---|\n| `agreements` | verified execution evidence — private orchestration state, append-only provider events, immutable executed agreements, and the provider-neutral execution service | [agents/agreements.md](agents/agreements.md) |\n| `commissions` | the neutral financial core — earners, versioned plans, earning events, commissions, adjustments, balances, and payout settlement | [agents/commissions.md](agents/commissions.md) |\n| `crm` | leads, configurable pipelines, opportunities, tenant-safe Lead follow-up, activity trail, and idempotent conversion links | [agents/crm.md](agents/crm.md) |\n| `referrals` | referrers, programs, versioned attribution policies, links/touches, referrals, exception review, and term snapshots | [agents/referrals.md](agents/referrals.md) |\n| `svelte` | props-driven presentational surfaces for CRM, the referrer portal, and operator review | [agents/svelte.md](agents/svelte.md) |\n\n## Currency\n\n**All monetary fields are integer cents** with `*Cents` suffixes; that scale is a Sales contract, not an implicit framework currency rule. Fresh PostgreSQL/DuckDB INTEGER columns are BIGINT, while JavaScript hydration rejects values outside the safe-integer range. Rates are decimal (`0`–`1`). Rounding happens once per calculation step via `roundCents()` (half-away-from-zero) and every Commission stores its `calculationTrace` so amounts are reproducible. Convert at display/commerce boundaries with `centsToAmount()`/`amountToCents()`.\n\n## Tenancy\n\nBusiness/domain models are generally `@TenantScoped({ mode: 'optional' })` with `@tenantId({ nullable: true })`. This deliberately departs from legacy smrt-affiliates (which was cross-tenant by design for ad networks): sales programs, earners, and payouts are tenant-owned, while `tenantId: null` remains available for intentional global/operator-level rows. Immutable tenant-bound execution evidence (`AgreementExecution`, `AgreementExecutionEvent`, `ExecutedAgreement`) and private orchestration fences (`CommissionAdjustmentOperation`) instead use required tenancy with a non-null owner; never weaken those rows to optional tenancy.\n\n## Cross-package references (plain strings)\n\n`profileId` → smrt-profiles Profile; `invoiceId` (CommissionPayout) → smrt-commerce Invoice; agreement artifacts → smrt-assets Asset ids. No static imports of unrelated sibling domain packages.\n\n## smrt-affiliates migration\n\n`@happyvertical/smrt-affiliates` is now a deprecated compatibility package that re-exports this package's commissions core under the legacy names (`Partner` → `Earner`, `Payout` → `CommissionPayout`, `Commission`, collections, enums) with `@deprecated` guidance and **no duplicate persistence model**. The data/API mapping lives in `packages/affiliates/MIGRATION.md`.\n\n## Gotchas\n\n- **Cents vs. rates**: `*Cents` fields are INTEGER (`= 0` defaults); `rate`/`probability`/`shareFraction` are DECIMAL (`= 0.0` defaults). Don't mix the two conventions on one field.\n- **Optional same-package foreign keys use null**: execution, supersession, earning-event, and payout references that are absent persist as `null`, never `''`; their physical database constraints remain enabled when a real id is present.\n- **Versioned terms are rows, not edits**: CommissionPlan / AttributionPolicy / ReferralAgreement amendments insert `version + 1`; active versions are save-guarded immutable.\n- **Idempotency is dedupeKey-based**: EarningEvent, Commission, and CommissionPayout carry natural keys (`conflictColumns`) — retried ingestion/settlement upserts instead of duplicating.\n- **Adjustments never rewrite**: correcting an earned/paid Commission means appending a CommissionAdjustment, not editing the Commission.\n- **Adjustment retries use the service**: CommissionAdjustment has no public operation field and rejects an untyped `operationId` constructor option; ordinary legacy creates without one remain compatible. `CommissionAdjustmentService` transactionally claims the private operation fence with insert-on-conflict-no-op, creates the explicit mapped adjustment id only for the winner, then verifies the persisted intent; the same globally unique operation UUID cannot be reused by another tenant.\n- **Manifest objects must remain root-importable**: generated consumer registration imports every manifest-advertised model and collection from `@happyvertical/smrt-sales`. The `CommissionAdjustmentOperation` and `ReferralClickOperation` model/collection values are therefore root and owning-subpath exports for runtime loading, while their `api: false`, `mcp: false`, and `cli: false` decorators keep them off generated application surfaces. Publish-pack validation imports the actual CLI-generated register against the packed tarball.\n- **CommissionPayout, not Payout**: avoids the pre-existing global table-name collision between commerce `Payout` and legacy affiliates `Payout` (`payouts`).\n- **Table names are global**: new models were named to avoid collisions across packages (`commission_payouts`, `sales_activities`, `attribution_policies`, …).\n- **Svelte validation includes interaction contracts**: intake and inbox components have runtime component tests; `typecheck` also runs `svelte-check` via `scripts/svelte-check-a11y.mjs`. Ship raw `.svelte` via `svelte-package`.\n- **Lead workflows stay generic**: call `LeadWorkflowService` for tenant-safe intake, audited follow-up, qualification, nonterminal Opportunity stage movement, and terminal closure with optional conversion links. Optional intake `profileId` is stored and fingerprinted but remains host-authorized. The service delegates CRM lifecycles to their owning collections; hosts supply authorization and create downstream records. `LeadCreateForm`, `LeadList`, and `LeadDetail` are props/callback only. See [agents/crm.md](agents/crm.md) for dedupe, retry, and inbox contracts.\n- **Model transition methods don't save**: `markEarned()/approve()/markPayable()/markPaid()` (Commission) and the payout transitions mutate + stamp timestamps only — callers save; the settlement/payout services do both.\n- **`reject()` alone strands rows**: the model method only flips status — always reject through `transitionPayoutForSource`, which releases the batch's membership in the same transaction; stamped rows on a rejected payout would be unsettleable forever.\n- **The payout source stamp is derived data, never authorization**: `sourceKind`/`sourceId` on CommissionPayout index the history listing; both the listing and the lifecycle service re-verify actual membership and fail closed when the stamp cannot be proven.\n- **`sweepClearing` treats `clearingEndsAt: null` as immediately sweepable** (no clearing configured ⇒ nothing to wait for).\n- **Agreement freeze scope**: activating a ReferralAgreement requires and freezes its execution/evidence refs along with its commercial terms. `effectiveTo` remains writable for explicit end-dating; signed bytes, hashes, signer evidence, and audit trail exist only on immutable ExecutedAgreement/Asset records.\n- **Circular FK pair by design**: `Referral.snapshotId ↔ ReferralTermSnapshot.referralId`. Fine on SQLite (and no cross-table DDL constraints are emitted for these string FKs); keep an eye on strict-DDL environments.\n- **Attribution resolution is single-writer per (target, program)**: `resolve()` is idempotent against PERSISTED state, but two workers resolving the same target concurrently can both pass the existence check and create duplicate credit — the collection layer has no cross-row transaction (the same stance as payout batching, which relies on disjoint scopes for concurrency). Run intake resolution serially per target (it naturally is, in a request handler); duplicates are visible in the portal and correctable via `override()`.\n- **recordClick inside your transaction participates, never nests**: a nested adapter `transaction()` takes an independent pooled connection (happyvertical/sdk#1108) — it deadlocks undetectably on locks the caller's transaction holds (PostgreSQL sees a promise-wait, not a lock-wait) and refuses caller-created uncommitted links as `unknown_code`. Pass the caller's transaction database as `RecordClickInput.transaction` or bind the collection to it (`{ db: tx, _reuseInitializedDb: true, _deferRuntimeInitialization: true }` — detected and honored automatically); a pool-level database passed as `transaction` is refused with typed reason `invalid_transaction`. Participating results are bound to the caller's transaction — carry ids across the commit boundary; the pool-bound default still self-transacts and rehydrates after commit.\n- **Click retries reuse one key**: pass the exact same non-empty, well-formed-Unicode `recordClick.idempotencyKey` (maximum 256 UTF-8 bytes) on transport/database retry. Omitting it remains source-compatible but creates and returns a one-shot UUID; it cannot deduplicate a later independent invocation. `maxEvidenceBytes` guards creation; changing it cannot turn an already-committed exact replay into failure. Replay validation is against the immutable operation/touch snapshot, not mutable current link fields: later link edits preserve exact replay, the result link is current, and the touch evidence is the original snapshot. The private optional-tenant `ReferralClickOperation` table is the serialization fence and must be included in schema migration. Never pre-bound caller evidence: Sales owns the authoritative final-envelope byte check.\n- **`ObjectRegistry.getConfig(X)` needs the class module imported** (decorator side effects) — manifest-only registration doesn't carry api/mcp/conflictColumns; tests asserting surface configs need a side-effect import of the module barrel.\n\n- **Governed sales use the owned service factory**: `SalesGovernanceService`\n accepts allowlisted PostgreSQL connection configuration only, owns its\n adapter and transaction executors, and guards private state/model writes.\n DuckDB support remains blocked on SDK #1452; SQLite is also rejected until\n same-process BUSY recovery is proven. Quote issue/sign always recheck\n current seller, host policy/inventory and exact finance approval; see\n [agents/crm.md](agents/crm.md#governed-sellers-quotes-and-canonical-advertiser-registration).\n\n- **Recipe `affiliates.referrals`** (`src/referrals/recipes.ts`, help in `src/referrals/referrals.recipe.md`): the affiliate feature for catalog hosts (referrers, programs, referrals, commissions, payouts). It lives here because `@happyvertical/smrt-affiliates` is a model-less shim and a recipe needs models of its own package. Fields it shows carry `@field({ description })` in plain words.\n",
13912
13933
  "moduleDocs": [
13913
13934
  {
13914
13935
  "path": "agents/agreements.md",
@@ -13937,6 +13958,389 @@
13937
13958
  }
13938
13959
  ],
13939
13960
  "recipes": [
13961
+ {
13962
+ "id": "affiliates.referrals",
13963
+ "className": "AffiliatesReferralsRecipe",
13964
+ "label": "Affiliates",
13965
+ "summary": "Reward partners for the customers they send you, and pay them what they earn.",
13966
+ "synonyms": [
13967
+ "referrals",
13968
+ "referral program",
13969
+ "partners",
13970
+ "commissions",
13971
+ "payouts",
13972
+ "refer a friend"
13973
+ ],
13974
+ "models": [
13975
+ "@happyvertical/smrt-sales:Referrer",
13976
+ "@happyvertical/smrt-sales:ReferralProgram",
13977
+ "@happyvertical/smrt-sales:Referral",
13978
+ "@happyvertical/smrt-sales:Earner",
13979
+ "@happyvertical/smrt-sales:Commission",
13980
+ "@happyvertical/smrt-sales:CommissionPayout"
13981
+ ],
13982
+ "nav": [
13983
+ {
13984
+ "label": "Affiliates",
13985
+ "model": "@happyvertical/smrt-sales:Referrer",
13986
+ "icon": "users",
13987
+ "description": "The partners and customers who send people your way.",
13988
+ "noun": "affiliate"
13989
+ },
13990
+ {
13991
+ "label": "Programs",
13992
+ "model": "@happyvertical/smrt-sales:ReferralProgram",
13993
+ "icon": "layers",
13994
+ "description": "The offers that decide what a referral earns.",
13995
+ "noun": "program"
13996
+ },
13997
+ {
13998
+ "label": "Referrals",
13999
+ "model": "@happyvertical/smrt-sales:Referral",
14000
+ "icon": "repeat",
14001
+ "description": "Each person sent to you, and whether it led to a sale."
14002
+ },
14003
+ {
14004
+ "label": "Commissions",
14005
+ "model": "@happyvertical/smrt-sales:Commission",
14006
+ "icon": "receipt",
14007
+ "description": "What each affiliate has earned and where it stands."
14008
+ },
14009
+ {
14010
+ "label": "Payouts",
14011
+ "model": "@happyvertical/smrt-sales:CommissionPayout",
14012
+ "icon": "bank",
14013
+ "description": "Payments made to affiliates for what they earned."
14014
+ }
14015
+ ],
14016
+ "requires": [],
14017
+ "section": {
14018
+ "id": "grow",
14019
+ "label": "Grow",
14020
+ "icon": "tag",
14021
+ "description": "Bring in new customers and reward the people who help."
14022
+ },
14023
+ "options": {
14024
+ "@happyvertical/smrt-sales:Referrer": {
14025
+ "fields": {
14026
+ "displayName": {
14027
+ "label": "Name",
14028
+ "order": 1
14029
+ },
14030
+ "status": {
14031
+ "order": 2
14032
+ },
14033
+ "profileId": {
14034
+ "label": "Person",
14035
+ "visibility": "advanced"
14036
+ },
14037
+ "tenantId": {
14038
+ "visibility": "hidden"
14039
+ },
14040
+ "earnerId": {
14041
+ "label": "Payee",
14042
+ "visibility": "advanced"
14043
+ },
14044
+ "metadata": {
14045
+ "visibility": "hidden"
14046
+ }
14047
+ }
14048
+ },
14049
+ "@happyvertical/smrt-sales:ReferralProgram": {
14050
+ "fields": {
14051
+ "name": {
14052
+ "order": 1
14053
+ },
14054
+ "key": {
14055
+ "label": "Short name",
14056
+ "order": 2
14057
+ },
14058
+ "status": {
14059
+ "order": 3
14060
+ },
14061
+ "defaultCommissionPlanKey": {
14062
+ "visibility": "advanced"
14063
+ },
14064
+ "defaultAttributionPolicyKey": {
14065
+ "visibility": "advanced"
14066
+ },
14067
+ "tenantId": {
14068
+ "visibility": "hidden"
14069
+ },
14070
+ "metadata": {
14071
+ "visibility": "hidden"
14072
+ }
14073
+ }
14074
+ },
14075
+ "@happyvertical/smrt-sales:Referral": {
14076
+ "fields": {
14077
+ "referrerId": {
14078
+ "label": "Affiliate",
14079
+ "order": 1
14080
+ },
14081
+ "programId": {
14082
+ "label": "Program",
14083
+ "order": 2
14084
+ },
14085
+ "status": {
14086
+ "order": 3
14087
+ },
14088
+ "tenantId": {
14089
+ "visibility": "hidden"
14090
+ },
14091
+ "policyKey": {
14092
+ "visibility": "hidden"
14093
+ },
14094
+ "policyVersion": {
14095
+ "visibility": "hidden"
14096
+ },
14097
+ "targetKind": {
14098
+ "visibility": "advanced"
14099
+ },
14100
+ "targetId": {
14101
+ "visibility": "advanced"
14102
+ },
14103
+ "creditFraction": {
14104
+ "visibility": "advanced"
14105
+ },
14106
+ "splitGroupId": {
14107
+ "visibility": "hidden"
14108
+ },
14109
+ "primaryTouchId": {
14110
+ "visibility": "hidden"
14111
+ },
14112
+ "attributedAt": {
14113
+ "visibility": "advanced"
14114
+ },
14115
+ "qualifiedAt": {
14116
+ "visibility": "advanced"
14117
+ },
14118
+ "expiresAt": {
14119
+ "visibility": "advanced"
14120
+ },
14121
+ "snapshotId": {
14122
+ "visibility": "hidden"
14123
+ },
14124
+ "metadata": {
14125
+ "visibility": "hidden"
14126
+ }
14127
+ }
14128
+ },
14129
+ "@happyvertical/smrt-sales:Earner": {
14130
+ "fields": {
14131
+ "displayName": {
14132
+ "label": "Name",
14133
+ "order": 1
14134
+ },
14135
+ "status": {
14136
+ "order": 2
14137
+ },
14138
+ "tenantId": {
14139
+ "visibility": "hidden"
14140
+ },
14141
+ "kind": {
14142
+ "visibility": "advanced"
14143
+ },
14144
+ "profileId": {
14145
+ "visibility": "advanced"
14146
+ },
14147
+ "payoutMethod": {
14148
+ "visibility": "advanced"
14149
+ },
14150
+ "payoutDestination": {
14151
+ "visibility": "advanced"
14152
+ },
14153
+ "payoutThresholdCents": {
14154
+ "visibility": "advanced"
14155
+ },
14156
+ "payoutScheduleKey": {
14157
+ "visibility": "advanced"
14158
+ },
14159
+ "currency": {
14160
+ "visibility": "advanced"
14161
+ },
14162
+ "metadata": {
14163
+ "visibility": "hidden"
14164
+ }
14165
+ }
14166
+ },
14167
+ "@happyvertical/smrt-sales:Commission": {
14168
+ "fields": {
14169
+ "earnerId": {
14170
+ "label": "Affiliate",
14171
+ "order": 1
14172
+ },
14173
+ "amountCents": {
14174
+ "label": "Amount",
14175
+ "order": 2
14176
+ },
14177
+ "currency": {
14178
+ "order": 3
14179
+ },
14180
+ "status": {
14181
+ "order": 4
14182
+ },
14183
+ "tenantId": {
14184
+ "visibility": "hidden"
14185
+ },
14186
+ "earningEventId": {
14187
+ "visibility": "hidden"
14188
+ },
14189
+ "planKey": {
14190
+ "visibility": "advanced"
14191
+ },
14192
+ "planVersion": {
14193
+ "visibility": "hidden"
14194
+ },
14195
+ "componentKey": {
14196
+ "visibility": "hidden"
14197
+ },
14198
+ "termsSnapshotKind": {
14199
+ "visibility": "hidden"
14200
+ },
14201
+ "termsSnapshotId": {
14202
+ "visibility": "hidden"
14203
+ },
14204
+ "basis": {
14205
+ "visibility": "advanced"
14206
+ },
14207
+ "baseAmountCents": {
14208
+ "visibility": "advanced"
14209
+ },
14210
+ "rate": {
14211
+ "visibility": "advanced"
14212
+ },
14213
+ "shareFraction": {
14214
+ "visibility": "hidden"
14215
+ },
14216
+ "splitGroupId": {
14217
+ "visibility": "hidden"
14218
+ },
14219
+ "clearingEndsAt": {
14220
+ "visibility": "advanced"
14221
+ },
14222
+ "earnedAt": {
14223
+ "visibility": "advanced"
14224
+ },
14225
+ "approvedAt": {
14226
+ "visibility": "advanced"
14227
+ },
14228
+ "payableAt": {
14229
+ "visibility": "advanced"
14230
+ },
14231
+ "paidAt": {
14232
+ "visibility": "advanced"
14233
+ },
14234
+ "payoutId": {
14235
+ "visibility": "advanced"
14236
+ },
14237
+ "sourceKind": {
14238
+ "visibility": "hidden"
14239
+ },
14240
+ "sourceId": {
14241
+ "visibility": "hidden"
14242
+ },
14243
+ "calculationTrace": {
14244
+ "visibility": "hidden"
14245
+ },
14246
+ "dedupeKey": {
14247
+ "visibility": "hidden"
14248
+ },
14249
+ "metadata": {
14250
+ "visibility": "hidden"
14251
+ }
14252
+ }
14253
+ },
14254
+ "@happyvertical/smrt-sales:CommissionPayout": {
14255
+ "fields": {
14256
+ "earnerId": {
14257
+ "label": "Affiliate",
14258
+ "order": 1
14259
+ },
14260
+ "totalAmountCents": {
14261
+ "label": "Total",
14262
+ "order": 2
14263
+ },
14264
+ "currency": {
14265
+ "order": 3
14266
+ },
14267
+ "status": {
14268
+ "order": 4
14269
+ },
14270
+ "paidAt": {
14271
+ "order": 5
14272
+ },
14273
+ "tenantId": {
14274
+ "visibility": "hidden"
14275
+ },
14276
+ "periodStart": {
14277
+ "visibility": "advanced"
14278
+ },
14279
+ "periodEnd": {
14280
+ "visibility": "advanced"
14281
+ },
14282
+ "commissionTotalCents": {
14283
+ "visibility": "advanced"
14284
+ },
14285
+ "adjustmentTotalCents": {
14286
+ "visibility": "advanced"
14287
+ },
14288
+ "payoutMethod": {
14289
+ "visibility": "advanced"
14290
+ },
14291
+ "payoutDestination": {
14292
+ "visibility": "advanced"
14293
+ },
14294
+ "paymentReference": {
14295
+ "visibility": "advanced"
14296
+ },
14297
+ "providerRef": {
14298
+ "visibility": "hidden"
14299
+ },
14300
+ "invoiceId": {
14301
+ "visibility": "hidden"
14302
+ },
14303
+ "notes": {
14304
+ "visibility": "advanced"
14305
+ },
14306
+ "idempotencyKey": {
14307
+ "visibility": "hidden"
14308
+ },
14309
+ "sourceKind": {
14310
+ "visibility": "hidden"
14311
+ },
14312
+ "sourceId": {
14313
+ "visibility": "hidden"
14314
+ },
14315
+ "metadata": {
14316
+ "visibility": "hidden"
14317
+ }
14318
+ }
14319
+ }
14320
+ },
14321
+ "help": {
14322
+ "markdown": "## Overview\n\nAffiliates lets partners and customers send people your way and earn a reward\nwhen it leads to a sale. You set up a program that says what a referral earns,\nsign up the people who will refer, record each person they send, and see what\neach affiliate has earned and what has been paid out.\n\n## Tasks\n\n### Sign up an affiliate\n\n1. Open the list of affiliates and choose **New**.\n2. Type their **{field:Referrer.displayName}**.\n3. Save. They stay pending until you approve them by setting the **{field:Referrer.status}**.\n\n### Set up a program\n\n1. Open the programs and choose **New**.\n2. Give the program a **{field:ReferralProgram.name}** your affiliates will understand.\n3. Set the **{field:ReferralProgram.status}** to active when you are ready to start.\n4. Save.\n\n### Record a referral\n\n1. Open the referrals and choose **New**.\n2. Pick the **{field:Referral.referrerId}** who sent the person and the **{field:Referral.programId}** it belongs to.\n3. Save. The referral stays pending until it leads to a sale.\n\n### Check what an affiliate has earned\n\n1. Open the commissions and find the affiliate.\n2. Read the **{field:Commission.amountCents}** and the **{field:Commission.status}** of each one.\n3. A commission moves from pending to approved, then payable, then paid as it is worked through.\n\n### Check a payout\n\nPayouts are made by the commission payout service, not by hand in the app. You\ncan see what has been paid.\n\n1. Open the payouts.\n2. Read the **{field:CommissionPayout.earnerId}**, the **{field:CommissionPayout.totalAmountCents}** and the **{field:CommissionPayout.status}**.\n3. A payout is waiting, approved, being sent, then completed. It shows failed or rejected if the money did not go out.\n",
14323
+ "fieldRefs": [
14324
+ "Commission.amountCents",
14325
+ "Commission.status",
14326
+ "CommissionPayout.earnerId",
14327
+ "CommissionPayout.status",
14328
+ "CommissionPayout.totalAmountCents",
14329
+ "Referral.programId",
14330
+ "Referral.referrerId",
14331
+ "ReferralProgram.name",
14332
+ "ReferralProgram.status",
14333
+ "Referrer.displayName",
14334
+ "Referrer.status"
14335
+ ]
14336
+ },
14337
+ "demo": {
14338
+ "mode": "server",
14339
+ "reasons": [
14340
+ "Needs a server because its package does not build for a browser: node:crypto; and its package depends on @happyvertical/smrt-tenancy, which do not build for a browser (#3624)."
14341
+ ]
14342
+ }
14343
+ },
13940
14344
  {
13941
14345
  "id": "sales.pipeline",
13942
14346
  "className": "SalesPipelineRecipe",