@amboras-dev/money 0.2.0 → 0.4.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 (78) hide show
  1. package/README.md +22 -11
  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-CZRg4TOP.cjs → allocate-AusholrC.cjs} +3 -4
  7. package/dist/{allocate-X7_jAtOj.js → allocate-BBRfdaSC.js} +3 -4
  8. package/dist/{amount-BV0O-aN8.cjs → amount-Bzxw9aD4.cjs} +32 -14
  9. package/dist/{amount-BoYBMgK9.js → amount-off8IMBJ.js} +32 -14
  10. package/dist/{arithmetic-BhNwisjb.js → arithmetic-C97-YafD.js} +4 -5
  11. package/dist/{arithmetic-DjT4Kzgt.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-DLhno7eX.cjs → format-92fzvTXY.cjs} +9 -9
  21. package/dist/{format-BdPnijvn.js → format-BJC513u4.js} +9 -9
  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 +10 -12
  27. package/dist/index.d.cts +17 -23
  28. package/dist/index.d.ts +17 -23
  29. package/dist/index.js +10 -12
  30. package/dist/input.cjs +7 -10
  31. package/dist/input.d.cts +16 -22
  32. package/dist/input.d.ts +16 -22
  33. package/dist/input.js +7 -10
  34. package/dist/{iso-minor-wSwJN57T.cjs → iso-minor-DyNjT9A1.cjs} +2 -3
  35. package/dist/{iso-minor-CqkWDxHS.js → iso-minor-odkY-e6I.js} +2 -3
  36. package/dist/{medusa-lRY9xlsf.cjs → medusa-CzVK0bth.cjs} +3 -4
  37. package/dist/{medusa-DBXJXXvF.d.ts → medusa-DofdvIgQ.d.cts} +1 -3
  38. package/dist/{medusa-4K8N1pUc.d.cts → medusa-DrbYmD2Y.d.ts} +1 -3
  39. package/dist/{medusa-DqJY1qpj.js → medusa-pKGlImn9.js} +3 -4
  40. package/dist/{money-B-3aFjNJ.d.cts → money-BkMeYDEW.d.cts} +0 -6
  41. package/dist/{money-B-3aFjNJ.d.ts → money-BkMeYDEW.d.ts} +0 -6
  42. package/dist/{money-WhnGiI00.js → money-BxbAw7BP.js} +7 -15
  43. package/dist/{money-B3wH_Z4S.cjs → money-DWBjuuEK.cjs} +7 -15
  44. package/dist/{number-text-DE-q4jO5.js → number-text-Bj-gZXiF.js} +3 -4
  45. package/dist/{number-text-BWu67jUq.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-D1VB9xXG.js → percent-CB3Ixjc_.js} +2 -3
  49. package/dist/{percent-C0InC2t2.cjs → percent-DABHiSBs.cjs} +2 -3
  50. package/dist/{rate-DBPiYgT1.d.ts → rate-BvvZUdSu.d.ts} +1 -3
  51. package/dist/{rate-tqK8ijG0.d.cts → rate-CEakW3ia.d.cts} +1 -3
  52. package/dist/{rate-CVeMii4a.cjs → rate-CEjWTATc.cjs} +3 -4
  53. package/dist/{rate-DicStBue.js → rate-D6S2_wgZ.js} +3 -4
  54. package/dist/{reporting-tpGJbqYp.d.ts → reporting-DFNyDBdi.d.cts} +2 -4
  55. package/dist/{reporting-CG-ai2sM.d.cts → reporting-jIBILvnr.d.ts} +2 -4
  56. package/dist/{retail-DRUTTQoJ.d.ts → retail-B04CjW3b.d.ts} +1 -3
  57. package/dist/{retail-Cml8YSKg.cjs → retail-CYGPU4ea.cjs} +4 -5
  58. package/dist/{retail-C_BPLvMj.d.cts → retail-CmEyJQOH.d.cts} +1 -3
  59. package/dist/{retail-1mu888J1.js → retail-DXgs11ov.js} +4 -5
  60. package/dist/{rounding-DHBtNCHG.cjs → rounding-4QKTRbMQ.cjs} +2 -3
  61. package/dist/{rounding-g9rvye0k.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 +39198 -12
  67. package/dist/testing.d.cts +20 -13
  68. package/dist/testing.d.ts +20 -13
  69. package/dist/testing.js +39198 -13
  70. package/dist/{wire-sW0yzqth.js → wire-B4QU8O3W.js} +350 -93
  71. package/dist/{wire-CaUN9Vf_.d.ts → wire-CVNicirK.d.ts} +165 -46
  72. package/dist/{wire-xjYXv4go.d.cts → wire-CtNJmWVP.d.cts} +165 -46
  73. package/dist/{wire-Bhuut8cW.cjs → wire-u2tmHY0Y.cjs} +403 -92
  74. package/dist/wire.cjs +10 -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
package/README.md CHANGED
@@ -152,7 +152,9 @@ unit apart).
152
152
  **Formatting** (for people, not byte-stable across ICU): `formatMoney` (display digits, or
153
153
  `widen` / `trim-zero`; explicit locale, default `en-US`), `formatMoneyCompact`,
