@classytic/primitives 0.31.0 → 0.32.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 +29 -0
- package/dist/bank-transaction.d.mts +1 -1
- package/dist/currency.d.mts +1 -1
- package/dist/currency.mjs +41 -1
- package/dist/monetization.d.mts +1 -1
- package/dist/{money-C9iLWiWR.d.mts → money-BGPS_Ena.d.mts} +7 -0
- package/dist/money.d.mts +1 -1
- package/dist/payment-events.d.mts +1 -1
- package/dist/payment-gateway.d.mts +1 -1
- package/dist/proration.d.mts +1 -1
- package/dist/quantity.d.mts +23 -2
- package/dist/quantity.mjs +31 -2
- package/dist/unit-cost-rate.d.mts +1 -1
- package/dist/unit-cost-rate.mjs +1 -1
- package/dist/usage-pricing.d.mts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
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.32.0 — 2026-10-12
|
|
7
|
+
|
|
8
|
+
### Added — `./quantity`: the step and running-sum helpers every package was copying
|
|
9
|
+
|
|
10
|
+
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.
|
|
11
|
+
- `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.
|
|
12
|
+
- `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).
|
|
13
|
+
- `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.
|
|
14
|
+
|
|
15
|
+
## 0.31.1 — 2026-10-11
|
|
16
|
+
|
|
17
|
+
### Fixed — `convertWithSnapshot` is exact (`./currency`)
|
|
18
|
+
- The conversion was computed in floating point (`minor / sourceFactor × rate × baseFactor`) and then
|
|
19
|
+
rounded, so a result that lands EXACTLY on a rounding boundary could round the wrong way. USD 5.30 at
|
|
20
|
+
110.15 is exactly 583.795 BDT; the float product was 583.79499…, giving 58379 paisa instead of 58380.
|
|
21
|
+
Over 1..99,999 cents the float form was off by one minor unit for 134 amounts at 110.15, 1,455 at 117.35
|
|
22
|
+
and 1,379 at 122.35.
|
|
23
|
+
- The rate is now read as the decimal it prints as (`String(rate)`, exponent form included) and the
|
|
24
|
+
conversion is done in BigInt, rounded once. Signature and rounding modes are unchanged
|
|
25
|
+
(`half-away-from-zero` default, `floor`, `ceil`).
|
|
26
|
+
- What changes: only results that were wrong. Under `half-away-from-zero`, amounts whose exact value is a
|
|
27
|
+
half-way point now round away from zero (one minor unit up in magnitude). Under `floor` / `ceil`, amounts
|
|
28
|
+
whose exact value is an integer are no longer nudged one unit off it (USD 2.20 at 110.15 `ceil` was 24234,
|
|
29
|
+
now 24233). A negative amount that rounds to zero now returns `0`, not `-0`. Every other result is
|
|
30
|
+
identical (checked against the float form across ~10.9M amount × rate × currency-pair × mode cases).
|
|
31
|
+
- `convertWithSnapshot` now throws `TypeError` for a non-integer `source.amount` (a `Money` invariant that
|
|
32
|
+
`money()` already enforces), rather than converting a fractional minor unit.
|
|
33
|
+
- `reverseWithSnapshot` is unchanged.
|
|
34
|
+
|
|
6
35
|
## 0.31.0 — 2026-10-11
|
|
7
36
|
|
|
8
37
|
### Added — `./quantity`: the 6-decimal quantity grid for fractional stock and sale quantities
|
package/dist/currency.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as FxRounding, B as reverseWithSnapshot, D as CURRENCY_PATTERN, E as CURRENCIES, F as currencyCode, I as isCurrencyCode, L as isFxSnapshot, M as InvalidCurrencyCodeError, N as MINOR_UNIT_FACTOR, O as CurrencyCode, P as convertWithSnapshot, R as isKnownCurrency, V as toCurrencyCode, j as FxSnapshot, k as CurrencyMismatchError, z as minorUnitFactor } from "./money-
|
|
1
|
+
import { A as FxRounding, B as reverseWithSnapshot, D as CURRENCY_PATTERN, E as CURRENCIES, F as currencyCode, I as isCurrencyCode, L as isFxSnapshot, M as InvalidCurrencyCodeError, N as MINOR_UNIT_FACTOR, O as CurrencyCode, P as convertWithSnapshot, R as isKnownCurrency, V as toCurrencyCode, j as FxSnapshot, k as CurrencyMismatchError, z as minorUnitFactor } from "./money-BGPS_Ena.mjs";
|
|
2
2
|
export { CURRENCIES, CURRENCY_PATTERN, CurrencyCode, CurrencyMismatchError, FxRounding, FxSnapshot, InvalidCurrencyCodeError, MINOR_UNIT_FACTOR, convertWithSnapshot, currencyCode, isCurrencyCode, isFxSnapshot, isKnownCurrency, minorUnitFactor, reverseWithSnapshot, toCurrencyCode };
|
package/dist/currency.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t as roundedDiv } from "./rounded-div-BWJb98Ws.mjs";
|
|
1
2
|
//#region src/money/currency.ts
|
|
2
3
|
/**
|
|
3
4
|
* Common ISO 4217 codes. Non-exhaustive — add more at the consumer level if
|
|
@@ -179,6 +180,34 @@ function roundMinor(raw, mode) {
|
|
|
179
180
|
if (mode === "ceil") return Math.ceil(raw);
|
|
180
181
|
return Math.round(Math.abs(raw)) * (raw < 0 ? -1 : 1);
|
|
181
182
|
}
|
|
183
|
+
/** `numerator / denominator` (denominator > 0) landed exactly by `mode`. */
|
|
184
|
+
function divideMinor(numerator, denominator, mode) {
|
|
185
|
+
if (mode === "half-away-from-zero") return roundedDiv(numerator, denominator, "half-up");
|
|
186
|
+
const quotient = numerator / denominator;
|
|
187
|
+
if (numerator % denominator === 0n) return quotient;
|
|
188
|
+
if (mode === "floor") return numerator < 0n ? quotient - 1n : quotient;
|
|
189
|
+
return numerator < 0n ? quotient : quotient + 1n;
|
|
190
|
+
}
|
|
191
|
+
const DECIMAL_NUMBER = /^(-?)(\d+)(?:\.(\d+))?(?:e([+-]\d+))?$/;
|
|
192
|
+
/**
|
|
193
|
+
* A finite number as the exact decimal its shortest round-trip string names:
|
|
194
|
+
* `110.15` → `{ int: 11015n, scale: 2 }`, so `110.15` means 110.15, not the
|
|
195
|
+
* binary double nearest to it.
|
|
196
|
+
*/
|
|
197
|
+
function decimalOf(value) {
|
|
198
|
+
const match = DECIMAL_NUMBER.exec(String(value));
|
|
199
|
+
if (match === null) throw new TypeError(`Cannot read ${value} as a decimal`);
|
|
200
|
+
const [, sign, whole, fraction = "", exponent = "0"] = match;
|
|
201
|
+
const digits = BigInt(`${sign}${whole}${fraction}`);
|
|
202
|
+
const power = Number(exponent) - fraction.length;
|
|
203
|
+
return power >= 0 ? {
|
|
204
|
+
int: digits * 10n ** BigInt(power),
|
|
205
|
+
scale: 0
|
|
206
|
+
} : {
|
|
207
|
+
int: digits,
|
|
208
|
+
scale: -power
|
|
209
|
+
};
|
|
210
|
+
}
|
|
182
211
|
/**
|
|
183
212
|
* Apply an {@link FxSnapshot} to a source-currency amount.
|
|
184
213
|
*
|
|
@@ -198,12 +227,23 @@ function roundMinor(raw, mode) {
|
|
|
198
227
|
* (USD has 2, JPY has 0). The conversion therefore goes
|
|
199
228
|
* `minor → major → ×rate → base minor`, not a naive `amount × rate`, which is
|
|
200
229
|
* off by 100× for any pair whose exponents differ.
|
|
230
|
+
*
|
|
231
|
+
* ## Exact, rounded once
|
|
232
|
+
*
|
|
233
|
+
* The rate is read as the decimal it prints as and the product is computed in
|
|
234
|
+
* BigInt: `minor × rate × baseFactor / sourceFactor`, rounded once. A float
|
|
235
|
+
* product misses exact half-way points (USD 5.30 × 110.15 = 583.795 BDT
|
|
236
|
+
* computes as 583.79499…), so it rounded to 58379 paisa instead of 58380.
|
|
201
237
|
*/
|
|
202
238
|
function convertWithSnapshot(source, fx, options = {}) {
|
|
203
239
|
if (source.currency !== fx.sourceCurrency) throw new CurrencyMismatchError(source.currency, fx.sourceCurrency);
|
|
204
240
|
if (!Number.isFinite(fx.rate)) throw new TypeError(`FxSnapshot.rate must be finite, got ${fx.rate}`);
|
|
241
|
+
if (!Number.isInteger(source.amount)) throw new TypeError(`Money.amount must be an integer in minor units, got ${source.amount}`);
|
|
242
|
+
const rate = decimalOf(fx.rate);
|
|
243
|
+
const numerator = BigInt(source.amount) * rate.int * BigInt(minorUnitFactor(fx.baseCurrency));
|
|
244
|
+
const denominator = BigInt(minorUnitFactor(fx.sourceCurrency)) * 10n ** BigInt(rate.scale);
|
|
205
245
|
return {
|
|
206
|
-
amount:
|
|
246
|
+
amount: Number(divideMinor(numerator, denominator, options.rounding ?? "half-away-from-zero")),
|
|
207
247
|
currency: currencyCode(fx.baseCurrency)
|
|
208
248
|
};
|
|
209
249
|
}
|
package/dist/monetization.d.mts
CHANGED
|
@@ -172,6 +172,13 @@ type FxRounding = 'half-away-from-zero' | 'floor' | 'ceil';
|
|
|
172
172
|
* (USD has 2, JPY has 0). The conversion therefore goes
|
|
173
173
|
* `minor → major → ×rate → base minor`, not a naive `amount × rate`, which is
|
|
174
174
|
* off by 100× for any pair whose exponents differ.
|
|
175
|
+
*
|
|
176
|
+
* ## Exact, rounded once
|
|
177
|
+
*
|
|
178
|
+
* The rate is read as the decimal it prints as and the product is computed in
|
|
179
|
+
* BigInt: `minor × rate × baseFactor / sourceFactor`, rounded once. A float
|
|
180
|
+
* product misses exact half-way points (USD 5.30 × 110.15 = 583.795 BDT
|
|
181
|
+
* computes as 583.79499…), so it rounded to 58379 paisa instead of 58380.
|
|
175
182
|
*/
|
|
176
183
|
declare function convertWithSnapshot(source: Money, fx: FxSnapshot, options?: {
|
|
177
184
|
rounding?: FxRounding;
|
package/dist/money.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as CashDestination, S as CASH_DESTINATIONS, T as isCashDestination, _ as negateMoney, a as compareMoney, b as sumMoney, c as fromMajor, d as isPositiveMoney, f as isZeroMoney, g as multiplyMoney, h as money, i as addMoney, k as CurrencyMismatchError, l as isMoney, m as minorUnitsToMajor, n as Money, o as equalsMoney, p as majorToMinorUnits, r as absMoney, s as formatMinorUnits, t as MinorUnits, u as isNegativeMoney, v as percentOfMinor, w as cashDestinationLabel, x as toMajor, y as subtractMoney } from "./money-
|
|
1
|
+
import { C as CashDestination, S as CASH_DESTINATIONS, T as isCashDestination, _ as negateMoney, a as compareMoney, b as sumMoney, c as fromMajor, d as isPositiveMoney, f as isZeroMoney, g as multiplyMoney, h as money, i as addMoney, k as CurrencyMismatchError, l as isMoney, m as minorUnitsToMajor, n as Money, o as equalsMoney, p as majorToMinorUnits, r as absMoney, s as formatMinorUnits, t as MinorUnits, u as isNegativeMoney, v as percentOfMinor, w as cashDestinationLabel, x as toMajor, y as subtractMoney } from "./money-BGPS_Ena.mjs";
|
|
2
2
|
export { CASH_DESTINATIONS, type CashDestination, CurrencyMismatchError, MinorUnits, Money, absMoney, addMoney, cashDestinationLabel, compareMoney, equalsMoney, formatMinorUnits, fromMajor, isCashDestination, isMoney, isNegativeMoney, isPositiveMoney, isZeroMoney, majorToMinorUnits, minorUnitsToMajor, money, multiplyMoney, negateMoney, percentOfMinor, subtractMoney, sumMoney, toMajor };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Money } from "./money-
|
|
1
|
+
import { n as Money } from "./money-BGPS_Ena.mjs";
|
|
2
2
|
import { PaymentMethodKind } from "./payment-method-kind.mjs";
|
|
3
3
|
import { ProviderUnknownCause } from "./payment-gateway.mjs";
|
|
4
4
|
//#region src/events/payment-events.d.ts
|
package/dist/proration.d.mts
CHANGED
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 };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { O as CurrencyCode, t as MinorUnits } from "./money-
|
|
1
|
+
import { O as CurrencyCode, t as MinorUnits } from "./money-BGPS_Ena.mjs";
|
|
2
2
|
import { t as RoundingMode } from "./rounded-div-D7lyJXt8.mjs";
|
|
3
3
|
//#region src/money/unit-cost-rate.d.ts
|
|
4
4
|
/** Fixed, ecosystem-wide rate scale: 6 decimal places below the minor unit. */
|
package/dist/unit-cost-rate.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as MinorUnits } from "./money-BBOARE5f.mjs";
|
|
2
1
|
import { t as roundedDiv } from "./rounded-div-BWJb98Ws.mjs";
|
|
2
|
+
import { t as MinorUnits } from "./money-BBOARE5f.mjs";
|
|
3
3
|
//#region src/money/unit-cost-rate.ts
|
|
4
4
|
/** Fixed, ecosystem-wide rate scale: 6 decimal places below the minor unit. */
|
|
5
5
|
const RATE_SCALE = 1e6;
|
package/dist/usage-pricing.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as Money } from "./money-
|
|
1
|
+
import { n as Money } from "./money-BGPS_Ena.mjs";
|
|
2
2
|
import { UsageRating } from "./monetization.mjs";
|
|
3
3
|
//#region src/money/usage-pricing.d.ts
|
|
4
4
|
/** What ONE unit of a sale line measured: `{ value: 2.3, unit: 'kg' }`. `unit` must equal the rating's `unitLabel`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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",
|