@smounters/kit 2.22.1 → 2.24.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 +16 -0
- package/README.md +20 -0
- package/dist/money/index.d.ts +26 -0
- package/dist/money/index.js +30 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.24.0 - 2026-09-11
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `kit/money`: `createMoney({ scale, pad })` — арифметика, привязанная к масштабу хранения, который
|
|
7
|
+
выбирает ПРИЛОЖЕНИЕ. Прежние `MONEY_SCALE`/`toMoneyAmount`/`applyDelta` остались как масштаб-4 по
|
|
8
|
+
умолчанию и ничего не меняют для тех, кто выбора не делал. Понадобилось потребителю, расширившему
|
|
9
|
+
денежные колонки до `numeric(40,20)`: на четырёх знаках срезалась точность токенов.
|
|
10
|
+
Добивки нулями у новой формы по умолчанию нет — при широком масштабе она отдаёт стену нулей, а
|
|
11
|
+
`"0.00000000000000000000" !== "0"` ломает сравнения ниже по течению.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `kit/money`: `Decimal.set` поднят с `precision: 50` до `80`. У самой decimal.js умолчание — 20
|
|
15
|
+
ЗНАЧАЩИХ цифр, и она молча округляет до них даже обычное сложение; на 18–20 знаках после запятой
|
|
16
|
+
это теряет хвост каждой суммы. Самая широкая колонка потребителя — 40 значащих цифр, и две такие
|
|
17
|
+
встречаются в одном умножении, поэтому запас взят двойной.
|
|
18
|
+
|
|
3
19
|
## 2.2.0 - 2026-07-30
|
|
4
20
|
|
|
5
21
|
### Added
|
package/README.md
CHANGED
|
@@ -104,6 +104,26 @@ quantizeToDecimals(due, 6, ROUND_UP); // сумма, достижимая у т
|
|
|
104
104
|
знаками не переведёт значение с восемью, и «ожидаемая» сумма, скруглённая до масштаба книги, окажется
|
|
105
105
|
неоплатной точно — а строгое сравнение назовёт это недоплатой, которой плательщик не мог избежать.
|
|
106
106
|
|
|
107
|
+
### Свой масштаб хранения
|
|
108
|
+
|
|
109
|
+
Функции выше привязаны к масштабу 4 — это фиат. Приложению, которое держит в той же книге токены,
|
|
110
|
+
четырёх знаков мало, а масштаб — **политика**, поэтому его задаёт потребитель:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { createMoney } from "@smounters/kit/money";
|
|
114
|
+
|
|
115
|
+
const money = createMoney({ scale: 20 }); // numeric(40,20)
|
|
116
|
+
|
|
117
|
+
money.toAmount("10.123456"); // "10.123456" — точность токена сохранена
|
|
118
|
+
money.toAmount("0.123456789012345678901") // null — точнее, чем хранит колонка
|
|
119
|
+
money.applyDelta("0", "100"); // "100", а не "100.00000000000000000000"
|
|
120
|
+
money.quantize("10.126", ROUND_HALF_UP); // "10.13" — округление, когда его ПОПРОСИЛИ
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Добивки нулями по умолчанию нет намеренно: при широком масштабе она возвращала бы стену нулей, а
|
|
124
|
+
`"0.00000000000000000000" !== "0"` ломает любое наивное сравнение ниже по течению. Нужна старая
|
|
125
|
+
форма — `createMoney({ scale: 4, pad: true })`.
|
|
126
|
+
|
|
107
127
|
## Защита от SSRF
|
|
108
128
|
|
|
109
129
|
```ts
|
package/dist/money/index.d.ts
CHANGED
|
@@ -33,6 +33,32 @@ export declare function quantizeToDecimals(amount: string, decimals: number, rou
|
|
|
33
33
|
export declare function sumAmounts(amounts: readonly string[]): Decimal;
|
|
34
34
|
/** Apply a signed delta to a balance, at money scale. */
|
|
35
35
|
export declare function applyDelta(balance: string, amount: string): string;
|
|
36
|
+
export interface MoneyOptions {
|
|
37
|
+
/** Decimal places the storage column keeps. Anything more precise is rejected, never rounded away. */
|
|
38
|
+
scale: number;
|
|
39
|
+
/**
|
|
40
|
+
* Pad results to `scale` (`"100"` → `"100.0000"`). Off by default: a wide column (scale 20) would
|
|
41
|
+
* otherwise hand every caller a string of trailing zeros, and `"0.00000000000000000000" !== "0"`
|
|
42
|
+
* breaks every naive comparison downstream.
|
|
43
|
+
*/
|
|
44
|
+
pad?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface MoneyOps {
|
|
47
|
+
readonly scale: number;
|
|
48
|
+
readonly zero: string;
|
|
49
|
+
fits(amount: string): boolean;
|
|
50
|
+
toAmount(amount: string): string | null;
|
|
51
|
+
applyDelta(balance: string, amount: string): string;
|
|
52
|
+
quantize(amount: string, rounding?: Decimal.Rounding): string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Money arithmetic bound to one storage scale.
|
|
56
|
+
*
|
|
57
|
+
* The scale is POLICY — a fiat-only ledger settles at 4 decimals, one that also holds tokens needs 18
|
|
58
|
+
* or more — so it belongs to the application, not to this package. The module-level `MONEY_SCALE`
|
|
59
|
+
* helpers remain as the scale-4 default for callers that never had to choose.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createMoney({ scale, pad }: MoneyOptions): MoneyOps;
|
|
36
62
|
export interface PostingLine {
|
|
37
63
|
/** Signed decimal string: debit > 0, credit < 0. */
|
|
38
64
|
amount: string;
|
package/dist/money/index.js
CHANGED
|
@@ -4,8 +4,12 @@ import { Decimal } from "decimal.js";
|
|
|
4
4
|
// Nothing here may use a JS number: a double cannot represent 0.1 exactly, and a ledger that cannot
|
|
5
5
|
// represent its own amounts stops balancing within a day. Amounts travel as decimal STRINGS end to end
|
|
6
6
|
// (numeric in the database, string on the wire) and are only turned into Decimal for the arithmetic.
|
|
7
|
-
// Precision must exceed the widest intermediate we produce
|
|
8
|
-
|
|
7
|
+
// Precision must exceed the widest intermediate we produce. The widest column a consumer stores is
|
|
8
|
+
// numeric(40,20) — 40 significant digits — and two of those can meet in one multiplication, so the
|
|
9
|
+
// library must carry twice that before it starts rounding. decimal.js defaults to 20 SIGNIFICANT
|
|
10
|
+
// digits and rounds even a plain addition down to them, silently: at 18 decimal places that loses
|
|
11
|
+
// the tail of every sum. Importing this module is what fixes it for the whole process.
|
|
12
|
+
Decimal.set({ precision: 80, toExpNeg: -80, toExpPos: 80 });
|
|
9
13
|
/** Money scale: `numeric(20,4)`. Four decimal places cover fiat and keep every posting exact. */
|
|
10
14
|
export const MONEY_SCALE = 4;
|
|
11
15
|
/** Ratio scale: `numeric(20,8)` — returns, coverage, shares of a fee. */
|
|
@@ -53,6 +57,30 @@ export function sumAmounts(amounts) {
|
|
|
53
57
|
export function applyDelta(balance, amount) {
|
|
54
58
|
return toDecimal(balance).plus(toDecimal(amount)).toFixed(MONEY_SCALE);
|
|
55
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Money arithmetic bound to one storage scale.
|
|
62
|
+
*
|
|
63
|
+
* The scale is POLICY — a fiat-only ledger settles at 4 decimals, one that also holds tokens needs 18
|
|
64
|
+
* or more — so it belongs to the application, not to this package. The module-level `MONEY_SCALE`
|
|
65
|
+
* helpers remain as the scale-4 default for callers that never had to choose.
|
|
66
|
+
*/
|
|
67
|
+
export function createMoney({ scale, pad = false }) {
|
|
68
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > 40) {
|
|
69
|
+
throw new RangeError(`Money scale must be an integer in 0..40, got ${scale}`);
|
|
70
|
+
}
|
|
71
|
+
const render = (d) => (pad ? d.toFixed(scale) : d.toFixed());
|
|
72
|
+
return {
|
|
73
|
+
scale,
|
|
74
|
+
zero: render(new Decimal(0)),
|
|
75
|
+
fits: (amount) => toDecimal(amount).decimalPlaces() <= scale,
|
|
76
|
+
toAmount: (amount) => {
|
|
77
|
+
const d = toDecimal(amount);
|
|
78
|
+
return d.decimalPlaces() > scale ? null : render(d);
|
|
79
|
+
},
|
|
80
|
+
applyDelta: (balance, amount) => render(toDecimal(balance).plus(toDecimal(amount))),
|
|
81
|
+
quantize: (amount, rounding = ROUND_HALF_UP) => toDecimal(amount).toFixed(scale, rounding),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
56
84
|
/**
|
|
57
85
|
* Is this a valid double-entry posting? At least two lines, no zero-amount line, and the signed amounts
|
|
58
86
|
* sum to EXACTLY zero. This is the invariant that makes a ledger auditable, so it is checked in code
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smounters/kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.24.0",
|
|
4
4
|
"description": "Batteries for Imperium services: structured logging, Redis module with a distributed lock, SSRF-safe fetch, decimal money, request context, env schemas",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"imperium",
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
"@bufbuild/protobuf": "^2.10.2",
|
|
102
102
|
"@bufbuild/protovalidate": "^1.0.0",
|
|
103
103
|
"@connectrpc/connect": "^2.1.1",
|
|
104
|
+
"@smounters/core": "2.0.1",
|
|
104
105
|
"fastify": "^5.8.4",
|
|
105
106
|
"ioredis": "^5.9.3",
|
|
106
|
-
"zod": "^4.3.6"
|
|
107
|
-
"@smounters/core": "2.0.1"
|
|
107
|
+
"zod": "^4.3.6"
|
|
108
108
|
},
|
|
109
109
|
"scripts": {
|
|
110
110
|
"build": "tsc -p tsconfig.json",
|