154
154
  `formatMoneyRange`, `moneyAxisFormatter`, `formatStored` (a possibly fractional stored amount,
155
- rounded half-up to a storage unit, then `formatMoney`).
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).
156
158
 
157
159
  Internals other entry points build on (never exported), each the only implementation of its concept:
158
160
  `internal/number-text.ts` `readNumberText` (the one separator engine), `internal/rational.ts`
@@ -168,18 +170,20 @@ import { money } from '@amboras-dev/money'
168
170
  import { ISO_MINOR_V2, conventionOf, decodeWire, encodeWire } from '@amboras-dev/money/wire'
169
171
 
170
172
  encodeWire('stripe', money(120000, 'JPY'), { convention: ISO_MINOR_V2 }) // 1200
171
- 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
172
175
  encodeWire('mollie', { amount: '107.8175', currency: 'USD' }) // '1.08': a fractional stored total
173
176
  ```
174
177
 
175
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.
176
179
 
177
- - Codec: `encodeWire`, `decodeWire`, `tryDecodeWire` (never throws), `wireFactor`, `pspWireDigits`, `oneWireUnit`, `wireBreakdown` (line items that sum to the encoded total), `wireKeyPart`.
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`.
178
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).
179
- - Provider tables: `PSP_IDS`, `pspSupportsCurrency`, `assertPspCurrency`, `pspCurrencies` (what the codec scales), `pspDefaultCurrency`, `pspMinimumAmount`.
180
- - Presentment (eligibility): `pspPresentmentCurrencies(psp, { accountCountry })` (what an account can charge buyers in) and `pspPresentment(psp)` (the rules as data: union, currencies needing an account country, account countries limited to a list).
181
- - Comparison: `sameWireAmount` (a webhook amount against the expected Money or FractionalMoney, exactly at storage scale; `acceptLegacyIsoDigits`, `textOnly`).
182
- - 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`.
183
187
  - Provider text: `parseProviderAmount` (carrier quotes, dispute amounts; `decimalSeparator: 'auto'` is the `/input` rule).
184
188
  - Destinations: `destinationAmount` (pixels, ad platforms, email tools), `feedPrice`, `googleMicros`, `eventMoney` (ClickHouse `Int64` contract).
185
189
  - Plugin slots: `slotMoney`, `readSlotMoney`.
@@ -248,6 +252,8 @@ parseAmountInput('1,2345', 'USD') // { ok: false, reason
248
252
  parseAmountInput('99.000', 'VND') // 99000 (3-digit tail groups in a currency without decimals)
249
253
  parseAmountInput('12.99.', 'USD') // { ok: false, reason: 'unparseable' } (groups must be thousands or lakh)
250
254
  parseAmountInput('1.500', 'TND', { decimalSeparator: '.' }) // 1.5 (an <input type="number"> value: '.' is always the decimal)
255
+ parseAmountInput('1.5', 'JPY', { decimalSeparator: '.' }) // { ok: false, reason: 'too_many_decimals' } (never rounded to 2)
256
+ parseAmountInput('1.0', 'JPY', { decimalSeparator: '.' }) // 1 (trailing zeros carry no extra precision)
251
257
  formatAmountInput(money(1500, 'TND'), { decimalSeparator: '.' }) // '1.5' (the number input's prefill, read back exactly)
252
258
  formatAmountInput(money(123456789, 'USD'), { grouped: true, locale: 'de-DE' }) // '1.234.567,89'
253
259
  parseImportAmount('1.299,00') // '1299'
@@ -260,17 +266,20 @@ inputStep('KWD') // '0.001'
260
266
  - `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.
