# How the configurator works

Configurable products let a merchant sell items that are priced by dimensions and options instead of a flat price: verandas priced by width and depth, cut-to-size panels, made-to-order furniture with material and accessory choices. The storefront flow is:

1. Fetch the product with `GET /v1/api/commerce/products/{id}`. A configurable product has `"type": "configurable"`.
2. Fetch its configuration schema with `GET /v1/api/commerce/configurator/products/{id}/schema` and render a configurator UI from it.
3. On every change to the buyer's selection, post the current selection to `POST /v1/api/commerce/configurator/products/{id}/quote` to get the live price with a line-item breakdown.

Both configurator endpoints use the same authentication as the catalog endpoints: a tenant API key with the `commerce:read` scope as `Authorization: Bearer {api_key}`. The quote endpoint is a pure calculation; it creates no cart or order and therefore also requires only `commerce:read`.

## Building blocks

A configurable product is described by four kinds of entities. All IDs are UUIDs.

### Dimensions

A dimension is a numeric input the buyer must provide, for example width and depth. Each dimension has a `key` (the request key used in quote calls), a display `label`, a display `unit` string (for example `"cm"`), and an integer `min` and `max`. Dimensions are ordered by position; that order determines how dimension values map to price matrix axes (the first dimension is `dim1`, the second is `dim2`).

A dimension can carry constraints: per-choice tightenings of its allowed range. When the buyer selects a choice that has a constraint on a dimension, the dimension's effective `min` is raised to the constraint's `min` (if higher) and its effective `max` is lowered to the constraint's `max` (if lower). Constraints only ever tighten the range, never widen it.

### Option groups and choices

Options are organized in groups. A group has a `type` of `single` (the buyer may pick at most one choice from the group) or `multi` (any number), and a `required` flag (at least one choice from the group must be selected). Groups and choices are both ordered by position.

Each choice can affect the price through a delta:

| delta_type | Meaning of delta_value |
| --- | --- |
| `fixed` | Amount in minor units (cents), added once. |
| `per_area` | Rate in minor units per square meter. The delta is `delta_value` times the area, where area is `dim1 * dim2 / 10000` square meters (dimensions are entered in cm). Requires the product to have exactly two dimensions; otherwise the quote fails with a 422. |
| `percent` | Percentage of the base price, stored times 100: a `delta_value` of `1050` means 10.50 percent. The delta is `base * delta_value / 10000`. |
| `percent_of_total` | Same encoding, but applied to the subtotal after all other deltas (see the pricing walkthrough below). |

Choices also carry `is_default` (a UI hint for the initial selection), an `images` array, and a `requires_quote` flag. When `requires_quote` is `true`, the storefront should route the buyer to a manual quote request instead of checkout; the quote endpoint still prices the configuration, the flag is informational.

### Choice rules

Rules relate choices across groups:

- `requires`: the choice is only valid when at least one of its required targets is selected. Targets are grouped by the target's option group: within one target group the relation is OR (any one target satisfies it), across target groups it is AND (each group's requirement must be satisfied).
- `excludes`: the choice cannot be combined with the target choice.

The schema exposes each choice's `requires` and `excludes` arrays as lists of choice IDs so the UI can disable invalid combinations up front. The quote endpoint enforces the same rules server-side and returns 422 with a human-readable message on violation.

### Price matrices

When a product has dimensions, its base price comes from a price matrix: a named 2D lookup table of dimension ranges to prices. Each matrix has a set of `conditions`, choice IDs that must all be selected for the matrix to be eligible. A matrix with no conditions is always eligible.

Matrix selection: among all eligible matrices (those whose entire condition set is contained in the buyer's selection), the one with the most conditions wins. Ties are broken by creation order (oldest first). Inside the winning matrix, the row whose `dim1` and `dim2` ranges contain the buyer's dimension values supplies the base price. Dimension values map to `dim1` and `dim2` in dimension position order.

If no matrix is eligible, or the winning matrix has no row covering the dimension values, the quote fails with a 422 `not_priceable` error. A product with no dimensions skips matrices entirely and uses its own `price_amount` as the base; if that is `null`, the quote is likewise `not_priceable`.

## Pricing walkthrough

Given a valid selection, the total is computed in two passes, all in integer minor units, with `PHP_ROUND_HALF_UP` rounding to whole minor units at each percentage or area step:

1. Resolve the base price (matrix row or product price). The breakdown starts with a line labeled `Base`.
2. Pass 1, in group and choice position order: every selected choice that is not `percent_of_total` adds its delta (`fixed`, `per_area`, or `percent` of the base). Each choice adds one breakdown line labeled `{Group name}: {Choice name}`, including zero-delta choices, which appear with amount 0.
3. Pass 2: each selected `percent_of_total` choice adds `pass-1 subtotal * delta_value / 10000`, computed against the same subtotal independently, so multiple `percent_of_total` choices do not compound each other. Each adds its own breakdown line.

The response's `total` equals the base plus all deltas. The currency is the product's currency.

## Money and units summary

- All amounts are integers in minor units (cents), with the product's ISO currency code.
- Percent values are stored times 100: divide `delta_value` by 10000 to get the fraction.
- `per_area` rates are minor units per square meter; dimensions are entered in centimeters and area is `dim1 * dim2 / 10000` square meters.
- Dimension values in quote requests must be integers, in the dimension's unit.

## Visibility guarantees

The schema only exposes active choices; soft-deleted and deactivated choices are filtered out. Dimension constraints, matrix conditions, and `requires` and `excludes` lists are pruned to the currently visible choice IDs, so every choice ID that appears anywhere in the schema is resolvable within that same schema.
