@amboras-dev/money 0.1.0 → 0.3.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.
Files changed (80) hide show
  1. package/README.md +25 -10
  2. package/dist/ai.cjs +12 -13
  3. package/dist/ai.d.cts +5 -9
  4. package/dist/ai.d.ts +5 -9
  5. package/dist/ai.js +12 -13
  6. package/dist/{allocate-D7tm_t_o.cjs → allocate-AusholrC.cjs} +3 -4
  7. package/dist/{allocate-CdnhvAda.js → allocate-BBRfdaSC.js} +3 -4
  8. package/dist/{amount-DHtvZSSm.cjs → amount-CwFzcxUl.cjs} +29 -12
  9. package/dist/{amount-D7dj76nH.js → amount-DHFOK2Yd.js} +29 -12
  10. package/dist/{arithmetic-C-Q_aI49.js → arithmetic-C97-YafD.js} +4 -5
  11. package/dist/{arithmetic-Bq-8mSal.cjs → arithmetic-D8A6IeZ0.cjs} +4 -5
  12. package/dist/billing.cjs +5 -8
  13. package/dist/billing.d.cts +1 -9
  14. package/dist/billing.d.ts +1 -9
  15. package/dist/billing.js +5 -8
  16. package/dist/commerce.cjs +15 -23
  17. package/dist/commerce.d.cts +2 -16
  18. package/dist/commerce.d.ts +2 -16
  19. package/dist/commerce.js +15 -23
  20. package/dist/{format-BnA1aLJk.cjs → format-92fzvTXY.cjs} +21 -6
  21. package/dist/{format-BdB7LEIp.js → format-BJC513u4.js} +16 -7
  22. package/dist/fx.cjs +12 -18
  23. package/dist/fx.d.cts +5 -15
  24. package/dist/fx.d.ts +5 -15
  25. package/dist/fx.js +12 -18
  26. package/dist/index.cjs +13 -12
  27. package/dist/index.d.cts +28 -22
  28. package/dist/index.d.ts +28 -22
  29. package/dist/index.js +11 -13
  30. package/dist/input.cjs +7 -10
  31. package/dist/input.d.cts +9 -16
  32. package/dist/input.d.ts +9 -16
  33. package/dist/input.js +7 -10
  34. package/dist/{iso-minor--gQWI1f2.cjs → iso-minor-DyNjT9A1.cjs} +2 -3
  35. package/dist/{iso-minor-CtaiIifs.js → iso-minor-odkY-e6I.js} +2 -3
  36. package/dist/{medusa-OJ2TJOxt.cjs → medusa-CzVK0bth.cjs} +24 -4
  37. package/dist/{medusa-DuQ3-KoD.d.cts → medusa-DofdvIgQ.d.cts} +11 -4
  38. package/dist/{medusa-RwqxaDGq.d.ts → medusa-DrbYmD2Y.d.ts} +11 -4
  39. package/dist/{medusa-CmxCni-G.js → medusa-pKGlImn9.js} +19 -5
  40. package/dist/{money-CbhsT0fB.d.cts → money-BkMeYDEW.d.cts} +19 -9
  41. package/dist/{money-CbhsT0fB.d.ts → money-BkMeYDEW.d.ts} +19 -9
  42. package/dist/{money-CIgELekG.js → money-BxbAw7BP.js} +25 -21
  43. package/dist/{money-D5-vlv0h.cjs → money-DWBjuuEK.cjs} +42 -20
  44. package/dist/{number-text-C82oh6JB.js → number-text-Bj-gZXiF.js} +3 -4
  45. package/dist/{number-text-BpvN6NAo.cjs → number-text-CWuVwrlg.cjs} +3 -4
  46. package/dist/{own-oP9DXFWv.cjs → own-C5imDkyB.cjs} +1 -2
  47. package/dist/{own-D7544D3k.js → own-DHxqIYqL.js} +1 -2
  48. package/dist/{percent-CKVI_c-m.js → percent-CB3Ixjc_.js} +2 -3
  49. package/dist/{percent-Dt8vNIAW.cjs → percent-DABHiSBs.cjs} +2 -3
  50. package/dist/{rate-CmeTGNex.d.ts → rate-BvvZUdSu.d.ts} +1 -3
  51. package/dist/{rate-toyS3EMf.d.cts → rate-CEakW3ia.d.cts} +1 -3
  52. package/dist/{rate-DGPfB_jc.cjs → rate-CEjWTATc.cjs} +3 -4
  53. package/dist/{rate-lLPViBuM.js → rate-D6S2_wgZ.js} +3 -4
  54. package/dist/{reporting-5xby-Zas.d.ts → reporting-DFNyDBdi.d.cts} +2 -4
  55. package/dist/{reporting-DEM97DhQ.d.cts → reporting-jIBILvnr.d.ts} +2 -4
  56. package/dist/{retail-BwcFJcVN.d.ts → retail-B04CjW3b.d.ts} +1 -3
  57. package/dist/{retail-DTk2XAMM.cjs → retail-CYGPU4ea.cjs} +4 -5
  58. package/dist/{retail-Mqd-sPMF.d.cts → retail-CmEyJQOH.d.cts} +1 -3
  59. package/dist/{retail-BsvpNv1F.js → retail-DXgs11ov.js} +4 -5
  60. package/dist/{rounding-oZ832yTD.cjs → rounding-4QKTRbMQ.cjs} +2 -3
  61. package/dist/{rounding-CVAPEfq_.js → rounding-CA1PVP3b.js} +2 -3
  62. package/dist/sql.cjs +9 -15
  63. package/dist/sql.d.cts +2 -12
  64. package/dist/sql.d.ts +2 -12
  65. package/dist/sql.js +9 -15
  66. package/dist/testing.cjs +72675 -12
  67. package/dist/testing.d.cts +46 -11
  68. package/dist/testing.d.ts +46 -11
  69. package/dist/testing.js +72674 -13
  70. package/dist/{wire-CI3Sm1iC.js → wire-B4QU8O3W.js} +584 -114
  71. package/dist/wire-CVNicirK.d.ts +428 -0
  72. package/dist/wire-CtNJmWVP.d.cts +428 -0
  73. package/dist/{wire-DQZ6fA3c.cjs → wire-u2tmHY0Y.cjs} +649 -113
  74. package/dist/wire.cjs +12 -1
  75. package/dist/wire.d.cts +2 -2
  76. package/dist/wire.d.ts +2 -2
  77. package/dist/wire.js +1 -2
  78. package/package.json +4 -3
  79. package/dist/wire-Cd4Q1Jmm.d.ts +0 -269
  80. package/dist/wire-hWuixv3w.d.cts +0 -269
package/README.md CHANGED
@@ -121,8 +121,10 @@ corrupt one), `sameCurrency`, `medusaCurrencyCode` (the only sanctioned lower-ca
121
121
  pinned to `@medusajs/utils` 2.13.4). `CurrencyInfo` also carries `symbolAliases`, `localeHint` and `commaDecimal` (TND: its decimal mark is a comma, so `'12,500'` is 12.5).
122
122
 
123
123
  **Medusa shapes**: `readStored` reads `.value ?? .raw.value ?? .raw_.value`, then
