@classytic/primitives 0.19.0 → 0.20.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 +906 -751
- package/README.md +160 -159
- package/dist/approval.d.mts +49 -10
- package/dist/approval.mjs +44 -10
- package/dist/bank-transaction.d.mts +1 -1
- package/dist/cadence-BFQVC1Qp.d.mts +107 -0
- package/dist/cadence.d.mts +1 -92
- package/dist/cadence.mjs +133 -4
- package/dist/calendar-B6fm-FbR.d.mts +46 -0
- package/dist/calendar.d.mts +1 -45
- package/dist/canonical.mjs +17 -0
- package/dist/context.d.mts +28 -1
- package/dist/currency.d.mts +2 -2
- package/dist/currency.mjs +134 -19
- package/dist/event-infra.d.mts +1 -34
- package/dist/event-infra.mjs +16 -46
- package/dist/idempotency.d.mts +253 -0
- package/dist/idempotency.mjs +226 -0
- package/dist/memory-outbox.d.mts +64 -0
- package/dist/memory-outbox.mjs +172 -0
- package/dist/monetization.d.mts +1 -1
- package/dist/money-DPo9fLBi.d.mts +347 -0
- package/dist/money.d.mts +1 -140
- package/dist/money.mjs +6 -15
- package/dist/outbox.d.mts +49 -1
- package/dist/outbox.mjs +64 -1
- package/dist/payment-events.d.mts +132 -4
- package/dist/payment-events.mjs +22 -2
- package/dist/payment-gateway.d.mts +210 -7
- package/dist/payment-gateway.mjs +18 -1
- package/dist/period.d.mts +138 -7
- package/dist/period.mjs +316 -5
- package/dist/person-parse.d.mts +82 -0
- package/dist/person-parse.mjs +109 -0
- package/dist/proration.d.mts +2 -2
- package/dist/proration.mjs +18 -6
- package/dist/sla-policy.d.mts +1 -1
- package/dist/state-machine.mjs +13 -0
- package/dist/suspension.d.mts +1 -1
- package/dist/timezone-tlG7ygwx.d.mts +114 -0
- package/dist/timezone.d.mts +2 -100
- package/dist/timezone.mjs +38 -3
- package/dist/unit-cost-rate.d.mts +1 -2
- package/package.json +244 -232
- package/dist/currency-BCJnJBlb.d.mts +0 -119
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { InvalidOutboxEventError, OutboxOwnershipError } from "./outbox.mjs";
|
|
2
|
+
//#region src/events/memory-outbox.ts
|
|
3
|
+
/**
|
|
4
|
+
* In-memory outbox store — reference implementation for dev / testing.
|
|
5
|
+
*
|
|
6
|
+
* Supports the full `OutboxStore` capability set (claim/lease, fail/retry,
|
|
7
|
+
* dedupe, visibleAt). Production deployments should pass a `repository` to
|
|
8
|
+
* `EventOutbox` instead of using this; the memory store exists for unit
|
|
9
|
+
* tests and single-process dev where durable persistence is noise.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_LEASE_MS = 3e4;
|
|
12
|
+
var MemoryOutboxStore = class {
|
|
13
|
+
entries = [];
|
|
14
|
+
seenDedupeKeys = /* @__PURE__ */ new Set();
|
|
15
|
+
/**
|
|
16
|
+
* Drop every entry (including delivered / failed / dead-lettered rows) and
|
|
17
|
+
* the dedupe-key memory.
|
|
18
|
+
*
|
|
19
|
+
* For tests only — it is the in-memory analogue of truncating the table, and
|
|
20
|
+
* exists so a suite can isolate cases against ONE store instance (the
|
|
21
|
+
* `reset` hook of `runOutboxStoreContract`). Not part of `OutboxStore`; a
|
|
22
|
+
* production store must never expose this.
|
|
23
|
+
*/
|
|
24
|
+
clear() {
|
|
25
|
+
this.entries.length = 0;
|
|
26
|
+
this.seenDedupeKeys.clear();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* For tests only — every stored event regardless of status, in insertion
|
|
30
|
+
* order. The in-memory analogue of `SELECT * FROM outbox`, used by the
|
|
31
|
+
* `/event-infra` contract test to assert what a `purge` left behind. Not part
|
|
32
|
+
* of `OutboxStore`; a production store must never expose this.
|
|
33
|
+
*/
|
|
34
|
+
all() {
|
|
35
|
+
return this.entries.map((e) => e.event);
|
|
36
|
+
}
|
|
37
|
+
async save(event, options) {
|
|
38
|
+
if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
|
|
39
|
+
if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
|
|
40
|
+
if (options?.dedupeKey) {
|
|
41
|
+
if (this.seenDedupeKeys.has(options.dedupeKey)) return;
|
|
42
|
+
this.seenDedupeKeys.add(options.dedupeKey);
|
|
43
|
+
}
|
|
44
|
+
this.entries.push({
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
event,
|
|
47
|
+
status: "pending",
|
|
48
|
+
attempts: 0,
|
|
49
|
+
visibleAt: options?.visibleAt?.getTime() ?? 0,
|
|
50
|
+
leaseOwner: null,
|
|
51
|
+
leaseExpiresAt: 0,
|
|
52
|
+
deliveredAt: null,
|
|
53
|
+
firstFailedAt: null,
|
|
54
|
+
lastFailedAt: null,
|
|
55
|
+
lastError: null,
|
|
56
|
+
dedupeKey: options?.dedupeKey
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async getPending(limit) {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
return this.entries.filter((e) => e.status === "pending" && e.visibleAt <= now && (e.leaseOwner === null || e.leaseExpiresAt <= now)).slice(0, limit).map((e) => e.event);
|
|
62
|
+
}
|
|
63
|
+
async claimPending(options) {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const limit = options?.limit ?? 100;
|
|
66
|
+
const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS;
|
|
67
|
+
const consumerId = options?.consumerId ?? "anonymous";
|
|
68
|
+
const typeFilter = options?.types ? new Set(options.types) : null;
|
|
69
|
+
const claimed = [];
|
|
70
|
+
for (const entry of this.entries) {
|
|
71
|
+
if (claimed.length >= limit) break;
|
|
72
|
+
if (entry.status !== "pending") continue;
|
|
73
|
+
if (entry.visibleAt > now) continue;
|
|
74
|
+
if (entry.leaseOwner !== null && entry.leaseExpiresAt > now) continue;
|
|
75
|
+
if (typeFilter && !typeFilter.has(entry.event.type)) continue;
|
|
76
|
+
entry.leaseOwner = consumerId;
|
|
77
|
+
entry.leaseExpiresAt = now + leaseMs;
|
|
78
|
+
entry.attempts++;
|
|
79
|
+
claimed.push(entry.event);
|
|
80
|
+
}
|
|
81
|
+
return claimed;
|
|
82
|
+
}
|
|
83
|
+
async acknowledge(eventId, options) {
|
|
84
|
+
const entry = this.entries.find((e) => e.event.meta.id === eventId);
|
|
85
|
+
if (!entry) return;
|
|
86
|
+
if (entry.status === "delivered") return;
|
|
87
|
+
if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
|
|
88
|
+
entry.status = "delivered";
|
|
89
|
+
entry.deliveredAt = Date.now();
|
|
90
|
+
entry.leaseOwner = null;
|
|
91
|
+
}
|
|
92
|
+
async fail(eventId, error, options) {
|
|
93
|
+
const entry = this.entries.find((e) => e.event.meta.id === eventId);
|
|
94
|
+
if (!entry) return;
|
|
95
|
+
if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
entry.lastError = error;
|
|
98
|
+
entry.leaseOwner = null;
|
|
99
|
+
entry.leaseExpiresAt = 0;
|
|
100
|
+
if (entry.firstFailedAt === null) entry.firstFailedAt = now;
|
|
101
|
+
entry.lastFailedAt = now;
|
|
102
|
+
if (options?.deadLetter) {
|
|
103
|
+
entry.status = "dead_letter";
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
entry.status = "pending";
|
|
107
|
+
entry.visibleAt = options?.retryAt ? options.retryAt.getTime() : 0;
|
|
108
|
+
}
|
|
109
|
+
async getDeadLettered(limit) {
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const entry of this.entries) {
|
|
112
|
+
if (out.length >= limit) break;
|
|
113
|
+
if (entry.status !== "dead_letter") continue;
|
|
114
|
+
out.push({
|
|
115
|
+
event: entry.event,
|
|
116
|
+
error: {
|
|
117
|
+
message: entry.lastError?.message ?? "unknown",
|
|
118
|
+
...entry.lastError?.code !== void 0 ? { code: entry.lastError.code } : {}
|
|
119
|
+
},
|
|
120
|
+
attempts: entry.attempts,
|
|
121
|
+
firstFailedAt: new Date(entry.firstFailedAt ?? entry.lastFailedAt ?? Date.now()),
|
|
122
|
+
lastFailedAt: new Date(entry.lastFailedAt ?? entry.firstFailedAt ?? Date.now())
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
async purge(olderThanMs) {
|
|
128
|
+
const cutoff = Date.now() - olderThanMs;
|
|
129
|
+
let purged = 0;
|
|
130
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
131
|
+
const entry = this.entries[i];
|
|
132
|
+
if (!entry) continue;
|
|
133
|
+
if (entry.status === "delivered" && entry.deliveredAt !== null && entry.deliveredAt < cutoff) {
|
|
134
|
+
if (entry.dedupeKey) this.seenDedupeKeys.delete(entry.dedupeKey);
|
|
135
|
+
this.entries.splice(i, 1);
|
|
136
|
+
purged++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return purged;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Re-drive one dead-lettered row. Scoped to `dead_letter`: a row that is merely
|
|
143
|
+
* backing off must keep its attempt count, and the id alone cannot tell the two apart.
|
|
144
|
+
*/
|
|
145
|
+
async requeue(eventId) {
|
|
146
|
+
const entry = this.entries.find((e) => e.event.meta.id === eventId && e.status === "dead_letter");
|
|
147
|
+
if (!entry) return false;
|
|
148
|
+
entry.status = "pending";
|
|
149
|
+
entry.attempts = 0;
|
|
150
|
+
entry.leaseOwner = null;
|
|
151
|
+
entry.leaseExpiresAt = 0;
|
|
152
|
+
entry.visibleAt = 0;
|
|
153
|
+
entry.lastError = null;
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
async countByStatus(status) {
|
|
157
|
+
return this.entries.filter((e) => e.status === status).length;
|
|
158
|
+
}
|
|
159
|
+
/** Age of the oldest PENDING row — see `MemoryEntry.createdAt` for why not `visibleAt`. */
|
|
160
|
+
async oldestPendingAgeMs() {
|
|
161
|
+
const pending = this.entries.filter((e) => e.status === "pending");
|
|
162
|
+
if (pending.length === 0) return null;
|
|
163
|
+
const oldest = Math.min(...pending.map((e) => e.createdAt));
|
|
164
|
+
return Date.now() - oldest;
|
|
165
|
+
}
|
|
166
|
+
/** Test helper: inspect entry by id */
|
|
167
|
+
_getEntry(eventId) {
|
|
168
|
+
return this.entries.find((e) => e.event.meta.id === eventId);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
//#endregion
|
|
172
|
+
export { MemoryOutboxStore };
|
package/dist/monetization.d.mts
CHANGED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { t as Brand } from "./brand-LLXqoh5J.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/money/currency.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* ISO 4217 currency code (3 uppercase letters).
|
|
6
|
+
*
|
|
7
|
+
* Branded `string` — any 3-letter string is structurally valid, but the brand
|
|
8
|
+
* prevents accidental mixing with raw strings. Use {@link toCurrencyCode} to
|
|
9
|
+
* construct one safely.
|
|
10
|
+
*/
|
|
11
|
+
type CurrencyCode = Brand<string, 'CurrencyCode'>;
|
|
12
|
+
/**
|
|
13
|
+
* Common ISO 4217 codes. Non-exhaustive — add more at the consumer level if
|
|
14
|
+
* needed. Listed here because shared-types ships with these by default.
|
|
15
|
+
*/
|
|
16
|
+
declare const CURRENCIES: {
|
|
17
|
+
readonly USD: "USD";
|
|
18
|
+
readonly EUR: "EUR";
|
|
19
|
+
readonly GBP: "GBP";
|
|
20
|
+
readonly JPY: "JPY";
|
|
21
|
+
readonly CNY: "CNY";
|
|
22
|
+
readonly INR: "INR";
|
|
23
|
+
readonly BDT: "BDT";
|
|
24
|
+
readonly AED: "AED";
|
|
25
|
+
readonly SAR: "SAR";
|
|
26
|
+
readonly PKR: "PKR";
|
|
27
|
+
readonly NPR: "NPR";
|
|
28
|
+
readonly LKR: "LKR";
|
|
29
|
+
readonly MYR: "MYR";
|
|
30
|
+
readonly SGD: "SGD";
|
|
31
|
+
readonly AUD: "AUD";
|
|
32
|
+
readonly CAD: "CAD";
|
|
33
|
+
readonly CHF: "CHF";
|
|
34
|
+
readonly HKD: "HKD";
|
|
35
|
+
readonly THB: "THB";
|
|
36
|
+
readonly IDR: "IDR";
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Number of minor units per major unit, by ISO 4217 code.
|
|
40
|
+
*
|
|
41
|
+
* **This table must list EVERY currency whose exponent is not 2.** ISO 4217
|
|
42
|
+
* defaults to 2 decimals, and {@link minorUnitFactor} falls back to 100 for a
|
|
43
|
+
* code it does not know — which is correct for the ~150 two-decimal currencies
|
|
44
|
+
* and catastrophically wrong for the others. A missing zero-decimal entry does
|
|
45
|
+
* not throw: it makes `fromMajor(1500, 'GNF')` store 150 000 minor units, a
|
|
46
|
+
* 100× error that reads as a plausible number all the way to the gateway.
|
|
47
|
+
*
|
|
48
|
+
* So the exceptions are enumerated EXHAUSTIVELY (all 0-, 3- and 4-decimal
|
|
49
|
+
* codes), leaving the `?? 100` fallback reachable only by a code that is not a
|
|
50
|
+
* real ISO 4217 currency — a typo, a lowercased code, or a placeholder. The
|
|
51
|
+
* two-decimal entries below are redundant with the default and kept only as
|
|
52
|
+
* documentation of the common set.
|
|
53
|
+
*/
|
|
54
|
+
declare const MINOR_UNIT_FACTOR: Readonly<Record<string, number>>;
|
|
55
|
+
/**
|
|
56
|
+
* ISO 4217 format — 3 uppercase letters. Exported so consumers can embed
|
|
57
|
+
* the SAME pattern in JSON-Schema-representable validators (zod `.regex`,
|
|
58
|
+
* Mongoose `match`, OpenAPI `pattern`) instead of hand-rolling a copy:
|
|
59
|
+
* `.refine(isCurrencyCode)` validates at runtime but is dropped by
|
|
60
|
+
* `z.toJSONSchema`, losing the constraint from generated API docs.
|
|
61
|
+
* `isCurrencyCode` / `toCurrencyCode` remain the runtime API.
|
|
62
|
+
*/
|
|
63
|
+
declare const CURRENCY_PATTERN: RegExp;
|
|
64
|
+
/**
|
|
65
|
+
* Narrow a raw string to a {@link CurrencyCode}. Returns `null` if the string
|
|
66
|
+
* is not a valid ISO 4217 format (3 uppercase letters). This does not verify
|
|
67
|
+
* the code is registered with ISO — use your own whitelist for that.
|
|
68
|
+
*/
|
|
69
|
+
declare function toCurrencyCode(value: string): CurrencyCode | null;
|
|
70
|
+
/**
|
|
71
|
+
* Validate + brand, or throw. The constructor form — same relationship to
|
|
72
|
+
* {@link toCurrencyCode} that `civilDate` has to `isCivilDate`.
|
|
73
|
+
*
|
|
74
|
+
* `Money` carries a {@link CurrencyCode}, not a bare string, and this is where
|
|
75
|
+
* an untrusted value earns the brand. Rejecting `'jpy'` here matters: lowercase
|
|
76
|
+
* misses {@link MINOR_UNIT_FACTOR}, so it silently becomes a 2-decimal currency
|
|
77
|
+
* and every JPY amount is off by 100×.
|
|
78
|
+
*/
|
|
79
|
+
declare function currencyCode(value: string): CurrencyCode;
|
|
80
|
+
declare class InvalidCurrencyCodeError extends Error {
|
|
81
|
+
readonly name = "InvalidCurrencyCodeError";
|
|
82
|
+
readonly value: string;
|
|
83
|
+
constructor(value: string);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Raised when an operation combines two currencies.
|
|
87
|
+
*
|
|
88
|
+
* Declared HERE rather than in `/money` so the FX seam below can throw it
|
|
89
|
+
* without a runtime import back into `/money` (which imports
|
|
90
|
+
* {@link minorUnitFactor} from this file). `/money` re-exports it, so
|
|
91
|
+
* `import { CurrencyMismatchError } from '@classytic/primitives/money'` still
|
|
92
|
+
* resolves.
|
|
93
|
+
*/
|
|
94
|
+
declare class CurrencyMismatchError extends Error {
|
|
95
|
+
readonly name = "CurrencyMismatchError";
|
|
96
|
+
readonly left: string;
|
|
97
|
+
readonly right: string;
|
|
98
|
+
constructor(left: string, right: string);
|
|
99
|
+
}
|
|
100
|
+
/** Type predicate for `unknown` input. */
|
|
101
|
+
declare function isCurrencyCode(value: unknown): value is CurrencyCode;
|
|
102
|
+
/**
|
|
103
|
+
* Minor units per major unit (100 for USD, 1 for JPY, 1000 for KWD, 10 000 for CLF).
|
|
104
|
+
*
|
|
105
|
+
* Falls back to 100 for an unlisted code — the ISO 4217 default exponent. Because
|
|
106
|
+
* {@link MINOR_UNIT_FACTOR} enumerates every non-2-decimal currency, that fallback
|
|
107
|
+
* is now only reached by codes that are not ISO 4217 currencies at all; use
|
|
108
|
+
* {@link isKnownCurrency} at a config/boot boundary if you need to reject those
|
|
109
|
+
* rather than assume two decimals for them.
|
|
110
|
+
*/
|
|
111
|
+
declare function minorUnitFactor(currency: string): number;
|
|
112
|
+
/**
|
|
113
|
+
* Does {@link MINOR_UNIT_FACTOR} carry an explicit exponent for this code?
|
|
114
|
+
*
|
|
115
|
+
* `false` means {@link minorUnitFactor} would ASSUME two decimals. That
|
|
116
|
+
* assumption is right for an ordinary ISO 4217 code and wrong by 100× for a
|
|
117
|
+
* mistyped or lowercased one (`'jpy'`), so a boot-time config validator that
|
|
118
|
+
* accepts an operator-supplied currency should gate on this instead of letting
|
|
119
|
+
* the default answer for it.
|
|
120
|
+
*/
|
|
121
|
+
declare function isKnownCurrency(currency: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Frozen FX conversion record — the cost of record for cross-currency
|
|
124
|
+
* transactions. Stored alongside a base-currency amount at the seam where
|
|
125
|
+
* currency crossed (procurement receipt, landed-cost entry, foreign refund).
|
|
126
|
+
*
|
|
127
|
+
* Invariant: once written, never mutated. Revaluation is a new snapshot on
|
|
128
|
+
* a new document — rewriting an existing snapshot corrupts audit history.
|
|
129
|
+
*
|
|
130
|
+
* `rate` is expressed as: 1 sourceCurrency = rate * baseCurrency
|
|
131
|
+
* (e.g. USD→BDT at 110 means rate = 110, so sourceAmount 10 USD → baseAmount 1100 BDT).
|
|
132
|
+
* This matches how banks quote — the source is the "from" currency.
|
|
133
|
+
*/
|
|
134
|
+
interface FxSnapshot {
|
|
135
|
+
/** Currency the transaction was invoiced / quoted in. ISO 4217. */
|
|
136
|
+
sourceCurrency: string;
|
|
137
|
+
/** Organisation's functional currency the base amount is expressed in. */
|
|
138
|
+
baseCurrency: string;
|
|
139
|
+
/** 1 unit of sourceCurrency equals this many units of baseCurrency. */
|
|
140
|
+
rate: number;
|
|
141
|
+
/** When the rate was captured — NOT when the document was last touched. */
|
|
142
|
+
snapshotAt: Date;
|
|
143
|
+
/**
|
|
144
|
+
* Optional provenance tag — e.g. 'manual', 'bangladesh-bank',
|
|
145
|
+
* 'openexchangerates.org', 'supplier-invoice'. Free-form; hosts use it for
|
|
146
|
+
* audit filtering and re-pricing policy.
|
|
147
|
+
*/
|
|
148
|
+
source?: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* How to land a converted amount on an integer number of minor units.
|
|
152
|
+
* Required to be deterministic and named, never implicit — the old signature
|
|
153
|
+
* returned an unrounded float and left the choice to whoever persisted it, so
|
|
154
|
+
* two call sites rounding differently produced two answers for one conversion.
|
|
155
|
+
*/
|
|
156
|
+
type FxRounding = 'half-away-from-zero' | 'floor' | 'ceil';
|
|
157
|
+
/**
|
|
158
|
+
* Apply an {@link FxSnapshot} to a source-currency amount.
|
|
159
|
+
*
|
|
160
|
+
* ## Why this takes `Money` and not a number
|
|
161
|
+
*
|
|
162
|
+
* It used to take a bare `number`, which meant nothing could check the amount
|
|
163
|
+
* was denominated in `fx.sourceCurrency`. Applying a USD→BDT snapshot to a EUR
|
|
164
|
+
* amount type-checked, ran, and returned a plausible wrong number — the
|
|
165
|
+
* arithmetic is identical, only the meaning is wrong, so no test that asserts a
|
|
166
|
+
* total would notice. Taking `Money` lets the seam enforce its own
|
|
167
|
+
* precondition: a mismatch throws {@link CurrencyMismatchError}.
|
|
168
|
+
*
|
|
169
|
+
* ## Minor-unit exponents are handled
|
|
170
|
+
*
|
|
171
|
+
* The rate is quoted in MAJOR units (1 source = `rate` base), while `Money`
|
|
172
|
+
* holds minor units, and the two currencies need not share an exponent
|
|
173
|
+
* (USD has 2, JPY has 0). The conversion therefore goes
|
|
174
|
+
* `minor → major → ×rate → base minor`, not a naive `amount × rate`, which is
|
|
175
|
+
* off by 100× for any pair whose exponents differ.
|
|
176
|
+
*/
|
|
177
|
+
declare function convertWithSnapshot(source: Money, fx: FxSnapshot, options?: {
|
|
178
|
+
rounding?: FxRounding;
|
|
179
|
+
}): Money;
|
|
180
|
+
/**
|
|
181
|
+
* Reverse an {@link FxSnapshot} — reconstruct the source-currency amount
|
|
182
|
+
* from a stored base amount. Useful for reporting "cost in original
|
|
183
|
+
* currency". Not round-trip safe under rounding; prefer storing the
|
|
184
|
+
* original source amount alongside the base amount when audit fidelity
|
|
185
|
+
* matters.
|
|
186
|
+
*
|
|
187
|
+
* Symmetrically guarded: a base amount that is not in `fx.baseCurrency` throws
|
|
188
|
+
* rather than dividing by a rate that does not describe it.
|
|
189
|
+
*/
|
|
190
|
+
declare function reverseWithSnapshot(base: Money, fx: FxSnapshot, options?: {
|
|
191
|
+
rounding?: FxRounding;
|
|
192
|
+
}): Money;
|
|
193
|
+
/**
|
|
194
|
+
* Structural validation — checks shape only. Does NOT verify the rate is
|
|
195
|
+
* economically sensible (8.5 USD→BDT would pass this check). Host-level
|
|
196
|
+
* guards / anomaly detection handle that.
|
|
197
|
+
*/
|
|
198
|
+
declare function isFxSnapshot(value: unknown): value is FxSnapshot;
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region src/money/money.d.ts
|
|
201
|
+
/**
|
|
202
|
+
* Wire-format monetary amount: integer minor units, branded with a phantom
|
|
203
|
+
* currency type so calls like `formatBdt(usdValue)` fail at compile time.
|
|
204
|
+
*
|
|
205
|
+
* Use this for HTTP/JSON fields where transmitting a `Money` struct would be
|
|
206
|
+
* overkill: `interface Order { grandTotal: MinorUnits<'BDT'>; ... }`. Pair
|
|
207
|
+
* with the structured {@link Money} for in-process arithmetic.
|
|
208
|
+
*
|
|
209
|
+
* Erases at runtime — branded types have zero cost.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* type Paisa = MinorUnits<'BDT'>;
|
|
213
|
+
* type UsdCents = MinorUnits<'USD'>;
|
|
214
|
+
* const p: Paisa = MinorUnits(489900, 'BDT');
|
|
215
|
+
* const c: UsdCents = p; // ❌ type error: BDT vs USD
|
|
216
|
+
*/
|
|
217
|
+
declare const __unitBrand: unique symbol;
|
|
218
|
+
declare const __currencyBrand: unique symbol;
|
|
219
|
+
type MinorUnits<C extends string = string> = number & {
|
|
220
|
+
readonly [__unitBrand]: 'minor';
|
|
221
|
+
readonly [__currencyBrand]: C;
|
|
222
|
+
};
|
|
223
|
+
/**
|
|
224
|
+
* Cast a raw number to {@link MinorUnits}. Pass the currency as the second
|
|
225
|
+
* argument to fix the phantom type. With no second arg, `MinorUnits<string>`
|
|
226
|
+
* — useful for generic SDK boundaries that don't know the currency yet.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* MinorUnits(489900, 'BDT') // typed MinorUnits<'BDT'>
|
|
230
|
+
* MinorUnits(199, 'USD') // typed MinorUnits<'USD'>
|
|
231
|
+
*/
|
|
232
|
+
declare function MinorUnits<C extends string = string>(amount: number, _currency?: C): MinorUnits<C>;
|
|
233
|
+
/**
|
|
234
|
+
* A monetary amount stored as an integer in the currency's minor unit.
|
|
235
|
+
*
|
|
236
|
+
* Examples:
|
|
237
|
+
* - `{ amount: 19_99, currency: 'USD' }` — 19.99 USD (2 decimals)
|
|
238
|
+
* - `{ amount: 1_500, currency: 'JPY' }` — 1500 JPY (0 decimals)
|
|
239
|
+
* - `{ amount: 500_000, currency: 'KWD' }` — 500.000 KWD (3 decimals)
|
|
240
|
+
*
|
|
241
|
+
* Storing as integers avoids IEEE 754 rounding across arithmetic. Never store
|
|
242
|
+
* `Money.amount` as a float.
|
|
243
|
+
*
|
|
244
|
+
* ## `currency` is a branded {@link CurrencyCode}, not a string
|
|
245
|
+
*
|
|
246
|
+
* It was declared `CurrencyCode | string`, and a union with `string` collapses
|
|
247
|
+
* TO `string` — so the brand was decoration on the one type in the package
|
|
248
|
+
* where it matters, and `money()` validated nothing. Any string reached the
|
|
249
|
+
* field: `'jpy'`, `''`, `'US Dollar'`, a mistyped `'BTD'`. None of those are in
|
|
250
|
+
* {@link MINOR_UNIT_FACTOR}, so `minorUnitFactor` assumes two decimals for them
|
|
251
|
+
* and every JPY-style amount is 100× wrong while looking perfectly ordinary.
|
|
252
|
+
*
|
|
253
|
+
* Construct through {@link money} / {@link fromMajor} (which validate and
|
|
254
|
+
* brand) or through `currencyCode()` at your own boundary. A raw object literal
|
|
255
|
+
* `{ amount, currency: someString }` is now a compile error — deliberately, so
|
|
256
|
+
* the validation cannot be bypassed by the shape that skipped it before.
|
|
257
|
+
*/
|
|
258
|
+
interface Money {
|
|
259
|
+
readonly amount: number;
|
|
260
|
+
readonly currency: CurrencyCode;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Construct {@link Money} from an integer minor-unit amount.
|
|
264
|
+
*
|
|
265
|
+
* @throws TypeError if `amount` is not a finite integer.
|
|
266
|
+
* @throws InvalidCurrencyCodeError if `currency` is not ISO 4217 format.
|
|
267
|
+
*/
|
|
268
|
+
declare function money(amount: number, currency: string): Money;
|
|
269
|
+
/**
|
|
270
|
+
* Construct {@link Money} from a major-unit number (e.g. dollars, taka) by
|
|
271
|
+
* scaling to the currency's minor unit and rounding half-away-from-zero.
|
|
272
|
+
*
|
|
273
|
+
* Uses `toPrecision(15)` to strip the trailing-bit noise that appears in
|
|
274
|
+
* multiply-then-round (e.g. `0.1 + 0.2 = 0.30000000000000004`). This is
|
|
275
|
+
* enough for the vast majority of decimals encountered in real-world pricing.
|
|
276
|
+
*
|
|
277
|
+
* **Precision caveat.** A handful of decimals cannot be represented exactly
|
|
278
|
+
* in IEEE 754 and will round to the IEEE-closest integer rather than the
|
|
279
|
+
* mathematically-expected one — e.g. `1.005` is stored as `1.00499999…` and
|
|
280
|
+
* rounds to `100` minor units, not `101`. If you need decimal-exact rounding,
|
|
281
|
+
* construct money from a string or use integer minor units directly via
|
|
282
|
+
* {@link money}.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* fromMajor(19.99, 'USD') // { amount: 1999, currency: 'USD' }
|
|
286
|
+
* fromMajor(0.1 + 0.2, 'USD') // { amount: 30, currency: 'USD' }
|
|
287
|
+
* fromMajor(1500, 'JPY') // { amount: 1500, currency: 'JPY' }
|
|
288
|
+
*/
|
|
289
|
+
declare function fromMajor(major: number, currency: string): Money;
|
|
290
|
+
/** Convert {@link Money} back to its major-unit floating-point representation. */
|
|
291
|
+
declare function toMajor(m: Money): number;
|
|
292
|
+
declare function addMoney(a: Money, b: Money): Money;
|
|
293
|
+
declare function subtractMoney(a: Money, b: Money): Money;
|
|
294
|
+
/**
|
|
295
|
+
* Multiply money by a scalar. The result is rounded to an integer number of
|
|
296
|
+
* minor units using half-away-from-zero rounding.
|
|
297
|
+
*/
|
|
298
|
+
declare function multiplyMoney(m: Money, scalar: number): Money;
|
|
299
|
+
/** Sum a list of {@link Money}. Returns zero in the given currency for empty input. */
|
|
300
|
+
declare function sumMoney(values: readonly Money[], currency: string): Money;
|
|
301
|
+
/** Strict equality (amount and currency both match). */
|
|
302
|
+
declare function equalsMoney(a: Money, b: Money): boolean;
|
|
303
|
+
/**
|
|
304
|
+
* Ordering predicate. Returns -1, 0, or 1. Throws on currency mismatch — use
|
|
305
|
+
* {@link equalsMoney} for a total equality check across currencies.
|
|
306
|
+
*/
|
|
307
|
+
declare function compareMoney(a: Money, b: Money): -1 | 0 | 1;
|
|
308
|
+
declare function isZeroMoney(m: Money): boolean;
|
|
309
|
+
declare function isPositiveMoney(m: Money): boolean;
|
|
310
|
+
declare function isNegativeMoney(m: Money): boolean;
|
|
311
|
+
declare function negateMoney(m: Money): Money;
|
|
312
|
+
declare function absMoney(m: Money): Money;
|
|
313
|
+
/**
|
|
314
|
+
* Decimal major-unit number → integer minor units by decimal exponent.
|
|
315
|
+
* Rounding: raw IEEE `Math.round` — half-up toward +∞ (so `-10.5 → -10`),
|
|
316
|
+
* documented above. Throws when the result leaves the safe-integer range
|
|
317
|
+
* (also catches `NaN`/`Infinity` inputs).
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* majorToMinorUnits(10.5) // 1050
|
|
321
|
+
* majorToMinorUnits(1000, 0) // 1000 (JPY-style)
|
|
322
|
+
* majorToMinorUnits(1.005) // 100 — IEEE, see note above
|
|
323
|
+
*/
|
|
324
|
+
declare function majorToMinorUnits(major: number, minorUnitDecimals?: number): number;
|
|
325
|
+
/** Integer minor units → decimal major-unit number by decimal exponent: `1050 → 10.5`. */
|
|
326
|
+
declare function minorUnitsToMajor(minor: number, minorUnitDecimals?: number): number;
|
|
327
|
+
/**
|
|
328
|
+
* Percentage of an integer minor-unit amount → integer minor units.
|
|
329
|
+
* EXACT multiply-then-round: `Math.round((minor × ratePercent) / 100)` —
|
|
330
|
+
* one rounding point, half-up toward +∞. Handles genuinely fractional rates
|
|
331
|
+
* (QST 9.975 %) without precision loss: `percentOfMinor(20000, 9.975) === 1995`.
|
|
332
|
+
*
|
|
333
|
+
* Hosts whose domain guarantees at-most-2-decimal rates MAY snap the rate to
|
|
334
|
+
* integer basis points BEFORE calling (be-prod's `#shared/money` seam does) —
|
|
335
|
+
* that is host policy layered on top, not part of this primitive.
|
|
336
|
+
*/
|
|
337
|
+
declare function percentOfMinor(minor: number, ratePercent: number): number;
|
|
338
|
+
/**
|
|
339
|
+
* Integer minor units → plain decimal string (no currency symbol, no locale):
|
|
340
|
+
* `formatMinorUnits(10550) → "105.50"`, `formatMinorUnits(1000, 0) → "1000"`.
|
|
341
|
+
* For localized currency strings use `Intl.NumberFormat` at the display layer.
|
|
342
|
+
*/
|
|
343
|
+
declare function formatMinorUnits(minor: number, minorUnitDecimals?: number): string;
|
|
344
|
+
/** Structural type guard. */
|
|
345
|
+
declare function isMoney(value: unknown): value is Money;
|
|
346
|
+
//#endregion
|
|
347
|
+
export { convertWithSnapshot as A, CURRENCY_PATTERN as C, FxSnapshot as D, FxRounding as E, minorUnitFactor as F, reverseWithSnapshot as I, toCurrencyCode as L, isCurrencyCode as M, isFxSnapshot as N, InvalidCurrencyCodeError as O, isKnownCurrency as P, CURRENCIES as S, CurrencyMismatchError as T, negateMoney as _, compareMoney as a, sumMoney as b, fromMajor as c, isPositiveMoney as d, isZeroMoney as f, multiplyMoney as g, money as h, addMoney as i, currencyCode as j, MINOR_UNIT_FACTOR as k, isMoney as l, minorUnitsToMajor as m, Money as n, equalsMoney as o, majorToMinorUnits as p, absMoney as r, formatMinorUnits as s, MinorUnits as t, isNegativeMoney as u, percentOfMinor as v, CurrencyCode as w, toMajor as x, subtractMoney as y };
|
package/dist/money.d.mts
CHANGED
|
@@ -1,141 +1,2 @@
|
|
|
1
|
-
import { r as
|
|
2
|
-
|
|
3
|
-
//#region src/money/money.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* Wire-format monetary amount: integer minor units, branded with a phantom
|
|
6
|
-
* currency type so calls like `formatBdt(usdValue)` fail at compile time.
|
|
7
|
-
*
|
|
8
|
-
* Use this for HTTP/JSON fields where transmitting a `Money` struct would be
|
|
9
|
-
* overkill: `interface Order { grandTotal: MinorUnits<'BDT'>; ... }`. Pair
|
|
10
|
-
* with the structured {@link Money} for in-process arithmetic.
|
|
11
|
-
*
|
|
12
|
-
* Erases at runtime — branded types have zero cost.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* type Paisa = MinorUnits<'BDT'>;
|
|
16
|
-
* type UsdCents = MinorUnits<'USD'>;
|
|
17
|
-
* const p: Paisa = MinorUnits(489900, 'BDT');
|
|
18
|
-
* const c: UsdCents = p; // ❌ type error: BDT vs USD
|
|
19
|
-
*/
|
|
20
|
-
declare const __unitBrand: unique symbol;
|
|
21
|
-
declare const __currencyBrand: unique symbol;
|
|
22
|
-
type MinorUnits<C extends string = string> = number & {
|
|
23
|
-
readonly [__unitBrand]: 'minor';
|
|
24
|
-
readonly [__currencyBrand]: C;
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Cast a raw number to {@link MinorUnits}. Pass the currency as the second
|
|
28
|
-
* argument to fix the phantom type. With no second arg, `MinorUnits<string>`
|
|
29
|
-
* — useful for generic SDK boundaries that don't know the currency yet.
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* MinorUnits(489900, 'BDT') // typed MinorUnits<'BDT'>
|
|
33
|
-
* MinorUnits(199, 'USD') // typed MinorUnits<'USD'>
|
|
34
|
-
*/
|
|
35
|
-
declare function MinorUnits<C extends string = string>(amount: number, _currency?: C): MinorUnits<C>;
|
|
36
|
-
/**
|
|
37
|
-
* A monetary amount stored as an integer in the currency's minor unit.
|
|
38
|
-
*
|
|
39
|
-
* Examples:
|
|
40
|
-
* - `{ amount: 19_99, currency: 'USD' }` — 19.99 USD (2 decimals)
|
|
41
|
-
* - `{ amount: 1_500, currency: 'JPY' }` — 1500 JPY (0 decimals)
|
|
42
|
-
* - `{ amount: 500_000, currency: 'KWD' }` — 500.000 KWD (3 decimals)
|
|
43
|
-
*
|
|
44
|
-
* Storing as integers avoids IEEE 754 rounding across arithmetic. Never store
|
|
45
|
-
* `Money.amount` as a float.
|
|
46
|
-
*/
|
|
47
|
-
interface Money {
|
|
48
|
-
readonly amount: number;
|
|
49
|
-
readonly currency: CurrencyCode | string;
|
|
50
|
-
}
|
|
51
|
-
declare class CurrencyMismatchError extends Error {
|
|
52
|
-
readonly name = "CurrencyMismatchError";
|
|
53
|
-
readonly left: string;
|
|
54
|
-
readonly right: string;
|
|
55
|
-
constructor(left: string, right: string);
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Construct {@link Money} from an integer minor-unit amount.
|
|
59
|
-
*
|
|
60
|
-
* @throws TypeError if `amount` is not a finite integer.
|
|
61
|
-
*/
|
|
62
|
-
declare function money(amount: number, currency: string): Money;
|
|
63
|
-
/**
|
|
64
|
-
* Construct {@link Money} from a major-unit number (e.g. dollars, taka) by
|
|
65
|
-
* scaling to the currency's minor unit and rounding half-away-from-zero.
|
|
66
|
-
*
|
|
67
|
-
* Uses `toPrecision(15)` to strip the trailing-bit noise that appears in
|
|
68
|
-
* multiply-then-round (e.g. `0.1 + 0.2 = 0.30000000000000004`). This is
|
|
69
|
-
* enough for the vast majority of decimals encountered in real-world pricing.
|
|
70
|
-
*
|
|
71
|
-
* **Precision caveat.** A handful of decimals cannot be represented exactly
|
|
72
|
-
* in IEEE 754 and will round to the IEEE-closest integer rather than the
|
|
73
|
-
* mathematically-expected one — e.g. `1.005` is stored as `1.00499999…` and
|
|
74
|
-
* rounds to `100` minor units, not `101`. If you need decimal-exact rounding,
|
|
75
|
-
* construct money from a string or use integer minor units directly via
|
|
76
|
-
* {@link money}.
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* fromMajor(19.99, 'USD') // { amount: 1999, currency: 'USD' }
|
|
80
|
-
* fromMajor(0.1 + 0.2, 'USD') // { amount: 30, currency: 'USD' }
|
|
81
|
-
* fromMajor(1500, 'JPY') // { amount: 1500, currency: 'JPY' }
|
|
82
|
-
*/
|
|
83
|
-
declare function fromMajor(major: number, currency: string): Money;
|
|
84
|
-
/** Convert {@link Money} back to its major-unit floating-point representation. */
|
|
85
|
-
declare function toMajor(m: Money): number;
|
|
86
|
-
declare function addMoney(a: Money, b: Money): Money;
|
|
87
|
-
declare function subtractMoney(a: Money, b: Money): Money;
|
|
88
|
-
/**
|
|
89
|
-
* Multiply money by a scalar. The result is rounded to an integer number of
|
|
90
|
-
* minor units using half-away-from-zero rounding.
|
|
91
|
-
*/
|
|
92
|
-
declare function multiplyMoney(m: Money, scalar: number): Money;
|
|
93
|
-
/** Sum a list of {@link Money}. Returns zero in the given currency for empty input. */
|
|
94
|
-
declare function sumMoney(values: readonly Money[], currency: string): Money;
|
|
95
|
-
/** Strict equality (amount and currency both match). */
|
|
96
|
-
declare function equalsMoney(a: Money, b: Money): boolean;
|
|
97
|
-
/**
|
|
98
|
-
* Ordering predicate. Returns -1, 0, or 1. Throws on currency mismatch — use
|
|
99
|
-
* {@link equalsMoney} for a total equality check across currencies.
|
|
100
|
-
*/
|
|
101
|
-
declare function compareMoney(a: Money, b: Money): -1 | 0 | 1;
|
|
102
|
-
declare function isZeroMoney(m: Money): boolean;
|
|
103
|
-
declare function isPositiveMoney(m: Money): boolean;
|
|
104
|
-
declare function isNegativeMoney(m: Money): boolean;
|
|
105
|
-
declare function negateMoney(m: Money): Money;
|
|
106
|
-
declare function absMoney(m: Money): Money;
|
|
107
|
-
/**
|
|
108
|
-
* Decimal major-unit number → integer minor units by decimal exponent.
|
|
109
|
-
* Rounding: raw IEEE `Math.round` — half-up toward +∞ (so `-10.5 → -10`),
|
|
110
|
-
* documented above. Throws when the result leaves the safe-integer range
|
|
111
|
-
* (also catches `NaN`/`Infinity` inputs).
|
|
112
|
-
*
|
|
113
|
-
* @example
|
|
114
|
-
* majorToMinorUnits(10.5) // 1050
|
|
115
|
-
* majorToMinorUnits(1000, 0) // 1000 (JPY-style)
|
|
116
|
-
* majorToMinorUnits(1.005) // 100 — IEEE, see note above
|
|
117
|
-
*/
|
|
118
|
-
declare function majorToMinorUnits(major: number, minorUnitDecimals?: number): number;
|
|
119
|
-
/** Integer minor units → decimal major-unit number by decimal exponent: `1050 → 10.5`. */
|
|
120
|
-
declare function minorUnitsToMajor(minor: number, minorUnitDecimals?: number): number;
|
|
121
|
-
/**
|
|
122
|
-
* Percentage of an integer minor-unit amount → integer minor units.
|
|
123
|
-
* EXACT multiply-then-round: `Math.round((minor × ratePercent) / 100)` —
|
|
124
|
-
* one rounding point, half-up toward +∞. Handles genuinely fractional rates
|
|
125
|
-
* (QST 9.975 %) without precision loss: `percentOfMinor(20000, 9.975) === 1995`.
|
|
126
|
-
*
|
|
127
|
-
* Hosts whose domain guarantees at-most-2-decimal rates MAY snap the rate to
|
|
128
|
-
* integer basis points BEFORE calling (be-prod's `#shared/money` seam does) —
|
|
129
|
-
* that is host policy layered on top, not part of this primitive.
|
|
130
|
-
*/
|
|
131
|
-
declare function percentOfMinor(minor: number, ratePercent: number): number;
|
|
132
|
-
/**
|
|
133
|
-
* Integer minor units → plain decimal string (no currency symbol, no locale):
|
|
134
|
-
* `formatMinorUnits(10550) → "105.50"`, `formatMinorUnits(1000, 0) → "1000"`.
|
|
135
|
-
* For localized currency strings use `Intl.NumberFormat` at the display layer.
|
|
136
|
-
*/
|
|
137
|
-
declare function formatMinorUnits(minor: number, minorUnitDecimals?: number): string;
|
|
138
|
-
/** Structural type guard. */
|
|
139
|
-
declare function isMoney(value: unknown): value is Money;
|
|
140
|
-
//#endregion
|
|
1
|
+
import { T as CurrencyMismatchError, _ 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, 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, x as toMajor, y as subtractMoney } from "./money-DPo9fLBi.mjs";
|
|
141
2
|
export { CurrencyMismatchError, MinorUnits, Money, absMoney, addMoney, compareMoney, equalsMoney, formatMinorUnits, fromMajor, isMoney, isNegativeMoney, isPositiveMoney, isZeroMoney, majorToMinorUnits, minorUnitsToMajor, money, multiplyMoney, negateMoney, percentOfMinor, subtractMoney, sumMoney, toMajor };
|