261
267
  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.
262
268
  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.
269
+ 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).
263
270
  **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).
264
- 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).
265
- It equals that function on every `.`-only value within the storage digits (`test/input/input.test.ts`), except one class:
266
- - `extra-fraction-digits`: in a currency storing 2 or 3 digits, more fraction digits than it stores is `too_many_decimals`, never rounded (`'1.239'` USD, admin 124 cents; KWD `'1.0005'`, admin 1001).
271
+ In `'.'` mode `.` is the decimal, `,` and grouping are `unparseable`, and a zero whole part may be omitted (`'.5'` is 0.5).
272
+ More fraction digits than the currency stores is `too_many_decimals` in every currency, never rounded: since 0.4.0 a currency storing 0 digits refuses any fraction (`'1.5'`, `'1.234'` and `'1200.5'` JPY and KRW; 0.3.0 rounded them half-up to ¥2, ¥1 and ¥1,201).
273
+ Trailing zeros carry no extra precision, so `'1.0'` and `'1.00'` JPY are ¥1, as with a locale; a sub-unit prefill (JPY 120050, `'1200.5'`) still reads back exactly through `original`.
274
+ It equals admin `numericEntryToMinorUnits` on every `.`-only value within the storage digits (`test/input/input.test.ts`), except one class:
275
+ - `extra-fraction-digits`: more fraction digits than the currency stores is `too_many_decimals`, never rounded (`'1.239'` USD, admin 124 cents; KWD `'1.0005'`, admin 1001; `'1.5'` JPY, admin ¥2).
267
276
  Failure reasons: `empty`, `unparseable`, `too_many_decimals`, `negative`, `zero`, `below_min`, `above_max`.
268
277
  - `formatAmountInput(m, { locale, decimalSeparator, grouped, digits })`: the field text for a stored amount, no symbol, exact: a sub-unit stored amount keeps its digits (JPY 120050, which Medusa writes today, is `'1200.5'`), never rounded on display.
269
278
  Without `locale` it prints the spelling `parseAmountInput` reads back without a locale: trailing fraction zeros dropped (USD `'12.5'`, KWD 1500 `'1.5'`, IQD 10000000 `'10000'`), and TND, which writes a decimal comma, with `,` as the decimal and `.` as the group (`'1,125'`, `'1.234,5'`).
270
279
  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).
271
280
  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.
272
281
  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`).
273
- 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.
282
+ 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).
274
283
  - `inputStep`, `inputPlaceholder`: from the storage digits, never a hardcoded `0.01`.
275
284
  - `parseImportAmount(text, { decimalSeparator: 'auto' | '.' | ',', currency, allowNegative, allowTrailingText })`: CSV, supplier, scraped and carrier text to `DecimalText` (major units), or `null`.
276
285
  `'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:
@@ -318,6 +327,7 @@ Framework-agnostic: no test runner or fast-check dependency.
318
327
  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> ...`).
319
328
  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.
320
329
  - `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).
