@happyvertical/smrt-sales 0.40.42 → 0.40.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +27 -0
  3. package/agents/crm.md +3 -0
  4. package/agents/svelte.md +3 -1
  5. package/dist/agreements.js +1 -1
  6. package/dist/chunks/{__smrt-register__-DJEOUcaR.js → __smrt-register__-_uoH8d0A.js} +2 -2
  7. package/dist/chunks/{__smrt-register__-DJEOUcaR.js.map → __smrt-register__-_uoH8d0A.js.map} +1 -1
  8. package/dist/chunks/{crm-DwEz7E2r.js → crm-B8XqwUBg.js} +472 -7
  9. package/dist/chunks/crm-B8XqwUBg.js.map +1 -0
  10. package/dist/commissions.js +1 -1
  11. package/dist/crm/collections/SalesActivityCollection.d.ts +2 -2
  12. package/dist/crm/collections/SalesActivityCollection.d.ts.map +1 -1
  13. package/dist/crm/index.d.ts +2 -0
  14. package/dist/crm/index.d.ts.map +1 -1
  15. package/dist/crm/models/SalesActivity.d.ts +17 -0
  16. package/dist/crm/models/SalesActivity.d.ts.map +1 -1
  17. package/dist/crm/services/LeadWorkflowService.d.ts +164 -0
  18. package/dist/crm/services/LeadWorkflowService.d.ts.map +1 -0
  19. package/dist/crm.js +3 -3
  20. package/dist/index.js +3 -3
  21. package/dist/manifest.json +1671 -1663
  22. package/dist/referrals.js +1 -1
  23. package/dist/smrt-knowledge.json +157 -156
  24. package/dist/svelte/__tests__/types.test.js +30 -1
  25. package/dist/svelte/components/LeadDetail.svelte +512 -0
  26. package/dist/svelte/components/LeadDetail.svelte.d.ts +31 -0
  27. package/dist/svelte/components/LeadDetail.svelte.d.ts.map +1 -0
  28. package/dist/svelte/index.d.ts +5 -3
  29. package/dist/svelte/index.d.ts.map +1 -1
  30. package/dist/svelte/index.js +3 -2
  31. package/dist/svelte/types.d.ts +44 -1
  32. package/dist/svelte/types.d.ts.map +1 -1
  33. package/dist/svelte/types.js +17 -0
  34. package/package.json +6 -6
  35. package/dist/chunks/crm-DwEz7E2r.js.map +0 -1
package/dist/referrals.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./chunks/__smrt-register__-DJEOUcaR.js";
1
+ import "./chunks/__smrt-register__-_uoH8d0A.js";
2
2
  import { A as ReferralClickOperation, B as ATTRIBUTION_RESOLUTION_MODES, C as ReferralTouchCollection, D as ReferralCollection, E as assertHttpTargetUrl, F as validateAttributionPolicyTerms, G as REFERRAL_STATUSES, H as REFERRAL_AGREEMENT_STATUSES, I as ATTRIBUTION_CONFLICT_BEHAVIORS, J as AttributionExceptionCollection, K as REFERRAL_TOUCH_KINDS, L as ATTRIBUTION_CREDIT_MODES, M as ReferralAgreement, N as AttributionPolicyCollection, O as Referral, P as AttributionPolicy, R as ATTRIBUTION_EXCEPTION_STATUSES, S as generateReferralCode, T as ReferralLink, U as REFERRAL_LINK_STATUSES, V as REFERRAL_AGREEMENT_APPROVAL_MODES, W as REFERRAL_PROGRAM_STATUSES, Y as AttributionException, _ as REFERRAL_CODE_ALPHABET, a as ReferralAgreementExecutionService, b as ReferralClickValidationError, c as ReferrerCollection, d as ReferralTermSnapshot, f as ReferralProgramCollection, g as MAX_REFERRAL_CLICK_IDEMPOTENCY_KEY_BYTES, h as MAX_CODE_GENERATION_ATTEMPTS, i as REFERRAL_AGREEMENT_SOURCE_KIND, j as ReferralAgreementCollection, k as ReferralClickOperationCollection, l as Referrer, m as DEFAULT_REFERRAL_CLICK_EVIDENCE_MAX_BYTES, n as REFERRAL_TERMS_SNAPSHOT_KIND, o as AttributionService, p as ReferralProgram, q as REFERRER_STATUSES, r as ReferralCommissionService, s as QualifiedReferralOverrideError, t as ReferralQualificationService, u as ReferralTermSnapshotCollection, v as REFERRAL_CODE_LENGTH, w as ReferralTouch, x as ReferralLinkCollection, y as ReferralClickReplayConflictError, z as ATTRIBUTION_POLICY_STATUSES } from "./chunks/referrals-BzTlbjJH.js";
