Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions docs/architecture/price-lists/api-and-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,34 @@ Exact prefix depends on deployment (`API_PATH_PREFIX`).

| Action | HTTP | Notes |
|--------|------|--------|
| List / retrieve | `GET` | Filter: `name`, `uuid`, `enabled` (`PriceListFilter`). |
| List | `GET` | Query contract below; uses [`PriceListFilter`](../../../koku/cost_models/price_list_view.py) + [`SettingsFilter`](../../../koku/api/settings/utils.py) (same bracket style as Settings APIs). |
| Retrieve | `GET` | Single object by `{uuid}`. |
| Create | `POST` | Body validated by [`PriceListSerializer`](../../../koku/cost_models/price_list_serializer.py). |
| Full update | `PUT` | Delegates to serializer `update` → [`PriceListManager.update`](../../../koku/cost_models/price_list_manager.py). |
| Delete | `DELETE` | [`perform_destroy`](../../../koku/cost_models/price_list_view.py) uses manager: **blocked** if any `PriceListCostModelMap` exists. |
| Affected cost models | `GET .../price-lists/{uuid}/affected-cost-models/` | Convenience read for impact analysis. |
| Duplicate | `POST .../price-lists/{uuid}/duplicate/` | Copies rates, dates, currency, description; new name `Copy of …` (max 255 chars); `version=1`, `enabled=true`; **no** cost-model attachments. See [`duplicate`](../../../koku/cost_models/price_list_view.py). |
| Affected cost models | `GET .../price-lists/{uuid}/affected-cost-models/` | Same assignment info as `assigned_cost_models` on the main resource, as a dedicated array endpoint. |

**List query parameters** (enforced in [`PriceListFilter.filter_queryset`](../../../koku/cost_models/price_list_view.py)): only **`offset`**, **`limit`**, **`filter`**, and **`order_by`** are accepted at the top level. Anything else → **400** (`Unsupported parameter or invalid value`).

**`filter[field]=value`** — allowed keys:

| Key | Behavior |
|-----|----------|
| `name` | Case-insensitive **substring**; comma-separated values are **AND**ed (each substring must match). |
| `uuid` | Exact UUID. |
| `enabled` | Boolean. |
| `currency` | Exact match, case-insensitive. |

Unknown `filter[...]` keys → **400**.

**`order_by[field]=asc|desc`** — `field` must resolve to a column or annotation on the list queryset (e.g. `name`, `effective_start_date`, `effective_end_date`, `updated_timestamp`, `currency`, **`assigned_cost_model_count`**). Invalid field or direction → **400**. Default ordering is **`name`** ascending (see `PriceListFilter.Meta.default_ordering`).

**Response shape** ([`PriceListSerializer.to_representation`](../../../koku/cost_models/price_list_serializer.py)): besides persisted fields, each price list includes read-only **`assigned_cost_model_count`** and **`assigned_cost_models`** (`{uuid, name, priority}` per map), so clients do not need a second request to see assignments (the `affected-cost-models` route remains for callers that only need that slice).

**Permissions**: [`CostModelsAccessPermission`](../../../koku/api/common/permissions/cost_models_access.py) on the viewset.

**Caching**: list/create/retrieve/update/destroy are wrapped with `@never_cache`.
**Caching**: list/create/retrieve/update/destroy, `affected-cost-models`, and `duplicate` are wrapped with `@never_cache`.

---

