{"_id":"@autotelic/effect-safe-money","_rev":"2-746ebea71d41012c95d79ae69f3b1214","name":"@autotelic/effect-safe-money","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@autotelic/effect-safe-money","version":"0.1.0","license":"UNLICENSED","_id":"@autotelic/effect-safe-money@0.1.0","maintainers":[{"name":"autotelic","email":"info+npm@autotelic.com"}],"dist":{"shasum":"f70e812b10cb74b598d393268e4fd6461dbb9f89","tarball":"https://registry.npmjs.org/@autotelic/effect-safe-money/-/effect-safe-money-0.1.0.tgz","fileCount":58,"integrity":"sha512-c+Ln1R6yhsGGgfndcGcl2Z5MP4FY3eHMnXMrbgwHoqIljT6X8NbI5GcnDvpwJYd1gv2wL51rvKeQSa5Xy1aRdg==","signatures":[{"sig":"MEYCIQCqSTQ0r1pZb70+YsAKwrU4z2u6QMrWnWOMU79GSFZKbAIhAP5lGQS7Qe1y/GuTCRfuL+YfqXeK0xPwwpwiIzwDfffk","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":530084},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"505d36b281d93f82339d71bcafea3e6bf194a94e","scripts":{"lint":"oxlint","test":"vitest run","build":"tsc -p tsconfig.build.json","format":"oxfmt","lint:fix":"oxlint --fix","typecheck":"tsc --noEmit","test:watch":"vitest","format:check":"oxfmt --check","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"autotelic","email":"info+npm@autotelic.com"},"_npmVersion":"11.17.0","description":"Safe-money operations built on Effect v4 (RC), linted with oxlint, formatted with oxfmt.","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.33.0","devDependencies":{"oxfmt":"0.64.0","oxlint":"1.79.0","vitest":"4.1.11","fast-check":"^4.9.0","typescript":"5.9.3","@types/node":"24.13.3","@oxlint/plugins":"1.79.0","@autotelic/plumb":"^0.3.2","@vitest/coverage-v8":"4.1.11"},"peerDependencies":{"effect":"^4.0.0-rc.110"},"_npmOperationalInternal":{"tmp":"tmp/effect-safe-money_0.1.0_1787604766072_0.4098919042504019","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@autotelic/effect-safe-money","version":"0.2.0","description":"Safe-money operations built on Effect v4 (RC), linted with oxlint, formatted with oxfmt.","license":"UNLICENSED","publishConfig":{"access":"public"},"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./oxlint-plugin":{"types":"./dist/oxlint-plugin/index.d.ts","import":"./dist/oxlint-plugin/index.js"},"./oxlint-plugin/presets/*":"./dist/oxlint-plugin/presets/*"},"scripts":{"build":"tsc -p tsconfig.build.json && node scripts/copy-presets.mjs","typecheck":"tsc --noEmit","lint":"oxlint","lint:fix":"oxlint --fix","format":"oxfmt","format:check":"oxfmt --check","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage"},"dependencies":{"@oxlint/plugins":"1.79.0"},"peerDependencies":{"effect":"^4.0.0-rc.110"},"devDependencies":{"@autotelic/plumb":"^0.3.2","@oxlint/plugins":"1.79.0","@types/node":"24.13.3","effect":"4.0.0-rc.110","@vitest/coverage-v8":"4.1.11","fast-check":"^4.9.0","oxfmt":"0.64.0","oxlint":"1.79.0","typescript":"5.9.3","vitest":"4.1.11"},"engines":{"node":">=20"},"packageManager":"pnpm@10.33.0","_id":"@autotelic/effect-safe-money@0.2.0","gitHead":"302916bb3199aa176c63c540aec06aead65915c1","_nodeVersion":"22.23.2","_npmVersion":"10.9.8","dist":{"integrity":"sha512-t49y60wilovxOGCEFEFeLO85MN1VQSXbYpGfzn1pakGeZTZFACkRj0UlGgUriFdIx//FXayMiKiwF+hwtS6R8w==","shasum":"643af684442a7ec0f89f25b0b483580b0dd298ac","tarball":"https://registry.npmjs.org/@autotelic/effect-safe-money/-/effect-safe-money-0.2.0.tgz","fileCount":157,"unpackedSize":677170,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDcKrb5xXmvnQJGM7wUPJKsTtKcnuyfs2x0D9+l7L9bFQIgJgDd14mrp4jZ3r2hzD0y2zmPxu5bpAELp/0x7d/tlNg="}]},"_npmUser":{"name":"autotelic","email":"info+npm@autotelic.com"},"directories":{},"maintainers":[{"name":"autotelic","email":"info+npm@autotelic.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/effect-safe-money_0.2.0_1787607350232_0.908668398375734"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-24T20:52:45.937Z","modified":"2026-08-24T21:35:50.573Z","0.1.0":"2026-08-24T20:52:46.241Z","0.2.0":"2026-08-24T21:35:50.374Z"},"license":"UNLICENSED","description":"Safe-money operations built on Effect v4 (RC), linted with oxlint, formatted with oxfmt.","maintainers":[{"name":"autotelic","email":"info+npm@autotelic.com"}],"readme":"<div align=\"center\">\n\n# @effect-safe-money\n\n**Type-safe money for TypeScript, built on Effect v4**\n\nExact arithmetic · Typed errors · Property-tested against a proven Haskell reference\n\n</div>\n\n---\n\n`@effect-safe-money` brings **correct-by-construction money types** to TypeScript.\nAmounts are exact bigint rationals under the hood — never floating point — and every operation that could overflow, lose precision, or mix currencies fails with a **typed error in the Effect channel**, not an exception at 2am.\n\nThe domain model is a faithful port of [`really-safe-money`](https://github.com/NorfairKing/really-safe-money), a battle-tested Haskell library, and its test suite is verified spec-for-spec against the upstream property tests.\n\n## ✨ Why\n\n- **No floating point.** Money is counted in exact minor units (`bigint`, Word64-domain) and converted through exact rationals — `0.1 + 0.2` is never `0.30000000000000004` here.\n- **Typed failures, not exceptions.** Overflow, negative results, non-representable decimals and currency mismatches surface as typed errors (`ValueOutOfDomainError`, `InvalidInputError`, …) in the `Effect` error channel. Arithmetic can neither create nor destroy money.\n- **Parse, don't validate.** Invalid states are unrepresentable: branded nominal types (`Amount`, `Currency`, `ConversionRate`), tagged enums (`Account.Positive | Account.Negative`), and total constructors at the boundary.\n- **Effect-native.** Built on [Effect](https://effect.website) v4 — integrates with `Schema`, `Option`, `Order`, `Brand`, and `effect/testing` property testing out of the box.\n- **Proven test suite.** Every upstream spec is ported and mapped; a generated ledger tracks coverage down to individual properties ([551 / 553 matched](docs/reference-test-coverage.md)).\n\n## 💡 Philosophy\n\nThe design follows Robert Vollmert's reference blog post, [_How to deal with money in software_](https://cs-syd.eu/posts/2022-08-22-how-to-deal-with-money-in-software), which distills years of production experience into rules for handling money correctly:\n\n> _\"…the only way to deal with money in software is to use exact rational numbers.\"_\n\nConcretely, this means:\n\n1. **Never use floating point for money.** Amounts live in exact minor units; conversion goes through exact rationals.\n2. **Separate magnitude from currency.** An `Amount` is pure count; `AmountOf` pins it to a `Currency`; `MultiAmount` keeps per-currency balances honest.\n3. **Make rounding explicit.** Conversion reports whether it rounded (`RoundedDown | DidNotRound | RoundedUp`) — loss is never silent.\n4. **Parse, don't validate.** Strings become `DecimalLiteral`s once, at the edge of the system, or fail immediately; afterwards every value is valid by construction.\n5. **Typed errors instead of panics.** Operations that cannot honor their contract fail in the type system's error channel, keeping arithmetic total.\n\n## 📦 Installation\n\n```sh\npnpm add effect-safe-money\n```\n\nRequires Node ≥ 20 and Effect `4.x`.\n\n## 🚀 Usage\n\n```ts\nimport { Effect } from \"effect\";\nimport { Amount, Currency, MultiAmount, QuantisationFactor } from \"effect-safe-money\";\n\nconst program = Effect.gen(function* () {\n  const qfUsd = yield* QuantisationFactor.fromWord32(100); // cents, exactly\n  const usd = Currency.make({ symbol: \"USD\", quantisationFactor: qfUsd });\n\n  const five = Amount.fromMinimalQuantisations(500n); // 5.00 USD, exactly\n  const three = Amount.fromMinimalQuantisations(300n);\n\n  const total = yield* Amount.add(five, three); // Effect<Amount, ValueOutOfDomainError>\n  const split = Amount.distribute(total, 3); // exact, remainder-aware splitting\n\n  return Amount.format(qfUsd, total); // \"8.00\"\n});\n```\n\nMulti-currency balances are first-class — amounts are keyed by currency and sums only succeed when every currency lines up:\n\n```ts\nconst wallet = yield * MultiAmount.add(eurBalance, usdBalance); // fails on currency mismatch\n\nconst conversion = MultiAmount.convertAll(\n  \"RoundNearest\",\n  qfEur,\n  rateProvider, // (from, to) => Option<ConversionRate>\n  wallet,\n);\nconst [converted, rounded] = yield * conversion;\n// `rounded` reports RoundedDown | DidNotRound | RoundedUp — loss is never silent\n```\n\nEvery fallible function comes in two flavors:\n\n| Flavor      | Returns                       | Use when                                  |\n| ----------- | ----------------------------- | ----------------------------------------- |\n| `add`       | `Effect<Amount, DomainError>` | You want failures in the Effect channel   |\n| `addOption` | `Option<Amount>`              | You want a pure, synchronous escape hatch |\n\n## 🧭 Paired lint rules\n\nInstalling the package brings its oxlint plugin with it — no second install. The plugin catches\n\"holding it wrong\" (family A `api/*`: deterministic checks on this library's own API) and points\nout what to change when migrating foreign money code onto it (family B `migration/*`: tiered\nconversion smells, from errors down to a warn-only radar).\n\nWire it into `oxlint.config.ts`:\n\n```ts\nexport default defineConfig({\n  jsPlugins: [\n    { name: \"safe-money\", specifier: \"@autotelic/effect-safe-money/oxlint-plugin\" },\n  ],\n  rules: {\n    // family A — correct use of the API; always an error\n    \"safe-money/api/no-double-roundtrip\": \"error\",\n    \"safe-money/api/no-minor-unit-literals\": \"error\",\n    \"safe-money/api/no-unhandled-effect-result\": \"error\",\n    \"safe-money/api/no-unsafe-constructor\": \"error\",\n    \"safe-money/api/require-currency-at-boundary\": \"error\",\n    // family B — migration funnel; pick a tier per rule\n    \"safe-money/migration/no-float-minor-units-conversion\": \"error\",\n    \"safe-money/migration/no-string-money-comparison\": \"error\",\n    \"safe-money/migration/bare-minor-unit-conversion\": \"warn\",\n    // ...\n  },\n});\n```\n\nPresets are included as JSON if you'd rather opt in wholesale:\n[`presets/recommended.json`](./src/oxlint-plugin/presets/recommended.json) (tier-1 error,\ntier-2 warn), [`presets/radar.json`](./src/oxlint-plugin/presets/radar.json) (everything warn),\nand [`presets/strict.json`](./src/oxlint-plugin/presets/strict.json). See\n[docs/proposal-shipped-lint-plugins.md](docs/proposal-shipped-lint-plugins.md) for the full rule\ncatalogue and severity doctrine.\n\n## 🧱 Modules\n\n| Module               | Description                                                           |\n| -------------------- | --------------------------------------------------------------------- |\n| `Amount`             | Non-negative Word64 counts of minimal quantisations — the atomic unit |\n| `Account`            | Signed balances as `Positive \\| Negative` tagged values               |\n| `AmountOf`           | An `Amount` pinned to a `Currency`                                    |\n| `Currency`           | Symbol + quantisation factor (e.g. `\"USD\"`, `100`)                    |\n| `QuantisationFactor` | Word32 minor-unit divisor per currency                                |\n| `ConversionRate`     | Positive rational exchange rates; invertible and composable           |\n| `MultiAmount`        | Balances across currencies with explicit rounding reports             |\n| `DecimalLiteral`     | Exact decimal literals with Word64/Int64 bounds                       |\n| `Rational`           | Bigint rationals with invalid-ratio semantics preserved               |\n\n## ✅ Correctness\n\nThe test suite is a direct port of [`really-safe-money`](https://github.com/NorfairKing/really-safe-money)'s hspec/validity property tests onto `effect/testing` (fast-check). Each test file cites its upstream spec, and a generated ledger — [`docs/reference-test-coverage.md`](docs/reference-test-coverage.md) — tracks every property individually: **551 of 553 upstream properties matched**, plus additional cases covering TypeScript-specific semantics.\n\n```sh\npnpm test              # full suite\npnpm test:coverage     # with v8 coverage\n```\n\n## 🛠️ Toolchain\n\nThis repo dogfoods the oxc toolchain end to end:\n\n- **[oxlint](https://oxc.rs)** — Rust-based linting (`oxlint.config.ts`), including vendored plumb rules\n- **[oxfmt](https://oxc.rs)** — Rust-based formatting (`.oxfmtrc.json`)\n- **TypeScript 5.9** strict · **Vitest 4**\n\n| Command             | Description               |\n| ------------------- | ------------------------- |\n| `pnpm typecheck`    | Type-check with `tsc`     |\n| `pnpm lint`         | Lint with oxlint          |\n| `pnpm format`       | Format with oxfmt         |\n| `pnpm format:check` | Verify formatting         |\n| `pnpm test`         | Run the test suite        |\n| `pnpm build`        | Emit `dist/` + type decls |\n\n## 🙏 Acknowledgements\n\n- [`really-safe-money`](https://github.com/NorfairKing/really-safe-money) — the Haskell reference implementation whose specs define correctness here\n- Robert Vollmert — [_How to deal with money in software_](https://cs-syd.eu/posts/2022-08-22-how-to-deal-with-money-in-software), the reference blog post behind the domain model\n- Alexis King — [_Parse, don't validate_](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/)\n- The [Effect](https://effect.website) and [Oxc](https://oxc.rs) teams\n\n## License\n\nUNLICENSED — private project. All rights reserved.\n","readmeFilename":"README.md"}