3
3
  export { ATTRIBUTION_CONFLICT_BEHAVIORS, ATTRIBUTION_CREDIT_MODES, ATTRIBUTION_EXCEPTION_STATUSES, ATTRIBUTION_POLICY_STATUSES, ATTRIBUTION_RESOLUTION_MODES, AttributionException, AttributionExceptionCollection, AttributionPolicy, AttributionPolicyCollection, AttributionService, DEFAULT_REFERRAL_CLICK_EVIDENCE_MAX_BYTES, MAX_CODE_GENERATION_ATTEMPTS, MAX_REFERRAL_CLICK_IDEMPOTENCY_KEY_BYTES, QualifiedReferralOverrideError, REFERRAL_AGREEMENT_APPROVAL_MODES, REFERRAL_AGREEMENT_SOURCE_KIND, REFERRAL_AGREEMENT_STATUSES, REFERRAL_CODE_ALPHABET, REFERRAL_CODE_LENGTH, REFERRAL_LINK_STATUSES, REFERRAL_PROGRAM_STATUSES, REFERRAL_STATUSES, REFERRAL_TERMS_SNAPSHOT_KIND, REFERRAL_TOUCH_KINDS, REFERRER_STATUSES, Referral, ReferralAgreement, ReferralAgreementCollection, ReferralAgreementExecutionService, ReferralClickOperation, ReferralClickOperationCollection, ReferralClickReplayConflictError, ReferralClickValidationError, ReferralCollection, ReferralCommissionService, ReferralLink, ReferralLinkCollection, ReferralProgram, ReferralProgramCollection, ReferralQualificationService, ReferralTermSnapshot, ReferralTermSnapshotCollection, ReferralTouch, ReferralTouchCollection, Referrer, ReferrerCollection, assertHttpTargetUrl, generateReferralCode, validateAttributionPolicyTerms };
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-31T12:37:59.657Z",
3
+ "generatedAt": "2026-08-02T02:44:25.993Z",
4
4
  "packageName": "@happyvertical/smrt-sales",
5
- "packageVersion": "0.40.42",
5
+ "packageVersion": "0.40.44",
6
6
  "sourceManifestPath": "dist/manifest.json",
7
7
  "agentDocPath": "AGENTS.md",
