# Hoe de configurator werkt

Met configureerbare producten kan een merchant artikelen verkopen die geprijsd worden op basis van afmetingen en opties in plaats van een vaste prijs: veranda's geprijsd op breedte en diepte, panelen op maat, meubels op bestelling met keuzes voor materiaal en accessoires. De storefront-flow is:

1. Haal het product op met `GET /v1/api/commerce/products/{id}`. Een configureerbaar product heeft `"capability": "made_to_measure"`. Bied de configurator alleen aan zolang `capability_active` `true` is.
2. Haal het configuratieschema op met `GET /v1/api/commerce/configurator/products/{id}/schema` en render daaruit een configurator-UI.
3. Stuur bij elke wijziging in de selectie van de koper de actuele selectie naar `POST /v1/api/commerce/configurator/products/{id}/quote` om de live prijs met een regel-voor-regel uitsplitsing op te halen.

Beide configurator-endpoints gebruiken dezelfde authenticatie als de catalogus-endpoints: een tenant-API-key die je meestuurt als `Authorization: Bearer {api_key}`. Een key met de scope `catalog:read` of `commerce:read` wordt geaccepteerd. De gate is de configurator-module: heeft de winkel die niet ingeschakeld, dan antwoorden beide endpoints `403 {"error":"Module not enabled: configurator"}` en rapporteren made-to-measure-producten `capability_active: false`. Het quote-endpoint is een pure berekening; het maakt geen cart of order aan en heeft daarom geen schrijfscope nodig.

## Bouwstenen

Een configureerbaar product wordt beschreven door vier soorten entiteiten. Alle ID's zijn UUID's.

### Dimensies

Een dimensie is een numerieke invoer die de koper moet opgeven, bijvoorbeeld breedte en diepte. Elke dimensie heeft een `key` (de request-key die je in quote-calls gebruikt), een weergave-`label`, een weergave-`unit` als string (bijvoorbeeld `"cm"`) en een integer `min` en `max`. Dimensiewaarden zijn gehele getallen. Dimensies zijn geordend op positie; die volgorde bepaalt hoe dimensiewaarden op de assen van de prijsmatrix worden afgebeeld (de eerste dimensie is `dim1`, de tweede is `dim2`).

Een dimensie kan constraints hebben: aanscherpingen van het toegestane bereik per keuze. Wanneer de koper een keuze selecteert die een constraint op een dimensie heeft, wordt de effectieve `min` van de dimensie verhoogd naar de `min` van de constraint (als die hoger is) en de effectieve `max` verlaagd naar de `max` van de constraint (als die lager is). Constraints maken het bereik alleen ooit krapper, nooit ruimer.

### Optiegroepen en keuzes

Opties zijn georganiseerd in groepen. Een groep heeft een `type` van `single` (de koper mag maximaal één keuze uit de groep kiezen) of `multi` (elk aantal), en een `required`-vlag (er moet minstens één keuze uit de groep geselecteerd zijn). Zowel groepen als keuzes zijn geordend op positie.

Elke keuze kan de prijs beïnvloeden via een delta:

| delta_type | Betekenis van delta_value |
| --- | --- |
| `fixed` | Bedrag in minor units (centen), eenmalig opgeteld. |
| `per_area` | Tarief in minor units per vierkante meter. De delta is `delta_value` maal de oppervlakte, waarbij de oppervlakte `dim1 * dim2 / 10000` vierkante meter is (afmetingen worden ingevoerd in cm). Vereist dat het product precies twee dimensies heeft; anders mislukt de quote met een 422. |
| `percent` | Percentage van de basisprijs, opgeslagen maal 100: een `delta_value` van `1050` betekent 10,50 procent. De delta is `base * delta_value / 10000`. |
| `percent_of_total` | Zelfde codering, maar toegepast op het subtotaal na alle andere delta's (zie de prijsberekening hieronder). |

Keuzes hebben daarnaast `is_default` (een UI-hint voor de initiële selectie), een `images`-array en een `requires_quote`-vlag. Wanneer `requires_quote` `true` is, hoort de storefront de koper naar een handmatige offerteaanvraag te leiden in plaats van naar checkout; het quote-endpoint prijst de configuratie nog steeds, de vlag is informatief.

### Keuzeregels

Regels leggen relaties tussen keuzes over groepen heen:

- `requires`: de keuze is alleen geldig wanneer minstens één van de vereiste targets geselecteerd is. Targets worden gegroepeerd per optiegroep van de target: binnen één targetgroep is de relatie OR (elke willekeurige target voldoet), tussen targetgroepen is het AND (aan de eis van elke groep moet worden voldaan).
- `excludes`: de keuze kan niet gecombineerd worden met de target-keuze.