330
+ - `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.
321
331
  - `DESTINATION_GOLDEN_VECTORS`: what `destinationAmount`, `feedPrice` and `googleMicros` must give per destination.
322
332
  - `checkWireDigits(psp, documented)`: the capability-consistency probe; returns mismatch messages.
323
333
  - `normalizeIntlSpaces(s)`, `sampleMoney(currency, major)`.
@@ -339,6 +349,7 @@ The full list, with the decision behind each, is in [`docs/money/API-SPEC.md`](.
339
349
  `formatMoney`, `moneyView` (`/ai`), `destinationAmount` and `feedPrice` (`/wire`) use the display digits; capability-kit, the plugins and Medusa's `formatMoney` use the storage digits today.
340
350
  This is deliberate: it is what the PSPs charge and what people read.
341
351
  - `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`.
352
+ A cap ("refund at most") passes `rounding: 'down'`, so HUF 435050 shows `HUF 4,350` and typing the amount shown stays within it.
342
353
  - Catalog feeds and destinations drop the sub-unit the same way: `feedPrice(money(435050, 'HUF'))` is `'4351 HUF'`, IQD 1500 is `'2 IQD'`.
343
354
  - 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'`.
344
355
  - 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-B3wH_Z4S.cjs");
3
- const require_format = require("./format-DLhno7eX.cjs");
4
- const require_number_text = require("./number-text-BWu67jUq.cjs");
5
- const require_amount = require("./amount-BV0O-aN8.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-Bzxw9aD4.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 { R as DecimalText, g as CurrencyCode, i as Money } from "./money-B-3aFjNJ.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 { R as DecimalText, g as CurrencyCode, i as Money } from "./money-B-3aFjNJ.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 { $ 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-WhnGiI00.js";
2
- import { t as formatMoney } from "./format-BdPnijvn.js";
3
- import { n as readNumberText, t as digitGroupingSource } from "./number-text-DE-q4jO5.js";
4
- import { s as parseAmountInput } from "./amount-BoYBMgK9.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-off8IMBJ.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-B3wH_Z4S.cjs");
2
- const require_rounding = require("./rounding-DHBtNCHG.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 { $ 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-WhnGiI00.js";
2
- import { a as unitSize } from "./rounding-g9rvye0k.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-B3wH_Z4S.cjs");
2
- const require_arithmetic = require("./arithmetic-DjT4Kzgt.cjs");
3
- const require_number_text = require("./number-text-BWu67jUq.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,19 @@ 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
- * 0 digits rounds a fraction half-up to a whole unit (`'1.5'` JPY is 2).
112
+ * 0 digits refuses any fraction as `'too_many_decimals'` (`'1.5'` JPY), never rounded;
113
+ * trailing zeros read as whole (`'1.0'` JPY is 1).
103
114
  * Otherwise more fraction digits than the currency stores is `'too_many_decimals'`, never rounded
104
115
  * (`'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.
116
+ * `'1200.50'`), except the ambiguous zero-digit text above. An unknown currency, or an `original` in another currency, throws.
106
117
  */
107
118
  function parseAmountInput(text, currency, options = {}) {
108
119
  const info = require_money.resolveCurrency(currency, { allowHistoric: true });
@@ -112,7 +123,10 @@ function parseAmountInput(text, currency, options = {}) {
112
123
  let value;
113
124
  if (echoesOriginal(shown, options)) value = require_money.storedDecimal(options.original).value;
114
125
  else {
115
- const separators = mode === "locale" ? localeSeparators(options.locale) : ENTRY_SEPARATORS[mode];
126
+ const separators = mode === "locale" ? {
127
+ ...localeSeparators(options.locale),
128
+ wholeCurrencyGrouping: false
129
+ } : ENTRY_SEPARATORS[mode];
116
130
  const read = require_number_text.readNumberText(text, {
117
131
  ...separators,
118
132
  currency: info.code
@@ -122,11 +136,16 @@ function parseAmountInput(text, currency, options = {}) {
122
136
  reason: read.reason,
123
137
  text: shown
124
138
  };
139
+ if (mode === "locale" && readsAsGroupOrFraction(read, info)) return {
140
+ ok: false,
141
+ reason: "too_many_decimals",
142
+ text: shown
143
+ };
125
144
  value = decimalOfText(read);
126
145
  }
127
146
  const result = entryMoney(value, info, {
128
147
  ...options,
129
- roundExtraDigits: mode !== "locale" && info.storageDigits === 0
148
+ roundExtraDigits: mode === "auto" && info.storageDigits === 0
130
149
  });
131
150
  if (!result.ok) return {
132
151
  ok: false,
@@ -196,7 +215,6 @@ function inputStep(currency) {
196
215
  function inputPlaceholder(currency) {
197
216
  return PLACEHOLDERS[require_money.resolveCurrency(currency, { allowHistoric: true }).storageDigits];
198
217
  }
199
- //#endregion
200
218
  Object.defineProperty(exports, "decimalOfText", {
201
219
  enumerable: true,
202
220
  get: function() {
@@ -1,7 +1,7 @@
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-WhnGiI00.js";
2
- import { r as compare } from "./arithmetic-BhNwisjb.js";
3
- import { n as readNumberText } from "./number-text-DE-q4jO5.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,19 @@ 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
- * 0 digits rounds a fraction half-up to a whole unit (`'1.5'` JPY is 2).
112
+ * 0 digits refuses any fraction as `'too_many_decimals'` (`'1.5'` JPY), never rounded;
113
+ * trailing zeros read as whole (`'1.0'` JPY is 1).
103
114
  * Otherwise more fraction digits than the currency stores is `'too_many_decimals'`, never rounded
104
115
  * (`'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.
116
+ * `'1200.50'`), except the ambiguous zero-digit text above. An unknown currency, or an `original` in another currency, throws.
106
117
  */
107
118
  function parseAmountInput(text, currency, options = {}) {
108
119
  const info = resolveCurrency(currency, { allowHistoric: true });
@@ -112,7 +123,10 @@ function parseAmountInput(text, currency, options = {}) {
112
123
  let value;
113
124
  if (echoesOriginal(shown, options)) value = storedDecimal(options.original).value;
114
125
  else {
115
- const separators = mode === "locale" ? localeSeparators(options.locale) : ENTRY_SEPARATORS[mode];
126
+ const separators = mode === "locale" ? {
127
+ ...localeSeparators(options.locale),
128
+ wholeCurrencyGrouping: false
129
+ } : ENTRY_SEPARATORS[mode];
116
130
  const read = readNumberText(text, {
117
131
  ...separators,
118
132
  currency: info.code
@@ -122,11 +136,16 @@ function parseAmountInput(text, currency, options = {}) {
122
136
  reason: read.reason,
123
137
  text: shown
124
138
  };
139
+ if (mode === "locale" && readsAsGroupOrFraction(read, info)) return {
140
+ ok: false,
141
+ reason: "too_many_decimals",
142
+ text: shown
143
+ };
125
144
  value = decimalOfText(read);
126
145
  }
127
146
  const result = entryMoney(value, info, {
128
147
  ...options,
129
- roundExtraDigits: mode !== "locale" && info.storageDigits === 0
148
+ roundExtraDigits: mode === "auto" && info.storageDigits === 0
130
149
  });
131
150
  if (!result.ok) return {
132
151
  ok: false,
@@ -196,5 +215,4 @@ function inputStep(currency) {
196
215
  function inputPlaceholder(currency) {
197
216
  return PLACEHOLDERS[resolveCurrency(currency, { allowHistoric: true }).storageDigits];
198
217
  }
199
- //#endregion
200
218
  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 { $ 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-WhnGiI00.js";
2
- import { t as mulDivToUnit } from "./rounding-g9rvye0k.js";
3
- import { n as hundredPercent } from "./percent-D1VB9xXG.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-B3wH_Z4S.cjs");
2
- const require_rounding = require("./rounding-DHBtNCHG.cjs");
3
- const require_percent = require("./percent-C0InC2t2.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-B3wH_Z4S.cjs");
3
- const require_percent = require("./percent-C0InC2t2.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;