Add unit API and article unitamount/unitSi patch.

Units use the selected company; article PATCH accepts name/unitamount/unitSi.
This commit is contained in:
seb
2026-08-01 06:15:30 +02:00
parent aba9779161
commit bfb1d8762a
13 changed files with 744 additions and 29 deletions

View File

@@ -53,6 +53,9 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/health.md`](routes/health.md) | `GET /health` |
| [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` |
| [`routes/thumb/get.md`](routes/thumb/get.md) | `GET /thumb/{fullHash}` |
| [`routes/unit/delete.md`](routes/unit/delete.md) | `DELETE /unit` |
| [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` |
| [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` |
Machine-readable spec: [`openapi.json`](openapi.json).

View File

@@ -32,12 +32,280 @@
"name": "Company",
"description": "Company CRUD"
},
{
"name": "Unit",
"description": "Company-scoped units of measure"
},
{
"name": "Image",
"description": "Content-addressed AVIF by fullHash"
}
],
"paths": {
"/unit": {
"get": {
"tags": [
"Unit"
],
"summary": "List units",
"description": "Lists units for the authenticated user's selected company (default-language symbol/name).",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "unit/list.md",
"responses": {
"200": {
"description": "Unit list",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"units": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Unit"
}
}
}
}
}
}
},
"400": {
"description": "No company selected",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Company not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"put": {
"tags": [
"Unit"
],
"summary": "Upsert unit",
"description": "Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language.\n`basesifactor` must be finite and `> 0`.\n",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "unit/upsert.md",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"si",
"basesi",
"basesifactor",
"symbol",
"name"
],
"properties": {
"si": {
"type": "string"
},
"basesi": {
"type": "string"
},
"basesifactor": {
"type": "number"
},
"symbol": {
"type": "string"
},
"name": {
"type": "string"
}
},
"example": {
"si": "l",
"basesi": "l",
"basesifactor": 1,
"symbol": "L",
"name": "Liter"
}
}
}
}
},
"responses": {
"200": {
"description": "Upserted unit",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"unit": {
"$ref": "#/components/schemas/Unit"
}
}
}
}
}
},
"400": {
"description": "Invalid unit or no company selected",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Company not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"delete": {
"tags": [
"Unit"
],
"summary": "Delete unit",
"description": "Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it.",
"security": [
{
"cookieAuth": []
}
],
"x-markdown": "unit/delete.md",
"parameters": [
{
"in": "query",
"name": "si",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "Deleted"
},
"400": {
"description": "Missing si or no company selected",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Not authorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Company or unit not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Unit in use",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/image/{fullHash}": {
"get": {
"tags": [
@@ -1110,7 +1378,7 @@
"Article"
],
"summary": "Update article by SKU",
"description": "Updates an article name by SKU; returns the same detail shape as GET.",
"description": "Patches article fields. At least one of `name`, `unitamount`, `unitSi` required.\n`unitSi` must already exist for the company. Returns the same detail shape as GET.\n",
"security": [
{
"cookieAuth": []
@@ -1134,16 +1402,22 @@
"application/json": {
"schema": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"unitamount": {
"type": "number",
"description": "Must be finite and `> 0`"
},
"unitSi": {
"type": "string",
"description": "Existing company unit `si`"
}
},
"example": {
"name": "Updated name"
"unitamount": 0.24,
"unitSi": "l"
}
}
}
@@ -1166,7 +1440,7 @@
}
},
"400": {
"description": "Validation error",
"description": "Validation error (Name required / Invalid unitamount)",
"content": {
"application/json": {
"schema": {
@@ -1196,7 +1470,7 @@
}
},
"404": {
"description": "Article not found",
"description": "Article or unit not found",
"content": {
"application/json": {
"schema": {
@@ -1283,7 +1557,7 @@
"Article"
],
"summary": "Replace POS prices by SKU",
"description": "Replace-all prices for the company POS customer group.\nEach entry: `price` (gross), `tax` (rate %, ensured via tax.ensure), optional `minmultiplier` (default 1).\nStored as net = gross / (1 + tax/100). Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.\n",
"description": "Replace-all prices for the company POS customer group.\nEach entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1).\nEmpty `prices` clears all POS prices. Returns the same shape as GET by SKU.\n",
"security": [
{
"cookieAuth": []
@@ -1321,11 +1595,11 @@
"properties": {
"price": {
"type": "number",
"description": "Gross price (Brutto)"
"description": "Net price"
},
"tax": {
"type": "number",
"description": "Tax rate percent (e.g. 19)"
"description": "Tax rate (e.g. 19)"
},
"minmultiplier": {
"type": "number",
@@ -1338,11 +1612,11 @@
"example": {
"prices": [
{
"price": 11.9,
"price": 10.5,
"tax": 19
},
{
"price": 99,
"price": 9.9,
"tax": 19,
"minmultiplier": 10
}
@@ -2042,6 +2316,33 @@
}
}
},
"Unit": {
"type": "object",
"required": [
"si",
"basesi",
"basesifactor"
],
"properties": {
"si": {
"type": "string"
},
"basesi": {
"type": "string"
},
"basesifactor": {
"type": "number"
},
"symbol": {
"type": "string",
"nullable": true
},
"name": {
"type": "string",
"nullable": true
}
}
},
"ArticleSummary": {
"type": "object",
"properties": {

View File

@@ -5,8 +5,8 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
Replace POS prices by SKU
Replace-all prices for the company POS customer group.
Each entry: `price` (gross), `tax` (rate %, ensured via tax.ensure), optional `minmultiplier` (default 1).
Stored as net = gross / (1 + tax/100). Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
Each entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1).
Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
## Path parameters
@@ -21,11 +21,11 @@ Stored as net = gross / (1 + tax/100). Empty `prices` clears all POS prices. Ret
{
"prices": [
{
"price": 11.9,
"price": 10.5,
"tax": 19
},
{
"price": 99,
"price": 9.9,
"tax": 19,
"minmultiplier": 10
}

View File

@@ -4,7 +4,9 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
Update article by SKU
Updates an article name by SKU; returns the same detail shape as GET.
Patches article fields. At least one of `name`, `unitamount`, `unitSi` required.
`unitSi` must already exist for the company. Returns the same detail shape as GET.
## Path parameters
@@ -16,7 +18,8 @@ Updates an article name by SKU; returns the same detail shape as GET.
```json
{
"name": "Updated name"
"unitamount": 0.24,
"unitSi": "l"
}
```
@@ -34,10 +37,10 @@ Updated article detail
| Status | Description |
|--------|-------------|
| 400 | Validation error — `{"error":"…"}` |
| 400 | Validation error (Name required / Invalid unitamount) — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Article not found — `{"error":"…"}` |
| 404 | Article or unit not found — `{"error":"…"}` |
---

View File

@@ -0,0 +1,33 @@
# `DELETE /unit`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Delete unit
Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it.
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `si` | yes | string | |
## Response `204`
Deleted
_No content_
## Errors
| Status | Description |
|--------|-------------|
| 400 | Missing si or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Company or unit not found — `{"error":"…"}` |
| 409 | Unit in use — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,30 @@
# `GET /unit`
Protected. Requires a valid session cookie (`credentials: 'include'`).
List units
Lists units for the authenticated user's selected company (default-language symbol/name).
## Response `200`
Unit list
```json
{
"units": []
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Company not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,44 @@
# `PUT /unit`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Upsert unit
Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language.
`basesifactor` must be finite and `> 0`.
## Body (JSON)
```json
{
"si": "l",
"basesi": "l",
"basesifactor": 1,
"symbol": "L",
"name": "Liter"
}
```
## Response `200`
Upserted unit
```json
{
"unit": { … }
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid unit or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Company not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._