# Sets, kits and production recipes

Product composition and stock handling describe different decisions. A native recipe defines components, variants, factors and the distribution of order fields. A published stock structure determines which physical goods fulfill that composition.

| Mode | Physical stock source | Fulfillment |
| --- | --- | --- |
| `virtual_set` | Physical components; no independent header stock | Allocate, produce when required, and ship each component |
| `physical_kit` | An actually assembled or received finished stock article | Allocate and ship finished kit quantities |
| `production_bom` | Recorded component consumption and actual finished output | Use confirmed output; a recipe or production start creates no finished stock |

Publication creates no reservation, consumption or output. Use manufacturing commands for actual assembly, disassembly, consumption, output and scrap. Warehouse manufacturing does not automatically provide the production proof required by a particular dispatch section.

An imported shop group can repeat an existing order line as a virtual head. When one unambiguous real part of that same group is already linked and its shop product, variant identity and quantity match exactly, the head stays grouping context without requiring another catalog article. This recognition adds no physical line, quantity or automatic production job. Different or missing identities, conflicting assignments and unresolved real parts still require clarification. This interpretation does not publish a stock structure or rewrite existing warehouse or production work.

## Endpoints and permissions

- `GET /v1/articles/{id}/stock-structure`: current definition, optionally selecting `variant_id` or an owned historical `definition_id`. Requires `products:read`.
- `GET /v1/articles/{id}/stock-structure/options`: active native recipe, current definition, variants and authorized operator, warehouse and condition references. Requires `products:read`, `inventory:read`, `conditions:read`, and the key creator's current product and warehouse viewing permissions.
- `GET /v1/articles/{id}/stock-structure/availability`: complete virtual sets available in one `operator_ref` and `warehouse_ref`, optionally for `variant_id`. Requires the same read scopes as options.
- `POST /v1/stock-structures/commands`: publish with `stock_structures:write`, `products:write` and current product editing permission.
- `POST /v1/stock-structures/commands/preview`: the same checks with complete rollback; additionally requires `products:read`.
- `GET /v1/stock-structures/commands/{idempotencyKey}`: the original receipt belonging to this API connection; requires `products:read`.

Test keys cannot publish real definitions or calculate against actual stock. Their options reads do not expose real article or warehouse choices. Use isolated simulator articles and inventory for complete write tests.

## Author a recipe directly

An ERP or authorized AI can create the first recipe without a prior UI edit. Native recipe authoring and publication of warehouse semantics are separate, explicit commands.

- `GET /v1/articles/{id}/structure`: complete active recipe and its current revision, plus the separately published warehouse definition and its stale flag. Requires `products:read`. `structure: null` means there is no active recipe for the selected article/variant.
- `GET /v1/articles/{id}/structure/options`: owned component products, native variants or compiler-variable choices. Requires `products:read`. Select `section=products|variants|variables`; use `component_product_id` for component variants, optional `search`, `offset` and `limit` (1–100). Follow `next_offset` until null. The response includes accepted providers and collection bounds.
- `POST /v1/product-structures/commands`: save or archive a native recipe. Requires **both** `products:write` and the separate opt-in `product_structures:write`, plus the key creator's current product editing permission. Existing product-write keys receive no new recipe authority automatically.
- `POST /v1/product-structures/commands/preview`: the identical checks with rollback; additionally requires `products:read`. `saved: false` is no stored version or command receipt.
- `GET /v1/product-structures/commands/{requestId}`: original receipt from this API connection, with current `products:read` authority. A null result leaves the outcome unknown.

Use one durable `request_id` for the exact intended change. Saving **immediately activates a changed native definition**; there is no separately stored draft. Identical current content can reuse the existing version, reported as `reused: true`. The preview reports the actual `result_revision` and `reused` decision. The recipe definition retains the same field names and meanings as the native editor:

| Field | Meaning |
| --- | --- |
| `parentProductId`, `parentProductVariantId` | Own article and optional variant; use explicit null for the main article |
| `expectedRevision` | Current recipe revision; 0 for its first creation |
| `parts` | 1–100 flat slots, each with stable `slotId`, owned `productId`, nullable `productVariantId`, `label`, positive integer `quantity`, and zero-based `sortOrder` |
| `routes` | At most 500 personalization rules; independently, the sum of all `targetSlotIds.length` values is at most 500 |
| Route `source` | `provider_field` with returned `provider` and `sourceKey`, or `compiler_variable` with actual `variableId` and `variableKey` |
| Route handling | `mode`: `append`, `replace` or `exclude`, `targetSlotIds`, `overwriteExisting`, optional `compileTargetVariable` |
| `fulfillmentMode` | `atomic` or `per_part`; a fulfillment rule, not permission to invent physical shipping evidence |

`max_route_targets: 500` in the options response means the **total target assignments across every rule**, not 500 targets per rule or 500 distinct slots. Each rule permits at most 100 unique slot IDs, all referring to the recipe's at most 100 parts. Reusing one slot in two rules consumes two target assignments. Five rules targeting 100 slots each consume 500 assignments; adding another single-target rule exceeds the total even though there are only six rules. An `exclude` rule submitted with `targetSlotIds: []` contributes one rule and zero targets. These limits are separate from shop bundle-rule limits.

Every route target must be a slot in the submitted complete recipe. Slot identifiers remain stable across deliberate edits. These component factors are integer counts; warehouse balances elsewhere use exact decimal strings. Unknown or foreign products/variants are unavailable and cannot be introduced through a field route.

```json
{
  "request_id": "e2000000-0000-4000-8000-000000000001",
  "operation": {
    "kind": "save",
    "definition": {
      "parentProductId": "e2000000-0000-4000-8000-000000000002",
      "parentProductVariantId": null,
      "expectedRevision": 0,
      "fulfillmentMode": "atomic",
      "parts": [{
        "slotId": "e2000000-0000-4000-8000-000000000003",
        "productId": "e2000000-0000-4000-8000-000000000004",
        "productVariantId": null,
        "label": "Cup",
        "quantity": 2,
        "sortOrder": 0
      }],
      "routes": []
    }
  }
}
```

Replace the synthetic article references with actual choices and create the request/slot UUIDs for your intention. Archive uses `operation: {kind: "archive", product_id, structure_id, expected_revision}`. Active physical work or a warehouse publication can prevent archiving. A new recipe does not rewrite frozen orders, manufacturing evidence or historical shipments. If `stock_publication_stale` is true, read and explicitly publish the new warehouse definition before starting work that requires the current recipe.

After an uncertain response, read the receipt with the original `request_id` or repeat the identical command. A new request ID is a new intention, even when identical current content happens to reuse a version. Historical profile versions may recover only a proven identical original command, never admit a new edit.

## Publish a definition

1. Create and activate the native recipe with `/v1/product-structures/commands`, product management or the shared product structure tools.
2. Read options for the article and optional variant. Retain the active `structure_version_id`, every `slot_id` and the current published `revision` (`0` if unpublished).
3. Choose a mode and a condition policy for every component. Preview the command, then submit the exact reviewed request.

```json
{
  "idempotency_key": "e1000000-0000-4000-8000-000000000001",
  "product_id": "e1000000-0000-4000-8000-000000000002",
  "variant_id": null,
  "structure_version_id": "e1000000-0000-4000-8000-000000000003",
  "expected_revision": 0,
  "mode": "virtual_set",
  "component_conditions": [
    {
      "slot_id": "e1000000-0000-4000-8000-000000000004",
      "policy": { "kind": "certified_released" }
    }
  ],
  "output_condition_policy": { "kind": "certified_released" },
  "reason": "Ship individual components from inspected stock"
}
```