124
- `.numeric ?? .numeric_`, then the primitive. `readStoredField` (`raw_<key>` first, no fallback
124
+ `.numeric ?? .numeric_`, then the primitive (`precision: 'round'` rounds a fractional amount
125
+ half-up to a whole storage unit instead of refusing it). `readStoredField` (`raw_<key>` first, no fallback
125
126
  when it is unreadable), `readStoredDecimal` (keeps fractions, canonical text),
127
+ `readStoredLiteral` (the amount literal verbatim, for SQL and ledger columns),
126
128
  `readCalculatedPrice`, `toMedusaRaw` (never an exponent), `toMedusaAmountInput` (`'0'` for zero),
127
129
  `moneyByCurrency` / `moneyFor` (per-currency settings and price lists), `isPositiveAmount`.
128
130
 
@@ -144,12 +146,15 @@ unit apart).
144
146
 
145
147
  **Decimal text**: `parseDecimalText`, `isDecimalLiteral` (the one storage-literal grammar),
146
148
  `assertDecimalLiteral`, `trimDecimalZeros`, `decimalSum`, `decimalSubtract`, `decimalMultiply`,
147
- `decimalDivide`, `decimalRound`, `decimalCompare`, `decimalClamp`, `decimalWithin`. Exact; `''`
148
- and exponents are refused.
149
+ `decimalDivide`, `decimalRound`, `decimalCompare`, `decimalClamp`, `decimalWithin`, `decimalEquals`
150
+ (never throws: a non-decimal is never equal). Exact; `''` and exponents are refused.
149
151
 
150
152
  **Formatting** (for people, not byte-stable across ICU): `formatMoney` (display digits, or
151
153
  `widen` / `trim-zero`; explicit locale, default `en-US`), `formatMoneyCompact`,
152
- `formatMoneyRange`, `moneyAxisFormatter`.
154
+ `formatMoneyRange`, `moneyAxisFormatter`, `formatStored` (a possibly fractional stored amount,
155
+ rounded half-up to a storage unit, then `formatMoney`). `rounding: 'down'` rounds toward zero
156
+ instead, for an upper bound that must never read above itself (a refund cap of HUF 435050 shows
157
+ `HUF 4,350`, not 4,351).
153
158
 
154
159
  Internals other entry points build on (never exported), each the only implementation of its concept:
155
160
  `internal/number-text.ts` `readNumberText` (the one separator engine), `internal/rational.ts`
@@ -165,14 +170,20 @@ import { money } from '@amboras-dev/money'
165
170
  import { ISO_MINOR_V2, conventionOf, decodeWire, encodeWire } from '@amboras-dev/money/wire'
166
171
 
167
172
  encodeWire('stripe', money(120000, 'JPY'), { convention: ISO_MINOR_V2 }) // 1200