8
8
  "sourceHashes": {
9
- "manifest": "61b9367bb0c8cf51cf91e64ac7a36ce288b8808e14fb551d10da47462800cf5e",
10
- "packageJson": "3438c6f087ff256a884ce8ecc96cc58522d15a7f653b982ae95e37be00c02aac",
11
- "agents": "2585fdd71db3fa5d7e0ad90ccf569ebc79d750454e86f0f98527e90bcedd8f61",
9
+ "manifest": "5b0bf32b7046f90a0ad1bda44951d75b8dadd1576d2bc1da1d4d24880494450b",
10
+ "packageJson": "03544c12bbeada0dd5915a9ab99be06cf9badf08bcbe7882e3b3411e651cf392",
11
+ "agents": "a58ac2ed4a0d43e22110674a94d61b62b981f7aa4a576ca8f36d0b1f20a64143",
12
12
  "moduleDoc:agents/agreements.md": "eb6ff147a0f60ee627d3337e8e9394d06fda955b4a4fff228537a9bd2803f886",
13
13
  "moduleDoc:agents/commissions.md": "e047024aafe5edcf072c1a6b7ea253c24cb15f2fa1beb2edd13651e735921f73",
14
- "moduleDoc:agents/crm.md": "ca80c4fe7634d9a1f81f0a6f39724a123504303816d8c58b5fe5557be26cecd8",
14
+ "moduleDoc:agents/crm.md": "e90f813e857782bbfbd4122c9f67b61914e3ef49284a58e976a5507391851b0c",
15
15
  "moduleDoc:agents/referrals.md": "e64b64aad4b3feea08f172a18e3992539d761b5f05ac4e5107338a700c0cc4ce",
16
- "moduleDoc:agents/svelte.md": "8e90d621e26bd971c7d7159356eeb7fd182b9e0d0cba02342662aa05874410f3"
16
+ "moduleDoc:agents/svelte.md": "e03d8efce0a3c5571648ec16858715ccf4377c9840acca403beed4e554205164"
17
17
  },
18
18
  "exports": [
19
19
  ".",
@@ -2215,6 +2215,152 @@
2215
2215
  "tags": [],
2216
2216
  "risks": []
2217
2217
  },
