# Cartregels

Cartregels bevatten de producten in een cart. Elke regel maakt bij het toevoegen een snapshot van de productnaam en SKU, slaat de bepaalde stukprijs op in minor units en, voor made-to-measure-producten, een volledige configuratiesnapshot. Alle drie de endpoints vereisen de scope `commerce:write`, een `active` cart, en doorlopen de cart-eigenaarschapscheck (zie de pagina Carts). Elk endpoint geeft de volledige cart-envelope terug, dezelfde vorm als `GET /v1/api/commerce/carts/{id}`.

## Regels per capability

Hoe een product wordt toegevoegd hangt af van de `capability` (zie [Producten](/nl/api/catalog/products)):

- **`standard`-producten met actieve varianten**: `variant_id` is verplicht en moet een actieve variant van het product zijn.
- **`standard`-producten zonder varianten**: `variant_id` moet worden weggelaten.
- **`made_to_measure`-producten**: `configuration` is verplicht en `variant_id` is niet toegestaan. De configuratie is een object `{"dimensions": {...}, "choices": ["choice_id", ...]}` dat door de configurator-engine wordt geprijsd; dimensies zijn gehele getallen. De regel slaat een `config_hash` op plus een volledige snapshot onder `attributes.configuration` (dimensions, choices, prijsuitsplitsing, componentenlijst, matrixcel). Componentvoorraad wordt per configurator-component gereserveerd.
- Voor `standard`-producten is `configuration` niet toegestaan.
- Een product waarvan `capability_active` `false` is, kan niet worden toegevoegd: de commerce-module verkoopt `standard`-producten en de configurator-module `made_to_measure`-producten.

## Hoeveelheden

`quantity` is een JSON-getal groter dan nul met maximaal 8 cijfers voor de komma en maximaal 3 decimalen (maximum `99999999.999`). Stuur het als JSON-getal of als gewone decimale string met een punt (`"0.75"`); een string met een decimale komma of een exponent (`"0,75"`, `"1e3"`) wordt geweigerd. Een product met `unit` `piece` accepteert alleen gehele getallen (`2` of `2.0`, nooit `2.5`); een product in elke andere eenheid (`kg`, `m2`, `hour`, ...) accepteert tot drie decimalen, bijvoorbeeld `0.75`. Hoeveelheden komen terug als JSON-getallen (`0.75`, `12`). Een regeltotaal is de stukprijs maal de hoeveelheid, half naar boven afgerond op hele minor units.

## Weigeringscodes

Wanneer een regel niet kan worden toegevoegd, bevat de 422-body een machineleesbare `code` op het hoogste niveau naast `message` en `errors`:

| `code` | Betekenis |
| --- | --- |
| `CAPABILITY_INACTIVE` | De capability van het product kan nu niet verkocht worden (`capability_active` is `false`): de module die het verkoopt is voor de winkel uitgeschakeld. Gemeld op `errors.product_id`. |
| `INVALID_CONFIGURATION` | De variant of configuratie past niet bij het product: een variant ontbreekt, is onbekend of niet toegestaan; een configuratie ontbreekt of is meegestuurd voor een `standard`-product; of de configurator-engine weigerde de configuratie (de foutkeys, bijvoorbeeld `dimensions.width`, worden doorgegeven). |
| `NOT_PURCHASABLE` | Het product kan niet verkocht worden, bijvoorbeeld omdat er geen prijs voor te bepalen is. Gemeld op `errors.product_id`. |

```json title="Response (422, line refusal)"
{
  "message": "Width must be a whole number.",
  "errors": {
    "dimensions.width": ["Width must be a whole number."]
  },
  "code": "INVALID_CONFIGURATION"
}
```

Gewone invoervalidatie (een ontbrekende of ongeldige `product_id` of `quantity`, een onbekend of inactief product, een hoeveelheid met decimalen voor een `piece`-product) gebruikt de standaard 422-envelope zonder `code`. Branch op `code` wanneer die aanwezig is, en val anders terug op `errors`.

Regels dedupliceren op de combinatie van product, variant en configuratiehash: dezelfde combinatie opnieuw toevoegen wordt samengevoegd met de bestaande regel, waarbij de hoeveelheden worden opgeteld en de stukprijs opnieuw wordt bepaald op de nieuwe staffeltrede.

Stukprijzen worden bepaald op basis van de pricingcontext van de request: de klant geïdentificeerd door `X-Customer-Token` indien aanwezig, anders de aan de cart gebonden klant, anders guest pricing.

## POST /v1/api/commerce/carts/{id}/lines

Voegt een product toe aan de cart, of voegt samen met een bestaande identieke regel. Geeft de volledige cart terug met status 201.

### Parameters

**Pad**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `id` | string (UUID) | ja | Cart-id. Moet een `active` cart zijn. |

