> ## Documentation Index
> Fetch the complete documentation index at: https://help.stralos.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Estimate rounding

> The roundUp contract in @stralos-co-nz/calculators and how api-estimate applies it to linked quantities and sell rates

Estimate rounding (calculators 2.1.0) lets an estimator round a linked quantity, and the sell rates of a tender estimate, up to a chosen step. The contract lives in `packages/calculators/src/roundUp.ts` in `stralos-domain`. For the user guide, see [Rounding](https://help.stralos.app/guides/estimators/work-items.md#rounding) and [Sell rate rounding](https://help.stralos.app/guides/estimators/tender-submission.md#sell-rate-rounding).

> [!NOTE]
>
> This is not `rounding.ts`, the half-up 2 dp snap every calculator quantity passes through. Estimate rounding is an explicit choice to lift a figure up to a step.

## Options

Each list is closed and in dropdown order. `none` and the plain steps are shared spellings.

### Linked quantity (`QUANTITY_ROUNDING_OPTIONS`)

| Token | Label |
| - | - |
| `none` | No rounding |
| `0.001` | Up to nearest 0.001 |
| `0.01` | Up to nearest 0.01 |
| `0.1` | Up to nearest 0.1 |
| `0.5` | Up to nearest 0.5 |
| `ends:0.9` | Up to next ending in .9 |
| `1` | Up to nearest 1 |
| `5` | Up to nearest 5 |
| `ends:9` | Up to next ending in 9 |
| `10` | Up to nearest 10 |
| `50` | Up to nearest 50 |
| `100` | Up to nearest 100 |
| `1000` | Up to nearest 1,000 |
| `10000` | Up to nearest 10,000 |

### Sell rate (`SELL_RATE_ROUNDING_OPTIONS`)

| Token | Label |
| - | - |
| `none` | No rounding |
| `ends:0.09` | Up to next ending in .09 |
| `0.1` | Up to nearest \$0.10 |
| `0.5` | Up to nearest \$0.50 |
| `ends:0.99` | Up to next ending in .99 |
| `1` | Up to nearest \$1 |
| `5` | Up to nearest \$5 |
| `ends:9.99` | Up to next ending in 9.99 |
| `10` | Up to nearest \$10 |
| `20` | Up to nearest \$20 |
| `50` | Up to nearest \$50 |
| `ends:99` | Up to next ending in 99 |
| `100` | Up to nearest \$100 |
| `ends:999` | Up to next ending in 999 |
| `1000` | Up to nearest \$1,000 |
| `2000` | Up to nearest \$2,000 |
| `5000` | Up to nearest \$5,000 |
| `10000` | Up to nearest \$10,000 |

There is no `ends:9999` option; `10000` is the largest sell-rate step. A stored token outside the lists is treated as unknown and left unrounded.

Use `quantityRoundingLabel(option)` and `sellRateRoundingLabel(option)` for dropdown text rather than formatting a token yourself. `isQuantityRoundingOption` and `isSellRateRoundingOption` are the type guards.

## Rules

`roundUp(value, option)` is pure and never throws. It keeps four rules, each pinned by a test:

1. **Always up (ceiling), never nearest.** A value already on a valid point stays where it is.
2. **Two kinds of step.** A plain step `S` is a multiple: `⌈x ÷ S⌉ × S` (123.56 with `10` gives 130). An `ends:E` step is the next number ending in `E`: with `M` the smallest power of ten above `E`, `⌈(x − E) ÷ M⌉ × M + E` (123.56 with `ends:0.9` gives 123.9; with `ends:9`, 129).
3. **Exact decimals.** Tokens are decimal strings and figures go through scaled BigInts. A `number` is read as the decimal it prints as at 15 significant figures, so `0.1 + 0.2` rounds as 0.3.
4. **Zero and negatives are left alone.** Zero stays zero under every option, so an `ends:` step never lifts a zero rate to 0.09. A negative figure has no agreed "up" and is returned as is.

`none`, an unknown token, `NaN`, and `±Infinity` also come back unchanged.

`roundUpDecimal(value, option)` applies the same rounding to a `ScaledDecimal` (`{ units: bigint; scale: number }`) for a caller that already holds one. The answer's scale is the finer of the value's and the step's.

## Suggested step for a new link

`suggestQuantityRounding(value)` returns the step that rounds to three significant figures: `10^(⌊log10 v⌋ − 2)`, clamped to `[0.001, 10000]`. The exponent comes from the decimal's digits, not `Math.log10`.

| Value | Suggested option | Result |
| - | - | - |
| 4.237 | `0.01` | 4.24 |
| 123.56 | `1` | 124 |
| 18,402 | `100` | 18,500 |
| 3,200,000 | `10000` | 3,200,000 |
| 0, negative, or non-finite | `none` | unchanged |

## Linked quantities

`QuantityLinkV1.rounding` is applied after `Σ × multiplier + offset`, always up. The resolver reports the figure before rounding as `unrounded`, and `quantityLinkRuns` adds the uplift to the last run so the runs still sum to the rounded quantity. See [Linked work-item quantity](https://help.stralos.app/internal/quantity-link.md#round-up-to).

api-estimate refuses a `rounding` on a build-up line priced in whole stock (`422 component_link_rounding_whole_stock`), because whole-stock ordering already rounds each run up. An unknown token on a work-item link is `422 quantity_link_invalid`.

## Sell rates in api-estimate

The estimate's `sellRateRounding` (default `none`) applies to every item without its own. A work item's `sellRateRounding` overrides it: `null` inherits the estimate and `none` keeps the item unrounded. An unknown token is `422 sell_rate_rounding_invalid`, and a frozen estimate refuses the change with `409 estimate_frozen`.

The rounding sits inside the item-rate snap (`src/pricing/itemRate.ts`): `rate0 = half-up 2 dp (price ÷ quantity)`, `rate = roundUp(rate0, option)`, `price = round2(quantity × rate)`. The stored `unitRate` is the rounded rate. `unitRateUnrounded` holds `rate0` only when rounding moved it. The uplift is extra sell (margin); the build-up underneath never moves. Rows that carry no rate (provisional sums, quantity zero or less) are never rounded, and neither is any row in a Shared Costs section (`effectiveSellRateRounding` returns `none`): the pool is spread into the schedule items, which round themselves.

With a rounding the snap is not idempotent below a quantity of 1, so every writer snaps a row once per pass from its rolled-up price and keeps the result.

The `estimate-rounding` PostHog flag hides both controls in the frontend only. A stored rounding keeps applying when the flag is off. See [Estimate rounding](https://help.stralos.app/internal/estimate-frontend.md#estimate-rounding) in the estimate frontend notes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.