Het schema stelt de `requires`- en `excludes`-arrays van elke keuze beschikbaar als lijsten van keuze-ID's, zodat de UI ongeldige combinaties vooraf kan uitschakelen. Het quote-endpoint handhaaft dezelfde regels server-side en retourneert bij een overtreding een 422 met een leesbare melding. Schema en quote zijn het altijd eens over welke keuzes bestaan: een keuze waarvan de `requires` niet vervuld kan worden door de andere gepubliceerde keuzes wordt helemaal niet gepubliceerd, dus het schema biedt nooit een keuze aan die het quote-endpoint zou afwijzen. Zie de zichtbaarheidsgaranties hieronder.

### Prijsmatrices

Wanneer een product dimensies heeft, komt de basisprijs uit een prijsmatrix: een benoemde 2D-opzoektabel van dimensiebereiken naar prijzen. Elke matrix heeft een set `conditions`: keuze-ID's die allemaal geselecteerd moeten zijn om de matrix in aanmerking te laten komen. Een matrix zonder conditions komt altijd in aanmerking.

Matrixselectie: van alle matrices die in aanmerking komen (die waarvan de volledige set conditions in de selectie van de koper zit), wint de matrix met de meeste conditions. Bij gelijkspel beslist de aanmaakvolgorde (oudste eerst). Binnen de winnende matrix levert de rij waarvan de `dim1`- en `dim2`-bereiken de dimensiewaarden van de koper bevatten de basisprijs. Dimensiewaarden worden op `dim1` en `dim2` afgebeeld in volgorde van dimensiepositie.

Als geen enkele matrix in aanmerking komt, of de winnende matrix geen rij heeft die de dimensiewaarden dekt, mislukt de quote met een 422 `not_priceable`-fout. Een product zonder dimensies slaat matrices volledig over en gebruikt zijn eigen `price_amount` als basis; als die `null` is, is de quote eveneens `not_priceable`.

## Prijsberekening stap voor stap

Bij een geldige selectie wordt het totaal berekend in twee passes, volledig in integer minor units, met `PHP_ROUND_HALF_UP`-afronding naar hele minor units bij elke percentage- of oppervlaktestap:

1. Bepaal de basisprijs (matrixrij of productprijs). De uitsplitsing begint met een regel met label `Base`.
2. Pass 1, in volgorde van groep- en keuzepositie: elke geselecteerde keuze die niet `percent_of_total` is, telt zijn delta op (`fixed`, `per_area`, of `percent` van de basis). Elke keuze voegt één uitsplitsingsregel toe met label `{Group name}: {Choice name}`, inclusief keuzes met delta nul, die verschijnen met bedrag 0.
3. Pass 2: elke geselecteerde `percent_of_total`-keuze telt `pass-1 subtotaal * delta_value / 10000` op, elk onafhankelijk berekend tegen hetzelfde subtotaal, zodat meerdere `percent_of_total`-keuzes elkaar niet versterken. Elke keuze voegt zijn eigen uitsplitsingsregel toe.

De `total` in de response is gelijk aan de basis plus alle delta's. De valuta is de valuta van het product.

## Samenvatting van geld en eenheden

- Alle bedragen zijn integers in minor units (centen), met de ISO-valutacode van het product.
- Percentagewaarden worden opgeslagen maal 100: deel `delta_value` door 10000 voor de fractie.
- `per_area`-tarieven zijn minor units per vierkante meter; afmetingen worden ingevoerd in centimeters en de oppervlakte is `dim1 * dim2 / 10000` vierkante meter.
- Dimensiewaarden in quote-requests en cart-regels moeten gehele getallen zijn, in de eenheid van de dimensie. `120` en `120.0` worden geaccepteerd; `120.5` wordt geweigerd.

## Zichtbaarheidsgaranties

Het schema toont alleen beschikbare keuzes. Soft-deleted en gedeactiveerde keuzes worden eruit gefilterd, net als elke keuze waarvan de `requires`-regel niet langer vervuld kan worden door de keuzes die overblijven. Dat filteren werkt door: het achterhouden van één keuze kan de eis van een andere keuze onvervulbaar maken, en het schema is uitgefilterd voordat het wordt geretourneerd. Dimensieconstraints en `requires`- en `excludes`-lijsten worden teruggesnoeid tot de op dat moment beschikbare keuze-ID's, en een prijsmatrix met een condition op een niet-beschikbare keuze wordt in zijn geheel uit `price_matrices` weggelaten in plaats van getoond met een ingekorte conditielijst, omdat zo'n matrix nooit in aanmerking kan komen. Daardoor is elk keuze-ID dat ergens in het schema voorkomt binnen datzelfde schema oplosbaar en kan elke keuze die het schema toont geselecteerd en geprijsd worden.