**Body**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `product_id` | string (UUID) | ja | Het toe te voegen product. Moet een actief product van de store zijn. |
| `variant_id` | string (UUID) of null | conditioneel | Verplicht wanneer het product actieve varianten heeft; anders niet toegestaan, en altijd niet toegestaan voor `made_to_measure`-producten. |
| `quantity` | number | ja | Groter dan nul, maximaal 3 decimalen; een geheel getal voor een `piece`-product (zie Hoeveelheden hierboven). De samengevoegde regelhoeveelheid mag `max_quantity_per_line` uit de policy niet overschrijden. |
| `configuration` | object | conditioneel | Verplicht voor `made_to_measure`-producten, anders niet toegestaan. Vorm: `{"dimensions": {...}, "choices": [choice_ids]}`, met dimensies als gehele getallen. Gevalideerd door de configurator-engine. |

**Headers**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `X-Customer-Token` | string | nee | Bepaalt de pricingcontext; vereist als de cart gebonden is aan een niet-guest klant. |

```bash title="Request"
curl -X POST https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/lines \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
    "quantity": 2
  }'
```

```javascript title="Request (JS)"
const res = await fetch(
  `https://api.borkol.com/v1/api/commerce/carts/${cartId}/lines`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      product_id: "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      variant_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
      quantity: 2,
    }),
  }
);
const { data: cart } = await res.json();
```

```json title="Response"
{
  "data": {
    "id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
    "status": "active",
    "lines": [
      {
        "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
        "product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "variant_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
        "name": "Garden Chair",
        "sku": "CHAIR-GRN-M",
        "quantity": 2,
        "unit_price_amount": 2250,
        "unit_price": { "amount": 2250, "currency": "EUR" },
        "line_total": { "amount": 4500, "currency": "EUR" },
        "attributes": null
      }
    ],
    "...": "same cart shape as GET /v1/api/commerce/carts/{id}"
  }
}
```

Voor een `made_to_measure`-product bevat de aangemaakte regel de configuratiesnapshot. `components[].qty` is een exacte decimale string per geconfigureerde eenheid (vermenigvuldig met de regelhoeveelheid voor de gereserveerde voorraad):

```json title="Response (made-to-measure line excerpt)"
{
  "attributes": {
    "configuration": {
      "dimensions": { "width": 120, "depth": 180 },
      "choices": ["c1d2e3f4-..."],
      "breakdown": [
        { "label": "Base", "amount": 18900 },
        { "label": "Motor: Motorised", "amount": 7500 }
      ],
      "components": [{ "product_id": "...", "qty": "2.16" }],
      "matrix": { "id": "...", "name": "Fabric blinds" }
    }
  }
}
```

### Fouten

Regelweigeringen zijn 422-fouten met de melding gekoppeld aan het betreffende veld in `errors` en een `code` op het hoogste niveau (zie Weigeringscodes hierboven):

| Status | Body | Wanneer |
| --- | --- | --- |
| 422 | `errors.product_id: ["Product is not available."]` | Product onbekend of niet actief. Geen `code`. |
| 422 | `errors.product_id: ["Product is not available."]`, `code: "NOT_PURCHASABLE"` | Het product bestaat en is actief maar kan niet verkocht worden, bijvoorbeeld omdat er geen prijs voor te bepalen is. |
| 422 | `errors.product_id: ["Product is not available."]`, `code: "CAPABILITY_INACTIVE"` | De capability van het product is niet actief voor de winkel (`capability_active` is `false`). |
| 422 | `errors.variant_id: ["Choose a variant for this product."]`, `code: "INVALID_CONFIGURATION"` | Product heeft actieve varianten en `variant_id` is weggelaten. |
| 422 | `errors.variant_id: ["Variant is not available."]`, `code: "INVALID_CONFIGURATION"` | Variant-id herleidt niet naar een actieve variant van het product. |
| 422 | `errors.variant_id: ["This product has no variants."]`, `code: "INVALID_CONFIGURATION"` | `variant_id` meegestuurd voor een product zonder varianten. |
| 422 | `errors.variant_id: ["Configurable products have no variants."]`, `code: "INVALID_CONFIGURATION"` | `variant_id` meegestuurd voor een `made_to_measure`-product. |
| 422 | `errors.configuration: ["Configure this product before adding it."]`, `code: "INVALID_CONFIGURATION"` | `made_to_measure`-product zonder een `configuration`. |
| 422 | `errors.configuration: ["This product is not configurable."]`, `code: "INVALID_CONFIGURATION"` | `configuration` meegestuurd voor een `standard`-product. |
| 422 | `errors.choices`, `errors.choices.{choiceId}`, `errors.dimensions.{key}`, `errors.configuration`, `code: "INVALID_CONFIGURATION"` | Fouten van de configurator-engine, gekoppeld aan hun eigen veldkeys, exact zoals gedocumenteerd in de [quote-foutcatalogus](/nl/api/configurator/quote), inclusief `{Label} must be a whole number.` voor een dimensie met decimalen. |
| 422 | `errors.quantity: ["The quantity must be a whole number."]` | Hoeveelheid met decimalen voor een `piece`-product. Geen `code`. |
| 422 | `errors.quantity: ["..."]` | Hoeveelheid ontbreekt, is geen gewone decimaal (komma, exponent, meer dan 3 decimalen of 8 cijfers voor de komma) of is niet groter dan nul. Geen `code`. |
| 422 | `{"message": "Quantity exceeds the per-line maximum.", "code": "cart_constraint"}` | Samengevoegde hoeveelheid zou `max_quantity_per_line` overschrijden. |
| 422 | `{"message": "Cart line limit reached.", "code": "cart_constraint"}` | Een nieuwe afzonderlijke regel toevoegen zou `max_lines` overschrijden. |
| 404 | `{"message": "..."}` | Cart onbekend, verlopen of niet `active`. |
| 403 | eigenaarschaps-, scope-, module- of schorsingsbody | Zie de pagina Carts. |
| 429 | `{"message": "Too Many Attempts."}` | Rate limit overschreden. |

## PATCH /v1/api/commerce/carts/{id}/lines/{lineId}

Wijzigt de hoeveelheid van een regel, met dezelfde hoeveelheidsregels als bij toevoegen (een geheel getal voor een `piece`-product). Regels van niet-geconfigureerde producten worden opnieuw geprijsd op de nieuwe hoeveelheid (staffelprijzen) met de pricingcontext van de request. Geconfigureerde regels, en regels waarvan de variant inmiddels is gedeactiveerd, behouden hun opgeslagen stukprijs; onbeschikbaarheid komt pas bij checkout aan het licht. Geeft de volledige cart terug met status 200.

### Parameters

**Pad**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `id` | string (UUID) | ja | Cart-id. Moet een `active` cart zijn. |
| `lineId` | string (UUID) | ja | Regel-id. Moet bij deze cart horen. |

**Body**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `quantity` | number | ja | Groter dan nul, maximaal 3 decimalen, een geheel getal voor een `piece`-product; maximaal `max_quantity_per_line` uit de policy. |

**Headers**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `X-Customer-Token` | string | nee | Bepaalt de pricingcontext; vereist als de cart gebonden is aan een niet-guest klant. |

```bash title="Request"
curl -X PATCH https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/lines/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9 \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{"quantity": 3}'
```

```json title="Response"
{
  "data": {
    "id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
    "lines": [{ "id": "5e6f7a8b-...", "quantity": 3, "...": "..." }],
    "...": "same cart shape as GET /v1/api/commerce/carts/{id}"
  }
}
```

### Fouten

| Status | Body | Wanneer |
| --- | --- | --- |
| 422 | `{"message": "...", "errors": {"quantity": ["..."]}}` | Ontbrekende of ongeldige hoeveelheid, niet groter dan nul, of met decimalen voor een `piece`-product. Dit endpoint antwoordt nooit met een weigerings-`code`: een regel die niet meer te bepalen is, houdt zijn prijs tot de checkout. |
| 422 | `{"message": "Quantity exceeds the per-line maximum.", "code": "cart_constraint"}` | Boven `max_quantity_per_line`. |
| 404 | `{"message": "..."}` | Cart onbekend of niet `active`, of de regel hoort niet bij deze cart. |
| 403 | eigenaarschaps-, scope-, module- of schorsingsbody | Zie de pagina Carts. |
| 429 | `{"message": "Too Many Attempts."}` | Rate limit overschreden. |

## DELETE /v1/api/commerce/carts/{id}/lines/{lineId}

Verwijdert een regel uit de cart en geeft eventuele voorraadreserveringen voor de cart vrij. Geeft de volledige cart terug, zonder de verwijderde regel, met status 200.

### Parameters

**Pad**

| Naam | Type | Verplicht | Beschrijving |
| --- | --- | --- | --- |
| `id` | string (UUID) | ja | Cart-id. Moet een `active` cart zijn. |
| `lineId` | string (UUID) | ja | Regel-id. Moet bij deze cart horen. |

Geen body.

```bash title="Request"
curl -X DELETE https://api.borkol.com/v1/api/commerce/carts/9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4/lines/5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9 \
  -H "Authorization: Bearer {api_key}"
```

```json title="Response"
{
  "data": {
    "id": "9d3f2a10-6b7c-4e6d-9f21-8a54c1d2e3f4",
    "lines": [],
    "...": "same cart shape as GET /v1/api/commerce/carts/{id}"
  }
}
```

### Fouten

| Status | Body | Wanneer |
| --- | --- | --- |
| 404 | `{"message": "..."}` | Cart onbekend of niet `active`, of de regel hoort niet bij deze cart. |
| 403 | eigenaarschaps-, scope-, module- of schorsingsbody | Zie de pagina Carts. |
| 429 | `{"message": "Too Many Attempts."}` | Rate limit overschreden. |
