@classytic/primitives 0.31.1 → 0.33.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 +18 -0
- package/dist/line-settlement.d.mts +5 -4
- package/dist/line-settlement.mjs +18 -13
- package/dist/quantity.d.mts +23 -2
- package/dist/quantity.mjs +31 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
4
4
|
adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## 0.33.0 — 2026-10-14
|
|
7
|
+
|
|
8
|
+
### Changed — `inventory/line-settlement` settles decimal quantities on the 6-dp grid
|
|
9
|
+
- `planSettlement`, `clampSettlement`, `settledTotal`, `outstandingOf`, `settledEntirelyIn` and `shareOfSettled` add, subtract and compare
|
|
10
|
+
on the grid (`addQuantity` / `subtractQuantity`), so 0.1 + 0.2 settles exactly 0.3 and a weighed line can be received in parts.
|
|
11
|
+
Whole-unit inputs behave as before.
|
|
12
|
+
- A non-positive or off-grid request is refused with code `not_positive_quantity` (was `not_positive_integer`; no in-repo consumer
|
|
13
|
+
branches on the old code).
|
|
14
|
+
|
|
15
|
+
## 0.32.0 — 2026-10-12
|
|
16
|
+
|
|
17
|
+
### Added — `./quantity`: the step and running-sum helpers every package was copying
|
|
18
|
+
|
|
19
|
+
Seven packages (order, cart, catalog, invoice, bd-tax, commerce-ui, pos-ui) each carried their own copy of the quantity grid; the pieces the grid did not yet offer are now here, so a consumer imports instead of copying.
|
|
20
|
+
- `isQuantityStep(step)` — a guard for a usable step (a positive number on the grid, at least one grid unit). True exactly when `quantityStepToGrid` accepts it, pinned by a property test. For untrusted input (a stored `quantityStep`, a request body), where a throw is the wrong answer.
|
|
21
|
+
- `isOnStepStrict(quantity, step)` — is the quantity ON the grid and a whole multiple of a valid step? STRICT and total: junk, an off-grid quantity or an invalid step is `false`, never a throw and never a snap. `isOnStep` is unchanged (it snaps first and throws on a bad step; its docs now say so and point here).
|
|
22
|
+
- `addQuantity(a, b)` / `subtractQuantity(a, b)` — arithmetic on the grid, so a running sum never drifts (`1.2 + 1.15` is `2.35`). Zero is `0`, never `-0`. A non-finite operand throws `QuantityError`, as everywhere in the grid.
|
|
23
|
+
|
|
6
24
|
## 0.31.1 — 2026-10-11
|
|
7
25
|
|
|
8
26
|
### Fixed — `convertWithSnapshot` is exact (`./currency`)
|
|
@@ -10,7 +10,8 @@ import { Result } from "./result.mjs";
|
|
|
10
10
|
* kernel's vocabulary; the arithmetic, the over-settlement guard and the state
|
|
11
11
|
* derivation are this file's, so no two kernels can answer differently.
|
|
12
12
|
*
|
|
13
|
-
* Quantities are whole units
|
|
13
|
+
* Quantities are decimals on the 6-dp grid (`./quantity`): whole units for a countable item, 2.35 for a weighed one.
|
|
14
|
+
* Every sum and comparison is exact on the grid. A bucket that is absent counts as zero.
|
|
14
15
|
*/
|
|
15
16
|
interface SettlementLine<B extends string> {
|
|
16
17
|
/** What the line promised to move. */
|
|
@@ -34,7 +35,7 @@ declare const documentSettlementState: <B extends string>(lines: ReadonlyArray<S
|
|
|
34
35
|
/** `true` when every line is settled entirely through `bucket` (e.g. all received, nothing lost). */
|
|
35
36
|
declare const settledEntirelyIn: <B extends string>(lines: ReadonlyArray<SettlementLine<B>>, bucket: B) => boolean;
|
|
36
37
|
type SettlementRefusal = {
|
|
37
|
-
readonly code: "
|
|
38
|
+
readonly code: "not_positive_quantity";
|
|
38
39
|
readonly requested: number;
|
|
39
40
|
} | {
|
|
40
41
|
readonly code: "exceeds_outstanding";
|
|
@@ -44,7 +45,7 @@ type SettlementRefusal = {
|
|
|
44
45
|
/**
|
|
45
46
|
* Settle `quantity` more units into `bucket`, or refuse.
|
|
46
47
|
*
|
|
47
|
-
* Refuses a non-positive
|
|
48
|
+
* Refuses a non-positive quantity, one off the 6-dp grid, and anything above what is
|
|
48
49
|
* outstanding — an over-settlement is an input error, never silently trimmed.
|
|
49
50
|
* Use {@link clampSettlement} only where the surface's CONTRACT is "take what fits".
|
|
50
51
|
*/
|
|
@@ -61,7 +62,7 @@ declare const clampSettlement: <B extends string>(line: SettlementLine<B>, bucke
|
|
|
61
62
|
* The share of a line-level amount (a value or a cost, integer MINOR units) owed by one
|
|
62
63
|
* settlement batch, given how many units the whole line had settled before and after it.
|
|
63
64
|
*
|
|
64
|
-
* `floor(total·after/q) − floor(total·before/q)` TELESCOPES: however a line is split into
|
|
65
|
+
* `floor(total·after/q) − floor(total·before/q)` (units on the 6-dp grid) TELESCOPES: however a line is split into
|
|
65
66
|
* batches, and across every outcome bucket, the shares sum to exactly `total` once the line
|
|
66
67
|
* is fully settled — no remainder to chase, no batch-order dependence.
|
|
67
68
|
*/
|
package/dist/line-settlement.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { QUANTITY_SCALE, addQuantity, compareQuantity, isOnQuantityGrid, quantityToGrid, snapQuantity, subtractQuantity } from "./quantity.mjs";
|
|
1
2
|
import { err, ok } from "./result.mjs";
|
|
2
3
|
//#region src/inventory/line-settlement.ts
|
|
3
4
|
/** Sum of every bucket. */
|
|
4
|
-
const settledTotal = (line) => Object.values(line.settled).reduce((sum, n) => sum
|
|
5
|
+
const settledTotal = (line) => Object.values(line.settled).reduce((sum, n) => addQuantity(sum, n ?? 0), 0);
|
|
5
6
|
/**
|
|
6
7
|
* `quantity − Σ buckets`. NOT clamped at zero: a negative means the line was
|
|
7
8
|
* over-settled by data written outside {@link planSettlement}, and hiding it
|
|
8
9
|
* would erase the only signal that happened.
|
|
9
10
|
*/
|
|
10
|
-
const outstandingOf = (line) => line.quantity
|
|
11
|
+
const outstandingOf = (line) => subtractQuantity(line.quantity, settledTotal(line));
|
|
11
12
|
/** `open` — nothing settled; `settled` — nothing outstanding; otherwise `partial`. */
|
|
12
13
|
const settlementState = (line) => {
|
|
13
14
|
if (outstandingOf(line) <= 0) return "settled";
|
|
@@ -21,21 +22,21 @@ const documentSettlementState = (lines) => {
|
|
|
21
22
|
return "partial";
|
|
22
23
|
};
|
|
23
24
|
/** `true` when every line is settled entirely through `bucket` (e.g. all received, nothing lost). */
|
|
24
|
-
const settledEntirelyIn = (lines, bucket) => lines.every((l) => (l.settled[bucket] ?? 0
|
|
25
|
+
const settledEntirelyIn = (lines, bucket) => lines.every((l) => compareQuantity(l.settled[bucket] ?? 0, l.quantity) >= 0);
|
|
25
26
|
/**
|
|
26
27
|
* Settle `quantity` more units into `bucket`, or refuse.
|
|
27
28
|
*
|
|
28
|
-
* Refuses a non-positive
|
|
29
|
+
* Refuses a non-positive quantity, one off the 6-dp grid, and anything above what is
|
|
29
30
|
* outstanding — an over-settlement is an input error, never silently trimmed.
|
|
30
31
|
* Use {@link clampSettlement} only where the surface's CONTRACT is "take what fits".
|
|
31
32
|
*/
|
|
32
33
|
const planSettlement = (line, bucket, quantity) => {
|
|
33
|
-
if (!
|
|
34
|
-
code: "
|
|
34
|
+
if (!(quantity > 0) || !isOnQuantityGrid(quantity)) return err({
|
|
35
|
+
code: "not_positive_quantity",
|
|
35
36
|
requested: quantity
|
|
36
37
|
});
|
|
37
38
|
const outstanding = outstandingOf(line);
|
|
38
|
-
if (quantity >
|
|
39
|
+
if (compareQuantity(quantity, outstanding) > 0) return err({
|
|
39
40
|
code: "exceeds_outstanding",
|
|
40
41
|
requested: quantity,
|
|
41
42
|
outstanding
|
|
@@ -47,7 +48,10 @@ const planSettlement = (line, bucket, quantity) => {
|
|
|
47
48
|
* `applied` is what was actually settled; `0` leaves the line unchanged.
|
|
48
49
|
*/
|
|
49
50
|
const clampSettlement = (line, bucket, requested) => {
|
|
50
|
-
const
|
|
51
|
+
const wanted = requested > 0 ? isOnQuantityGrid(requested) ? requested : Math.floor(requested * QUANTITY_SCALE) / QUANTITY_SCALE : 0;
|
|
52
|
+
const fits = snapQuantity(wanted);
|
|
53
|
+
const outstanding = outstandingOf(line);
|
|
54
|
+
const applied = compareQuantity(fits, outstanding) <= 0 ? fits : Math.max(0, outstanding);
|
|
51
55
|
return {
|
|
52
56
|
line: applied > 0 ? withBucket(line, bucket, applied) : line,
|
|
53
57
|
applied
|
|
@@ -57,22 +61,23 @@ const clampSettlement = (line, bucket, requested) => {
|
|
|
57
61
|
* The share of a line-level amount (a value or a cost, integer MINOR units) owed by one
|
|
58
62
|
* settlement batch, given how many units the whole line had settled before and after it.
|
|
59
63
|
*
|
|
60
|
-
* `floor(total·after/q) − floor(total·before/q)` TELESCOPES: however a line is split into
|
|
64
|
+
* `floor(total·after/q) − floor(total·before/q)` (units on the 6-dp grid) TELESCOPES: however a line is split into
|
|
61
65
|
* batches, and across every outcome bucket, the shares sum to exactly `total` once the line
|
|
62
66
|
* is fully settled — no remainder to chase, no batch-order dependence.
|
|
63
67
|
*/
|
|
64
68
|
const shareOfSettled = (total, quantity, settledBefore, settledAfter) => {
|
|
65
69
|
if (!Number.isInteger(total) || total < 0) throw new RangeError(`total must be a non-negative integer, got ${total}`);
|
|
66
|
-
if (!
|
|
67
|
-
if (!(0 <= settledBefore && settledBefore <=
|
|
68
|
-
const
|
|
70
|
+
if (!(quantity > 0) || !isOnQuantityGrid(quantity)) throw new RangeError(`quantity must be positive with at most 6 decimals, got ${quantity}`);
|
|
71
|
+
if (!(0 <= settledBefore && compareQuantity(settledBefore, settledAfter) <= 0 && compareQuantity(settledAfter, quantity) <= 0)) throw new RangeError(`need 0 <= before (${settledBefore}) <= after (${settledAfter}) <= quantity (${quantity})`);
|
|
72
|
+
const q = quantityToGrid(quantity);
|
|
73
|
+
const at = (n) => Number(BigInt(total) * quantityToGrid(n) / q);
|
|
69
74
|
return at(settledAfter) - at(settledBefore);
|
|
70
75
|
};
|
|
71
76
|
const withBucket = (line, bucket, quantity) => ({
|
|
72
77
|
quantity: line.quantity,
|
|
73
78
|
settled: {
|
|
74
79
|
...line.settled,
|
|
75
|
-
[bucket]: (line.settled[bucket] ?? 0
|
|
80
|
+
[bucket]: addQuantity(line.settled[bucket] ?? 0, quantity)
|
|
76
81
|
}
|
|
77
82
|
});
|
|
78
83
|
//#endregion
|
package/dist/quantity.d.mts
CHANGED
|
@@ -28,11 +28,32 @@ declare function snapQuantity(quantity: number, step?: number, mode?: RoundingMo
|
|
|
28
28
|
* boundary, where a 7th decimal is a caller error rather than float drift.
|
|
29
29
|
*/
|
|
30
30
|
declare function isOnQuantityGrid(value: number): boolean;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Is `quantity` a whole multiple of `step`, within half a grid unit? Snaps `quantity` first and THROWS
|
|
33
|
+
* ({@link QuantityError}) on a non-finite quantity or an invalid step. Use {@link isOnStepStrict} where a
|
|
34
|
+
* malformed input must answer `false` instead.
|
|
35
|
+
*/
|
|
32
36
|
declare function isOnStep(quantity: number, step: number): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Is `step` a usable quantity step: a positive number on the grid, at least one grid unit? True exactly
|
|
39
|
+
* when {@link quantityStepToGrid} accepts it. A guard for untrusted input (a stored `quantityStep`, a
|
|
40
|
+
* request body) where a throw is the wrong answer.
|
|
41
|
+
*/
|
|
42
|
+
declare function isQuantityStep(step: unknown): step is number;
|
|
43
|
+
/**
|
|
44
|
+
* Is `quantity` ON the grid and a whole multiple of a valid `step`? STRICT and total: an off-grid or
|
|
45
|
+
* non-finite quantity, or an invalid step, is `false` — never a throw and never a snap (`2.3500004`
|
|
46
|
+
* is off the 0.001 step here, on it for {@link isOnStep}). The check for an input boundary, such as a
|
|
47
|
+
* line quantity against the line's step.
|
|
48
|
+
*/
|
|
49
|
+
declare function isOnStepStrict(quantity: number, step: number): boolean;
|
|
50
|
+
/** `a + b` on the grid, so a running sum never drifts (`1.2 + 1.15` is 2.35, not 2.3499…). */
|
|
51
|
+
declare function addQuantity(a: number, b: number): number;
|
|
52
|
+
/** `a − b` on the grid, so a running balance never drifts. */
|
|
53
|
+
declare function subtractQuantity(a: number, b: number): number;
|
|
33
54
|
/** Compare two quantities on the grid: equal when they snap to the same grid unit. */
|
|
34
55
|
declare function compareQuantity(a: number, b: number): -1 | 0 | 1;
|
|
35
56
|
/** `compareQuantity(a, b) === 0`. */
|
|
36
57
|
declare function quantitiesEqual(a: number, b: number): boolean;
|
|
37
58
|
//#endregion
|
|
38
|
-
export { QUANTITY_SCALE, QuantityError, QuantityErrorCode, type RoundingMode, compareQuantity, isOnQuantityGrid, isOnStep, quantitiesEqual, quantityStepToGrid, quantityToGrid, snapQuantity };
|
|
59
|
+
export { QUANTITY_SCALE, QuantityError, QuantityErrorCode, type RoundingMode, addQuantity, compareQuantity, isOnQuantityGrid, isOnStep, isOnStepStrict, isQuantityStep, quantitiesEqual, quantityStepToGrid, quantityToGrid, snapQuantity, subtractQuantity };
|
package/dist/quantity.mjs
CHANGED
|
@@ -70,10 +70,39 @@ function isOnQuantityGrid(value) {
|
|
|
70
70
|
const grid = Math.round(scaled);
|
|
71
71
|
return Number.isSafeInteger(grid) && Math.abs(grid - scaled) <= 1e-9 * Math.max(1, Math.abs(scaled));
|
|
72
72
|
}
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Is `quantity` a whole multiple of `step`, within half a grid unit? Snaps `quantity` first and THROWS
|
|
75
|
+
* ({@link QuantityError}) on a non-finite quantity or an invalid step. Use {@link isOnStepStrict} where a
|
|
76
|
+
* malformed input must answer `false` instead.
|
|
77
|
+
*/
|
|
74
78
|
function isOnStep(quantity, step) {
|
|
75
79
|
return quantityToGrid(quantity) % quantityStepToGrid(step) === 0n;
|
|
76
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Is `step` a usable quantity step: a positive number on the grid, at least one grid unit? True exactly
|
|
83
|
+
* when {@link quantityStepToGrid} accepts it. A guard for untrusted input (a stored `quantityStep`, a
|
|
84
|
+
* request body) where a throw is the wrong answer.
|
|
85
|
+
*/
|
|
86
|
+
function isQuantityStep(step) {
|
|
87
|
+
return typeof step === "number" && Number.isFinite(step) && step > 0 && isOnQuantityGrid(step) && quantityToGrid(step) > 0n;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Is `quantity` ON the grid and a whole multiple of a valid `step`? STRICT and total: an off-grid or
|
|
91
|
+
* non-finite quantity, or an invalid step, is `false` — never a throw and never a snap (`2.3500004`
|
|
92
|
+
* is off the 0.001 step here, on it for {@link isOnStep}). The check for an input boundary, such as a
|
|
93
|
+
* line quantity against the line's step.
|
|
94
|
+
*/
|
|
95
|
+
function isOnStepStrict(quantity, step) {
|
|
96
|
+
return isOnQuantityGrid(quantity) && isQuantityStep(step) && isOnStep(quantity, step);
|
|
97
|
+
}
|
|
98
|
+
/** `a + b` on the grid, so a running sum never drifts (`1.2 + 1.15` is 2.35, not 2.3499…). */
|
|
99
|
+
function addQuantity(a, b) {
|
|
100
|
+
return gridToNumber(quantityToGrid(a) + quantityToGrid(b));
|
|
101
|
+
}
|
|
102
|
+
/** `a − b` on the grid, so a running balance never drifts. */
|
|
103
|
+
function subtractQuantity(a, b) {
|
|
104
|
+
return gridToNumber(quantityToGrid(a) - quantityToGrid(b));
|
|
105
|
+
}
|
|
77
106
|
/** Compare two quantities on the grid: equal when they snap to the same grid unit. */
|
|
78
107
|
function compareQuantity(a, b) {
|
|
79
108
|
const ga = quantityToGrid(a);
|
|
@@ -85,4 +114,4 @@ function quantitiesEqual(a, b) {
|
|
|
85
114
|
return compareQuantity(a, b) === 0;
|
|
86
115
|
}
|
|
87
116
|
//#endregion
|
|
88
|
-
export { QUANTITY_SCALE, QuantityError, compareQuantity, isOnQuantityGrid, isOnStep, quantitiesEqual, quantityStepToGrid, quantityToGrid, snapQuantity };
|
|
117
|
+
export { QUANTITY_SCALE, QuantityError, addQuantity, compareQuantity, isOnQuantityGrid, isOnStep, isOnStepStrict, isQuantityStep, quantitiesEqual, quantityStepToGrid, quantityToGrid, snapQuantity, subtractQuantity };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
|
|
5
5
|
"author": "Classytic",
|
|
6
6
|
"license": "MIT",
|