fix(18-07): money accepts trailing zeros beyond the field scale

A scale-2 money field over a four-decimal column (the documented fee
example) was served "12.5000" and answered money.too_precise when the
untouched value was saved back. Trailing zeros carry no precision, so the
server check and the SPA parser count only significant fraction digits;
the value still reaches the column unchanged, never rounded or truncated.

- cabana validateMoneyValue and admin parseMoneyInput trim trailing zeros
  before the scale comparison
- forms docs and cabana README name the rule; embedded dist rebuilt
- MoneyField spec asserts 12.5000 at scale 2 is accepted, 12.5010 refused
This commit is contained in:
2026-10-13 16:53:58 +02:00
parent 5141059026
commit eb60c329d8
8 changed files with 20 additions and 12 deletions

View File

@@ -20,8 +20,10 @@ const digitsOnly = /^[0-9]+$/
* Turns typed text into the canonical money string: spaces and U+00A0 are
* stripped, one comma or dot is the decimal separator, an optional leading
* minus is kept, digits only otherwise. Empty text is null. More than
* `scale` fraction digits is `too_precise`: the value is never rounded or
* truncated. Anything else (letters, two separators, grouping) is `invalid`.
* `scale` significant fraction digits is `too_precise`: the value is never
* rounded or truncated, and trailing zeros beyond the scale are kept and
* accepted (they carry no precision), as the server does. Anything else
* (letters, two separators, grouping) is `invalid`.
*/
export function parseMoneyInput(text: string, scale: number): MoneyParseResult {
const compact = text.replace(/[\s ]/g, '')
@@ -42,7 +44,7 @@ export function parseMoneyInput(text: string, scale: number): MoneyParseResult {
if (!digitsOnly.test(whole ?? '') || (separators.length === 1 && !digitsOnly.test(fraction))) {
return { value: null, error: 'invalid' }
}
if (fraction.length > scale) {
if (fraction.replace(/0+$/, '').length > scale) {
return { value: null, error: 'too_precise' }
}
const canonical = fraction === '' ? whole : `${whole}.${fraction}`

View File

@@ -65,6 +65,9 @@ describe('money helpers', () => {
expect(parseMoneyInput('', 4)).toEqual({ value: null, error: null })
expect(parseMoneyInput(' ', 4)).toEqual({ value: null, error: null })
expect(parseMoneyInput('12.34567', 4)).toEqual({ value: null, error: 'too_precise' })
// Trailing zeros beyond the scale carry no precision: kept, accepted.
expect(parseMoneyInput('12.5000', 2)).toEqual({ value: '12.5000', error: null })
expect(parseMoneyInput('12.5010', 2)).toEqual({ value: null, error: 'too_precise' })
expect(parseMoneyInput('1.234,56', 4)).toEqual({ value: null, error: 'invalid' })
expect(parseMoneyInput('12a', 4)).toEqual({ value: null, error: 'invalid' })
expect(parseMoneyInput('1e3', 4)).toEqual({ value: null, error: 'invalid' })