Include every slot of the active flat recipe exactly once. Factors are positive whole-piece counts taken from that native recipe; the command cannot overwrite them with different values. Stock quantities are exact decimal strings with at most six decimal places. `available_sets` is a whole count represented as a string. The documented unit is `piece`. Independently stocked variants require an unambiguous physical SKU.

The response includes `definition_id`, `revision`, each component's actual stock article, and frozen factors and policies. A `virtual_set` header has `stock_product_id: null`. For a physical kit or production recipe, that field identifies the actual finished stock article.

## Choose condition policies deliberately

`certified_released` accepts explicitly released conditions and excludes unclassified legacy stock. This policy is deliberately broad: subsequent released versions can also qualify. Use `selected` when eligibility must remain limited to particular condition versions and operators:

```json
{
  "kind": "selected",
  "condition_refs": ["e1000000-0000-4000-8000-000000000005"]
}
```

Obtain references from the authorized options response. Each represents an immutable condition version of a warehouse operator. New versions are not automatically added to a concrete selection. Quarantined or blocked goods do not become eligible through an arbitrary supplied reference. An explicit selection can include the clearly identified legacy condition; doing so does not retrospectively certify inspection.

Component policies govern eligible input goods. `output_condition_policy` governs the eligibility of finished physical goods for orders. Real output may initially enter quarantine; physical receiving and release are separate decisions.

## Availability without counting shared stock twice

A set needs two pieces of one SKU in slot A and three pieces of that same SKU in slot B. With 20 free eligible pieces, **four complete sets** are available. Both slots share the same 20 pieces. Do not add their individual `eligible_free` values.

The calculation considers reservations, exclusive holds, storage location, current partner releases and condition policies. Overlapping condition selections also share physical supply. Calculation stays within one operator warehouse; goods from separate warehouses are not silently combined.

This response is a current observation, not a reservation. The same components may support different offered sets. Their individually reported availability is not a simultaneous promise; an actual allocation is required to bind goods.

## Dispatch, production and returns

Orders retain their frozen composition. Physical components have their own `component_id`; reservations, production evidence and package contents remain bound to those actual quantities. The commercial header quantity is not the sum of components.

A shipment may contain only some required components. Its proven complete header quantity can therefore be `0` despite real component movement. Read `/v1/orders/{id}/dispatches`, including `components` and package contents, for these cases. A later shipment can supply missing components and prove a positive complete header quantity. Earlier shipment receipts are not rewritten. The older shipment-only reader may reject such a case with `shipment_components_require_dispatches`.

A virtual-set return refers to an actually shipped physical component from the return-source choices. It never receives a virtual header as stock. A physical kit remains a real finished article; disassembly requires its proven original manufacturing composition, not a guess based on today's active recipe.

## Conflicts, receipts and limits

The active native recipe and expected publication revision are checked at write time. On conflict, read again and review a new deliberate decision. Existing physical goods, physical history, unfinished manufacturing or bound orders can prevent a mode change. Changing modes cannot reinterpret historical goods.

Persist the complete request and `idempotency_key` before sending. After an uncertain response, read the original receipt or retry the exact original request. The receipt remains bound to that API connection and its unchanged arguments. A later current definition does not replace the historical reply. A `null` lookup does not prove failure.

Options are bounded to 100 components, 250 variants, 100 operators, 1,000 visible condition versions and 250 warehouses. Exceeding a bound is rejected explicitly rather than silently truncated. Nested virtual sets and unresolved variants are not supported physical component sources. External shop bundles must preserve their actually imported commercial lines; a missing external header line is never invented from component totals.

When a shop supplies real commercial child lines and only a logical header without an order line, each child keeps its original order-line identifier and already-total quantity. Its stock factor is `1`; the logical header creates no extra demand. A group with both a commercial header and commercial children remains ambiguous and is rejected for allocation until its source structure is clarified. A physical-kit definition cannot silently become shipping of its recipe inputs.
