# 09 - Vendor BOM

## Storage contract

Vendor BOM is stored in `local_configurations.vendor_spec` and synced with `qt_configurations.vendor_spec`.

Each row uses this canonical shape:

```json
{
  "sort_order": 10,
  "vendor_partnumber": "ABC-123",
  "quantity": 2,
  "description": "row description",
  "unit_price": 4500.0,
  "total_price": 9000.0,
  "lot_mappings": [
    { "lot_name": "LOT_A", "quantity_per_pn": 1 }
  ]
}
```

Rules:
- `lot_mappings[]` is the only persisted PN -> LOT mapping contract;
- QuoteForge does not use legacy BOM tables;
- apply flow rebuilds cart rows from `lot_mappings[]`.

## Partnumber books

Partnumber books are pull-only snapshots from PriceForge.

Local tables:
- `local_partnumber_books`
- `local_partnumber_book_items`

Server tables:
- `qt_partnumber_books`
- `qt_partnumber_book_items`

Resolution flow:
1. load the active local book;
2. find `vendor_partnumber`;
3. copy `lots_json` into `lot_mappings[]`;
4. keep unresolved rows editable in the UI.

## CFXML import

`POST /api/projects/:uuid/vendor-import` imports one vendor workspace into an existing project.

Rules:
- accepted file field is `file`;
- maximum file size is `1 GiB`;
- one `ProprietaryGroupIdentifier` becomes one QuoteForge configuration;
- software rows stay inside their hardware group and never become standalone configurations;
- primary group row is selected structurally, without vendor-specific SKU hardcoding;
- imported configuration order follows workspace order.

Imported configuration fields:
- `name` from primary row `ProductName`
- `server_count` from primary row `Quantity`
- `server_model` from primary row `ProductDescription`
- `article` or `support_code` from `ProprietaryProductIdentifier`

Imported BOM rows become `vendor_spec` rows and are resolved through the active local partnumber book when possible.

## Inspur BOM import

The same endpoint `POST /api/projects/:uuid/vendor-import` also accepts Inspur text BOM exports.

Format: one component per line, `<partnumber>*<quantity>`. A leading `|` character is optional and stripped. Trailing whitespace around `*` is normalised.

Example:
```
|CPU_AMD_9535-EPYC2.4_64C_256M_300W*1
|PowerSupply_1300W_Titanium_220VACor240VDC_GaN*2
```

Rules:
- the entire file becomes a single configuration (`server_count = 1`);
- configuration `name` is derived from the uploaded filename (without extension);
- lines that do not contain `*<digits>` are skipped;
- no price data is present in the format; `unit_price` and `total_price` are left nil.
