@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.
Files changed (29) hide show
  1. package/README.md +65 -0
  2. package/agents/commissions.md +2 -1
  3. package/dist/agreements.js +1 -1
  4. package/dist/chunks/{__smrt-register__-Czzs3Vof.js → __smrt-register__-DE9qSi5T.js} +2 -2
  5. package/dist/chunks/__smrt-register__-DE9qSi5T.js.map +1 -0
  6. package/dist/chunks/{commissions-Cip6QZ9Q.js → commissions-D5x24d4o.js} +443 -67
  7. package/dist/chunks/commissions-D5x24d4o.js.map +1 -0
  8. package/dist/chunks/{referrals-CYgrGkEo.js → referrals-BRdTkEFt.js} +2 -2
  9. package/dist/chunks/{referrals-CYgrGkEo.js.map → referrals-BRdTkEFt.js.map} +1 -1
  10. package/dist/commissions/models/Earner.d.ts +41 -1
  11. package/dist/commissions/models/Earner.d.ts.map +1 -1
  12. package/dist/commissions/models/index.d.ts +1 -1
  13. package/dist/commissions/models/index.d.ts.map +1 -1
  14. package/dist/commissions/services/CommissionPayoutService.d.ts.map +1 -1
  15. package/dist/commissions/services/WaterfallCalculationService.d.ts +33 -1
  16. package/dist/commissions/services/WaterfallCalculationService.d.ts.map +1 -1
  17. package/dist/commissions/services/index.d.ts +1 -1
  18. package/dist/commissions/services/index.d.ts.map +1 -1
  19. package/dist/commissions/types.d.ts +169 -1
  20. package/dist/commissions/types.d.ts.map +1 -1
  21. package/dist/commissions.js +3 -3
  22. package/dist/crm.js +1 -1
  23. package/dist/index.js +4 -4
  24. package/dist/manifest.json +93 -5
  25. package/dist/referrals.js +2 -2
  26. package/dist/smrt-knowledge.json +132 -6
  27. package/package.json +8 -8
  28. package/dist/chunks/__smrt-register__-Czzs3Vof.js.map +0 -1
  29. package/dist/chunks/commissions-Cip6QZ9Q.js.map +0 -1
@@ -1048,6 +1048,80 @@ var CommissionPayoutCollection = class extends SmrtCollection {
1048
1048
  }
1049
1049
  };
1050
1050
  //#endregion
1051
+ //#region src/commissions/types.ts
1052
+ var EARNER_STATUSES = [
1053
+ "pending",
1054
+ "active",
1055
+ "suspended"
1056
+ ];
1057
+ var EARNER_SOURCE_ATTRIBUTION_STATUSES = ["active", "inactive"];
1058
+ var EARNER_KINDS = [
1059
+ "person",
1060
+ "organisation",
1061
+ "treasury"
1062
+ ];
1063
+ var PAYOUT_METHODS = [
1064
+ "bank_transfer",
1065
+ "check",
1066
+ "paypal",
1067
+ "credit",
1068
+ "other",
1069
+ "stripe_connect",
1070
+ "crypto"
1071
+ ];
1072
+ var PAYOUT_METHODS_REQUIRING_DESTINATION = ["stripe_connect", "crypto"];
1073
+ var COMMISSION_PLAN_STATUSES = [
1074
+ "draft",
1075
+ "active",
1076
+ "superseded",
1077
+ "retired"
1078
+ ];
1079
+ var COMMISSION_STATUSES = [
1080
+ "pending",
1081
+ "earned",
1082
+ "approved",
1083
+ "payable",
1084
+ "paid"
1085
+ ];
1086
+ var COMMISSION_BASES = [
1087
+ "fixed",
1088
+ "gross",
1089
+ "net",
1090
+ "margin",
1091
+ "custom"
1092
+ ];
1093
+ var COMMISSION_ADJUSTMENT_KINDS = [
1094
+ "refund",
1095
+ "credit",
1096
+ "chargeback",
1097
+ "dispute",
1098
+ "correction"
1099
+ ];
1100
+ var COMMISSION_PAYOUT_STATUSES = [
1101
+ "pending",
1102
+ "approved",
1103
+ "processing",
1104
+ "completed",
1105
+ "failed",
1106
+ "rejected"
1107
+ ];
1108
+ var EARNING_EVENT_KINDS = [
1109
+ "conversion",
1110
+ "agreement_execution",
1111
+ "invoice_payment",
1112
+ "collected_revenue",
1113
+ "recognized_margin",
1114
+ "milestone"
1115
+ ];
1116
+ var ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES = [
1117
+ "earned",
1118
+ "approved",
1119
+ "payable",
1120
+ "paid"
1121
+ ];
1122
+ var WATERFALL_POOL_RESIDUAL_RULES = ["remainder_sink", "largest_remainder"];
1123
+ var WATERFALL_POOL_ZERO_WEIGHT_RULES = ["remainder_sink", "equal"];
1124
+ //#endregion
1051
1125
  //#region src/commissions/services/WaterfallCalculationService.ts