168
- decodeWire('stripe', 1200, 'JPY', conventionOf(paymentIntent)) // { amount: 120000, currency: 'JPY' }
173
+ decodeWire('stripe', 1200, 'JPY', { convention: conventionOf(paymentIntent) }) // { amount: 120000, currency: 'JPY' }
174
+ decodeWire('stripe', -44, 'USD', { convention: ISO_MINOR_V2, allowNegative: true }) // signed balance read
175
+ encodeWire('mollie', { amount: '107.8175', currency: 'USD' }) // '1.08': a fractional stored total
169
176
  ```
170
177
 
171
- - Codec: `encodeWire`, `decodeWire`, `tryDecodeWire` (never throws), `wireFactor`, `pspWireDigits`, `oneWireUnit`, `wireBreakdown` (line items that sum to the encoded total), `wireKeyPart`.
178
+ A Medusa session amount can be fractional at storage scale (107.8175). `encodeWire` and `sameWireAmount` take it as a `FractionalMoney` and round it by the PSP's own rule: Stripe and Adyen to a whole storage unit first, whole-unit PSPs (MoMo, Payoo, VNPay, Paystack XOF) refuse it, a pre-cutover Razorpay payment refuses it, every other PSP rounds once, half-up, at its wire unit.
179
+
180
+ - Codec: `encodeWire`, `decodeWire` (one options object `{ convention, allowNegative }`; a negative provider amount throws unless `allowNegative: true`), `tryDecodeWire` (never throws), `wireFactor`, `pspWireDigits`, `oneWireUnit`, `wireBreakdown` (line items that sum to the encoded total), `wireKeyPart`.
172
181
  - Cutover marker: `WIRE_SCALE_KEY`, `WIRE_SCALE_ISO_MINOR_V2`, `WIRE_SCALE_MARKER`, `ISO_MINOR_V2`, `PRE_CUTOVER`, `conventionOf` (gated PSPs need the convention; there is no hidden default).
173
- - Provider tables: `PSP_IDS`, `pspSupportsCurrency`, `assertPspCurrency`, `pspCurrencies`, `pspDefaultCurrency`, `pspMinimumAmount`.
174
- - Comparison: `sameWireAmount` (a webhook amount against the expected Money, at storage scale).
175
- - Provider quirks: `verificationCharge`, `RAZORPAY_AFA_FREE_CAP`, `iyzicoPrice`, `authorizeNetReversibleAmount`, `jsonDecimalLiteral`.
182
+ - Wire-rounding marker: `WIRE_ROUNDING_KEY`, `WIRE_ROUNDING_EXACT`, `WIRE_ROUNDING_MARKER` (stamped on new sessions of the PSPs Medusa encoded in floating point before 0.2.0), `mayHoldLegacyFloatRounding` (no marker: the session predates that deploy).
183
+ - Provider tables: `PSP_IDS`, `pspSupportsCurrency`, `assertPspCurrency` (typed `PspCurrency<P>`: `AuthorizeNetCurrency`, `IyzicoCurrency`, `PaystackCurrency`, `'VND'` for the short fixed lists; `PAYSTACK_CURRENCIES` is Paystack's list as a tuple for validators and pickers), `pspCurrencies` (what the codec scales), `pspDefaultCurrency` (single-currency PSPs only), `pspHomeCurrency` (the currency every account charges without enabling anything: Razorpay INR, iyzico TRY, the VND rails), `pspHomeCountry` (the one country every account is registered in: iyzico TR, the VND rails VN), `pspMinimumAmount`.
184
+ - Presentment (eligibility): `pspPresentmentCurrencies(psp, { accountCountry, includeOptIn })` (what an account can charge buyers in), `pspPresentment(psp)` (the rules as data: union, currencies needing an account country, account countries limited to a list, Paystack's per-country lists with USD as an explicit opt-in, and `optInCompanions`), `pspAccountCountryForCurrency` (Paystack NGN names NG; USD names nothing).
185
+ - Comparison: `sameWireAmount` (a webhook amount against the expected Money or FractionalMoney, exactly at storage scale; `acceptLegacyIsoDigits`, `textOnly`, and `acceptLegacyFloatRounding` for the spelling Medusa's pre-0.2.0 float codec sent PayPal, Mollie, Airwallex, iyzico, Authorize.Net and Payoo: USD 1072.5 went as `'10.72'`, 0.2.0 sends `'10.73'`; pass `mayHoldLegacyFloatRounding(sessionData)`), and `legacyFloatRefundClamp` (a refund of such a capture lowered to what the PSP still holds, after any partial refunds: `'10.00'` where Medusa's remaining encodes `'10.01'`).
186
+ - Provider quirks: `verificationCharge` (currency optional for Razorpay, INR only, and Mollie, EUR fallback), `RAZORPAY_AFA_FREE_CAP`, `iyzicoPrice`, `authorizeNetReversibleAmount`, `jsonDecimalLiteral`.
176
187
  - Provider text: `parseProviderAmount` (carrier quotes, dispute amounts; `decimalSeparator: 'auto'` is the `/input` rule).
177
188
  - Destinations: `destinationAmount` (pixels, ad platforms, email tools), `feedPrice`, `googleMicros`, `eventMoney` (ClickHouse `Int64` contract).
178
189
  - Plugin slots: `slotMoney`, `readSlotMoney`.
@@ -253,6 +264,7 @@ inputStep('KWD') // '0.001'
253
264
  - `parseAmountInput(text, currency, { locale, decimalSeparator, allowNegative, allowZero, min, max, original })`: strips the currency's symbols and code and refuses more fraction digits than the currency stores.
254
265
  Without `locale` (the chat wizard and any field that has none) it reads the `'auto'` rule below, which is today's admin `parseMajorUnits` except the listed differences, and a currency storing 0 digits rounds a fraction half-up to a whole unit (`'1200.5'` JPY is 1201), as admin does.
255
266
  With `locale` it reads that locale's separators: the group separator is valid only in thousands groups (`'1,234,567'`) or Indian lakh groups (`'12,34,567'`: three digits on the right, then pairs), and only when every group fits (`'1,2345'` and `'12,3,456'` are `unparseable`), and extra digits are `too_many_decimals` in every currency.
267
+ The locale's decimal separator is never read as grouping: in a currency storing 0 digits, en-US `'1.234'`, `'19.999'` and `'1.000'` JPY are `too_many_decimals` (a thousands group to some, a fraction to others, and trailing zeros would let `'1.000'` through as ¥1); grouping uses the locale's group separator (`'1,234'` en-US, `'1.234'` de-DE).
256
268
  **Every number input (`<input type="number">`) must pass `{ decimalSeparator: '.' }`**: browsers always serialise its value with a `.` decimal and no grouping, so the free-text rule would misread it (TND and IQD `'1.500'` are 1,500 there, 1.5 here).
257
269
  In `'.'` mode `.` is the decimal, `,` and grouping are `unparseable`, a zero whole part may be omitted (`'.5'` is 0.5), and a currency storing 0 digits rounds a fraction half-up to a whole unit, as admin `numericEntryToMinorUnits` does (`'1200.5'` JPY is 1201).
258
270
  It equals that function on every `.`-only value within the storage digits (`test/input/input.test.ts`), except one class:
@@ -263,7 +275,7 @@ inputStep('KWD') // '0.001'
263
275
  Two amounts have no such spelling and read back only through `original`: IQD with a three-digit fraction (`'1.125'` reads as 1,125, since IQD displays no decimals) and a sub-unit amount of a currency storing 0 digits (`'1200.5'` JPY reads as a whole 1,201).
264
276
  With `locale` it uses that locale's separators and the storage digits (`'12,50'` de-DE); `digits: 'storage'` or `'trim-zero'` overrides the zeros either way.
265
277
  With `decimalSeparator: '.'` (a number input's prefill) it prints `.` as the decimal, never groups and drops trailing zeros (TND 1500 is `'1.5'`), so `parseAmountInput(text, currency, { decimalSeparator: '.' })` reads it back exactly (a sub-unit JPY amount through `original`).
266
- Prefill and save with `parseAmountInput(text, currency, { original: m })`: the untouched prefill text, or any text equal in value to `m`, returns `m` unchanged, so saving an untouched field never rewrites the amount.
278
+ Prefill and save with `parseAmountInput(text, currency, { original: m })`: the untouched prefill text, or any text equal in value to `m`, returns `m` unchanged, so saving an untouched field never rewrites the amount (an ambiguous zero-digit text such as en-US `'1.000'` JPY is refused even then).
267
279
  - `inputStep`, `inputPlaceholder`: from the storage digits, never a hardcoded `0.01`.
268
280
  - `parseImportAmount(text, { decimalSeparator: 'auto' | '.' | ',', currency, allowNegative, allowTrailingText })`: CSV, supplier, scraped and carrier text to `DecimalText` (major units), or `null`.
269
281
  `'auto'` is the only comma/dot rule in the package (`/ai` `moneyMentions`, `/wire` `parseProviderAmount` and `moneyTemplateOf` use it too), and it treats `.` and `,` the same way:
@@ -310,6 +322,8 @@ Framework-agnostic: no test runner or fast-check dependency.
310
322
  `/wire`'s `encodeWire` must reproduce every vector (decision D2: wire bytes feed idempotency keys, Redis keys and HMAC strings).
311
323
  A positive amount whose wire rounds to 0 (68 vectors: one storage unit of a zero-decimal currency at Stripe, Adyen, Razorpay, Airwallex, PayPal HUF/JPY/TWD, Mollie ISK/JPY) is a refusal vector by package policy: `encodeWire` refuses it with `wire_refused` (`... rounds to 0 at <Tag> ...`).
312
324
  Medusa today puts 0 on the wire at initiate for Stripe and Adyen, and for Razorpay, PayPal and Airwallex everywhere; it refuses at refund and capture for Stripe and Adyen, and in Mollie, iyzico and Authorize.Net.
325
+ - `PSP_FRACTIONAL_GOLDEN_VECTORS`: the same codecs fed fractional storage amounts (0.4, 0.5, ties, 107.8175, a fraction of a dong); `policy` and `medusa` mark where `encodeWire` departs from them on purpose (the sub-unit refusal, exact half-up at ties float `toFixed` rounds down, the real amount in a refusal message).
326
+ - `PSP_LEGACY_FLOAT_GOLDEN_VECTORS`: for the PSPs Medusa encoded in floating point before 0.2.0, what those codecs sent at whole, fractional and tie amounts, and their ISO-digit spelling; `sameWireAmount`'s `acceptLegacyFloatRounding` accepts every one.
313
327
  - `DESTINATION_GOLDEN_VECTORS`: what `destinationAmount`, `feedPrice` and `googleMicros` must give per destination.
314
328
  - `checkWireDigits(psp, documented)`: the capability-consistency probe; returns mismatch messages.
315
329
  - `normalizeIntlSpaces(s)`, `sampleMoney(currency, major)`.
@@ -331,6 +345,7 @@ The full list, with the decision behind each, is in [`docs/money/API-SPEC.md`](.
331
345
  `formatMoney`, `moneyView` (`/ai`), `destinationAmount` and `feedPrice` (`/wire`) use the display digits; capability-kit, the plugins and Medusa's `formatMoney` use the storage digits today.
332
346
  This is deliberate: it is what the PSPs charge and what people read.
333
347
  - `formatMoney` (default `digits: 'display'`) rounds half-up: HUF 435050 was `HUF 4,350.50` and is `HUF 4,351`; IQD 1500 was `IQD 1.500` and is `IQD 2`.
348
+ A cap ("refund at most") passes `rounding: 'down'`, so HUF 435050 shows `HUF 4,350` and typing the amount shown stays within it.
334
349
  - Catalog feeds and destinations drop the sub-unit the same way: `feedPrice(money(435050, 'HUF'))` is `'4351 HUF'`, IQD 1500 is `'2 IQD'`.
335
350
  - Model views keep the value but show fewer zeros: `moneyView` returns the exact `amount` and widens `display` only for a sub-unit value (`'HUF 4,350.5'`), so HUF 435000 is `'HUF 4,350'` where capability-kit shows `'HUF 4,350.00'`, and IQD 1500 is `'IQD 1.5'` where it shows `'IQD 1.500'`.
336
351
  - Machine files that need storage precision use `toMajorDecimal(m)` or `formatAmountInput(m)`.
package/dist/ai.cjs CHANGED
@@ -1,9 +1,9 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_money = require("./money-D5-vlv0h.cjs");
3
- const require_format = require("./format-BnA1aLJk.cjs");
4
- const require_number_text = require("./number-text-BpvN6NAo.cjs");
5
- const require_amount = require("./amount-DHtvZSSm.cjs");
6
- //#region src/ai/mentions.ts
2
+ const require_money = require("./money-DWBjuuEK.cjs");
3
+ const require_format = require("./format-92fzvTXY.cjs");
4
+ const require_number_text = require("./number-text-CWuVwrlg.cjs");
5
+ const require_amount = require("./amount-CwFzcxUl.cjs");
6
+ // src/ai/mentions.ts
7
7
  const ACTIVE = require_money.CURRENCIES.filter((c) => c.status === "active");
8
8
  const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
9
  const LETTER_START = /^\p{L}/u;
@@ -84,8 +84,7 @@ function moneyMentions(text, options = {}) {
84
84
  }
85
85
  return mentions;
86
86
  }
87
- //#endregion
88
- //#region src/ai/wording.ts
87
+ // src/ai/wording.ts
89
88
  /** The one unit sentence every money field carries, for the model and for people. Verbatim. */
90
89
  const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
91
90
  /** JSON Schema keyword marking a money field. Verbatim. */
@@ -99,8 +98,7 @@ function moneyFieldDescription(describe) {
99
98
  const lead = typeof describe === "string" ? describe.trim() : "";
100
99
  return lead ? `${lead} ${MONEY_UNIT_SENTENCE}` : MONEY_UNIT_SENTENCE;
101
100
  }
102
- //#endregion
103
- //#region src/ai/model-money.ts
101
+ // src/ai/model-money.ts
104
102
  /**
105
103
  * `{ amount: 19.99, currency: 'USD', display: '$19.99' }`. `amount` is exact; `display` shows the
106
104
  * display digits, widened to keep a stored value finer than them (JPY 550 is `'¥5.5'`, HUF 435050
@@ -126,7 +124,7 @@ function failure(reason, input, code, digits) {
126
124
  error: `${{
127
125
  empty: `Not an amount: ${shown}.`,
128
126
  unparseable: `Not an amount: ${shown}.`,
129
- too_many_decimals: `${shown} has more decimal places than ${code} uses (at most ${digits}).`,
127
+ too_many_decimals: digits === 0 ? `${shown} has more decimal places than ${code} uses (at most 0). Write thousands with ',' or without a separator (1,500 or 1500).` : `${shown} has more decimal places than ${code} uses (at most ${digits}).`,
130
128
  negative: "The amount must not be negative.",
131
129
  zero: "The amount must be more than zero.",
132
130
  below_min: `${shown} is out of range for ${code}.`,
@@ -148,8 +146,10 @@ function fromNumber(input, currency, options) {
148
146
  * An amount from a model tool call (major units, as the merchant says it) as Money, or an
149
147
  * error to send back to the model ending with `MONEY_UNIT_SENTENCE`. Numbers go through
150
148
  * `fromMajor` (float noise such as 0.1 + 0.2 allowed); strings through `/input`'s entry rule
151
- * in English (`'1,299.00'`, `'$1,299'`, `'99.000'` VND, `'1.500'` JPY). More fraction digits
152
- * than the currency stores and negative amounts are refused, never rounded or flipped. An
149
+ * in English (`'1,299.00'`, `'$1,299'`, `'99,000'` VND). More fraction digits than the
150
+ * currency stores and negative amounts are refused, never rounded or flipped; so is a `.` before
151
+ * three digits in a currency without decimals (`'99.000'` VND, `'1.500'` JPY: a thousands group
152
+ * or a fraction?), and the error asks the model for `1,500` or `1500`. An
153
153
  * unknown currency throws `currency_invalid` (it is the tool's, not the model's).
154
154
  */
155
155
  function parseModelMoney(input, currency, options = {}) {
@@ -172,7 +172,6 @@ function parseModelMoney(input, currency, options = {}) {
172
172
  money: result.money
173
173
  } : failure(result.reason, input, info.code, info.storageDigits);
174
174
  }
175
- //#endregion
176
175
  exports.MONEY_META_KEY = MONEY_META_KEY;
177
176
  exports.MONEY_UNIT_SENTENCE = MONEY_UNIT_SENTENCE;
178
177
  exports.RATE_UNIT_SENTENCE = RATE_UNIT_SENTENCE;
package/dist/ai.d.cts CHANGED
@@ -1,5 +1,4 @@
1
- import { L as DecimalText, h as CurrencyCode, r as Money } from "./money-CbhsT0fB.cjs";
2
- //#region src/ai/mentions.d.ts
1
+ import { R as DecimalText, g as CurrencyCode, i as Money } from "./money-BkMeYDEW.cjs";
3
2
  /** One price found in text. `code` is set when the mark names one currency; `amount` when the number reads (see the separator engine's 'auto' rule). */
4
3
  interface MoneyMention {
5
4
  /** The matched text, mark and number. */
@@ -43,8 +42,6 @@ export declare function moneyLiteralPattern(options?: {
43
42
  export declare function moneyMentions(text: string, options?: {
44
43
  currency?: string;
45
44
  }): MoneyMention[];
46
- //#endregion
47
- //#region src/ai/model-money.d.ts
48
45
  /** A stored amount as the model sees it: the major amount, the code and a display string. */
49
46
  interface MoneyView {
50
47
  readonly amount: number;
@@ -76,13 +73,13 @@ interface ParseModelMoneyOptions {
76
73
  * An amount from a model tool call (major units, as the merchant says it) as Money, or an
77
74
  * error to send back to the model ending with `MONEY_UNIT_SENTENCE`. Numbers go through
78
75
  * `fromMajor` (float noise such as 0.1 + 0.2 allowed); strings through `/input`'s entry rule
79
- * in English (`'1,299.00'`, `'$1,299'`, `'99.000'` VND, `'1.500'` JPY). More fraction digits
80
- * than the currency stores and negative amounts are refused, never rounded or flipped. An
76
+ * in English (`'1,299.00'`, `'$1,299'`, `'99,000'` VND). More fraction digits than the
77
+ * currency stores and negative amounts are refused, never rounded or flipped; so is a `.` before
78
+ * three digits in a currency without decimals (`'99.000'` VND, `'1.500'` JPY: a thousands group
79
+ * or a fraction?), and the error asks the model for `1,500` or `1500`. An
81
80
  * unknown currency throws `currency_invalid` (it is the tool's, not the model's).
82
81
  */
83
82
  export declare function parseModelMoney(input: unknown, currency: string, options?: ParseModelMoneyOptions): ParseModelMoneyResult;
84
- //#endregion
85
- //#region src/ai/wording.d.ts
86
83
  /** The one unit sentence every money field carries, for the model and for people. Verbatim. */
87
84
  export declare const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
88
85
  /** JSON Schema keyword marking a money field. Verbatim. */
@@ -93,5 +90,4 @@ export declare const STORAGE_UNIT_SENTENCE = "Store API amounts are storage unit
93
90
  export declare const RATE_UNIT_SENTENCE = "1 unit of the store currency equals this many units of the other currency.";
94
91
  /** A money field's description: `describe` (trimmed) followed by the unit sentence, or the sentence alone. */
95
92
  export declare function moneyFieldDescription(describe?: string): string;
96
- //#endregion
97
93
  export type { MoneyMention, MoneyView, ParseModelMoneyOptions, ParseModelMoneyResult };
package/dist/ai.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { L as DecimalText, h as CurrencyCode, r as Money } from "./money-CbhsT0fB.js";
2
- //#region src/ai/mentions.d.ts
1
+ import { R as DecimalText, g as CurrencyCode, i as Money } from "./money-BkMeYDEW.js";
3
2
  /** One price found in text. `code` is set when the mark names one currency; `amount` when the number reads (see the separator engine's 'auto' rule). */
4
3
  interface MoneyMention {
5
4
  /** The matched text, mark and number. */
@@ -43,8 +42,6 @@ export declare function moneyLiteralPattern(options?: {
43
42
  export declare function moneyMentions(text: string, options?: {
44
43
  currency?: string;
45
44
  }): MoneyMention[];
46
- //#endregion
47
- //#region src/ai/model-money.d.ts
48
45
  /** A stored amount as the model sees it: the major amount, the code and a display string. */
49
46
  interface MoneyView {
50
47
  readonly amount: number;
@@ -76,13 +73,13 @@ interface ParseModelMoneyOptions {
76
73
  * An amount from a model tool call (major units, as the merchant says it) as Money, or an
77
74
  * error to send back to the model ending with `MONEY_UNIT_SENTENCE`. Numbers go through
78
75
  * `fromMajor` (float noise such as 0.1 + 0.2 allowed); strings through `/input`'s entry rule
79
- * in English (`'1,299.00'`, `'$1,299'`, `'99.000'` VND, `'1.500'` JPY). More fraction digits
80
- * than the currency stores and negative amounts are refused, never rounded or flipped. An
76
+ * in English (`'1,299.00'`, `'$1,299'`, `'99,000'` VND). More fraction digits than the
77
+ * currency stores and negative amounts are refused, never rounded or flipped; so is a `.` before
78
+ * three digits in a currency without decimals (`'99.000'` VND, `'1.500'` JPY: a thousands group
79
+ * or a fraction?), and the error asks the model for `1,500` or `1500`. An
81
80
  * unknown currency throws `currency_invalid` (it is the tool's, not the model's).
82
81
  */
83
82
  export declare function parseModelMoney(input: unknown, currency: string, options?: ParseModelMoneyOptions): ParseModelMoneyResult;
84
- //#endregion
85
- //#region src/ai/wording.d.ts
86
83
  /** The one unit sentence every money field carries, for the model and for people. Verbatim. */
87
84
  export declare const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
88
85
  /** JSON Schema keyword marking a money field. Verbatim. */
@@ -93,5 +90,4 @@ export declare const STORAGE_UNIT_SENTENCE = "Store API amounts are storage unit
93
90
  export declare const RATE_UNIT_SENTENCE = "1 unit of the store currency equals this many units of the other currency.";
94
91
  /** A money field's description: `describe` (trimmed) followed by the unit sentence, or the sentence alone. */
95
92
  export declare function moneyFieldDescription(describe?: string): string;
96
- //#endregion
97
93
  export type { MoneyMention, MoneyView, ParseModelMoneyOptions, ParseModelMoneyResult };
package/dist/ai.js CHANGED
@@ -1,8 +1,8 @@
1
- import { E as resolveCurrency, Q as describeValue, Z as MoneyError, dt as toDecimalText, l as toMajorNumber, m as currencyMarks, n as fromMajor, s as storedDecimal, u as CURRENCIES } from "./money-CIgELekG.js";
2
- import { t as formatMoney } from "./format-BdB7LEIp.js";
3
- import { n as readNumberText, t as digitGroupingSource } from "./number-text-C82oh6JB.js";
4
- import { s as parseAmountInput } from "./amount-D7dj76nH.js";
5
- //#region src/ai/mentions.ts
1
+ import { $ as describeValue, E as resolveCurrency, Q as MoneyError, l as toMajorNumber, m as currencyMarks, n as fromMajor, pt as toDecimalText, s as storedDecimal, u as CURRENCIES } from "./money-BxbAw7BP.js";
2
+ import { t as formatMoney } from "./format-BJC513u4.js";
3
+ import { n as readNumberText, t as digitGroupingSource } from "./number-text-Bj-gZXiF.js";
4
+ import { s as parseAmountInput } from "./amount-DHFOK2Yd.js";
5
+ // src/ai/mentions.ts
6
6
  const ACTIVE = CURRENCIES.filter((c) => c.status === "active");
7
7
  const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8
8
  const LETTER_START = /^\p{L}/u;
@@ -83,8 +83,7 @@ function moneyMentions(text, options = {}) {
83
83
  }
84
84
  return mentions;
85
85
  }
86
- //#endregion
87
- //#region src/ai/wording.ts
86
+ // src/ai/wording.ts
88
87
  /** The one unit sentence every money field carries, for the model and for people. Verbatim. */
89
88
  const MONEY_UNIT_SENTENCE = "Amount in normal currency units as the merchant says it (99 means 99.00; 1.5 means 1.50). Never cents or minor units.";
90
89
  /** JSON Schema keyword marking a money field. Verbatim. */
@@ -98,8 +97,7 @@ function moneyFieldDescription(describe) {
98
97
  const lead = typeof describe === "string" ? describe.trim() : "";
99
98
  return lead ? `${lead} ${MONEY_UNIT_SENTENCE}` : MONEY_UNIT_SENTENCE;
100
99
  }
101
- //#endregion
102
- //#region src/ai/model-money.ts
100
+ // src/ai/model-money.ts
103
101
  /**
104
102
  * `{ amount: 19.99, currency: 'USD', display: '$19.99' }`. `amount` is exact; `display` shows the
105
103
  * display digits, widened to keep a stored value finer than them (JPY 550 is `'¥5.5'`, HUF 435050
@@ -125,7 +123,7 @@ function failure(reason, input, code, digits) {
125
123
  error: `${{
126
124
  empty: `Not an amount: ${shown}.`,
127
125
  unparseable: `Not an amount: ${shown}.`,
128
- too_many_decimals: `${shown} has more decimal places than ${code} uses (at most ${digits}).`,
126
+ too_many_decimals: digits === 0 ? `${shown} has more decimal places than ${code} uses (at most 0). Write thousands with ',' or without a separator (1,500 or 1500).` : `${shown} has more decimal places than ${code} uses (at most ${digits}).`,
129
127
  negative: "The amount must not be negative.",
130
128
  zero: "The amount must be more than zero.",
131
129
  below_min: `${shown} is out of range for ${code}.`,
@@ -147,8 +145,10 @@ function fromNumber(input, currency, options) {
147
145
  * An amount from a model tool call (major units, as the merchant says it) as Money, or an
148
146
  * error to send back to the model ending with `MONEY_UNIT_SENTENCE`. Numbers go through
149
147
  * `fromMajor` (float noise such as 0.1 + 0.2 allowed); strings through `/input`'s entry rule
150
- * in English (`'1,299.00'`, `'$1,299'`, `'99.000'` VND, `'1.500'` JPY). More fraction digits
151
- * than the currency stores and negative amounts are refused, never rounded or flipped. An
148
+ * in English (`'1,299.00'`, `'$1,299'`, `'99,000'` VND). More fraction digits than the
149
+ * currency stores and negative amounts are refused, never rounded or flipped; so is a `.` before
150
+ * three digits in a currency without decimals (`'99.000'` VND, `'1.500'` JPY: a thousands group
151
+ * or a fraction?), and the error asks the model for `1,500` or `1500`. An
152
152
  * unknown currency throws `currency_invalid` (it is the tool's, not the model's).
153
153
  */
154
154
  function parseModelMoney(input, currency, options = {}) {
@@ -171,5 +171,4 @@ function parseModelMoney(input, currency, options = {}) {
171
171
  money: result.money
172
172
  } : failure(result.reason, input, info.code, info.storageDigits);
173
173
  }
174
- //#endregion
175
174
  export { MONEY_META_KEY, MONEY_UNIT_SENTENCE, RATE_UNIT_SENTENCE, STORAGE_UNIT_SENTENCE, currencySymbolAliases, moneyFieldDescription, moneyLiteralPattern, moneyMentions, moneyView, parseModelMoney };
@@ -1,6 +1,6 @@
1
- const require_money = require("./money-D5-vlv0h.cjs");
2
- const require_rounding = require("./rounding-oZ832yTD.cjs");
3
- //#region src/allocate.ts
1
+ const require_money = require("./money-DWBjuuEK.cjs");
2
+ const require_rounding = require("./rounding-4QKTRbMQ.cjs");
3
+ // src/allocate.ts
4
4
  const invalid = (message) => new require_money.MoneyError("amount_invalid", message);
5
5
  const sumOf = (values) => values.reduce((a, b) => a + b, 0n);
6
6
  const floorShares = (units, weights, total) => weights.map((w) => require_money.divRound(units * w, total, "floor"));
@@ -195,7 +195,6 @@ function splitUnitPrice(net, quantity, options = {}) {
195
195
  });
196
196
  return parts;
197
197
  }
198
- //#endregion
199
198
  Object.defineProperty(exports, "allocate", {
200
199
  enumerable: true,
201
200
  get: function() {
@@ -1,6 +1,6 @@
1
- import { Q as describeValue, Y as mulDiv, Z as MoneyError, dt as toDecimalText, it as fractionDigitsOf, lt as roundToScale, nt as divRound, s as storedDecimal, t as boundedMoney, z as decimalOf } from "./money-CIgELekG.js";
2
- import { a as unitSize } from "./rounding-CVAPEfq_.js";
3
- //#region src/allocate.ts
1
+ import { $ as describeValue, B as decimalOf, Q as MoneyError, X as mulDiv, at as fractionDigitsOf, dt as roundToScale, pt as toDecimalText, rt as divRound, s as storedDecimal, t as boundedMoney } from "./money-BxbAw7BP.js";
2
+ import { a as unitSize } from "./rounding-CA1PVP3b.js";
3
+ // src/allocate.ts
4
4
  const invalid = (message) => new MoneyError("amount_invalid", message);
5
5
  const sumOf = (values) => values.reduce((a, b) => a + b, 0n);
6
6
  const floorShares = (units, weights, total) => weights.map((w) => divRound(units * w, total, "floor"));
@@ -195,5 +195,4 @@ function splitUnitPrice(net, quantity, options = {}) {
195
195
  });
196
196
  return parts;
197
197
  }
198
- //#endregion
199
198
  export { splitUnitPrice as i, allocateUnits as n, allocateWaterfall as r, allocate as t };
@@ -1,7 +1,7 @@
1
- const require_money = require("./money-D5-vlv0h.cjs");
2
- const require_arithmetic = require("./arithmetic-Bq-8mSal.cjs");
3
- const require_number_text = require("./number-text-BpvN6NAo.cjs");
4
- //#region src/input/amount.ts
1
+ const require_money = require("./money-DWBjuuEK.cjs");
2
+ const require_arithmetic = require("./arithmetic-D8A6IeZ0.cjs");
3
+ const require_number_text = require("./number-text-CWuVwrlg.cjs");
4
+ // src/input/amount.ts
5
5
  /** Internal: the exact signed decimal a successful separator-engine read holds. */
6
6
  function decimalOfText(read) {
7
7
  return {
@@ -69,6 +69,15 @@ function echoesOriginal(shown, options) {
69
69
  grouped: true
70
70
  });
71
71
  }
72
+ /**
73
+ * In a currency storing 0 digits, the locale's decimal separator before exactly three digits
74
+ * after a whole part of at most three (en-US `'1.234'`, `'19.999'`, `'1.000'` JPY) reads as a
75
+ * thousands group to some people and as a fraction to others; trailing zeros would even let
76
+ * `'1.000'` through as 1 yen. Neither reading is safe, so a locale field refuses it.
77
+ */
78
+ function readsAsGroupOrFraction(read, info) {
79
+ return info.storageDigits === 0 && read.decimalMark !== null && read.scale === 3 && read.units < 1000000n;
80
+ }
72
81
  /** Which reading a field's options ask for; `decimalSeparator` and `locale` together, or a `decimalSeparator` other than `'.'`, throw. */
73
82
  function entryMode(options) {
74
83
  if (options.decimalSeparator === void 0) return options.locale === void 0 ? "auto" : "locale";
@@ -92,17 +101,18 @@ const ENTRY_SEPARATORS = {
92
101
  * against `,` (see `parseImportAmount`): `'29,99'` EUR is 29.99, `'1.299,00'` and `'1,299'`
93
102
  * are 1299, KWD `'1,5'` is 1.5; a currency storing 0 digits rounds a fraction half-up to a
94
103
  * whole unit (`'1200.5'` JPY is 1201).
95
- * - With `locale`: its decimal separator is the decimal and its group separator is valid only in
96
- * 3-digit (or Indian lakh) groups; in a currency that stores no fraction (JPY, VND) a separator
97
- * before exactly three digits in grouping position is grouping (`'1.500'` JPY is 1500).
98
- * en-US `'29,99'` USD is `'unparseable'`; de-DE `'29,99'` EUR is 29.99; en-US `'1200.5'` JPY
99
- * is `'too_many_decimals'`.
104
+ * - With `locale`: its decimal separator is always the decimal and its group separator is valid
105
+ * only in 3-digit (or Indian lakh) groups. In a currency that stores no fraction (JPY, VND) the
106
+ * decimal separator before exactly three digits is ambiguous (en-US `'1.234'` JPY could be
107
+ * 1,234 or 1.234) and is `'too_many_decimals'`, never read as grouping; grouping takes the
108
+ * locale's group separator (`'1,234'` en-US, `'1.234'` de-DE). en-US `'29,99'` USD is
109
+ * `'unparseable'`; de-DE `'29,99'` EUR is 29.99; en-US `'1200.5'` JPY is `'too_many_decimals'`.
100
110
  * - With `decimalSeparator: '.'` (every `<input type="number">`): `.` is the decimal, nothing
101
111
  * groups (`'1,299'` is `'unparseable'`), TND and IQD `'1.500'` are 1.5, and a currency storing
102
112
  * 0 digits rounds a fraction half-up to a whole unit (`'1.5'` JPY is 2).
103
113
  * Otherwise more fraction digits than the currency stores is `'too_many_decimals'`, never rounded
104
114
  * (`'1,2345'` USD). Text equal to `original` returns it unchanged (JPY 120050 shown as
105
- * `'1200.50'`). An unknown currency, or an `original` in another currency, throws.
115
+ * `'1200.50'`), except the ambiguous zero-digit text above. An unknown currency, or an `original` in another currency, throws.
106
116
  */
107
117
  function parseAmountInput(text, currency, options = {}) {
108
118
  const info = require_money.resolveCurrency(currency, { allowHistoric: true });
@@ -112,7 +122,10 @@ function parseAmountInput(text, currency, options = {}) {
112
122
  let value;
113
123
  if (echoesOriginal(shown, options)) value = require_money.storedDecimal(options.original).value;
114
124
  else {
115
- const separators = mode === "locale" ? localeSeparators(options.locale) : ENTRY_SEPARATORS[mode];
125
+ const separators = mode === "locale" ? {
126
+ ...localeSeparators(options.locale),
127
+ wholeCurrencyGrouping: false
128
+ } : ENTRY_SEPARATORS[mode];
116
129
  const read = require_number_text.readNumberText(text, {
117
130
  ...separators,
118
131
  currency: info.code
@@ -122,6 +135,11 @@ function parseAmountInput(text, currency, options = {}) {
122
135
  reason: read.reason,
123
136
  text: shown
124
137
  };
138
+ if (mode === "locale" && readsAsGroupOrFraction(read, info)) return {
139
+ ok: false,
140
+ reason: "too_many_decimals",
141
+ text: shown
142
+ };
125
143
  value = decimalOfText(read);
126
144
  }
127
145
  const result = entryMoney(value, info, {
@@ -196,7 +214,6 @@ function inputStep(currency) {
196
214
  function inputPlaceholder(currency) {
197
215
  return PLACEHOLDERS[require_money.resolveCurrency(currency, { allowHistoric: true }).storageDigits];
198
216
  }
199
- //#endregion
200
217
  Object.defineProperty(exports, "decimalOfText", {
201
218
  enumerable: true,
202
219
  get: function() {
@@ -1,7 +1,7 @@
1
- import { E as resolveCurrency, K as trimDecimalZeros, M as resolveLocale, N as separatorsOf, Z as MoneyError, c as toMajorDecimal, et as compareDecimal, it as fractionDigitsOf, n as fromMajor, rt as formatDecimal, s as storedDecimal } from "./money-CIgELekG.js";
2
- import { r as compare } from "./arithmetic-C-Q_aI49.js";
3
- import { n as readNumberText } from "./number-text-C82oh6JB.js";
4
- //#region src/input/amount.ts
1
+ import { E as resolveCurrency, M as resolveLocale, N as separatorsOf, Q as MoneyError, at as fractionDigitsOf, c as toMajorDecimal, it as formatDecimal, n as fromMajor, q as trimDecimalZeros, s as storedDecimal, tt as compareDecimal } from "./money-BxbAw7BP.js";
2
+ import { r as compare } from "./arithmetic-C97-YafD.js";
3
+ import { n as readNumberText } from "./number-text-Bj-gZXiF.js";
4
+ // src/input/amount.ts
5
5
  /** Internal: the exact signed decimal a successful separator-engine read holds. */
6
6
  function decimalOfText(read) {
7
7
  return {
@@ -69,6 +69,15 @@ function echoesOriginal(shown, options) {
69
69
  grouped: true
70
70
  });
71
71
  }
72
+ /**
73
+ * In a currency storing 0 digits, the locale's decimal separator before exactly three digits
74
+ * after a whole part of at most three (en-US `'1.234'`, `'19.999'`, `'1.000'` JPY) reads as a
75
+ * thousands group to some people and as a fraction to others; trailing zeros would even let
76
+ * `'1.000'` through as 1 yen. Neither reading is safe, so a locale field refuses it.
77
+ */
78
+ function readsAsGroupOrFraction(read, info) {
79
+ return info.storageDigits === 0 && read.decimalMark !== null && read.scale === 3 && read.units < 1000000n;
80
+ }
72
81
  /** Which reading a field's options ask for; `decimalSeparator` and `locale` together, or a `decimalSeparator` other than `'.'`, throw. */
73
82
  function entryMode(options) {
74
83
  if (options.decimalSeparator === void 0) return options.locale === void 0 ? "auto" : "locale";
@@ -92,17 +101,18 @@ const ENTRY_SEPARATORS = {
92
101
  * against `,` (see `parseImportAmount`): `'29,99'` EUR is 29.99, `'1.299,00'` and `'1,299'`
93
102
  * are 1299, KWD `'1,5'` is 1.5; a currency storing 0 digits rounds a fraction half-up to a
94
103
  * whole unit (`'1200.5'` JPY is 1201).
95
- * - With `locale`: its decimal separator is the decimal and its group separator is valid only in
96
- * 3-digit (or Indian lakh) groups; in a currency that stores no fraction (JPY, VND) a separator
97
- * before exactly three digits in grouping position is grouping (`'1.500'` JPY is 1500).
98
- * en-US `'29,99'` USD is `'unparseable'`; de-DE `'29,99'` EUR is 29.99; en-US `'1200.5'` JPY
99
- * is `'too_many_decimals'`.
104
+ * - With `locale`: its decimal separator is always the decimal and its group separator is valid
105
+ * only in 3-digit (or Indian lakh) groups. In a currency that stores no fraction (JPY, VND) the
106
+ * decimal separator before exactly three digits is ambiguous (en-US `'1.234'` JPY could be
107
+ * 1,234 or 1.234) and is `'too_many_decimals'`, never read as grouping; grouping takes the
108
+ * locale's group separator (`'1,234'` en-US, `'1.234'` de-DE). en-US `'29,99'` USD is
109
+ * `'unparseable'`; de-DE `'29,99'` EUR is 29.99; en-US `'1200.5'` JPY is `'too_many_decimals'`.
100
110
  * - With `decimalSeparator: '.'` (every `<input type="number">`): `.` is the decimal, nothing
101
111
  * groups (`'1,299'` is `'unparseable'`), TND and IQD `'1.500'` are 1.5, and a currency storing
102
112
  * 0 digits rounds a fraction half-up to a whole unit (`'1.5'` JPY is 2).
103
113
  * Otherwise more fraction digits than the currency stores is `'too_many_decimals'`, never rounded
104
114
  * (`'1,2345'` USD). Text equal to `original` returns it unchanged (JPY 120050 shown as
105
- * `'1200.50'`). An unknown currency, or an `original` in another currency, throws.
115
+ * `'1200.50'`), except the ambiguous zero-digit text above. An unknown currency, or an `original` in another currency, throws.
106
116
  */
107
117
  function parseAmountInput(text, currency, options = {}) {
108
118
  const info = resolveCurrency(currency, { allowHistoric: true });
@@ -112,7 +122,10 @@ function parseAmountInput(text, currency, options = {}) {
112
122
  let value;
113
123
  if (echoesOriginal(shown, options)) value = storedDecimal(options.original).value;
114
124
  else {
115
- const separators = mode === "locale" ? localeSeparators(options.locale) : ENTRY_SEPARATORS[mode];
125
+ const separators = mode === "locale" ? {
126
+ ...localeSeparators(options.locale),
127
+ wholeCurrencyGrouping: false
128
+ } : ENTRY_SEPARATORS[mode];
116
129
  const read = readNumberText(text, {
117
130
  ...separators,
118
131
  currency: info.code
@@ -122,6 +135,11 @@ function parseAmountInput(text, currency, options = {}) {
122
135
  reason: read.reason,
123
136
  text: shown
124
137
  };
138
+ if (mode === "locale" && readsAsGroupOrFraction(read, info)) return {
139
+ ok: false,
140
+ reason: "too_many_decimals",
141
+ text: shown
142
+ };
125
143
  value = decimalOfText(read);
126
144
  }
127
145
  const result = entryMoney(value, info, {
@@ -196,5 +214,4 @@ function inputStep(currency) {
196
214
  function inputPlaceholder(currency) {
197
215
  return PLACEHOLDERS[resolveCurrency(currency, { allowHistoric: true }).storageDigits];
198
216
  }
199
- //#endregion
200
217
  export { inputStep as a, inputPlaceholder as i, formatAmountInput as n, localeSeparators as o, groupDigits as r, parseAmountInput as s, decimalOfText as t };
@@ -1,7 +1,7 @@
1
- import { E as resolveCurrency, Q as describeValue, Z as MoneyError, at as isPositiveDecimal, ct as pow10, s as storedDecimal, t as boundedMoney, z as decimalOf } from "./money-CIgELekG.js";
2
- import { t as mulDivToUnit } from "./rounding-CVAPEfq_.js";
3
- import { n as hundredPercent } from "./percent-CKVI_c-m.js";
4
- //#region src/arithmetic.ts
1
+ import { $ as describeValue, B as decimalOf, E as resolveCurrency, Q as MoneyError, ot as isPositiveDecimal, s as storedDecimal, t as boundedMoney, ut as pow10 } from "./money-BxbAw7BP.js";
2
+ import { t as mulDivToUnit } from "./rounding-CA1PVP3b.js";
3
+ import { n as hundredPercent } from "./percent-CB3Ixjc_.js";
4
+ // src/arithmetic.ts
5
5
  /** Internal: the shared currency of `a` and `b` and their stored integers. Throws `currency_mismatch`. */
6
6
  function pair(a, b, verb) {
7
7
  const left = storedDecimal(a).info;
@@ -128,5 +128,4 @@ function excessOver(a, b, tolerance) {
128
128
  const delta = x - y;
129
129
  return boundedMoney(delta < limit ? 0n : delta, info);
130
130
  }
131
- //#endregion
132
131
  export { within as _, equals as a, isPositive as c, min as d, multiply as f, sum as g, subtract as h, divide as i, isZero as l, remaining as m, clamp as n, excessOver as o, percentOf as p, compare as r, isNegative as s, add as t, max as u };
@@ -1,7 +1,7 @@
1
- const require_money = require("./money-D5-vlv0h.cjs");
2
- const require_rounding = require("./rounding-oZ832yTD.cjs");
3
- const require_percent = require("./percent-Dt8vNIAW.cjs");
4
- //#region src/arithmetic.ts
1
+ const require_money = require("./money-DWBjuuEK.cjs");
2
+ const require_rounding = require("./rounding-4QKTRbMQ.cjs");
3
+ const require_percent = require("./percent-DABHiSBs.cjs");
4
+ // src/arithmetic.ts
5
5
  /** Internal: the shared currency of `a` and `b` and their stored integers. Throws `currency_mismatch`. */
6
6
  function pair(a, b, verb) {
7
7
  const left = require_money.storedDecimal(a).info;
@@ -128,7 +128,6 @@ function excessOver(a, b, tolerance) {
128
128
  const delta = x - y;
129
129
  return require_money.boundedMoney(delta < limit ? 0n : delta, info);
130
130
  }
131
- //#endregion
132
131
  Object.defineProperty(exports, "add", {
133
132
  enumerable: true,
134
133
  get: function() {
package/dist/billing.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_money = require("./money-D5-vlv0h.cjs");
3
- const require_percent = require("./percent-Dt8vNIAW.cjs");
4
- //#region src/billing/credits.ts
2
+ const require_money = require("./money-DWBjuuEK.cjs");
3
+ const require_percent = require("./percent-DABHiSBs.cjs");
4
+ // src/billing/credits.ts
5
5
  function wholeNumber(value, name, min) {
6
6
  if (typeof value !== "number" || !Number.isSafeInteger(value) || value < min) throw new require_money.MoneyError("amount_invalid", `${name} must be an integer >= ${min}, got ${require_money.describeValue(value)}`);
7
7
  return BigInt(value);
@@ -64,8 +64,7 @@ function bucketCredits(credits, step = 50) {
64
64
  const s = wholeNumber(step, "step", 1);
65
65
  return Number(require_money.divRound(c, s, "ceil") * s);
66
66
  }
67
- //#endregion
68
- //#region src/billing/plans.ts
67
+ // src/billing/plans.ts
69
68
  function checkSame(m, other, verb) {
70
69
  const info = require_money.storedDecimal(m).info;
71
70
  const own = require_money.storedDecimal(other).info;
@@ -144,8 +143,7 @@ function grossUpForPercentOffs(m, percents) {
144
143
  if (!(kept > 0)) return m;
145
144
  return require_money.boundedMoney(BigInt(Math.round(m.amount / kept)), info);
146
145
  }
147
- //#endregion
148
- //#region src/billing/usage.ts
146
+ // src/billing/usage.ts
149
147
  const USD_DIGITS = 6;
150
148
  /**
151
149
  * The exact sum of `quantity x rate x multiplier` over `lines`, rounded half-up once to 6
@@ -172,7 +170,6 @@ function usdCost(lines) {
172
170
  }
173
171
  return require_money.toDecimalText(require_money.roundToScale(total, USD_DIGITS, "half-up"));
174
172
  }
175
- //#endregion
176
173
  exports.annualPrice = annualPrice;
177
174
  exports.applyCoupons = applyCoupons;
178
175
  exports.bucketCredits = bucketCredits;