2218
+ {
2219
+ "name": "LeadCollection",
2220
+ "qualifiedName": "@happyvertical/smrt-sales:LeadCollection",
2221
+ "collection": "leads",
2222
+ "tableName": "lead_collections",
2223
+ "packageName": "@happyvertical/smrt-sales",
2224
+ "extends": "SmrtCollection",
2225
+ "fields": [],
2226
+ "relationships": [],
2227
+ "methods": [
2228
+ "activitiesIncludingMerged",
2229
+ "findByOwner",
2230
+ "findByStatus",
2231
+ "mergeLeads",
2232
+ "qualify"
2233
+ ],
2234
+ "surfaces": [],
2235
+ "relationshipFeatures": [
2236
+ "uuidColumns"
2237
+ ],
2238
+ "tags": [],
2239
+ "risks": []
2240
+ },
2241
+ {
2242
+ "name": "OpportunityCollection",
2243
+ "qualifiedName": "@happyvertical/smrt-sales:OpportunityCollection",
2244
+ "collection": "opportunities",
2245
+ "tableName": "opportunity_collections",
2246
+ "packageName": "@happyvertical/smrt-sales",
2247
+ "extends": "SmrtCollection",
2248
+ "fields": [],
2249
+ "relationships": [],
2250
+ "methods": [
2251
+ "findByLead",
2252
+ "findOpen",
2253
+ "moveToStage"
2254
+ ],
2255
+ "surfaces": [],
2256
+ "relationshipFeatures": [
2257
+ "uuidColumns"
2258
+ ],
2259
+ "tags": [],
2260
+ "risks": []
2261
+ },
2262
+ {
2263
+ "name": "OpportunityConversionCollection",
2264
+ "qualifiedName": "@happyvertical/smrt-sales:OpportunityConversionCollection",
2265
+ "collection": "opportunityconversions",
2266
+ "tableName": "opportunity_conversion_collections",
2267
+ "packageName": "@happyvertical/smrt-sales",
2268
+ "extends": "SmrtCollection",
2269
+ "fields": [],
2270
+ "relationships": [],
2271
+ "methods": [
2272
+ "findByOpportunity",
2273
+ "recordConversion"
2274
+ ],
2275
+ "surfaces": [],
2276
+ "relationshipFeatures": [
2277
+ "uuidColumns"
2278
+ ],
2279
+ "tags": [],
2280
+ "risks": []
2281
+ },
2282
+ {
2283
+ "name": "PipelineDefinitionCollection",
2284
+ "qualifiedName": "@happyvertical/smrt-sales:PipelineDefinitionCollection",
2285
+ "collection": "pipelinedefinitions",
2286
+ "tableName": "pipeline_definition_collections",
2287
+ "packageName": "@happyvertical/smrt-sales",
2288
+ "extends": "SmrtCollection",
2289
+ "fields": [],
2290
+ "relationships": [],
2291
+ "methods": [
2292
+ "ensureDefaultPipeline",
2293
+ "findByKey",
2294
+ "findDefault",
2295
+ "getStages"
2296
+ ],
2297
+ "surfaces": [],
2298
+ "relationshipFeatures": [
2299
+ "uuidColumns"
2300
+ ],
2301
+ "tags": [],
2302
+ "risks": []
2303
+ },
2304
+ {
2305
+ "name": "PipelineStageCollection",
2306
+ "qualifiedName": "@happyvertical/smrt-sales:PipelineStageCollection",
2307
+ "collection": "pipelinestages",
2308
+ "tableName": "pipeline_stage_collections",
2309
+ "packageName": "@happyvertical/smrt-sales",
2310
+ "extends": "SmrtCollection",
2311
+ "fields": [],
2312
+ "relationships": [],
2313
+ "methods": [
2314
+ "findByKey",
2315
+ "findByPipeline"
2316
+ ],
2317
+ "surfaces": [],
2318
+ "relationshipFeatures": [
2319
+ "uuidColumns"
2320
+ ],
2321
+ "tags": [],
2322
+ "risks": []
2323
+ },
2324
+ {
2325
+ "name": "SalesActivityCollection",
2326
+ "qualifiedName": "@happyvertical/smrt-sales:SalesActivityCollection",
2327
+ "collection": "salesactivities",
2328
+ "tableName": "sales_activity_collections",
2329
+ "packageName": "@happyvertical/smrt-sales",
2330
+ "extends": "SmrtCollection",
2331
+ "fields": [],
2332
+ "relationships": [],
2333
+ "methods": [
2334
+ "findBySubject",
2335
+ "findOpenTasks"
2336
+ ],
2337
+ "surfaces": [],
2338
+ "relationshipFeatures": [
2339
+ "uuidColumns"
2340
+ ],
2341
+ "tags": [],
2342
+ "risks": []
2343
+ },
2344
+ {
2345
+ "name": "SalesRepresentativeCollection",
2346
+ "qualifiedName": "@happyvertical/smrt-sales:SalesRepresentativeCollection",
2347
+ "collection": "salesrepresentatives",
2348
+ "tableName": "sales_representative_collections",
2349
+ "packageName": "@happyvertical/smrt-sales",
2350
+ "extends": "SmrtCollection",
2351
+ "fields": [],
2352
+ "relationships": [],
2353
+ "methods": [
2354
+ "findActive",
2355
+ "findByProfile"
2356
+ ],
2357
+ "surfaces": [],
2358
+ "relationshipFeatures": [
2359
+ "uuidColumns"
2360
+ ],
2361
+ "tags": [],
2362
+ "risks": []
2363
+ },
2218
2364
  {
2219
2365
  "name": "Lead",
2220
2366
  "qualifiedName": "@happyvertical/smrt-sales:Lead",
@@ -3127,6 +3273,7 @@
3127
3273
  "methods": [
3128
3274
  "getMetadata",
3129
3275
  "isOpenTask",
3276
+ "save",
3130
3277
  "setMetadata"
3131
3278
  ],
3132
3279
  "surfaces": [
@@ -3326,152 +3473,6 @@
3326
3473
  "tags": [],
3327
3474
  "risks": []
3328
3475
  },
3329
- {
3330
- "name": "LeadCollection",
3331
- "qualifiedName": "@happyvertical/smrt-sales:LeadCollection",
3332
- "collection": "leads",
3333
- "tableName": "lead_collections",
3334
- "packageName": "@happyvertical/smrt-sales",
3335
- "extends": "SmrtCollection",
3336
- "fields": [],
3337
- "relationships": [],
3338
- "methods": [
3339
- "activitiesIncludingMerged",
3340
- "findByOwner",
3341
- "findByStatus",
3342
- "mergeLeads",
3343
- "qualify"
3344
- ],
3345
- "surfaces": [],
3346
- "relationshipFeatures": [
3347
- "uuidColumns"
3348
- ],
3349
- "tags": [],
3350
- "risks": []
3351
- },
3352
- {
3353
- "name": "OpportunityCollection",
3354
- "qualifiedName": "@happyvertical/smrt-sales:OpportunityCollection",
3355
- "collection": "opportunities",
3356
- "tableName": "opportunity_collections",
3357
- "packageName": "@happyvertical/smrt-sales",
3358
- "extends": "SmrtCollection",
3359
- "fields": [],
3360
- "relationships": [],
3361
- "methods": [
3362
- "findByLead",
3363
- "findOpen",
3364
- "moveToStage"
3365
- ],
3366
- "surfaces": [],
3367
- "relationshipFeatures": [
3368
- "uuidColumns"
3369
- ],
3370
- "tags": [],
3371
- "risks": []
3372
- },
3373
- {
3374
- "name": "OpportunityConversionCollection",
3375
- "qualifiedName": "@happyvertical/smrt-sales:OpportunityConversionCollection",
3376
- "collection": "opportunityconversions",
3377
- "tableName": "opportunity_conversion_collections",
3378
- "packageName": "@happyvertical/smrt-sales",
3379
- "extends": "SmrtCollection",
3380
- "fields": [],
3381
- "relationships": [],
3382
- "methods": [
3383
- "findByOpportunity",
3384
- "recordConversion"
3385
- ],
3386
- "surfaces": [],
3387
- "relationshipFeatures": [
3388
- "uuidColumns"
3389
- ],
3390
- "tags": [],
3391
- "risks": []
3392
- },
3393
- {
3394
- "name": "PipelineDefinitionCollection",
3395
- "qualifiedName": "@happyvertical/smrt-sales:PipelineDefinitionCollection",
3396
- "collection": "pipelinedefinitions",
3397
- "tableName": "pipeline_definition_collections",
3398
- "packageName": "@happyvertical/smrt-sales",
3399
- "extends": "SmrtCollection",
3400
- "fields": [],
3401
- "relationships": [],
3402
- "methods": [
3403
- "ensureDefaultPipeline",
3404
- "findByKey",
3405
- "findDefault",
3406
- "getStages"
3407
- ],
3408
- "surfaces": [],
3409
- "relationshipFeatures": [
3410
- "uuidColumns"
3411
- ],
3412
- "tags": [],
3413
- "risks": []
3414
- },
3415
- {
3416
- "name": "PipelineStageCollection",
3417
- "qualifiedName": "@happyvertical/smrt-sales:PipelineStageCollection",
3418
- "collection": "pipelinestages",
3419
- "tableName": "pipeline_stage_collections",
3420
- "packageName": "@happyvertical/smrt-sales",
3421
- "extends": "SmrtCollection",
3422
- "fields": [],
3423
- "relationships": [],
3424
- "methods": [
3425
- "findByKey",
3426
- "findByPipeline"
3427
- ],
3428
- "surfaces": [],
3429
- "relationshipFeatures": [
3430
- "uuidColumns"
3431
- ],
3432
- "tags": [],
3433
- "risks": []
3434
- },
3435
- {
3436
- "name": "SalesActivityCollection",
3437
- "qualifiedName": "@happyvertical/smrt-sales:SalesActivityCollection",
3438
- "collection": "salesactivities",
3439
- "tableName": "sales_activity_collections",
3440
- "packageName": "@happyvertical/smrt-sales",
3441
- "extends": "SmrtCollection",
3442
- "fields": [],
3443
- "relationships": [],
3444
- "methods": [
3445
- "findBySubject",
3446
- "findOpenTasks"
3447
- ],
3448
- "surfaces": [],
3449
- "relationshipFeatures": [
3450
- "uuidColumns"
3451
- ],
3452
- "tags": [],
3453
- "risks": []
3454
- },
3455
- {
3456
- "name": "SalesRepresentativeCollection",
3457
- "qualifiedName": "@happyvertical/smrt-sales:SalesRepresentativeCollection",
3458
- "collection": "salesrepresentatives",
3459
- "tableName": "sales_representative_collections",
3460
- "packageName": "@happyvertical/smrt-sales",
3461
- "extends": "SmrtCollection",
3462
- "fields": [],
3463
- "relationships": [],
3464
- "methods": [
3465
- "findActive",
3466
- "findByProfile"
3467
- ],
3468
- "surfaces": [],
3469
- "relationshipFeatures": [
3470
- "uuidColumns"
3471
- ],
3472
- "tags": [],
3473
- "risks": []
3474
- },
3475
3476
  {
3476
3477
  "name": "AttributionExceptionCollection",
3477
3478
  "qualifiedName": "@happyvertical/smrt-sales:AttributionExceptionCollection",
@@ -6637,7 +6638,7 @@
6637
6638
  "polymorphicAssociations": 0,
6638
6639
  "uuidColumns": 136
6639
6640
  },
6640
- "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, 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; 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- **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 module is svelte-check-gated**: no runtime component tests; `typecheck` runs `svelte-check` via `scripts/svelte-check-a11y.mjs`. Ship raw `.svelte` via `svelte-package`.\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",
6641
+ "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; 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- **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 module is svelte-check-gated**: no runtime component tests; `typecheck` runs `svelte-check` via `scripts/svelte-check-a11y.mjs`. Ship raw `.svelte` via `svelte-package`.\n- **Lead follow-up stays generic**: call `LeadWorkflowService` for audited Lead assignment/status/activity/task mutations under ambient tenancy; it locks rows, rejects cross-tenant identifiers, and has no authorization, SLA, automatic-owner, reminder, website-contact, or conversion policy. `LeadDetail` is props/callback only; host applications map state and call the service.\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",
6641
6642
  "moduleDocs": [
6642
6643
  {
6643
6644
  "path": "agents/agreements.md",
@@ -6652,7 +6653,7 @@
6652
6653
  {
6653
6654
  "path": "agents/crm.md",
6654
6655
  "module": "crm",
6655
- "content": "# smrt-sales/crm\n\nPer-module semantics for `@happyvertical/smrt-sales/crm`. Package orientation, the\ncross-module invariants (currency, tenancy, cross-package refs, roles vs.\nmoney), and the Gotchas that apply before editing anything live in\n[../AGENTS.md](../AGENTS.md) — read that first.\n\n- **SalesRepresentative**: role model (`profileId`, `earnerId`, `status`).\n- **Lead**: identified prospect with owner assignment, generic acquisition source (`sourceKind`/`sourceId`), preserved `acquisitionContext` JSON, and audited merge (`mergedIntoId`; merges preserve activity + acquisition history on both sides).\n- **PipelineDefinition / PipelineStage**: configurable ordered stages with default `new → qualified → discovery → proposal → negotiation → closed_won | closed_lost` (seeded via `ensureDefaultPipeline()`); stages carry `probability` and `isWon`/`isLost` terminal flags.\n- **Opportunity**: qualified engagement — owner, pipeline + stage, `expectedValueCents`, `probability`, `expectedCloseAt`, outcome. Stage movement validated against the pipeline; terminal stages set `won|lost` status.\n- **SalesActivity**: activity/next-action trail for Leads and Opportunities (`subjectKind`/`subjectId`), also the audit trail for assignment, qualification, merges, and stage movement.\n- **OpportunityConversion**: idempotent conversion links (`targetKind`/`targetId` — client, project, contract, subscription, …) with a composite natural key. CRM never creates downstream records itself and never mutates referral or commission state.\n"
6656
+ "content": "# smrt-sales/crm\n\nPer-module semantics for `@happyvertical/smrt-sales/crm`. Package orientation, the\ncross-module invariants (currency, tenancy, cross-package refs, roles vs.\nmoney), and the Gotchas that apply before editing anything live in\n[../AGENTS.md](../AGENTS.md) — read that first.\n\n- **SalesRepresentative**: role model (`profileId`, `earnerId`, `status`).\n- **Lead**: identified prospect with owner assignment, generic acquisition source (`sourceKind`/`sourceId`), preserved `acquisitionContext` JSON, and audited merge (`mergedIntoId`; merges preserve activity + acquisition history on both sides).\n- **PipelineDefinition / PipelineStage**: configurable ordered stages with default `new → qualified → discovery → proposal → negotiation → closed_won | closed_lost` (seeded via `ensureDefaultPipeline()`); stages carry `probability` and `isWon`/`isLost` terminal flags.\n- **Opportunity**: qualified engagement — owner, pipeline + stage, `expectedValueCents`, `probability`, `expectedCloseAt`, outcome. Stage movement validated against the pipeline; terminal stages set `won|lost` status.\n- **SalesActivity**: activity/next-action trail for Leads and Opportunities (`subjectKind`/`subjectId`), also the audit trail for assignment, qualification, merges, and stage movement.\n- **LeadWorkflowService**: the required tenant-safe pre-qualification mutation seam. It transactionally locks the Lead (and completion task on PostgreSQL), accepts only active same-tenant representatives, writes assignment/status/completion audits alongside their mutations, and returns merge-aware timeline/work-state reads. It owns `new | disqualified → working`, `new | working → disqualified`, human follow-up (`note | call | email | meeting`), and task scheduling/completion; qualification and merging remain collection lifecycles.\n- **OpportunityConversion**: idempotent conversion links (`targetKind`/`targetId` — client, project, contract, subscription, …) with a composite natural key. CRM never creates downstream records itself and never mutates referral or commission state.\n\nWorkflow calls require ambient tenant context and an actor profile id. Fail closed for foreign/missing Lead, representative, and task ids without revealing their existence. Human metadata is plain JSON-object data; framework audit metadata is generated separately. Keep queue projection pure (`now`/optional timezone are injected) and do not add assignment, reminder, SLA, authorization, or conversion policy.\n"
6656
6657
  },
6657
6658
  {
6658
6659
  "path": "agents/referrals.md",
@@ -6662,7 +6663,7 @@
6662
6663
  {
6663
6664
  "path": "agents/svelte.md",
6664
6665
  "module": "svelte",
6665
- "content": "# smrt-sales/svelte\n\nPer-module semantics for `@happyvertical/smrt-sales/svelte`. Package orientation, the\ncross-module invariants (currency, tenancy, cross-package refs, roles vs.\nmoney), and the Gotchas that apply before editing anything live in\n[../AGENTS.md](../AGENTS.md) — read that first.\n\nProps-driven presentational components (no data fetching, no model-class imports, Provider-free smrt-ui primitives, `--smrt-*` tokens only): CRM — `SalesDashboard`, `LeadList`, `OpportunityBoard`, `OpportunityDetail`; referrer portal — `ReferralLinkManager`, `ReferralStatusList`, `ReferrerEarningsSummary`, `CommissionBreakdown` (trace-explained amounts), `PayoutHistoryList`, `ExecutedAgreementsList`; operator — `AttributionConflictQueue` (award editor + required resolution reason), `PayoutBatchReview`, `CommissionExpenseSummary` (explicitly distinct from client invoices). Monetary props stay integer cents; `format.ts` converts at render. View-model prop types are exported interfaces (never inline intersected generics in `$props()`); pure helpers (dashboard math, award validation mirroring the service, payout action gating) are unit-tested while components are svelte-check-gated.\n"
6666
+ "content": "# smrt-sales/svelte\n\nPer-module semantics for `@happyvertical/smrt-sales/svelte`. Package orientation, the\ncross-module invariants (currency, tenancy, cross-package refs, roles vs.\nmoney), and the Gotchas that apply before editing anything live in\n[../AGENTS.md](../AGENTS.md) — read that first.\n\nProps-driven presentational components (no data fetching, no model-class imports, Provider-free smrt-ui primitives, `--smrt-*` tokens only): CRM — `SalesDashboard`, `LeadList`, `LeadDetail`, `OpportunityBoard`, `OpportunityDetail`; referrer portal — `ReferralLinkManager`, `ReferralStatusList`, `ReferrerEarningsSummary`, `CommissionBreakdown` (trace-explained amounts), `PayoutHistoryList`, `ExecutedAgreementsList`; operator — `AttributionConflictQueue` (award editor + required resolution reason), `PayoutBatchReview`, `CommissionExpenseSummary` (explicitly distinct from client invoices). Monetary props stay integer cents; `format.ts` converts at render. View-model prop types are exported interfaces (never inline intersected generics in `$props()`); pure helpers (dashboard math, Lead workflow action gating, award validation mirroring the service, payout action gating) are unit-tested while components are svelte-check-gated.\n\n`LeadDetail` renders a complete host-supplied timeline and callback-only assignment, start/reopen, disqualification, human-activity, next-action, completion, and optional qualification controls. Its `busy` prop disables every mutation affordance; the host owns tenancy, authorization, service invocation, refresh, and any policy around due dates or reminders.\n"
6666
6667
  }
6667
6668
  ]
6668
6669
  }
@@ -4,7 +4,7 @@
4
4
  * expense reconciliation totals.
5
5
  */
6
6
  import { describe, expect, it } from 'vitest';
7
- import { adjacentStageIds, buildShareUrl, canQualifyLead, equalSplitAwards, formatCommissionFormula, formatPlanRef, groupOpportunitiesByStage, isHttpUrl, isOverdue, openOpportunityCount, openPipelineTotals, payoutActionsFor, payoutStatusTimeline, pipelineValueByStage, sumExpenseRowsByCurrency, uniqueCandidateReferrerIds, validateAwards, winRate, } from '../types.js';
7
+ import { adjacentStageIds, buildShareUrl, canQualifyLead, equalSplitAwards, formatCommissionFormula, formatPlanRef, groupOpportunitiesByStage, isHttpUrl, isOverdue, leadWorkflowActionsFor, openOpportunityCount, openPipelineTotals, payoutActionsFor, payoutStatusTimeline, pipelineValueByStage, sumExpenseRowsByCurrency, uniqueCandidateReferrerIds, validateAwards, winRate, } from '../types.js';
8
8
  const stages = [
9
9
  { id: 's1', name: 'New' },
10
10
  { id: 's2', name: 'Proposal' },
@@ -120,6 +120,35 @@ describe('lead helpers', () => {
120
120
  expect(isOverdue(undefined, now)).toBe(false);
121
121
  expect(isOverdue('not-a-date', now)).toBe(false);
122
122
  });
123
+ it('gates generic workflow callbacks without importing CRM models', () => {
124
+ expect(leadWorkflowActionsFor('new')).toMatchObject({
125
+ canAssign: true,
126
+ canStartWorking: true,
127
+ canDisqualify: true,
128
+ canRecordActivity: true,
129
+ canScheduleNextAction: true,
130
+ canCompleteNextAction: true,
131
+ canQualify: true,
132
+ });
133
+ expect(leadWorkflowActionsFor('disqualified')).toMatchObject({
134
+ canAssign: false,
135
+ canStartWorking: true,
136
+ canDisqualify: false,
137
+ canRecordActivity: false,
138
+ canScheduleNextAction: false,
139
+ canCompleteNextAction: false,
140
+ canQualify: false,
141
+ });
142
+ expect(leadWorkflowActionsFor('qualified')).toEqual({
143
+ canAssign: false,
144
+ canStartWorking: false,
145
+ canDisqualify: false,
146
+ canRecordActivity: false,
147
+ canScheduleNextAction: false,
148
+ canCompleteNextAction: false,
149
+ canQualify: false,
150
+ });
151
+ });
123
152
  });
124
153
  describe('referral link helpers', () => {
125
154
  it('builds share URLs, normalising trailing slashes and encoding the code', () => {