@happyvertical/smrt-sales 0.57.1 → 0.57.3
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 +70 -2
- package/agents/commissions.md +1 -1
- package/agents/referrals.md +1 -0
- package/dist/agreements/collections/AgreementCollection.d.ts +50 -0
- package/dist/agreements/collections/AgreementCollection.d.ts.map +1 -0
- package/dist/agreements/index.d.ts +2 -0
- package/dist/agreements/index.d.ts.map +1 -1
- package/dist/agreements/models/Agreement.d.ts +67 -0
- package/dist/agreements/models/Agreement.d.ts.map +1 -0
- package/dist/agreements/types.d.ts +32 -0
- package/dist/agreements/types.d.ts.map +1 -1
- package/dist/agreements.d.ts +2 -2
- package/dist/agreements.js +3 -3
- package/dist/chunks/__smrt-register__-DTUQW7wz.js +6 -0
- package/dist/chunks/__smrt-register__-DTUQW7wz.js.map +1 -0
- package/dist/chunks/{agreements-BB_tNU0U.js → agreements-jBiVmttD.js} +450 -4
- package/dist/chunks/agreements-jBiVmttD.js.map +1 -0
- package/dist/chunks/{commissions-Cs4MsxVg.js → commissions-Cip6QZ9Q.js} +350 -6
- package/dist/chunks/commissions-Cip6QZ9Q.js.map +1 -0
- package/dist/chunks/{referrals-CrFDsdq-.js → referrals-CYgrGkEo.js} +17 -3
- package/dist/chunks/{referrals-CrFDsdq-.js.map → referrals-CYgrGkEo.js.map} +1 -1
- package/dist/commissions/collections/CommissionAdjustmentOperationCollection.d.ts.map +1 -1
- package/dist/commissions/collections/CommissionPlanCollection.d.ts +5 -1
- package/dist/commissions/collections/CommissionPlanCollection.d.ts.map +1 -1
- package/dist/commissions/models/CommissionPlan.d.ts +20 -1
- package/dist/commissions/models/CommissionPlan.d.ts.map +1 -1
- package/dist/commissions/services/WaterfallCalculationService.d.ts +22 -0
- package/dist/commissions/services/WaterfallCalculationService.d.ts.map +1 -0
- package/dist/commissions/services/index.d.ts +1 -0
- package/dist/commissions/services/index.d.ts.map +1 -1
- package/dist/commissions/types.d.ts +100 -0
- package/dist/commissions/types.d.ts.map +1 -1
- package/dist/commissions.js +3 -3
- package/dist/crm.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/manifest.json +1399 -514
- package/dist/referrals/services/ReferralQualificationService.d.ts +4 -3
- package/dist/referrals/services/ReferralQualificationService.d.ts.map +1 -1
- package/dist/referrals.js +2 -2
- package/dist/smrt-knowledge.json +985 -34
- package/package.json +6 -6
- package/dist/chunks/__smrt-register__-BnG4rTI9.js +0 -6
- package/dist/chunks/__smrt-register__-BnG4rTI9.js.map +0 -1
- package/dist/chunks/agreements-BB_tNU0U.js.map +0 -1
- package/dist/chunks/commissions-Cs4MsxVg.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SmrtCollection, SmrtObject, crossPackageRef, field, foreignKey, smrt } from "@happyvertical/smrt-core";
|
|
2
2
|
import { TenantContextError, TenantScoped, assertTenantReadAllowed, getCurrentTenant, requireTenantId, tenantId } from "@happyvertical/smrt-tenancy";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
5
|
//#region src/commissions/models/CommissionAdjustment.ts
|
|
5
6
|
var __defProp$7 = Object.defineProperty;
|
|
6
7
|
var __getOwnPropDesc$7 = Object.getOwnPropertyDescriptor;
|
|
@@ -265,7 +266,7 @@ var CommissionAdjustmentOperationCollection = class extends SmrtCollection {
|
|
|
265
266
|
const inserted = await this.query(`INSERT INTO ${this.tableName} (
|
|
266
267
|
id, slug, context, tenant_id, adjustment_id
|
|
267
268
|
) VALUES (?, ?, ?, ?, ?)
|
|
268
|
-
ON CONFLICT
|
|
269
|
+
ON CONFLICT DO NOTHING
|
|
269
270
|
RETURNING id`, [
|
|
270
271
|
input.operationId,
|
|
271
272
|
input.operationId,
|
|
@@ -1047,6 +1048,229 @@ var CommissionPayoutCollection = class extends SmrtCollection {
|
|
|
1047
1048
|
}
|
|
1048
1049
|
};
|
|
1049
1050
|
//#endregion
|
|
1051
|
+
//#region src/commissions/services/WaterfallCalculationService.ts
|
|
1052
|
+
var WaterfallRecipientResolverRegistry = class {
|
|
1053
|
+
resolvers = /* @__PURE__ */ new Map();
|
|
1054
|
+
register(kind, resolver) {
|
|
1055
|
+
if (!kind) throw new Error("Waterfall recipient resolver kind is required");
|
|
1056
|
+
if (this.resolvers.has(kind)) throw new Error(`Waterfall recipient resolver '${kind}' is already registered`);
|
|
1057
|
+
this.resolvers.set(kind, resolver);
|
|
1058
|
+
}
|
|
1059
|
+
resolve(descriptor, event) {
|
|
1060
|
+
return this.resolvers.get(descriptor.kind)?.(descriptor, event) ?? null;
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
function calculateWaterfall(plan, event, options = {}) {
|
|
1064
|
+
validateWaterfallPlan(plan);
|
|
1065
|
+
const planFingerprint = fingerprintPlan(plan);
|
|
1066
|
+
const calculationEvent = options.frozenInputs ? {
|
|
1067
|
+
grossAmountCents: options.frozenInputs.grossAmountCents,
|
|
1068
|
+
currency: options.frozenInputs.currency,
|
|
1069
|
+
facts: options.frozenInputs.facts,
|
|
1070
|
+
customBases: options.frozenInputs.customBases,
|
|
1071
|
+
applicationFeeSettledCents: options.frozenInputs.applicationFeeSettledCents
|
|
1072
|
+
} : event;
|
|
1073
|
+
validateEvent(plan, calculationEvent);
|
|
1074
|
+
if (options.frozenInputs && (options.frozenInputs.planKey !== plan.planKey || options.frozenInputs.planVersion !== plan.version || options.frozenInputs.planFingerprint !== planFingerprint)) throw new Error("Waterfall frozen inputs belong to a different plan version");
|
|
1075
|
+
const facts = options.frozenInputs?.facts ?? event.facts ?? {};
|
|
1076
|
+
const frozenInputs = {
|
|
1077
|
+
planKey: options.frozenInputs?.planKey ?? plan.planKey,
|
|
1078
|
+
planVersion: options.frozenInputs?.planVersion ?? plan.version,
|
|
1079
|
+
planFingerprint: options.frozenInputs?.planFingerprint ?? planFingerprint,
|
|
1080
|
+
currency: options.frozenInputs?.currency ?? event.currency,
|
|
1081
|
+
grossAmountCents: options.frozenInputs?.grossAmountCents ?? event.grossAmountCents,
|
|
1082
|
+
facts: { ...facts },
|
|
1083
|
+
customBases: { ...options.frozenInputs?.customBases ?? event.customBases },
|
|
1084
|
+
recipientByStep: Object.assign(/* @__PURE__ */ Object.create(null), Object.fromEntries(Object.entries(options.frozenInputs?.recipientByStep ?? {}).map(([key, recipient]) => [key, recipient ? freezeRecipient(recipient) : null]))),
|
|
1085
|
+
remainderSink: options.frozenInputs ? freezeRecipient(options.frozenInputs.remainderSink) : resolveSink(plan, calculationEvent, options),
|
|
1086
|
+
applicationFeeSettledCents: options.frozenInputs?.applicationFeeSettledCents ?? event.applicationFeeSettledCents ?? 0
|
|
1087
|
+
};
|
|
1088
|
+
assertWholeNonNegative(frozenInputs.applicationFeeSettledCents, "applicationFeeSettledCents");
|
|
1089
|
+
let remainingCents = calculationEvent.grossAmountCents;
|
|
1090
|
+
let redirectedToSinkCents = 0;
|
|
1091
|
+
let totalRecipientAllocatedCents = 0;
|
|
1092
|
+
let remainingFeeOffsetCents = frozenInputs.applicationFeeSettledCents;
|
|
1093
|
+
const steps = [];
|
|
1094
|
+
for (const step of plan.steps) {
|
|
1095
|
+
if (!conditionsMatch(step.conditions, facts)) {
|
|
1096
|
+
steps.push({
|
|
1097
|
+
key: step.key,
|
|
1098
|
+
baseCents: 0,
|
|
1099
|
+
amountCents: 0,
|
|
1100
|
+
recipient: null,
|
|
1101
|
+
redirectedToSinkCents: 0,
|
|
1102
|
+
applicationFeeOffsetCents: 0,
|
|
1103
|
+
payableCents: 0,
|
|
1104
|
+
skipped: "condition_not_met"
|
|
1105
|
+
});
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const baseCents = resolveBase(step, calculationEvent, remainingCents);
|
|
1109
|
+
const amountCents = resolveAmount(step, baseCents);
|
|
1110
|
+
if (amountCents > remainingCents) throw new Error(`Waterfall step '${step.key}' allocates ${amountCents} cents but only ${remainingCents} cents remain`);
|
|
1111
|
+
const recipient = resolveStepRecipient(step, calculationEvent, options, frozenInputs);
|
|
1112
|
+
let applicationFeeOffsetCents = 0;
|
|
1113
|
+
if (recipient && descriptorMatches(plan.applicationFeeOffsetRecipient, step.recipient)) {
|
|
1114
|
+
applicationFeeOffsetCents = Math.min(remainingFeeOffsetCents, amountCents);
|
|
1115
|
+
remainingFeeOffsetCents -= applicationFeeOffsetCents;
|
|
1116
|
+
}
|
|
1117
|
+
remainingCents -= amountCents;
|
|
1118
|
+
if (recipient) totalRecipientAllocatedCents += amountCents;
|
|
1119
|
+
else redirectedToSinkCents += amountCents;
|
|
1120
|
+
steps.push({
|
|
1121
|
+
key: step.key,
|
|
1122
|
+
baseCents,
|
|
1123
|
+
amountCents,
|
|
1124
|
+
recipient,
|
|
1125
|
+
redirectedToSinkCents: recipient ? 0 : amountCents,
|
|
1126
|
+
applicationFeeOffsetCents,
|
|
1127
|
+
payableCents: recipient ? amountCents - applicationFeeOffsetCents : 0
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
if (remainingFeeOffsetCents !== 0) throw new Error("Waterfall application fee offset has no resolved configured recipient allocation");
|
|
1131
|
+
const totalSinkCents = remainingCents + redirectedToSinkCents;
|
|
1132
|
+
const closure = totalRecipientAllocatedCents + totalSinkCents - calculationEvent.grossAmountCents;
|
|
1133
|
+
if (closure !== 0) throw new Error(`Waterfall closure failed by ${closure} cents`);
|
|
1134
|
+
return { trace: {
|
|
1135
|
+
planKey: plan.planKey,
|
|
1136
|
+
planVersion: plan.version,
|
|
1137
|
+
currency: plan.currency,
|
|
1138
|
+
grossAmountCents: calculationEvent.grossAmountCents,
|
|
1139
|
+
steps,
|
|
1140
|
+
remainderSink: frozenInputs.remainderSink,
|
|
1141
|
+
remainderSinkCents: totalSinkCents,
|
|
1142
|
+
totalRecipientAllocatedCents,
|
|
1143
|
+
totalSinkCents,
|
|
1144
|
+
closureCents: 0,
|
|
1145
|
+
roundingMode: "half_away_from_zero",
|
|
1146
|
+
frozenInputs
|
|
1147
|
+
} };
|
|
1148
|
+
}
|
|
1149
|
+
function resolveSink(plan, event, options) {
|
|
1150
|
+
const sink = options.resolver?.resolve(plan.remainderSink, event) ?? null;
|
|
1151
|
+
if (!sink) throw new Error("Waterfall remainderSink must resolve to a recipient");
|
|
1152
|
+
return freezeRecipient(sink);
|
|
1153
|
+
}
|
|
1154
|
+
function resolveStepRecipient(step, event, options, frozen) {
|
|
1155
|
+
if (Object.hasOwn(frozen.recipientByStep, step.key)) {
|
|
1156
|
+
const existing = frozen.recipientByStep[step.key];
|
|
1157
|
+
return existing ? freezeRecipient(existing) : null;
|
|
1158
|
+
}
|
|
1159
|
+
const recipient = options.resolver?.resolve(step.recipient, event) ?? null;
|
|
1160
|
+
const frozenRecipient = recipient ? freezeRecipient(recipient) : null;
|
|
1161
|
+
Object.defineProperty(frozen.recipientByStep, step.key, {
|
|
1162
|
+
value: frozenRecipient,
|
|
1163
|
+
enumerable: true,
|
|
1164
|
+
writable: true,
|
|
1165
|
+
configurable: true
|
|
1166
|
+
});
|
|
1167
|
+
return frozenRecipient;
|
|
1168
|
+
}
|
|
1169
|
+
function freezeRecipient(recipient) {
|
|
1170
|
+
return {
|
|
1171
|
+
kind: recipient.kind,
|
|
1172
|
+
id: recipient.id,
|
|
1173
|
+
...recipient.metadata ? { metadata: structuredClone(recipient.metadata) } : {}
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
function resolveBase(step, event, remainderCents) {
|
|
1177
|
+
if (step.base === "gross") return event.grossAmountCents;
|
|
1178
|
+
if (step.base === "previous_remainder") return remainderCents;
|
|
1179
|
+
const value = event.customBases?.[step.customBaseKey ?? ""];
|
|
1180
|
+
if (value === void 0) throw new Error(`Waterfall step '${step.key}' requires custom base '${step.customBaseKey ?? ""}'`);
|
|
1181
|
+
assertWholeNonNegative(value, `custom base '${step.customBaseKey}'`);
|
|
1182
|
+
return value;
|
|
1183
|
+
}
|
|
1184
|
+
function resolveAmount(step, baseCents) {
|
|
1185
|
+
const raw = step.amount.kind === "percent" ? roundPercentCents(baseCents, step.amount.rate) : step.amount.amountCents;
|
|
1186
|
+
return Math.min(step.capCents ?? Number.MAX_SAFE_INTEGER, Math.max(step.floorCents ?? 0, raw));
|
|
1187
|
+
}
|
|
1188
|
+
function roundPercentCents(baseCents, rate) {
|
|
1189
|
+
const [numerator, denominator] = decimalFraction(rate);
|
|
1190
|
+
const rounded = (BigInt(baseCents) * numerator + denominator / 2n) / denominator;
|
|
1191
|
+
if (rounded > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Waterfall percent allocation exceeds safe integer cents");
|
|
1192
|
+
return Number(rounded);
|
|
1193
|
+
}
|
|
1194
|
+
function decimalFraction(value) {
|
|
1195
|
+
const [coefficient, exponentText] = value.toString().toLowerCase().split("e");
|
|
1196
|
+
const exponent = exponentText ? Number(exponentText) : 0;
|
|
1197
|
+
const [whole, fractional = ""] = coefficient.split(".");
|
|
1198
|
+
const digits = `${whole}${fractional}`;
|
|
1199
|
+
const scale = fractional.length - exponent;
|
|
1200
|
+
if (scale <= 0) return [BigInt(digits) * 10n ** BigInt(-scale), 1n];
|
|
1201
|
+
return [BigInt(digits), 10n ** BigInt(scale)];
|
|
1202
|
+
}
|
|
1203
|
+
function fingerprintPlan(plan) {
|
|
1204
|
+
return JSON.stringify({
|
|
1205
|
+
tenantId: plan.tenantId ?? null,
|
|
1206
|
+
planKey: plan.planKey,
|
|
1207
|
+
version: plan.version,
|
|
1208
|
+
currency: plan.currency,
|
|
1209
|
+
steps: plan.steps,
|
|
1210
|
+
remainderSink: plan.remainderSink,
|
|
1211
|
+
applicationFeeOffsetRecipient: plan.applicationFeeOffsetRecipient ?? null
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
function conditionsMatch(conditions, facts) {
|
|
1215
|
+
if (!conditions) return true;
|
|
1216
|
+
return [
|
|
1217
|
+
"product",
|
|
1218
|
+
"zone",
|
|
1219
|
+
"source",
|
|
1220
|
+
"member"
|
|
1221
|
+
].every((key) => conditions[key] === void 0 || conditions[key].includes(facts[key] ?? ""));
|
|
1222
|
+
}
|
|
1223
|
+
function descriptorMatches(a, b) {
|
|
1224
|
+
return a !== void 0 && isDeepStrictEqual(a, b);
|
|
1225
|
+
}
|
|
1226
|
+
function validateWaterfallPlan(plan) {
|
|
1227
|
+
if (!plan.planKey || !Number.isInteger(plan.version) || plan.version < 1) throw new Error("Waterfall plan requires a planKey and positive integer version");
|
|
1228
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1229
|
+
for (const step of plan.steps) {
|
|
1230
|
+
if (typeof step.key !== "string" || !step.key || keys.has(step.key)) throw new Error(`Waterfall step keys must be non-empty strings and unique: '${step.key}'`);
|
|
1231
|
+
keys.add(step.key);
|
|
1232
|
+
if ([
|
|
1233
|
+
"__proto__",
|
|
1234
|
+
"constructor",
|
|
1235
|
+
"prototype"
|
|
1236
|
+
].includes(step.key)) throw new Error(`Waterfall step '${step.key}' uses a reserved key`);
|
|
1237
|
+
if (!step.recipient?.kind) throw new Error(`Waterfall step '${step.key}' requires a recipient descriptor`);
|
|
1238
|
+
if (step.base === "custom" && !step.customBaseKey) throw new Error(`Waterfall step '${step.key}' requires customBaseKey for custom base`);
|
|
1239
|
+
if (![
|
|
1240
|
+
"gross",
|
|
1241
|
+
"previous_remainder",
|
|
1242
|
+
"custom"
|
|
1243
|
+
].includes(step.base)) throw new Error(`Waterfall step '${step.key}' has an invalid base`);
|
|
1244
|
+
if (!["percent", "fixed"].includes(step.amount.kind)) throw new Error(`Waterfall step '${step.key}' has an invalid amount kind`);
|
|
1245
|
+
if (step.conditions !== void 0) {
|
|
1246
|
+
if (!step.conditions || typeof step.conditions !== "object" || Array.isArray(step.conditions)) throw new Error(`Waterfall step '${step.key}' conditions must be an object`);
|
|
1247
|
+
for (const [key, values] of Object.entries(step.conditions)) {
|
|
1248
|
+
if (![
|
|
1249
|
+
"product",
|
|
1250
|
+
"zone",
|
|
1251
|
+
"source",
|
|
1252
|
+
"member"
|
|
1253
|
+
].includes(key)) throw new Error(`Waterfall step '${step.key}' has an unknown condition '${key}'`);
|
|
1254
|
+
if (!Array.isArray(values) || values.some((value) => typeof value !== "string" || !value)) throw new Error(`Waterfall step '${step.key}' condition '${key}' must be an array of non-empty strings`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (step.amount.kind === "percent" && (!Number.isFinite(step.amount.rate) || step.amount.rate < 0 || step.amount.rate > 1)) throw new Error(`Waterfall step '${step.key}' percent rate must be in [0, 1]`);
|
|
1258
|
+
if (step.amount.kind === "fixed") assertWholeNonNegative(step.amount.amountCents, `Waterfall step '${step.key}' fixed amount`);
|
|
1259
|
+
if (step.floorCents !== void 0) assertWholeNonNegative(step.floorCents, `Waterfall step '${step.key}' floor`);
|
|
1260
|
+
if (step.capCents !== void 0) assertWholeNonNegative(step.capCents, `Waterfall step '${step.key}' cap`);
|
|
1261
|
+
if (step.floorCents !== void 0 && step.capCents !== void 0 && step.floorCents > step.capCents) throw new Error(`Waterfall step '${step.key}' floor cannot exceed cap`);
|
|
1262
|
+
}
|
|
1263
|
+
if (!plan.remainderSink?.kind) throw new Error("Waterfall plan requires a remainderSink descriptor");
|
|
1264
|
+
if (plan.applicationFeeOffsetRecipient && !plan.applicationFeeOffsetRecipient.kind) throw new Error("Waterfall application fee offset requires a recipient descriptor");
|
|
1265
|
+
}
|
|
1266
|
+
function validateEvent(plan, event) {
|
|
1267
|
+
if (plan.currency !== event.currency) throw new Error("Waterfall plan and event currency must match; no FX is performed");
|
|
1268
|
+
assertWholeNonNegative(event.grossAmountCents, "grossAmountCents");
|
|
1269
|
+
}
|
|
1270
|
+
function assertWholeNonNegative(value, label) {
|
|
1271
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a non-negative safe integer cents value`);
|
|
1272
|
+
}
|
|
1273
|
+
//#endregion
|
|
1050
1274
|
//#region src/commissions/types.ts
|
|
1051
1275
|
var EARNER_STATUSES = [
|
|
1052
1276
|
"pending",
|
|
@@ -1174,6 +1398,8 @@ var CommissionPlan = class extends SmrtObject {
|
|
|
1174
1398
|
status = "draft";
|
|
1175
1399
|
/** When this version takes effect. Frozen once the plan activates. */
|
|
1176
1400
|
effectiveFrom = null;
|
|
1401
|
+
/** Optional exclusive end of this version's effective period. */
|
|
1402
|
+
effectiveTo = null;
|
|
1177
1403
|
/** ISO 4217 currency the plan's terms are denominated in. */
|
|
1178
1404
|
currency = "USD";
|
|
1179
1405
|
/**
|
|
@@ -1182,6 +1408,12 @@ var CommissionPlan = class extends SmrtObject {
|
|
|
1182
1408
|
* {@link setComponents} (the setter validates).
|
|
1183
1409
|
*/
|
|
1184
1410
|
components = "[]";
|
|
1411
|
+
/** Ordered v2 waterfall steps. Empty keeps the v1 component contract. */
|
|
1412
|
+
steps = "[]";
|
|
1413
|
+
/** V2 residual recipient descriptor. */
|
|
1414
|
+
remainderSink = "{}";
|
|
1415
|
+
/** V2 checkout-fee offset recipient descriptor. */
|
|
1416
|
+
applicationFeeOffsetRecipient = "{}";
|
|
1185
1417
|
/** Additional metadata as a JSON string. */
|
|
1186
1418
|
metadata = "{}";
|
|
1187
1419
|
constructor(options = {}) {
|
|
@@ -1193,8 +1425,12 @@ var CommissionPlan = class extends SmrtObject {
|
|
|
1193
1425
|
if (options.description !== void 0) this.description = options.description;
|
|
1194
1426
|
if (options.status !== void 0) this.status = options.status;
|
|
1195
1427
|
if (options.effectiveFrom !== void 0) this.effectiveFrom = CommissionPlan.coerceDate(options.effectiveFrom);
|
|
1428
|
+
if (options.effectiveTo !== void 0) this.effectiveTo = CommissionPlan.coerceDate(options.effectiveTo);
|
|
1196
1429
|
if (options.currency !== void 0) this.currency = options.currency;
|
|
1197
1430
|
if (options.components !== void 0) this.components = options.components;
|
|
1431
|
+
if (options.steps !== void 0) this.steps = options.steps;
|
|
1432
|
+
if (options.remainderSink !== void 0) this.remainderSink = options.remainderSink;
|
|
1433
|
+
if (options.applicationFeeOffsetRecipient !== void 0) this.applicationFeeOffsetRecipient = options.applicationFeeOffsetRecipient;
|
|
1198
1434
|
if (options.metadata !== void 0) this.metadata = options.metadata;
|
|
1199
1435
|
}
|
|
1200
1436
|
/**
|
|
@@ -1207,6 +1443,7 @@ var CommissionPlan = class extends SmrtObject {
|
|
|
1207
1443
|
async initialize() {
|
|
1208
1444
|
await super.initialize();
|
|
1209
1445
|
this.effectiveFrom = CommissionPlan.coerceDate(this.effectiveFrom);
|
|
1446
|
+
this.effectiveTo = CommissionPlan.coerceDate(this.effectiveTo);
|
|
1210
1447
|
if (await this.isSaved()) {
|
|
1211
1448
|
loadedPlanStatus.set(this, this.status);
|
|
1212
1449
|
if (this.status !== "draft") frozenPlanSnapshot.set(this, this.serializeFrozenSnapshot());
|
|
@@ -1237,6 +1474,45 @@ var CommissionPlan = class extends SmrtObject {
|
|
|
1237
1474
|
validateCommissionPlanComponents(components);
|
|
1238
1475
|
this.components = JSON.stringify(components);
|
|
1239
1476
|
}
|
|
1477
|
+
getSteps() {
|
|
1478
|
+
try {
|
|
1479
|
+
const parsed = JSON.parse(this.steps);
|
|
1480
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
1481
|
+
} catch {
|
|
1482
|
+
return [];
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
setSteps(steps) {
|
|
1486
|
+
this.steps = JSON.stringify(steps);
|
|
1487
|
+
}
|
|
1488
|
+
getRemainderSink() {
|
|
1489
|
+
try {
|
|
1490
|
+
const parsed = JSON.parse(this.remainderSink);
|
|
1491
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
1492
|
+
} catch {
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
setRemainderSink(sink) {
|
|
1497
|
+
this.remainderSink = JSON.stringify(sink);
|
|
1498
|
+
}
|
|
1499
|
+
/** Return v2 terms when this plan has waterfall steps; v1 plans remain v1. */
|
|
1500
|
+
getWaterfallPlan() {
|
|
1501
|
+
const steps = this.getSteps();
|
|
1502
|
+
const remainderSink = this.getRemainderSink();
|
|
1503
|
+
if (steps.length === 0) return null;
|
|
1504
|
+
if (!remainderSink) throw new Error("CommissionPlan waterfall steps require a remainder sink");
|
|
1505
|
+
const applicationFeeOffsetRecipient = this.getRecipientDescriptor(this.applicationFeeOffsetRecipient);
|
|
1506
|
+
return {
|
|
1507
|
+
planKey: this.planKey,
|
|
1508
|
+
version: this.version,
|
|
1509
|
+
tenantId: this.tenantId,
|
|
1510
|
+
currency: this.currency,
|
|
1511
|
+
steps,
|
|
1512
|
+
remainderSink,
|
|
1513
|
+
...applicationFeeOffsetRecipient ? { applicationFeeOffsetRecipient } : {}
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1240
1516
|
/** Parse {@link metadata}; returns `{}` on empty/invalid JSON. */
|
|
1241
1517
|
getMetadata() {
|
|
1242
1518
|
if (!this.metadata) return {};
|
|
@@ -1258,6 +1534,8 @@ var CommissionPlan = class extends SmrtObject {
|
|
|
1258
1534
|
activate() {
|
|
1259
1535
|
if (this.status !== "draft") throw new Error(`CommissionPlan ${this.planKey}@${this.version}: cannot activate from status '${this.status}'`);
|
|
1260
1536
|
validateCommissionPlanComponents(this.getComponents());
|
|
1537
|
+
this.validateEffectiveWindow();
|
|
1538
|
+
this.validateWaterfallTerms();
|
|
1261
1539
|
this.status = "active";
|
|
1262
1540
|
}
|
|
1263
1541
|
/** Transition `active → superseded` (a newer version took over). */
|
|
@@ -1290,7 +1568,11 @@ var CommissionPlan = class extends SmrtObject {
|
|
|
1290
1568
|
this.assertStatusTransition(prior);
|
|
1291
1569
|
this.assertFrozenIdentityUnchanged();
|
|
1292
1570
|
await this.assertNaturalKeyNotTaken();
|
|
1293
|
-
if (this.status === "active")
|
|
1571
|
+
if (this.status === "active") {
|
|
1572
|
+
validateCommissionPlanComponents(this.getComponents());
|
|
1573
|
+
this.validateEffectiveWindow();
|
|
1574
|
+
this.validateWaterfallTerms();
|
|
1575
|
+
}
|
|
1294
1576
|
const result = await super.save();
|
|
1295
1577
|
loadedPlanStatus.set(this, this.status);
|
|
1296
1578
|
if (this.status !== "draft" && !frozenPlanSnapshot.has(this)) frozenPlanSnapshot.set(this, this.serializeFrozenSnapshot());
|
|
@@ -1342,9 +1624,63 @@ var CommissionPlan = class extends SmrtObject {
|
|
|
1342
1624
|
version: this.version,
|
|
1343
1625
|
currency: this.currency,
|
|
1344
1626
|
components: this.components,
|
|
1345
|
-
effectiveFrom: this.effectiveFrom ? this.effectiveFrom.toISOString() : null
|
|
1627
|
+
effectiveFrom: this.effectiveFrom ? this.effectiveFrom.toISOString() : null,
|
|
1628
|
+
effectiveTo: this.effectiveTo ? this.effectiveTo.toISOString() : null,
|
|
1629
|
+
steps: this.steps,
|
|
1630
|
+
remainderSink: this.remainderSink,
|
|
1631
|
+
applicationFeeOffsetRecipient: this.applicationFeeOffsetRecipient
|
|
1346
1632
|
});
|
|
1347
1633
|
}
|
|
1634
|
+
validateWaterfallTerms() {
|
|
1635
|
+
let steps;
|
|
1636
|
+
try {
|
|
1637
|
+
steps = JSON.parse(this.steps);
|
|
1638
|
+
} catch {
|
|
1639
|
+
throw new Error("CommissionPlan waterfall steps must be valid JSON");
|
|
1640
|
+
}
|
|
1641
|
+
if (!Array.isArray(steps)) throw new Error("CommissionPlan waterfall steps must be an array");
|
|
1642
|
+
if (steps.length === 0) return;
|
|
1643
|
+
if (this.getComponents().length > 0) throw new Error("CommissionPlan cannot combine v1 components with v2 waterfall steps");
|
|
1644
|
+
const remainderSink = this.parseRequiredDescriptor(this.remainderSink, "remainder sink");
|
|
1645
|
+
const applicationFeeOffsetRecipient = this.parseOptionalDescriptor(this.applicationFeeOffsetRecipient, "application fee offset recipient");
|
|
1646
|
+
validateWaterfallPlan({
|
|
1647
|
+
planKey: this.planKey,
|
|
1648
|
+
version: this.version,
|
|
1649
|
+
tenantId: this.tenantId,
|
|
1650
|
+
currency: this.currency,
|
|
1651
|
+
steps,
|
|
1652
|
+
remainderSink,
|
|
1653
|
+
...applicationFeeOffsetRecipient ? { applicationFeeOffsetRecipient } : {}
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
parseRequiredDescriptor(serialized, label) {
|
|
1657
|
+
const descriptor = this.parseOptionalDescriptor(serialized, label);
|
|
1658
|
+
if (!descriptor?.kind) throw new Error(`CommissionPlan waterfall ${label} is required`);
|
|
1659
|
+
return descriptor;
|
|
1660
|
+
}
|
|
1661
|
+
parseOptionalDescriptor(serialized, label) {
|
|
1662
|
+
let parsed;
|
|
1663
|
+
try {
|
|
1664
|
+
parsed = JSON.parse(serialized);
|
|
1665
|
+
} catch {
|
|
1666
|
+
throw new Error(`CommissionPlan waterfall ${label} must be valid JSON`);
|
|
1667
|
+
}
|
|
1668
|
+
if (parsed === null) return null;
|
|
1669
|
+
if (typeof parsed !== "object" || Array.isArray(parsed) || Object.getPrototypeOf(parsed) !== Object.prototype) throw new Error(`CommissionPlan waterfall ${label} must be an object`);
|
|
1670
|
+
return Object.keys(parsed).length === 0 ? null : parsed;
|
|
1671
|
+
}
|
|
1672
|
+
validateEffectiveWindow() {
|
|
1673
|
+
if (this.effectiveFrom && this.effectiveTo && this.effectiveTo <= this.effectiveFrom) throw new Error("CommissionPlan effectiveTo must be later than effectiveFrom");
|
|
1674
|
+
}
|
|
1675
|
+
getRecipientDescriptor(serialized) {
|
|
1676
|
+
try {
|
|
1677
|
+
const parsed = JSON.parse(serialized);
|
|
1678
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1679
|
+
return Object.keys(parsed).length === 0 ? null : parsed;
|
|
1680
|
+
} catch {
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1348
1684
|
static coerceDate(value) {
|
|
1349
1685
|
if (value == null) return null;
|
|
1350
1686
|
if (value instanceof Date) return value;
|
|
@@ -1405,17 +1741,19 @@ var CommissionPlanCollection = class extends SmrtCollection {
|
|
|
1405
1741
|
status: "active"
|
|
1406
1742
|
},
|
|
1407
1743
|
orderBy: "version DESC"
|
|
1408
|
-
})).find((plan) => plan.effectiveFrom === null || plan.effectiveFrom <= at) ?? null;
|
|
1744
|
+
})).find((plan) => (plan.effectiveFrom === null || plan.effectiveFrom <= at) && (plan.effectiveTo === null || plan.effectiveTo > at)) ?? null;
|
|
1409
1745
|
const atIso = at.toISOString();
|
|
1410
1746
|
if (tenantId === null) return (await this.query(`SELECT * FROM ${this.tableName}
|
|
1411
1747
|
WHERE tenant_id IS NULL
|
|
1412
1748
|
AND plan_key = ?
|
|
1413
1749
|
AND status = ?
|
|
1414
1750
|
AND (effective_from IS NULL OR effective_from <= ?)
|
|
1751
|
+
AND (effective_to IS NULL OR effective_to > ?)
|
|
1415
1752
|
ORDER BY version DESC
|
|
1416
1753
|
LIMIT 1`, [
|
|
1417
1754
|
planKey,
|
|
1418
1755
|
"active",
|
|
1756
|
+
atIso,
|
|
1419
1757
|
atIso
|
|
1420
1758
|
], { allowRawOnTenantScoped: true }))[0] ?? null;
|
|
1421
1759
|
assertTenantReadAllowed(tenantId, "CommissionPlanCollection.latestActiveByKey");
|
|
@@ -1424,12 +1762,14 @@ var CommissionPlanCollection = class extends SmrtCollection {
|
|
|
1424
1762
|
AND plan_key = ?
|
|
1425
1763
|
AND status = ?
|
|
1426
1764
|
AND (effective_from IS NULL OR effective_from <= ?)
|
|
1765
|
+
AND (effective_to IS NULL OR effective_to > ?)
|
|
1427
1766
|
ORDER BY CASE WHEN tenant_id = ? THEN 0 ELSE 1 END, version DESC
|
|
1428
1767
|
LIMIT 1`, [
|
|
1429
1768
|
tenantId,
|
|
1430
1769
|
planKey,
|
|
1431
1770
|
"active",
|
|
1432
1771
|
atIso,
|
|
1772
|
+
atIso,
|
|
1433
1773
|
tenantId
|
|
1434
1774
|
], { allowRawOnTenantScoped: true }))[0] ?? null;
|
|
1435
1775
|
}
|
|
@@ -1456,7 +1796,11 @@ var CommissionPlanCollection = class extends SmrtCollection {
|
|
|
1456
1796
|
description: changes.description ?? latest.description,
|
|
1457
1797
|
currency: changes.currency ?? latest.currency,
|
|
1458
1798
|
effectiveFrom: changes.effectiveFrom !== void 0 ? changes.effectiveFrom : latest.effectiveFrom,
|
|
1799
|
+
effectiveTo: changes.effectiveTo !== void 0 ? changes.effectiveTo : null,
|
|
1459
1800
|
components: changes.components !== void 0 ? JSON.stringify(changes.components) : latest.components,
|
|
1801
|
+
steps: changes.steps !== void 0 ? JSON.stringify(changes.steps) : latest.steps,
|
|
1802
|
+
remainderSink: changes.remainderSink !== void 0 ? JSON.stringify(changes.remainderSink) : latest.remainderSink,
|
|
1803
|
+
applicationFeeOffsetRecipient: changes.applicationFeeOffsetRecipient !== void 0 ? JSON.stringify(changes.applicationFeeOffsetRecipient) : latest.applicationFeeOffsetRecipient,
|
|
1460
1804
|
metadata: changes.metadata !== void 0 ? JSON.stringify(changes.metadata) : latest.metadata
|
|
1461
1805
|
});
|
|
1462
1806
|
}
|
|
@@ -3616,6 +3960,6 @@ var EarnerAttributionService = class EarnerAttributionService {
|
|
|
3616
3960
|
}
|
|
3617
3961
|
};
|
|
3618
3962
|
//#endregion
|
|
3619
|
-
export { EARNING_EVENT_KINDS as A, COMMISSION_ADJUSTMENT_KINDS as C, COMMISSION_STATUSES as D, COMMISSION_PLAN_STATUSES as E,
|
|
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 };
|
|
3620
3964
|
|
|
3621
|
-
//# sourceMappingURL=commissions-
|
|
3965
|
+
//# sourceMappingURL=commissions-Cip6QZ9Q.js.map
|