@classytic/primitives 0.11.0 → 0.13.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 +39 -0
- package/dist/unit-cost-rate.d.mts +56 -0
- package/dist/unit-cost-rate.mjs +116 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,45 @@
|
|
|
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.13.0] - 2026-07-22
|
|
7
|
+
|
|
8
|
+
### Added — `/unit-cost-rate`: exact per-unit monetary rates (bigint-safe)
|
|
9
|
+
|
|
10
|
+
- **`@classytic/primitives/unit-cost-rate`** — fractional per-unit monetary
|
|
11
|
+
rates stored as scaled integers (`scaledAmount = minorPerUnit × RATE_SCALE`,
|
|
12
|
+
`RATE_SCALE = 1_000_000`). Solves the WAC / FIFO valuation problem where
|
|
13
|
+
dividing a total by a fractional quantity yields a sub-minor-unit rate
|
|
14
|
+
(100 paisa ÷ 3 units = 33.333… paisa/unit) that must not lose precision.
|
|
15
|
+
Exports:
|
|
16
|
+
- `unitCostRate(minorPerUnit, currency)` — direct rate constructor
|
|
17
|
+
- `unitCostRateFromTotal(totalMinor, quantity, currency)` — WAC/FIFO
|
|
18
|
+
constructor; bigint-safe, QTY_SCALE grid for fractional quantities
|
|
19
|
+
- `extendedAmount(rate, qty, mode?)` — the ONE rate→total rounding boundary;
|
|
20
|
+
bigint multiplication to keep large amounts exact; default `'half-even'`
|
|
21
|
+
(banker's rounding), `'half-up'` available for legacy parity
|
|
22
|
+
- `rateMinorPerUnit(rate)` — display helper (fractional number, NOT for math)
|
|
23
|
+
- `isUnitCostRate(value)` — structural type guard
|
|
24
|
+
- `UnitCostRate<C>` interface, `UnitCostRateError`, `UnitCostRateErrorCode`,
|
|
25
|
+
`RoundingMode`, `RATE_SCALE`
|
|
26
|
+
- For distributing a top-down charge across lines with no lost minor unit,
|
|
27
|
+
compose with `allocate` from `@classytic/primitives/split-allocation`.
|
|
28
|
+
|
|
29
|
+
### Changed — `/outbox` is now the canonical contract owner
|
|
30
|
+
|
|
31
|
+
- **`@classytic/primitives/outbox` owns the transactional-outbox contract.**
|
|
32
|
+
Previously this file was documented as a mirror of `@classytic/arc`'s copy
|
|
33
|
+
("arc is source of truth, keep bit-identical") while arc's docs said the
|
|
34
|
+
opposite — a manual-sync drift risk. Settled: primitives owns pure
|
|
35
|
+
cross-package contracts (events + outbox); arc owns runtime behavior
|
|
36
|
+
(`EventOutbox` relay, stores, backoff, Fastify integration) and, from
|
|
37
|
+
arc 2.24+, imports + re-exports this contract instead of duplicating it.
|
|
38
|
+
- **Cross-package `instanceof` now works.** Because arc re-exports these exact
|
|
39
|
+
`OutboxOwnershipError` / `InvalidOutboxEventError` classes, a store built
|
|
40
|
+
against primitives throws the same class identity arc's relay catches.
|
|
41
|
+
Under the old duplication, arc's `instanceof` checks silently missed
|
|
42
|
+
primitives-thrown ownership errors.
|
|
43
|
+
- No export names or signatures changed — contract surface is identical.
|
|
44
|
+
|
|
6
45
|
## [0.11.0] - 2026-07-08
|
|
7
46
|
|
|
8
47
|
### Added — eKYC vocabulary + secure OTP primitive
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { r as CurrencyCode } from "./currency-BCJnJBlb.mjs";
|
|
2
|
+
import { MinorUnits } from "./money.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/money/unit-cost-rate.d.ts
|
|
5
|
+
/** Fixed, ecosystem-wide rate scale: 6 decimal places below the minor unit. */
|
|
6
|
+
declare const RATE_SCALE = 1000000;
|
|
7
|
+
/** Half-to-even (banker's) is the default; half-up available for legacy parity. */
|
|
8
|
+
type RoundingMode = 'half-even' | 'half-up';
|
|
9
|
+
/**
|
|
10
|
+
* Minor units per unit, stored as an integer scaled by {@link RATE_SCALE}.
|
|
11
|
+
* `scale` is carried so a value is self-describing on the wire even if
|
|
12
|
+
* `RATE_SCALE` ever changes; it is always `RATE_SCALE` today.
|
|
13
|
+
*/
|
|
14
|
+
interface UnitCostRate<C extends string = string> {
|
|
15
|
+
/** Integer: minor units per unit × `scale`. */
|
|
16
|
+
readonly scaledAmount: number;
|
|
17
|
+
readonly currency: C | CurrencyCode | string;
|
|
18
|
+
/** Always {@link RATE_SCALE}. */
|
|
19
|
+
readonly scale: number;
|
|
20
|
+
}
|
|
21
|
+
type UnitCostRateErrorCode = 'NOT_FINITE' | 'NEGATIVE' | 'NOT_INTEGER' | 'UNSAFE_INTEGER' | 'NON_POSITIVE_QUANTITY';
|
|
22
|
+
declare class UnitCostRateError extends Error {
|
|
23
|
+
readonly name = "UnitCostRateError";
|
|
24
|
+
readonly code: UnitCostRateErrorCode;
|
|
25
|
+
constructor(code: UnitCostRateErrorCode, message: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Construct a rate from a (possibly fractional) minor-units-per-unit value.
|
|
29
|
+
*
|
|
30
|
+
* @example unitCostRate(33.3333, 'BDT') // { scaledAmount: 33_333_300, ... }
|
|
31
|
+
*/
|
|
32
|
+
declare function unitCostRate<C extends string = string>(minorPerUnit: number, currency: C): UnitCostRate<C>;
|
|
33
|
+
/**
|
|
34
|
+
* Weighted-average constructor: the rate implied by an integer minor-unit
|
|
35
|
+
* `totalMinor` spread over a (possibly fractional) `quantity`. This is the WAC
|
|
36
|
+
* / layer-average rate. Overflow-safe via bigint; quantity rounded to the
|
|
37
|
+
* {@link QTY_SCALE} grid.
|
|
38
|
+
*
|
|
39
|
+
* @example unitCostRateFromTotal(100, 3, 'BDT') // 33.333… paisa/unit
|
|
40
|
+
*/
|
|
41
|
+
declare function unitCostRateFromTotal<C extends string = string>(totalMinor: number, quantity: number, currency: C): UnitCostRate<C>;
|
|
42
|
+
/**
|
|
43
|
+
* The extended monetary amount for `quantity` units at `rate`, rounded to
|
|
44
|
+
* integer {@link MinorUnits} — the ONE rate→total rounding boundary.
|
|
45
|
+
*
|
|
46
|
+
* `round(quantity × scaledAmount / RATE_SCALE)`, computed in bigint so large
|
|
47
|
+
* amounts never lose precision. A fractional quantity is honoured to
|
|
48
|
+
* {@link QTY_SCALE} precision.
|
|
49
|
+
*/
|
|
50
|
+
declare function extendedAmount<C extends string = string>(rate: UnitCostRate<C>, quantity: number, mode?: RoundingMode): MinorUnits<C>;
|
|
51
|
+
/** The rate as a plain (fractional) minor-units-per-unit number — for display only. */
|
|
52
|
+
declare function rateMinorPerUnit(rate: UnitCostRate): number;
|
|
53
|
+
/** Structural guard. */
|
|
54
|
+
declare function isUnitCostRate(value: unknown): value is UnitCostRate;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { RATE_SCALE, RoundingMode, UnitCostRate, UnitCostRateError, UnitCostRateErrorCode, extendedAmount, isUnitCostRate, rateMinorPerUnit, unitCostRate, unitCostRateFromTotal };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { MinorUnits } from "./money.mjs";
|
|
2
|
+
//#region src/money/unit-cost-rate.ts
|
|
3
|
+
/** Fixed, ecosystem-wide rate scale: 6 decimal places below the minor unit. */
|
|
4
|
+
const RATE_SCALE = 1e6;
|
|
5
|
+
/**
|
|
6
|
+
* Quantity scale used internally so a fractional quantity (e.g. `2.5`, a UoM
|
|
7
|
+
* split) participates in exact integer/bigint arithmetic. 6 dp of quantity
|
|
8
|
+
* precision — quantities finer than 1e-6 unit are rounded to this grid.
|
|
9
|
+
*/
|
|
10
|
+
const QTY_SCALE = 1e6;
|
|
11
|
+
const DEFAULT_ROUNDING = "half-even";
|
|
12
|
+
var UnitCostRateError = class extends Error {
|
|
13
|
+
name = "UnitCostRateError";
|
|
14
|
+
code;
|
|
15
|
+
constructor(code, message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function assertSafe(scaled) {
|
|
21
|
+
if (!Number.isFinite(scaled)) throw new UnitCostRateError("NOT_FINITE", `unit cost rate is not finite: ${scaled}`);
|
|
22
|
+
if (!Number.isSafeInteger(scaled)) throw new UnitCostRateError("UNSAFE_INTEGER", `scaled unit cost ${scaled} exceeds Number.MAX_SAFE_INTEGER — value too large for this rate scale`);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Scale a (possibly fractional) quantity onto the {@link QTY_SCALE} grid as a
|
|
26
|
+
* bigint. Guards against a quantity so large that `quantity × QTY_SCALE` loses
|
|
27
|
+
* integer precision as a double BEFORE reaching bigint (≈ >9e9 units).
|
|
28
|
+
*/
|
|
29
|
+
function scaleQuantity(quantity) {
|
|
30
|
+
const q = Math.round(quantity * QTY_SCALE);
|
|
31
|
+
if (!Number.isSafeInteger(q)) throw new UnitCostRateError("UNSAFE_INTEGER", `quantity ${quantity} is too large to scale precisely — exceeds safe-integer range`);
|
|
32
|
+
return BigInt(q);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Construct a rate from a (possibly fractional) minor-units-per-unit value.
|
|
36
|
+
*
|
|
37
|
+
* @example unitCostRate(33.3333, 'BDT') // { scaledAmount: 33_333_300, ... }
|
|
38
|
+
*/
|
|
39
|
+
function unitCostRate(minorPerUnit, currency) {
|
|
40
|
+
if (!Number.isFinite(minorPerUnit)) throw new UnitCostRateError("NOT_FINITE", `minorPerUnit is not finite: ${minorPerUnit}`);
|
|
41
|
+
if (minorPerUnit < 0) throw new UnitCostRateError("NEGATIVE", `unit cost cannot be negative: ${minorPerUnit}`);
|
|
42
|
+
const scaledAmount = Math.round(minorPerUnit * RATE_SCALE);
|
|
43
|
+
assertSafe(scaledAmount);
|
|
44
|
+
return {
|
|
45
|
+
scaledAmount,
|
|
46
|
+
currency,
|
|
47
|
+
scale: RATE_SCALE
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Weighted-average constructor: the rate implied by an integer minor-unit
|
|
52
|
+
* `totalMinor` spread over a (possibly fractional) `quantity`. This is the WAC
|
|
53
|
+
* / layer-average rate. Overflow-safe via bigint; quantity rounded to the
|
|
54
|
+
* {@link QTY_SCALE} grid.
|
|
55
|
+
*
|
|
56
|
+
* @example unitCostRateFromTotal(100, 3, 'BDT') // 33.333… paisa/unit
|
|
57
|
+
*/
|
|
58
|
+
function unitCostRateFromTotal(totalMinor, quantity, currency) {
|
|
59
|
+
if (!Number.isInteger(totalMinor)) throw new UnitCostRateError("NOT_INTEGER", `totalMinor must be integer minor units: ${totalMinor}`);
|
|
60
|
+
if (!Number.isFinite(quantity) || quantity <= 0) throw new UnitCostRateError("NON_POSITIVE_QUANTITY", `quantity must be > 0, got ${quantity}`);
|
|
61
|
+
const qScaled = scaleQuantity(quantity);
|
|
62
|
+
const numerator = BigInt(totalMinor) * BigInt(RATE_SCALE) * BigInt(QTY_SCALE);
|
|
63
|
+
const scaled = Number(roundedDiv(numerator, qScaled, "half-even"));
|
|
64
|
+
assertSafe(scaled);
|
|
65
|
+
return {
|
|
66
|
+
scaledAmount: scaled,
|
|
67
|
+
currency,
|
|
68
|
+
scale: RATE_SCALE
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The extended monetary amount for `quantity` units at `rate`, rounded to
|
|
73
|
+
* integer {@link MinorUnits} — the ONE rate→total rounding boundary.
|
|
74
|
+
*
|
|
75
|
+
* `round(quantity × scaledAmount / RATE_SCALE)`, computed in bigint so large
|
|
76
|
+
* amounts never lose precision. A fractional quantity is honoured to
|
|
77
|
+
* {@link QTY_SCALE} precision.
|
|
78
|
+
*/
|
|
79
|
+
function extendedAmount(rate, quantity, mode = DEFAULT_ROUNDING) {
|
|
80
|
+
if (!Number.isFinite(quantity) || quantity < 0) throw new UnitCostRateError("NON_POSITIVE_QUANTITY", `quantity must be >= 0, got ${quantity}`);
|
|
81
|
+
const qScaled = scaleQuantity(quantity);
|
|
82
|
+
const numerator = BigInt(rate.scaledAmount) * qScaled;
|
|
83
|
+
const denominator = BigInt(rate.scale) * BigInt(QTY_SCALE);
|
|
84
|
+
const minor = Number(roundedDiv(numerator, denominator, mode));
|
|
85
|
+
if (!Number.isSafeInteger(minor)) throw new UnitCostRateError("UNSAFE_INTEGER", `extended amount ${minor} exceeds safe integer range`);
|
|
86
|
+
return MinorUnits(minor, rate.currency);
|
|
87
|
+
}
|
|
88
|
+
/** The rate as a plain (fractional) minor-units-per-unit number — for display only. */
|
|
89
|
+
function rateMinorPerUnit(rate) {
|
|
90
|
+
return rate.scaledAmount / rate.scale;
|
|
91
|
+
}
|
|
92
|
+
/** Structural guard. */
|
|
93
|
+
function isUnitCostRate(value) {
|
|
94
|
+
return typeof value === "object" && value !== null && Number.isInteger(value.scaledAmount) && typeof value.currency === "string" && typeof value.scale === "number";
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Rounded integer division of two bigints, `numerator / denominator`
|
|
98
|
+
* (`denominator > 0`), honouring the rounding mode at the exact half.
|
|
99
|
+
* Sign-safe for negative numerators.
|
|
100
|
+
*/
|
|
101
|
+
function roundedDiv(numerator, denominator, mode) {
|
|
102
|
+
const neg = numerator < 0n;
|
|
103
|
+
const n = neg ? -numerator : numerator;
|
|
104
|
+
const q = n / denominator;
|
|
105
|
+
const remainder = n - q * denominator;
|
|
106
|
+
if (remainder === 0n) return neg ? -q : q;
|
|
107
|
+
const twiceRem = remainder * 2n;
|
|
108
|
+
let up;
|
|
109
|
+
if (twiceRem > denominator) up = true;
|
|
110
|
+
else if (twiceRem < denominator) up = false;
|
|
111
|
+
else up = mode === "half-up" ? true : q % 2n !== 0n;
|
|
112
|
+
const result = up ? q + 1n : q;
|
|
113
|
+
return neg ? -result : result;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { RATE_SCALE, UnitCostRateError, extendedAmount, isUnitCostRate, rateMinorPerUnit, unitCostRate, unitCostRateFromTotal };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"types": "./dist/currency.d.mts",
|
|
30
30
|
"default": "./dist/currency.mjs"
|
|
31
31
|
},
|
|
32
|
+
"./unit-cost-rate": {
|
|
33
|
+
"types": "./dist/unit-cost-rate.d.mts",
|
|
34
|
+
"default": "./dist/unit-cost-rate.mjs"
|
|
35
|
+
},
|
|
32
36
|
"./address": {
|
|
33
37
|
"types": "./dist/address.d.mts",
|
|
34
38
|
"default": "./dist/address.mjs"
|