Expand Down
161 changes: 120 additions & 41 deletions docs/specs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@
],
"summary": "List the price lists",
"operationId": "listPriceLists",
"description": "List endpoint accepts only **offset**, **limit**, **filter**, and **order_by** query parameters. Any other top-level parameter returns 400.",
"parameters": [
{
"$ref": "#/components/parameters/QueryOffset"
Expand All @@ -513,49 +514,10 @@
"$ref": "#/components/parameters/QueryLimit"
},
{
"name": "name",
"required": false,
"in": "query",
"description": "Filter by price list name (case-insensitive substring match).",
"schema": {
"type": "string"
}
},
{
"name": "uuid",
"required": false,
"in": "query",
"description": "Filter by exact price list UUID.",
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "enabled",
"required": false,
"in": "query",
"description": "Filter by enabled status. Use true to show only active price lists or false to show only disabled ones.",
"schema": {
"type": "boolean"
}
"$ref": "#/components/parameters/PriceListQueryFilter"
},
{
"name": "ordering",
"required": false,
"in": "query",
"description": "Order response by allowed fields. Prefix with '-' for descending order. Default is 'name'.",
"schema": {
"type": "string",
"enum": [
"name",
"-name",
"effective_start_date",
"-effective_start_date",
"updated_timestamp",
"-updated_timestamp"
]
}
"$ref": "#/components/parameters/PriceListQueryOrderBy"
}
],
"security": [
Expand All @@ -574,6 +536,16 @@
}
}
},
"400": {
"description": "Bad Request (unsupported query parameter, invalid filter key, or invalid order_by field/direction).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized"
},
Expand Down Expand Up @@ -917,6 +889,78 @@
}
}
},
"/price-lists/{price_list_uuid}/duplicate/": {
"post": {
"tags": [
"Price Lists"
],
"summary": "Duplicate a price list.",
"description": "Creates a new price list by copying rates, currency, validity dates, and description from the source. The new list is named `Copy of {original name}` (truncated to 255 characters if needed), **version** resets to **1**, **enabled** is **true**, and it is **not** linked to any cost model. Request body is optional and ignored.",
"operationId": "duplicatePriceList",
"parameters": [
{
"name": "price_list_uuid",
Comment thread
bacciotti marked this conversation as resolved.
"in": "path",
"description": "UUID of the Price List to duplicate.",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"security": [
{
"basic_auth": []
}
],
"responses": {
"201": {
"description": "The newly created price list (same shape as create response).",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PriceListOut"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized"
},
"404": {
"description": "Not Found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"500": {
"description": "Unexpected Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/forecasts/aws/costs/": {
"summary": "AWS Cost Forecasts",
"get": {
Expand Down Expand Up @@ -6920,6 +6964,28 @@
"maximum": 1000
}
},
"PriceListQueryFilter": {
"name": "filter",
"required": false,
"in": "query",
"description": "Filter price lists with `filter[field]=value`. Allowed keys: **name** (case-insensitive substring; comma-separated values are ANDed), **uuid** (exact UUID), **enabled** (boolean), **currency** (exact, case-insensitive). Unknown filter keys return 400.",
"style": "deepObject",
"explode": true,
"schema": {
"type": "object"
}
},
"PriceListQueryOrderBy": {
"name": "order_by",
"required": false,
"in": "query",
"description": "Sort with `order_by[field]=asc` or `order_by[field]=desc`. Allowed fields match the `price_list` queryset (including annotation **assigned_cost_model_count**): e.g. **name**, **effective_start_date**, **effective_end_date**, **updated_timestamp**, **currency**, **assigned_cost_model_count**. Unknown fields return 400. Default is ascending **name**.",
"style": "deepObject",
"explode": true,
"schema": {
"type": "object"
}
},
"ReportQueryLimit": {
"in": "query",
"name": "limit",
Expand Down Expand Up @@ -7377,6 +7443,19 @@
"type": "string",
"format": "date-time",
"readOnly": true
},
"assigned_cost_model_count": {
"type": "integer",
"readOnly": true,
"description": "Number of cost models that reference this price list."
},
"assigned_cost_models": {
"type": "array",
"readOnly": true,
"description": "Cost models linked to this price list (uuid, name, priority). Same shape as rows from `GET .../affected-cost-models/`, inlined for convenience.",
"items": {
"$ref": "#/components/schemas/PriceListAffectedCostModel"
}
}
}
}
Expand Down
Loading