# Edara API v3: Bundles

Read the [guide](../guide.md) first: authentication, paging, errors, rate limits and the shared `PagedResult`, `BatchResult` and `*Reference` shapes.

- `GET /v3/bundles`: Lists bundles.
- `GET /v3/bundles/{id}`: Gets a bundle by id.
- `POST /v3/bundles`: Creates a bundle.
- `DELETE /v3/bundles/{id}`: Deletes a bundle.

## Endpoints

### `GET /v3/bundles`: Lists bundles.

Operation `GetBundles` · permission `read:bundle`

**Query string**: `GetBundlesQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `offset` | `int?` | 0 | >= 0 | Number of items to skip before returning results. Defaults to 0. |
| `limit` | `int?` | 100 | 1..1000 (out of range → 400) | Maximum number of items to return. Defaults to 100, maximum 1000. |
| `updateDate` | `DateTime?` |  |  | Returns only bundles inserted or updated at or after this timestamp. |

**Responses**: OK `PagedResult<BundleResponse>`: Paged list of bundles.

**Notes**

- `updateDate` is the only filter. It returns bundles created or updated strictly after that time, and without it you get every bundle.
- Inactive bundles are included, because there is no filter on active status. Results are ordered by `id`.
- When nothing matches, you get `200` with an empty list, not `404`.
- When you page past the last bundle, `totalCount` comes back as 0, so do not read the total from an empty page.
- Every bundle comes back with its full `bundleDetails`, including each line's `taxRate` and `tax`, so large pages are slow.
- A line whose stock item no longer exists is left out of `bundleDetails`.

---

### `GET /v3/bundles/{id}`: Gets a bundle by id.

Operation `GetBundleById` · permission `read:bundle`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The bundle id. |

**Responses**: OK `BundleResponse`: The bundle.

**Notes**

- Inactive bundles are returned too. An id that does not exist gives `404`.
- Lines are returned in the order of their line id. Each line carries a `stockItem` reference and a `unitOfMeasure` reference.
- `taxRate` and `tax` on each line are the stock item's current values. They are not stored on the bundle, so they can change between calls.

---

### `POST /v3/bundles`: Creates a bundle.

Operation `CreateBundle` · permission `create:bundle`

**Body**: `CreateBundleRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `description` | `string` |  | NotEmpty() .MaximumLength(500) | Bundle description. Must be unique across active bundles. |
| `notes` | `string` |  | MaximumLength(500) | Free-form notes. |
| `tags` | `string[]` |  | Must(tags => tags == null \|\| string.Join(",", tags).Length <= 4000) | Tags; persisted as a comma-separated value. |
| `bundleDetails` | `List<CreateBundleItemRequest>` |  | NotEmpty(); each: SetValidator(new CreateBundleItemRequestValidator()) | Bundle line items. At least one required. |

**Responses**: Created `BundleResponse`: Bundle created.

**Business errors** (HTTP 409, match on `errorCode`):

- `DupplicatedName`: Another bundle already uses the same description.

**Notes**

- The combined length of all `tags` must be at most 4000 characters.
- Each `stockItemId` only has to exist. If any does not, you get `400` with a message that lists the missing ids.
- `unitOfMeasureId` only has to be an existing unit of measure, and one that does not exist gives `400`. The API does not check that it belongs to the stock item's units of measure, so check that yourself.
- The same stock item can appear on more than one line, and `price` is not checked against `unitPrice` times `quantity`. Prices are stored exactly as you send them.
- A new bundle is always created with `isActive` true. A missing `dimensionsValue` is stored as 0 and a missing `dimensions` as an empty string.
- Line breaks and tabs are removed from `notes` before saving.
- The bundle and all its lines are saved together. If anything fails, nothing is created.
- A `description` already used by any bundle, active or inactive, gives `409` with `errorCode` `DupplicatedName`. Note the spelling of the value.
- Success returns `201` with the bundle as it was stored, including the values the server set.

---

### `DELETE /v3/bundles/{id}`: Deletes a bundle.

Operation `DeleteBundleById` · permission `delete:bundle`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The bundle id. |

**Responses**: NoContent: (not declared; read from the method body)

**Business errors** (HTTP 409, match on `errorCode`):

- `ItemCannotDeleteItInUse`: The bundle is referenced by existing documents or related records.

**Notes**

- This permanently deletes the bundle and its lines. Success returns `204`, and an id that does not exist returns `404`.
- A bundle used on any sales document cannot be deleted. You get `409` with `errorCode` `ItemCannotDeleteItInUse` and an empty message, and the bundle is left unchanged.

## Types

### `BundleItemResponse`

Response describing a bundle line item.

| field | type | description |
|---|---|---|
| `id` | `int` | Bundle detail line identifier. |
| `bundleId` | `int` | Parent bundle identifier. |
| `stockItem` | `StockItemReference` | Stock-item reference. |
| `quantity` | `decimal` | Quantity per bundle. |
| `unitOfMeasure` | `UnitOfMeasureReference` | Unit-of-measure detail reference. |
| `unitPrice` | `decimal` | Unit price. |
| `price` | `decimal` | Line total price. |
| `taxRate` | `decimal?` | Tax rate inherited from the stock item. |
| `tax` | `TaxReference` | Tax reference inherited from the stock item. |
| `dimensionsValue` | `decimal?` | Dimension multiplier for dimension-based lines. |
| `dimensions` | `string` | Dimension expression. |
| `comments` | `string` | Free-form line comment. |

### `BundleResponse`

Response describing a bundle.

| field | type | description |
|---|---|---|
| `id` | `int` | Bundle identifier. |
| `description` | `string` | Bundle description. |
| `notes` | `string` | Free-form notes. |
| `isActive` | `bool` | Whether the bundle is active. |
| `tags` | `string[]` | Tags assigned to the bundle. |
| `bundleDetails` | `List<BundleItemResponse>` | Bundle line items. |

### `CreateBundleItemRequest`

Request to create a bundle line item.

| field | type | validation | description |
|---|---|---|---|
| `stockItemId` | `int?` | NotNull() .GreaterThan(0) | Stock item identifier. Required. |
| `quantity` | `decimal` | GreaterThan(0m) | Quantity per bundle. |
| `unitOfMeasureId` | `int?` |  | Unit-of-measure detail identifier overriding the stock item default. |
| `unitPrice` | `decimal` | GreaterThanOrEqualTo(0m) | Unit price. |
| `price` | `decimal` | GreaterThanOrEqualTo(0m) | Line total price. |
| `dimensionsValue` | `decimal?` |  | Dimension multiplier for dimension-based lines; defaults to zero. |
| `dimensions` | `string` | MaximumLength(50) | Dimension expression (e.g. "L*W*H"). |
| `comments` | `string` | MaximumLength(100) | Optional line comment. |

### `StockItemReference`

Lightweight stock-item reference (id + code + description).

| field | type | description |
|---|---|---|
| `id` | `int` | The stock-item id. |
| `code` | `string` | The stock-item code. |
| `description` | `string` | The stock-item description. |

### `TaxReference`

Lightweight tax reference (id + name + rate).

| field | type | description |
|---|---|---|
| `id` | `int` | The tax id. |
| `name` | `string` | The tax name. |
| `rate` | `decimal` | The tax rate as a percentage. |

### `UnitOfMeasureReference`

Lightweight unit-of-measure reference (id + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The unit-of-measure id. |
| `name` | `string` | The unit-of-measure name. |
