Skip to content

Decimal inputs reject the locale decimal separator; tariff prices with a comma return 500 #17

Description

@MarcoSpittka

Problem

Saving a tariff price with a comma as decimal separator, e.g. 0,49 from a German mobile keyboard, fails with HTTP 500 on PATCH /v1/pricing-groups/:id/tariffs/:tariffId (and on POST .../tariffs). Only 0.49 can be saved.

Two causes:

  1. API: nonNegativePrice and taxRate in routes/pricing.ts validate with .refine(). zod-to-json-schema strips refines when zodSchema() builds the JSON Schema that Fastify validates against, so "0,49" passes validation and Postgres rejects it for the numeric column. The same limitation is already worked around in carbon.ts and notifications.ts.
  2. CSMS: tariff prices and station coordinates are plain text inputs whose value is sent verbatim. The other decimal fields use <input type="number">, whose accepted separator follows the browser locale instead of the selected UI language. Input the browser cannot parse is reported as an empty value, which the tariff form would send as null and silently clear the price.

i18next and Intl only format numbers; there is no standard parser for localized number input.

Proposed fix (PR follows)

  • API: express the price and tax rate bounds as .regex() patterns, which end up as pattern in the JSON Schema and are enforced by Ajv. "0,49" then returns 400 VALIDATION_ERROR. The API keeps . as its only decimal separator.
  • CSMS: a DecimalInput component that shows the decimal separator of i18n.language, accepts . and , when typing or pasting, and always hands the canonical string ("0.49") to the form. Used for all decimal fields. Thousands separators are intentionally not supported so that 1.000 is never misread; pasted text with several separators is ignored rather than guessed.

New dependency

DecimalInput builds on react-number-format (MIT, v5.4.5, peer dependency React 19 supported). It handles caret position, pasting, sign and decimal scale, and keeps the value as a string, so money amounts are not converted to floating point. Alternatives considered: react-aria NumberField (thorough locale parsing, but float-based and a second component model next to Radix/shadcn) and a hand-written parser. Happy to adjust if you prefer another approach.

Related

About 20 other .refine() calls in request schemas are also not enforced for the same reason, e.g. latitude/longitude ranges in sites.ts and checks in panels.ts and smart-charging.ts. They are out of scope here. Switching validation to fastify-type-provider-zod (already listed in packages/api/package.json, currently unused) would make all refines effective, but touches every route and the OpenAPI generation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions