@classytic/primitives 0.28.0 → 0.30.0
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/CHANGELOG.md +1217 -1119
- package/dist/approval.d.mts +98 -6
- package/dist/approval.mjs +61 -4
- package/dist/canonical.d.mts +6 -1
- package/dist/canonical.mjs +10 -4
- package/dist/durable-events.d.mts +83 -0
- package/dist/durable-events.mjs +115 -0
- package/dist/environment.d.mts +30 -1
- package/dist/environment.mjs +72 -1
- package/dist/events.d.mts +45 -7
- package/dist/events.mjs +174 -7
- package/dist/idempotency.d.mts +4 -0
- package/dist/idempotency.mjs +4 -0
- package/dist/item-stock.d.mts +43 -0
- package/dist/item-stock.mjs +67 -0
- package/dist/legal-form.d.mts +12 -0
- package/dist/legal-form.mjs +27 -0
- package/dist/line-settlement.d.mts +70 -0
- package/dist/line-settlement.mjs +79 -0
- package/dist/memory-outbox.d.mts +1 -0
- package/dist/memory-outbox.mjs +18 -2
- package/dist/monetization.d.mts +3 -1
- package/dist/monetization.mjs +7 -1
- package/dist/outbox.d.mts +46 -5
- package/dist/outbox.mjs +43 -1
- package/dist/payment-events.d.mts +6 -0
- package/dist/period.d.mts +22 -1
- package/dist/period.mjs +30 -1
- package/dist/phone.d.mts +13 -1
- package/dist/phone.mjs +21 -1
- package/dist/proration.d.mts +11 -1
- package/dist/proration.mjs +24 -1
- package/dist/signed-token.d.mts +54 -0
- package/dist/signed-token.mjs +139 -0
- package/dist/split-allocation.d.mts +20 -2
- package/dist/split-allocation.mjs +57 -1
- package/package.json +364 -285
|
@@ -88,6 +88,7 @@ function resolveWeights(subjects, method) {
|
|
|
88
88
|
function distribute(total, weights) {
|
|
89
89
|
const weightSum = weights.reduce((a, b) => a + b, 0);
|
|
90
90
|
if (weightSum === 0) return weights.map(() => 0);
|
|
91
|
+
if (weights.every(Number.isSafeInteger) && Number.isSafeInteger(weightSum)) return distributeExact(total, weights, weightSum);
|
|
91
92
|
const raw = weights.map((w) => total * w / weightSum);
|
|
92
93
|
const floored = raw.map((r) => Math.floor(r));
|
|
93
94
|
const residue = total - floored.reduce((a, b) => a + b, 0);
|
|
@@ -107,9 +108,64 @@ function distribute(total, weights) {
|
|
|
107
108
|
}
|
|
108
109
|
return result;
|
|
109
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Largest remainder in exact integer arithmetic, for integer weights (minor-unit amounts). Shares
|
|
113
|
+
* and remainders are compared as BigInt quotients of one denominator, so neither the floor nor the
|
|
114
|
+
* remainder ranking can drift once `total × weight` passes 2^53.
|
|
115
|
+
*/
|
|
116
|
+
function distributeExact(total, weights, weightSum) {
|
|
117
|
+
const t = BigInt(total);
|
|
118
|
+
const d = BigInt(weightSum);
|
|
119
|
+
const shares = weights.map((w) => {
|
|
120
|
+
const n = t * BigInt(w);
|
|
121
|
+
return {
|
|
122
|
+
floor: n / d,
|
|
123
|
+
rem: n % d
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const result = shares.map((s) => Number(s.floor));
|
|
127
|
+
const residue = total - result.reduce((a, b) => a + b, 0);
|
|
128
|
+
const order = shares.map((s, i) => ({
|
|
129
|
+
i,
|
|
130
|
+
rem: s.rem
|
|
131
|
+
})).sort((a, b) => a.rem === b.rem ? a.i - b.i : a.rem > b.rem ? -1 : 1);
|
|
132
|
+
for (let k = 0; k < residue; k++) {
|
|
133
|
+
const entry = order[k];
|
|
134
|
+
if (entry === void 0) throw new Error("split-allocation invariant: residue exceeds the subject count");
|
|
135
|
+
result[entry.i] = (result[entry.i] ?? 0) + 1;
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
110
139
|
/** True if `sum(parts[i].amount) === result.total`. Useful as an assertion in host code. */
|
|
111
140
|
function isBalanced(result) {
|
|
112
141
|
return result.parts.reduce((acc, p) => acc + p.amount, 0) === result.total;
|
|
113
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* THE discount allocation: spread a document-level discount over its eligible lines, by value,
|
|
145
|
+
* exact largest remainder. Every document that splits one discount — the order, its invoice,
|
|
146
|
+
* its credit notes, a return's value — calls this, so they agree to the minor unit.
|
|
147
|
+
*
|
|
148
|
+
* Returns a FULL map in input order: every subject id, `0` for an ineligible or zero-weight one.
|
|
149
|
+
* `0` discount allocates zeros; a discount equal to the eligible base allocates all of it; a
|
|
150
|
+
* discount above it throws `DISCOUNT_EXCEEDS_BASE` — the figures disagree.
|
|
151
|
+
*/
|
|
152
|
+
function allocateDiscount(discount, subjects) {
|
|
153
|
+
if (!Number.isInteger(discount) || discount < 0) throw new SplitAllocationError("INVALID_TOTAL", `discount must be a non-negative integer, got ${discount}`);
|
|
154
|
+
const out = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const s of subjects) {
|
|
156
|
+
if (out.has(s.id)) throw new SplitAllocationError("DUPLICATE_SUBJECT_ID", `duplicate subject id: ${s.id}`);
|
|
157
|
+
if (!Number.isFinite(s.weight) || s.weight < 0) throw new SplitAllocationError("INVALID_WEIGHT", `subject ${s.id}: weight must be a non-negative number`);
|
|
158
|
+
out.set(s.id, 0);
|
|
159
|
+
}
|
|
160
|
+
const eligible = subjects.filter((s) => s.discountable !== false && s.weight > 0);
|
|
161
|
+
const base = eligible.reduce((sum, s) => sum + s.weight, 0);
|
|
162
|
+
if (discount > base) throw new SplitAllocationError("DISCOUNT_EXCEEDS_BASE", `discount ${discount} exceeds the discountable base ${base}`);
|
|
163
|
+
if (discount === 0) return out;
|
|
164
|
+
for (const part of allocate(discount, eligible.map((s) => ({
|
|
165
|
+
id: s.id,
|
|
166
|
+
weight: s.weight
|
|
167
|
+
})), "by-value").parts) out.set(part.id, part.amount);
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
114
170
|
//#endregion
|
|
115
|
-
export { SplitAllocationError, allocate, isBalanced };
|
|
171
|
+
export { SplitAllocationError, allocate, allocateDiscount, isBalanced };
|