Skip to Content
FrontendAppsPosPOS Boundaries (KVS Limitation)

POS Boundaries (KVS Limitation)

Configurable, KVS-driven guardrails on numeric inputs in the POS app (payment amounts, refund amounts, line prices/quantities, invoice taxes). The goal is to catch fat-finger errors (5000 instead of 50.00, missed decimal point, wrong sign on a refund) without hard-blocking legitimate large transactions.

How it works

Each guarded input has a boundary context (e.g. payment, refund, globalLinePrice). For each context, the app holds:

  • a default max hardcoded in src/configs/PosBoundaries/index.ts,
  • an optional KVS-overridden max saved per user (each user keeps their own value),
  • an optional expected sign (positive for payments, negative for refunds) — non-zero magnitude is always required.

On submit, useBoundaryGuardedSubmit calls usePosBoundaries(context).isOutOfRange(value). If out of range, it either warns the user (modal or inline) or blocks the submit outright, depending on the current user’s warning preferences.

The boundary check is configurable and only fires warnings by default — it is not a hard block. A separate, hardcoded absolute cap (ABSOLUTE_MAX_PAYMENT_AMOUNT = 1_000_000_000) protects against absurd inputs in the payment form via Zod validation; see #617 .

Supported boundary contexts

ContextDefault maxExpected signUsed by
payment500positivePayment form on /invoice/[id]/payment
refund200negativeSame payment form, when invoice total to pay is < 0
globalLinePrice200any non-zeroInline price edit and manual-line dialog (/lines)
inlineLineQuantity50any non-zeroInline quantity edit
globalLineQuantity50any non-zeroManual-line dialog (/lines)
invoiceTax100any non-zeroAdd/Edit taxes dialog

Source of truth: POS_BOUNDARY_SECTIONS.

KVS keys

One KVS row per boundary, with keys built via getFQCN():

BoundaryKVS key
paymentpfqcn_phprPos_PosBoundaries_config_payment_amount_max
refundpfqcn_phprPos_PosBoundaries_config_refund_amount_max
globalLinePricepfqcn_phprPos_PosBoundaries_config_global_line_price_max
inlineLineQuantitypfqcn_phprPos_PosBoundaries_config_inline_line_quantity_max
globalLineQuantitypfqcn_phprPos_PosBoundaries_config_global_line_quantity_max
invoiceTaxpfqcn_phprPos_PosBoundaries_config_invoice_tax_amount_max

Plus one row for warning preferences:

  • pfqcn_phprPos_PosBoundaries_config_warning_preferences — JSON value: { "displayStyle": "modal" | "inline", "behavior": "warn" | "block" }

The value field stores a numeric string (e.g. "500") for the boundaries; the warning preferences row stores a JSON string.

Scoping

The Parameters page saves boundaries and warning preferences as user-specific rows: every save writes user = <current user> and corporation = -1 (the “all corporations” sentinel, set so the rows are also visible via useKVS calls that inject corporation=-1 as a filter). Each user keeps their own settings.

The read path uses pickScopedKvsEntry, which resolves the most specific match in this order: user > corporation > unscoped. The currentCorporationIds array passed to the picker is [], so corp matching only resolves the -1 “all corps” sentinel.

Why per-key (and not one consolidated JSON)

#633 originally proposed consolidating the 6 boundary keys into a single JSON-payload row to save round-trips through useKVS (which fetches one key at a time). We kept per-key entries instead and load them in parallel via a server action (loadAllBoundaryMaxes), because per-key gives:

  • simpler save semantics (PUT vs POST per row),
  • easier ad-hoc admin edits via the generic KVS CRUD UI.

Warning preferences

Two independent dimensions, both stored in the per-user warning_preferences KVS row:

SettingValuesEffect
displayStylemodal | inlineWhere the warning appears: blocking dialog vs inline message under the input.
behaviorwarn | blockwarn lets the user confirm and proceed. block rejects the submit entirely.

Defaults : { displayStyle: "modal", behavior: "warn" }.

Parameters page : /parameters/pos/boundaries

The boundaries config form (PosBoundariesConfigForm.tsx) is URL-driven. Query string params are the authoritative form state:

  • on mount, KVS values seed the URL for any missing param,
  • typing in a field pushes to the URL,
  • clicking a profile preset pushes the full set to the URL,
  • on save, current URL values are written back to KVS.

This makes any form state deep-linkable (useful for support, demos, repro of edge cases) and refresh-safe.

Supported query strings

Boundary maxes (the context name is the param name) :

  • payment — payment max
  • refund — refund max (absolute value; stored value is the positive magnitude)
  • globalLinePrice
  • inlineLineQuantity
  • globalLineQuantity
  • invoiceTax

Warning preferences :

  • displayStylemodal or inline
  • behaviorwarn or block

Example :

/parameters/pos/boundaries?payment=2000&refund=1000&globalLinePrice=1000&behavior=block

Pre-built profiles

Three pre-built tiers fill all maxes coherently (POS_BOUNDARY_PROFILES) :

ProfilePaymentRefundLine priceInline qtyGlobal qtyTax
Tier 22000100010002020300
Tier 3250002500020000553000
Tier 42500001000002000005530000

Clicking a profile pushes its values into the URL. The user can then fine-tune individual fields before saving.

Code integration

To add a boundary check to a new form :

  1. Pick or add a context in src/configs/PosBoundaries/index.ts (BoundaryContext union + a preset under the right BoundarySubSection).

  2. Wire useBoundaryGuardedSubmit in the form hook :

    const { submit, pending, confirmPending, cancelPending } = useBoundaryGuardedSubmit<FormValues>({ context: "payment", // or whichever context applies pickValue: (v) => parseFloat(v?.amount ?? "0"), onSubmit: handleSubmit, // your real submit forceCheck: () => totalToPay === 0, // optional: always check (e.g. zero-total guardrail) format: "currency", // controls toast formatting });
  3. Render <BoundaryWarning /> below the form to display the modal/inline confirmation when pending !== null.

  4. Use submit (not your own handleSubmit) as the form’s submit handler. The hook intercepts, runs the boundary check, and either calls onSubmit immediately or stages a pending confirmation.

usePosBoundaries(context) can also be used directly if you need to read { max, isOutOfRange, isLoaded, expectedSign } without going through the guarded-submit flow.

Last updated on