@happyvertical/smrt-sales 0.57.9 → 0.57.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/agents/commissions.md +2 -1
- package/dist/agreements.js +1 -1
- package/dist/chunks/{__smrt-register__-Czzs3Vof.js → __smrt-register__-uEzmP44h.js} +2 -2
- package/dist/chunks/__smrt-register__-uEzmP44h.js.map +1 -0
- package/dist/chunks/{commissions-Cip6QZ9Q.js → commissions-D5x24d4o.js} +443 -67
- package/dist/chunks/commissions-D5x24d4o.js.map +1 -0
- package/dist/chunks/{referrals-CYgrGkEo.js → referrals-BRdTkEFt.js} +2 -2
- package/dist/chunks/{referrals-CYgrGkEo.js.map → referrals-BRdTkEFt.js.map} +1 -1
- package/dist/commissions/models/Earner.d.ts +41 -1
- package/dist/commissions/models/Earner.d.ts.map +1 -1
- package/dist/commissions/models/index.d.ts +1 -1
- package/dist/commissions/models/index.d.ts.map +1 -1
- package/dist/commissions/services/CommissionPayoutService.d.ts.map +1 -1
- package/dist/commissions/services/WaterfallCalculationService.d.ts +33 -1
- package/dist/commissions/services/WaterfallCalculationService.d.ts.map +1 -1
- package/dist/commissions/services/index.d.ts +1 -1
- package/dist/commissions/services/index.d.ts.map +1 -1
- package/dist/commissions/types.d.ts +169 -1
- package/dist/commissions/types.d.ts.map +1 -1
- package/dist/commissions.js +3 -3
- package/dist/crm.js +1 -1
- package/dist/index.js +4 -4
- package/dist/manifest.json +93 -5
- package/dist/referrals.js +2 -2
- package/dist/smrt-knowledge.json +132 -6
- package/package.json +8 -8
- package/dist/chunks/__smrt-register__-Czzs3Vof.js.map +0 -1
- package/dist/chunks/commissions-Cip6QZ9Q.js.map +0 -1
|
@@ -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
|
|
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 {
|
|
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-
|
|
4341
|
+
//# sourceMappingURL=commissions-D5x24d4o.js.map
|