1052
1126
  var WaterfallRecipientResolverRegistry = class {
1053
1127
  resolvers = /* @__PURE__ */ new Map();
@@ -1060,6 +1134,54 @@ var WaterfallRecipientResolverRegistry = class {
1060
1134
  return this.resolvers.get(descriptor.kind)?.(descriptor, event) ?? null;
1061
1135
  }
1062
1136
  };
1137
+ async function prepareWaterfallPools(plan, event, options = {}) {
1138
+ validateWaterfallPlan(plan);
1139
+ const period = normalizePeriod(options.metricPeriod);
1140
+ const facts = event.facts ?? {};
1141
+ const pools = /* @__PURE__ */ Object.create(null);
1142
+ for (const step of plan.steps) {
1143
+ if (!step.pool || !conditionsMatch(step.conditions, facts)) continue;
1144
+ const resolved = options.resolver?.resolve(step.recipient, event) ?? null;
1145
+ if (!resolved) {
1146
+ pools[step.key] = null;
1147
+ continue;
1148
+ }
1149
+ const pool = freezeRecipient(resolved);
1150
+ if (!options.poolMembers) throw new Error(`Waterfall pool step '${step.key}' requires a poolMembers resolver`);
1151
+ const members = sortMembers((await options.poolMembers(pool, event)).map((member) => freezeRecipient(assertRecipient(member, `pool '${step.key}' member`))), step.key);
1152
+ const metric = step.pool.metric ?? null;
1153
+ let weights = members.map(() => 1);
1154
+ if (metric !== null) {
1155
+ if (!options.metricProvider) throw new Error(`Waterfall pool step '${step.key}' requires a metricProvider for metric '${metric}'`);
1156
+ const values = await options.metricProvider.getMetricValues({
1157
+ metric,
1158
+ pool,
1159
+ members,
1160
+ period,
1161
+ event
1162
+ });
1163
+ weights = weightsFromMetricValues(step.key, members, values);
1164
+ }
1165
+ pools[step.key] = {
1166
+ pool,
1167
+ metric,
1168
+ period: metric === null ? null : period,
1169
+ members: members.map((recipient, index) => ({
1170
+ recipient,
1171
+ weight: weights[index]
1172
+ }))
1173
+ };
1174
+ }
1175
+ return pools;
1176
+ }
1177
+ async function calculateWaterfallWithPools(plan, event, options = {}) {
1178
+ if (options.frozenInputs || options.pools) return calculateWaterfall(plan, event, options);
1179
+ const pools = await prepareWaterfallPools(plan, event, options);
1180
+ return calculateWaterfall(plan, event, {
1181
+ ...options,
1182
+ pools
1183
+ });
1184
+ }
1063
1185
  function calculateWaterfall(plan, event, options = {}) {
1064
1186
  validateWaterfallPlan(plan);
1065
1187
  const planFingerprint = fingerprintPlan(plan);
@@ -1085,6 +1207,11 @@ function calculateWaterfall(plan, event, options = {}) {
1085
1207
  remainderSink: options.frozenInputs ? freezeRecipient(options.frozenInputs.remainderSink) : resolveSink(plan, calculationEvent, options),
1086
1208
  applicationFeeSettledCents: options.frozenInputs?.applicationFeeSettledCents ?? event.applicationFeeSettledCents ?? 0
1087
1209
  };
1210
+ if (plan.steps.some((step) => step.pool)) {
1211
+ const poolsByStep = /* @__PURE__ */ Object.create(null);
1212
+ for (const [key, snapshot] of Object.entries(options.frozenInputs?.poolsByStep ?? {})) poolsByStep[key] = normalizePoolSnapshot(snapshot, key);
1213
+ frozenInputs.poolsByStep = poolsByStep;
1214
+ }
1088
1215
  assertWholeNonNegative(frozenInputs.applicationFeeSettledCents, "applicationFeeSettledCents");
1089
1216
  let remainingCents = calculationEvent.grossAmountCents;
1090
1217
  let redirectedToSinkCents = 0;
@@ -1108,6 +1235,38 @@ function calculateWaterfall(plan, event, options = {}) {
1108
1235
  const baseCents = resolveBase(step, calculationEvent, remainingCents);
1109
1236
  const amountCents = resolveAmount(step, baseCents);
1110
1237
  if (amountCents > remainingCents) throw new Error(`Waterfall step '${step.key}' allocates ${amountCents} cents but only ${remainingCents} cents remain`);
1238
+ if (step.pool) {
1239
+ const { recipient: recipient2, snapshot } = resolvePoolStep(step, calculationEvent, options, frozenInputs);
1240
+ remainingCents -= amountCents;
1241
+ if (!recipient2 || !snapshot) {
1242
+ redirectedToSinkCents += amountCents;
1243
+ steps.push({
1244
+ key: step.key,
1245
+ baseCents,
1246
+ amountCents,
1247
+ recipient: null,
1248
+ redirectedToSinkCents: amountCents,
1249
+ applicationFeeOffsetCents: 0,
1250
+ payableCents: 0
1251
+ });
1252
+ continue;
1253
+ }
1254
+ const pool = allocatePool(amountCents, step.pool, snapshot);
1255
+ const memberCents = amountCents - pool.toSinkCents;
1256
+ totalRecipientAllocatedCents += memberCents;
1257
+ redirectedToSinkCents += pool.toSinkCents;
1258
+ steps.push({
1259
+ key: step.key,
1260
+ baseCents,
1261
+ amountCents,
1262
+ recipient: recipient2,
1263
+ redirectedToSinkCents: pool.toSinkCents,
1264
+ applicationFeeOffsetCents: 0,
1265
+ payableCents: memberCents,
1266
+ pool: pool.trace
1267
+ });
1268
+ continue;
1269
+ }
1111
1270
  const recipient = resolveStepRecipient(step, calculationEvent, options, frozenInputs);
1112
1271
  let applicationFeeOffsetCents = 0;
1113
1272
  if (recipient && descriptorMatches(plan.applicationFeeOffsetRecipient, step.recipient)) {
@@ -1166,6 +1325,176 @@ function resolveStepRecipient(step, event, options, frozen) {
1166
1325
  });
1167
1326
  return frozenRecipient;
1168
1327
  }
1328
+ function resolvePoolStep(step, event, options, frozen) {
1329
+ const poolsByStep = frozen.poolsByStep;
1330
+ if (Object.hasOwn(frozen.recipientByStep, step.key)) {
1331
+ const recipient2 = frozen.recipientByStep[step.key];
1332
+ if (!recipient2) return {
1333
+ recipient: null,
1334
+ snapshot: null
1335
+ };
1336
+ const snapshot2 = Object.hasOwn(poolsByStep, step.key) ? poolsByStep[step.key] : void 0;
1337
+ if (!snapshot2) throw new Error(`Waterfall frozen inputs lack the pool snapshot for step '${step.key}'`);
1338
+ if (!isDeepStrictEqual(snapshot2.pool, recipient2)) throw new Error(`Waterfall frozen pool snapshot for step '${step.key}' does not match its frozen recipient`);
1339
+ assertSnapshotMetric(step, snapshot2);
1340
+ return {
1341
+ recipient: freezeRecipient(recipient2),
1342
+ snapshot: snapshot2
1343
+ };
1344
+ }
1345
+ if (!options.pools || !Object.hasOwn(options.pools, step.key)) throw new Error(`Waterfall pool step '${step.key}' requires prepared pool inputs; use prepareWaterfallPools() or calculateWaterfallWithPools()`);
1346
+ const prepared = options.pools[step.key];
1347
+ const snapshot = prepared ? normalizePoolSnapshot(prepared, step.key) : null;
1348
+ if (snapshot) assertSnapshotMetric(step, snapshot);
1349
+ const current = options.resolver?.resolve(step.recipient, event) ?? null;
1350
+ if (!isDeepStrictEqual(current ? freezeRecipient(current) : null, snapshot ? snapshot.pool : null)) throw new Error(`Waterfall prepared pool for step '${step.key}' does not match the pool its recipient descriptor resolves to`);
1351
+ const recipient = snapshot ? freezeRecipient(snapshot.pool) : null;
1352
+ Object.defineProperty(frozen.recipientByStep, step.key, {
1353
+ value: recipient,
1354
+ enumerable: true,
1355
+ writable: true,
1356
+ configurable: true
1357
+ });
1358
+ if (snapshot) poolsByStep[step.key] = snapshot;
1359
+ return {
1360
+ recipient,
1361
+ snapshot
1362
+ };
1363
+ }
1364
+ function assertSnapshotMetric(step, snapshot) {
1365
+ const expected = step.pool?.metric ?? null;
1366
+ if (snapshot.metric !== expected) throw new Error(`Waterfall pool snapshot for step '${step.key}' uses metric ${JSON.stringify(snapshot.metric)} but the plan step declares ${JSON.stringify(expected)}`);
1367
+ }
1368
+ function allocatePool(amountCents, spec, snapshot) {
1369
+ const residualRule = spec.residual ?? "remainder_sink";
1370
+ const members = snapshot.members;
1371
+ const totalWeight = members.reduce((sum, member) => sum + member.weight, 0);
1372
+ const base = {
1373
+ metric: snapshot.metric,
1374
+ period: snapshot.period ? { ...snapshot.period } : null,
1375
+ totalWeight,
1376
+ residualRule
1377
+ };
1378
+ const useEqual = members.length > 0 && totalWeight === 0 && (spec.zeroWeight ?? "remainder_sink") === "equal";
1379
+ if (members.length === 0 || totalWeight === 0 && !useEqual) return {
1380
+ toSinkCents: amountCents,
1381
+ trace: {
1382
+ ...base,
1383
+ distribution: "remainder_sink",
1384
+ residualToSinkCents: 0,
1385
+ members: members.map((member) => ({
1386
+ recipient: freezeRecipient(member.recipient),
1387
+ weight: member.weight,
1388
+ shareFraction: 0,
1389
+ amountCents: 0
1390
+ }))
1391
+ }
1392
+ };
1393
+ const effective = members.map((member) => BigInt(useEqual ? 1 : member.weight));
1394
+ const total = effective.reduce((sum, weight) => sum + weight, 0n);
1395
+ const amount = BigInt(amountCents);
1396
+ const floors = effective.map((weight) => amount * weight / total);
1397
+ const remainders = effective.map((weight) => amount * weight % total);
1398
+ let residual = Number(amount - floors.reduce((sum, cents2) => sum + cents2, 0n));
1399
+ const cents = floors.map(Number);
1400
+ let residualToSinkCents = residual;
1401
+ if (residualRule === "largest_remainder") {
1402
+ const order = members.map((_, index) => index).sort((a, b) => compareBigInt(remainders[b], remainders[a]) || compareBigInt(effective[b], effective[a]) || compareCodeUnits(members[a].recipient.kind, members[b].recipient.kind) || compareCodeUnits(members[a].recipient.id, members[b].recipient.id));
1403
+ for (const index of order) {
1404
+ if (residual === 0) break;
1405
+ cents[index] += 1;
1406
+ residual -= 1;
1407
+ }
1408
+ residualToSinkCents = 0;
1409
+ }
1410
+ const memberTraces = members.map((member, index) => ({
1411
+ recipient: freezeRecipient(member.recipient),
1412
+ weight: member.weight,
1413
+ shareFraction: Number(effective[index]) / Number(total),
1414
+ amountCents: cents[index]
1415
+ }));
1416
+ return {
1417
+ toSinkCents: residualToSinkCents,
1418
+ trace: {
1419
+ ...base,
1420
+ distribution: useEqual || snapshot.metric === null ? "equal" : "weighted",
1421
+ residualToSinkCents,
1422
+ members: memberTraces
1423
+ }
1424
+ };
1425
+ }
1426
+ function compareBigInt(a, b) {
1427
+ return a === b ? 0 : a < b ? -1 : 1;
1428
+ }
1429
+ function compareCodeUnits(a, b) {
1430
+ return a === b ? 0 : a < b ? -1 : 1;
1431
+ }
1432
+ function assertRecipient(value, label) {
1433
+ const recipient = value;
1434
+ if (!recipient || typeof recipient !== "object" || typeof recipient.kind !== "string" || !recipient.kind || typeof recipient.id !== "string" || !recipient.id) throw new Error(`Waterfall ${label} must have non-empty kind and id`);
1435
+ return recipient;
1436
+ }
1437
+ function sortMembers(members, stepKey) {
1438
+ const sorted = [...members].sort((a, b) => compareCodeUnits(a.kind, b.kind) || compareCodeUnits(a.id, b.id));
1439
+ for (let index = 1; index < sorted.length; index++) if (sorted[index].kind === sorted[index - 1].kind && sorted[index].id === sorted[index - 1].id) throw new Error(`Waterfall pool step '${stepKey}' lists member ${sorted[index].kind}:${sorted[index].id} more than once`);
1440
+ return sorted;
1441
+ }
1442
+ function weightsFromMetricValues(stepKey, members, values) {
1443
+ if (!Array.isArray(values)) throw new Error(`Waterfall pool step '${stepKey}' metric provider must return an array`);
1444
+ const indexByMember = /* @__PURE__ */ new Map();
1445
+ for (const [index, member] of members.entries()) indexByMember.set(JSON.stringify([member.kind, member.id]), index);
1446
+ const weights = members.map(() => 0);
1447
+ const seen = /* @__PURE__ */ new Set();
1448
+ for (const entry of values) {
1449
+ const index = indexByMember.get(JSON.stringify([entry?.kind, entry?.id]));
1450
+ if (index === void 0) throw new Error(`Waterfall pool step '${stepKey}' metric value names a non-member`);
1451
+ if (seen.has(index)) throw new Error(`Waterfall pool step '${stepKey}' metric value repeats a member`);
1452
+ seen.add(index);
1453
+ assertWeight(entry.value, stepKey);
1454
+ weights[index] = entry.value;
1455
+ }
1456
+ return weights;
1457
+ }
1458
+ function assertWeight(value, stepKey) {
1459
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`Waterfall pool step '${stepKey}' weights must be non-negative safe integers`);
1460
+ }
1461
+ function normalizePoolSnapshot(snapshot, stepKey) {
1462
+ if (!snapshot || typeof snapshot !== "object" || !Array.isArray(snapshot.members)) throw new Error(`Waterfall pool snapshot for step '${stepKey}' must list members`);
1463
+ if (snapshot.metric !== null && (typeof snapshot.metric !== "string" || !snapshot.metric)) throw new Error(`Waterfall pool snapshot for step '${stepKey}' has an invalid metric`);
1464
+ const members = sortMembers(snapshot.members.map((member) => {
1465
+ assertWeight(member?.weight, stepKey);
1466
+ return {
1467
+ ...freezeRecipient(assertRecipient(member.recipient, `pool '${stepKey}' member`)),
1468
+ weight: member.weight
1469
+ };
1470
+ }), stepKey);
1471
+ if (snapshot.metric === null && members.some((member) => member.weight !== 1)) throw new Error(`Waterfall pool snapshot for step '${stepKey}' uses equal weights, so every weight must be 1`);
1472
+ let total = 0;
1473
+ for (const member of members) {
1474
+ total += member.weight;
1475
+ if (!Number.isSafeInteger(total)) throw new Error(`Waterfall pool step '${stepKey}' total weight exceeds the safe integer range`);
1476
+ }
1477
+ return {
1478
+ pool: freezeRecipient(assertRecipient(snapshot.pool, `pool '${stepKey}'`)),
1479
+ metric: snapshot.metric,
1480
+ period: snapshot.period ? normalizePeriod(snapshot.period) : null,
1481
+ members: members.map(({ weight, ...recipient }) => ({
1482
+ recipient,
1483
+ weight
1484
+ }))
1485
+ };
1486
+ }
1487
+ function normalizePeriod(period) {
1488
+ if (!period) return null;
1489
+ const start = new Date(period.start);
1490
+ const end = new Date(period.end);
1491
+ if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) throw new Error("Waterfall metric period must use valid dates");
1492
+ if (end <= start) throw new Error("Waterfall metric period end must be later than start");
1493
+ return {
1494
+ start: start.toISOString(),
1495
+ end: end.toISOString()
1496
+ };
1497
+ }
1169
1498
  function freezeRecipient(recipient) {
1170
1499
  return {
1171
1500
  kind: recipient.kind,
@@ -1235,6 +1564,7 @@ function validateWaterfallPlan(plan) {
1235
1564
  "prototype"
1236
1565
  ].includes(step.key)) throw new Error(`Waterfall step '${step.key}' uses a reserved key`);
1237
1566
  if (!step.recipient?.kind) throw new Error(`Waterfall step '${step.key}' requires a recipient descriptor`);
1567
+ if (step.pool !== void 0) validatePoolSpec(plan, step);
1238
1568
  if (step.base === "custom" && !step.customBaseKey) throw new Error(`Waterfall step '${step.key}' requires customBaseKey for custom base`);
1239
1569
  if (![
1240
1570
  "gross",
@@ -1263,6 +1593,20 @@ function validateWaterfallPlan(plan) {
1263
1593
  if (!plan.remainderSink?.kind) throw new Error("Waterfall plan requires a remainderSink descriptor");
1264
1594
  if (plan.applicationFeeOffsetRecipient && !plan.applicationFeeOffsetRecipient.kind) throw new Error("Waterfall application fee offset requires a recipient descriptor");
1265
1595
  }
1596
+ function validatePoolSpec(plan, step) {
1597
+ const pool = step.pool;
1598
+ if (!pool || typeof pool !== "object" || Array.isArray(pool) || Object.getPrototypeOf(pool) !== Object.prototype) throw new Error(`Waterfall step '${step.key}' pool must be an object`);
1599
+ const spec = pool;
1600
+ for (const key of Object.keys(spec)) if (![
1601
+ "metric",
1602
+ "residual",
1603
+ "zeroWeight"
1604
+ ].includes(key)) throw new Error(`Waterfall step '${step.key}' pool has an unknown option '${key}'`);
1605
+ if (spec.metric !== void 0 && (typeof spec.metric !== "string" || !spec.metric)) throw new Error(`Waterfall step '${step.key}' pool metric must be a non-empty string`);
1606
+ if (spec.residual !== void 0 && !WATERFALL_POOL_RESIDUAL_RULES.includes(spec.residual)) throw new Error(`Waterfall step '${step.key}' pool residual is invalid`);
1607
+ if (spec.zeroWeight !== void 0 && !WATERFALL_POOL_ZERO_WEIGHT_RULES.includes(spec.zeroWeight)) throw new Error(`Waterfall step '${step.key}' pool zeroWeight is invalid`);
1608
+ if (descriptorMatches(plan.applicationFeeOffsetRecipient, step.recipient)) throw new Error(`Waterfall pool step '${step.key}' cannot be the application fee offset recipient`);
1609
+ }
1266
1610
  function validateEvent(plan, event) {
1267
1611
  if (plan.currency !== event.currency) throw new Error("Waterfall plan and event currency must match; no FX is performed");
1268
1612
  assertWholeNonNegative(event.grossAmountCents, "grossAmountCents");
@@ -1271,70 +1615,6 @@ function assertWholeNonNegative(value, label) {
1271
1615
  if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a non-negative safe integer cents value`);
1272
1616
  }
1273
1617
  //#endregion
1274
- //#region src/commissions/types.ts
1275
- var EARNER_STATUSES = [
1276
- "pending",
1277
- "active",
1278
- "suspended"
1279
- ];
1280
- var EARNER_SOURCE_ATTRIBUTION_STATUSES = ["active", "inactive"];
1281
- var PAYOUT_METHODS = [
1282
- "bank_transfer",
1283
- "check",
1284
- "paypal",
1285
- "credit",
1286
- "other"
1287
- ];
1288
- var COMMISSION_PLAN_STATUSES = [
1289
- "draft",
1290
- "active",
1291
- "superseded",
1292
- "retired"
1293
- ];
1294
- var COMMISSION_STATUSES = [
1295
- "pending",
1296
- "earned",
1297
- "approved",
1298
- "payable",
1299
- "paid"
1300
- ];
1301
- var COMMISSION_BASES = [
1302
- "fixed",
1303
- "gross",
1304
- "net",
1305
- "margin",
1306
- "custom"
1307
- ];
1308
- var COMMISSION_ADJUSTMENT_KINDS = [
1309
- "refund",
1310
- "credit",
1311
- "chargeback",
1312
- "dispute",
1313
- "correction"
1314
- ];
1315
- var COMMISSION_PAYOUT_STATUSES = [
1316
- "pending",
1317
- "approved",
1318
- "processing",
1319
- "completed",
1320
- "failed",
1321
- "rejected"
1322
- ];
1323
- var EARNING_EVENT_KINDS = [
1324
- "conversion",
1325
- "agreement_execution",
1326
- "invoice_payment",
1327
- "collected_revenue",
1328
- "recognized_margin",
1329
- "milestone"
1330
- ];
1331
- var ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES = [
1332
- "earned",
1333
- "approved",
1334
- "payable",
1335
- "paid"
1336
- ];
1337
- //#endregion
1338
1618
  //#region src/commissions/models/CommissionPlan.ts
1339
1619
  var __defProp$3 = Object.defineProperty;
1340
1620
  var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
@@ -1822,8 +2102,39 @@ var __decorateClass$2 = (decorators, target, key, kind) => {
1822
2102
  return result;
1823
2103
  };
1824
2104
  var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
2105
+ function validatePayoutDestination(destination) {
2106
+ const value = destination;
2107
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Earner payoutDestination must be an object");
2108
+ const requireText = (key) => {
2109
+ if (typeof value[key] !== "string" || !value[key].trim()) throw new Error(`Earner payoutDestination.${key} is required`);
2110
+ };
2111
+ let allowed;
2112
+ if (value.method === "stripe_connect") {
2113
+ allowed = ["method", "connectedAccountId"];
2114
+ requireText("connectedAccountId");
2115
+ if (!/^acct_[A-Za-z0-9]+$/.test(value.connectedAccountId)) throw new Error("Earner payoutDestination.connectedAccountId must be a Stripe connected account id ('acct_…')");
2116
+ } else if (value.method === "crypto") {
2117
+ allowed = [
2118
+ "method",
2119
+ "network",
2120
+ "asset",
2121
+ "address",
2122
+ "memo"
2123
+ ];
2124
+ requireText("network");
2125
+ requireText("asset");
2126
+ requireText("address");
2127
+ if (value.memo !== void 0 && typeof value.memo !== "string") throw new Error("Earner payoutDestination.memo must be a string");
2128
+ } else throw new Error(`Earner payoutDestination.method must be one of ${PAYOUT_METHODS_REQUIRING_DESTINATION.join(", ")}`);
2129
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new Error(`Earner payoutDestination has an unexpected field '${key}'`);
2130
+ }
1825
2131
  var Earner = class extends SmrtObject {
1826
2132
  tenantId = null;
2133
+ /**
2134
+ * Party kind: `person` (default), `organisation`, or `treasury`.
2135
+ * Organisation and treasury earners need no person Profile.
2136
+ */
2137
+ kind = "person";
1827
2138
  profileId = "";
1828
2139
  /** Human-readable display name for portals and operator views. */
1829
2140
  displayName = "";
@@ -1832,6 +2143,13 @@ var Earner = class extends SmrtObject {
1832
2143
  /** Preferred payout delivery method. */
1833
2144
  payoutMethod = "bank_transfer";
1834
2145
  /**
2146
+ * Typed payout-rail descriptor as a JSON string (`{}` when none). Required
2147
+ * for an active earner whose method is `stripe_connect` or `crypto`, and
2148
+ * its `method` must match {@link payoutMethod}. Use
2149
+ * {@link getPayoutDestination}/{@link setPayoutDestination}.
2150
+ */
2151
+ payoutDestination = "{}";
2152
+ /**
1835
2153
  * Minimum unsettled balance (integer cents) before a payout batch is
1836
2154
  * created. Default $50.00 = 5000 cents.
1837
2155
  */
@@ -1852,10 +2170,12 @@ var Earner = class extends SmrtObject {
1852
2170
  constructor(options = {}) {
1853
2171
  super(options);
1854
2172
  if (options.tenantId !== void 0) this.tenantId = options.tenantId;
2173
+ if (options.kind !== void 0) this.kind = options.kind;
1855
2174
  if (options.profileId !== void 0) this.profileId = options.profileId;
1856
2175
  if (options.displayName !== void 0) this.displayName = options.displayName;
1857
2176
  if (options.status !== void 0) this.status = options.status;
1858
2177
  if (options.payoutMethod !== void 0) this.payoutMethod = options.payoutMethod;
2178
+ if (options.payoutDestination !== void 0) this.payoutDestination = options.payoutDestination;
1859
2179
  if (options.payoutThresholdCents !== void 0) this.payoutThresholdCents = options.payoutThresholdCents;
1860
2180
  if (options.payoutScheduleKey !== void 0) this.payoutScheduleKey = options.payoutScheduleKey;
1861
2181
  if (options.currency !== void 0) this.currency = options.currency;
@@ -1884,6 +2204,60 @@ var Earner = class extends SmrtObject {
1884
2204
  setMetadata(data) {
1885
2205
  this.metadata = JSON.stringify(data ?? {});
1886
2206
  }
2207
+ isOrganisation() {
2208
+ return this.kind === "organisation";
2209
+ }
2210
+ isTreasury() {
2211
+ return this.kind === "treasury";
2212
+ }
2213
+ /** Parse {@link payoutDestination}; `null` when none or unparseable. */
2214
+ getPayoutDestination() {
2215
+ try {
2216
+ const parsed = JSON.parse(this.payoutDestination || "{}");
2217
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) && Object.keys(parsed).length > 0 ? parsed : null;
2218
+ } catch {
2219
+ return null;
2220
+ }
2221
+ }
2222
+ /**
2223
+ * Validate and store a payout destination, and set {@link payoutMethod}
2224
+ * to its method. `null` clears it (the method is left unchanged).
2225
+ */
2226
+ setPayoutDestination(destination) {
2227
+ if (destination === null) {
2228
+ this.payoutDestination = "{}";
2229
+ return;
2230
+ }
2231
+ validatePayoutDestination(destination);
2232
+ this.payoutDestination = JSON.stringify(destination);
2233
+ this.payoutMethod = destination.method;
2234
+ }
2235
+ /**
2236
+ * Save with field guards: `kind` and `payoutMethod` must be known values,
2237
+ * a stored destination must be valid and match `payoutMethod`, and an
2238
+ * active Stripe Connect / crypto earner must have a destination.
2239
+ */
2240
+ async save() {
2241
+ this.assertPayoutTerms();
2242
+ return await super.save();
2243
+ }
2244
+ assertPayoutTerms() {
2245
+ const label = `Earner ${this.id ?? "<new>"}`;
2246
+ if (!EARNER_KINDS.includes(this.kind)) throw new Error(`${label}: unknown kind '${this.kind}'`);
2247
+ if (!PAYOUT_METHODS.includes(this.payoutMethod)) throw new Error(`${label}: unknown payoutMethod '${this.payoutMethod}'`);
2248
+ let parsed;
2249
+ try {
2250
+ parsed = JSON.parse(this.payoutDestination || "{}");
2251
+ } catch {
2252
+ throw new Error(`${label}: payoutDestination must be valid JSON`);
2253
+ }
2254
+ const hasDestination = !!parsed && typeof parsed === "object" && !Array.isArray(parsed) && Object.keys(parsed).length > 0;
2255
+ if (hasDestination) {
2256
+ validatePayoutDestination(parsed);
2257
+ if (parsed.method !== this.payoutMethod) throw new Error(`${label}: payoutDestination method '${parsed.method}' does not match payoutMethod '${this.payoutMethod}'`);
2258
+ } else if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${label}: payoutDestination must be a JSON object`);
2259
+ if (this.status === "active" && !hasDestination && PAYOUT_METHODS_REQUIRING_DESTINATION.includes(this.payoutMethod)) throw new Error(`${label}: an active ${this.payoutMethod} earner requires a payoutDestination`);
2260
+ }
1887
2261
  };
1888
2262
  __publicField$2(Earner, "__smrtPackage__", "@happyvertical/smrt-sales");
1889
2263
  __decorateClass$2([tenantId({ nullable: true })], Earner.prototype, "tenantId", 2);
@@ -2927,6 +3301,8 @@ var CommissionPayoutService = class CommissionPayoutService {
2927
3301
  settledAdjustmentIds: []
2928
3302
  };
2929
3303
  }
3304
+ const payoutMethod = input.payoutMethod ?? earner.payoutMethod;
3305
+ if (PAYOUT_METHODS_REQUIRING_DESTINATION.includes(payoutMethod) && earner.getPayoutDestination()?.method !== payoutMethod) throw new Error(`CommissionPayoutService: earner '${input.earnerId}' has no ${payoutMethod} payoutDestination`);
2930
3306
  const commissions = await this.gatherBatchCommissions(input);
2931
3307
  const eligibleAdjustments = await this.findEligibleUnsettledAdjustments(input.earnerId, input.currency, CommissionPayoutService.adjustmentParentPredicate(input));
2932
3308
  const commissionTotalCents = commissions.reduce((sum, c) => sum + c.amountCents, 0);
@@ -2952,7 +3328,7 @@ var CommissionPayoutService = class CommissionPayoutService {
2952
3328
  currency: input.currency,
2953
3329
  periodStart: input.periodStart ?? null,
2954
3330
  periodEnd: input.periodEnd ?? null,
2955
- payoutMethod: input.payoutMethod ?? earner.payoutMethod,
3331
+ payoutMethod,
2956
3332
  status: "pending",
2957
3333
  commissionTotalCents,
2958
3334
  adjustmentTotalCents,
@@ -3960,6 +4336,6 @@ var EarnerAttributionService = class EarnerAttributionService {
3960
4336
  }
3961
4337
  };
3962
4338
  //#endregion
3963
- export { EARNING_EVENT_KINDS as A, CommissionAdjustmentOperation as B, COMMISSION_ADJUSTMENT_KINDS as C, COMMISSION_STATUSES as D, COMMISSION_PLAN_STATUSES as E, CommissionPayoutCollection as F, CommissionAdjustment as H, CommissionPayout as I, CommissionCollection as L, WaterfallRecipientResolverRegistry as M, calculateWaterfall as N, EARNER_SOURCE_ATTRIBUTION_STATUSES as O, validateWaterfallPlan as P, Commission as R, ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES as S, COMMISSION_PAYOUT_STATUSES as T, CommissionAdjustmentCollection as V, EarnerCollection as _, CommissionBalanceService as a, CommissionPlan as b, CommissionAdjustmentValidationError as c, centsToAmount as d, roundCents as f, EarnerSourceAttribution as g, EarnerSourceAttributionCollection as h, CommissionCalculationService as i, PAYOUT_METHODS as j, EARNER_STATUSES as k, amountToCents as l, EarningEvent as m, CommissionSettlementService as n, CommissionAdjustmentReplayConflictError as o, EarningEventCollection as p, CommissionPayoutService as r, CommissionAdjustmentService as s, EarnerAttributionService as t, calculateCommissionAmountCents as u, Earner as v, COMMISSION_BASES as w, validateCommissionPlanComponents as x, CommissionPlanCollection as y, CommissionAdjustmentOperationCollection as z };
4339
+ export { COMMISSION_BASES as A, WATERFALL_POOL_RESIDUAL_RULES as B, WaterfallRecipientResolverRegistry as C, validateWaterfallPlan as D, prepareWaterfallPools as E, EARNER_SOURCE_ATTRIBUTION_STATUSES as F, Commission as G, CommissionPayoutCollection as H, EARNER_STATUSES as I, CommissionAdjustmentCollection as J, CommissionAdjustmentOperationCollection as K, EARNING_EVENT_KINDS as L, COMMISSION_PLAN_STATUSES as M, COMMISSION_STATUSES as N, ADJUSTMENT_SETTLEABLE_COMMISSION_STATUSES as O, EARNER_KINDS as P, PAYOUT_METHODS as R, validateCommissionPlanComponents as S, calculateWaterfallWithPools as T, CommissionPayout as U, WATERFALL_POOL_ZERO_WEIGHT_RULES as V, CommissionCollection as W, CommissionAdjustment as Y, EarnerCollection as _, CommissionBalanceService as a, CommissionPlanCollection as b, CommissionAdjustmentValidationError as c, centsToAmount as d, roundCents as f, EarnerSourceAttribution as g, EarnerSourceAttributionCollection as h, CommissionCalculationService as i, COMMISSION_PAYOUT_STATUSES as j, COMMISSION_ADJUSTMENT_KINDS as k, amountToCents as l, EarningEvent as m, CommissionSettlementService as n, CommissionAdjustmentReplayConflictError as o, EarningEventCollection as p, CommissionAdjustmentOperation as q, CommissionPayoutService as r, CommissionAdjustmentService as s, EarnerAttributionService as t, calculateCommissionAmountCents as u, Earner as v, calculateWaterfall as w, CommissionPlan as x, validatePayoutDestination as y, PAYOUT_METHODS_REQUIRING_DESTINATION as z };
3964
4340
 
3965
- //# sourceMappingURL=commissions-Cip6QZ9Q.js.map
4341
+ //# sourceMappingURL=commissions-D5x24d4o.js.map