Compare commits

..

2 Commits

Author SHA1 Message Date
seb
1a5e3f23e2 u 2026-10-23 22:22:30 +02:00
seb
e83116c83d u 2026-10-23 20:31:38 +02:00
11 changed files with 149 additions and 79 deletions

View File

@@ -126,8 +126,8 @@ _none_
- Params: `sku: str, body: { eans: str[] }` - Params: `sku: str, body: { eans: str[] }`
- Returns: `204 void` - Returns: `204 void`
#### Get article history by SKU — `GET /article/{sku}/history` #### Get article history by SKU — `GET /article/{sku}/history`
- Params: `sku: str, query: limit?: int` - Params: `sku: str, query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { history: { id: int, date: str, text: str }[], truncated: bool }` - Returns: `200 { history: { id: int, date: str, text: str }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Add article image — `POST /article/{sku}/image` #### Add article image — `POST /article/{sku}/image`
- Params: `sku: str, image/avif` - Params: `sku: str, image/avif`
- Returns: `201 { image: { picId: int, sort: int, full: { w: int, h: int, size: int }, thumb: { w: int, h: int }, hash: str, thumbHash: str } }` - Returns: `201 { image: { picId: int, sort: int, full: { w: int, h: int, size: int }, thumb: { w: int, h: int }, hash: str, thumbHash: str } }`

View File

@@ -92,8 +92,8 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Params: `sku: str, body: { eans: str[] }` - Params: `sku: str, body: { eans: str[] }`
- Returns: `204 void` - Returns: `204 void`
#### Get article history by SKU — `GET /article/{sku}/history` #### Get article history by SKU — `GET /article/{sku}/history`
- Params: `sku: str, query: limit?: int` - Params: `sku: str, query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { history: { id: int, date: str, text: str }[], truncated: bool }` - Returns: `200 { history: { id: int, date: str, text: str }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Delete article image — `DELETE /article/{sku}/image/{fullHash}` #### Delete article image — `DELETE /article/{sku}/image/{fullHash}`
- Params: `sku: str, fullHash: str` - Params: `sku: str, fullHash: str`
- Returns: `204 void` - Returns: `204 void`

View File

@@ -5,6 +5,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
Get article history by SKU Get article history by SKU
Returns `articlehistory` rows for the article, newest first (`date` DESC, `id` DESC). Returns `articlehistory` rows for the article, newest first (`date` DESC, `id` DESC).
Supports pagination, search filter, and date cutoff via `seekMonths`.
Auth like GET by SKU. Auth like GET by SKU.
@@ -18,11 +19,14 @@ Auth like GET by SKU.
| Param | Required | Type | Description | | Param | Required | Type | Description |
|-------|----------|------|-------------| |-------|----------|------|-------------|
| `limit` | no | integer | Maximum number of entries to return (newest first). Omit for all entries. | | `offset` | no | integer | Paging offset |
| `limit` | no | integer | Page size |
| `search` | no | string | Optional search filter for history text |
| `seekMonths` | no | integer | Negative integer cutoff months ago (e.g. -13) |
## Response `200` ## Response `200`
Article history (newest first) Paged article history list
```json ```json
{ {
@@ -37,7 +41,11 @@ Article history (newest first)
"date": "2026-08-01T09:00:00.000Z", "date": "2026-08-01T09:00:00.000Z",
"text": "Name geändert von \"\" auf \"Tee\"" "text": "Name geändert von \"\" auf \"Tee\""
} }
] ],
"total": 2,
"unfilteredTotal": 2,
"offset": 0,
"limit": 50
} }
``` ```
@@ -45,7 +53,7 @@ Article history (newest first)
| Status | Description | | Status | Description |
|--------|-------------| |--------|-------------|
| 400 | Missing sku — `{"error":"…"}` | | 400 | Missing sku or invalid query parameters — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` | | 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` | | 403 | Not authorized — `{"error":"…"}` |
| 404 | Article not found — `{"error":"…"}` | | 404 | Article not found — `{"error":"…"}` |

View File

@@ -4,7 +4,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
Update shipment state or metadata Update shipment state or metadata
Updates delivery state (`new`, `delivered`, `returned`) for the shipment. Updates delivery state (`new`, `delivered`) for the shipment.
Emits SSE events (`shipment`, `order`) so connected clients rerender in real time. Emits SSE events (`shipment`, `order`) so connected clients rerender in real time.

View File

@@ -7,7 +7,6 @@ Ship shipment manifest item(s)
Books shipment for a shipment manifest item on the ledger (warehouse_id = null). Books shipment for a shipment manifest item on the ledger (warehouse_id = null).
If the connected article has stock tracking enabled, an adjustment entry is also If the connected article has stock tracking enabled, an adjustment entry is also
booked to deduct physical stock from the target warehouse. booked to deduct physical stock from the target warehouse.
If line amount is negative, books as a customer retoure (incoming).
## Body (JSON) ## Body (JSON)

104
package-lock.json generated
View File

@@ -32,7 +32,7 @@
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor", "react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
"selfsigned": "^5.5.0", "selfsigned": "^5.5.0",
"sharp": "^0.35.5", "sharp": "^0.35.5",
"skelehash-wasm": "^0.1.0", "skelehash-wasm": "file:../skelehash-wasm",
"swagger-ui-dist": "^5.33.3", "swagger-ui-dist": "^5.33.3",
"vm2": "^3.12.2", "vm2": "^3.12.2",
"zod": "^4.6.7" "zod": "^4.6.7"
@@ -72,6 +72,7 @@
} }
}, },
"../llmorbiter": { "../llmorbiter": {
"name": "llmorbiter",
"version": "0.1.0", "version": "0.1.0",
"license": "0BSD", "license": "0BSD",
"dependencies": { "dependencies": {
@@ -81,6 +82,13 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"../skelehash-wasm": {
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"sharp": "^0.35.5"
}
},
"node_modules/@alcalzone/ansi-tokenize": { "node_modules/@alcalzone/ansi-tokenize": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.1.tgz", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.1.tgz",
@@ -216,9 +224,9 @@
} }
}, },
"node_modules/@hpcc-js/wasm-graphviz": { "node_modules/@hpcc-js/wasm-graphviz": {
"version": "1.29.2", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.29.2.tgz", "resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.30.0.tgz",
"integrity": "sha512-AkArBAZERT5OxH52/+kvCHB07cZ6ZNd9GnOvjw+S880bR88D6i27XfK6xj4dBhUDxNA2QPPMdgEcky3x4TJ3KA==", "integrity": "sha512-T20IKosOZuex2qV049fQokVJy2MWv8w9Md5V/4mRKCXDf49fBCR50VD1yBlPYHaQlHPsFGRdqXN//r59vZliWQ==",
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
@@ -787,9 +795,9 @@
} }
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.32.1", "version": "1.32.2",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.2.tgz",
"integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", "integrity": "sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hono/node-server": "^1.19.9 || ^2.0.5", "@hono/node-server": "^1.19.9 || ^2.0.5",
@@ -1467,18 +1475,18 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.3.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz",
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "^3.1.2", "bytes": "^3.1.2",
"content-type": "^2.0.0", "content-type": "^2.1.0",
"debug": "^4.4.3", "debug": "^4.4.3",
"http-errors": "^2.0.1", "http-errors": "^2.0.1",
"iconv-lite": "^0.7.2", "iconv-lite": "^0.7.3",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"qs": "^6.15.2", "qs": "^6.16.1",
"raw-body": "^3.0.2", "raw-body": "^3.0.2",
"type-is": "^2.1.0" "type-is": "^2.1.0"
}, },
@@ -2267,9 +2275,9 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "18.0.8", "version": "18.0.9",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.8.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.9.tgz",
"integrity": "sha512-2KDeoFoK/zRtcnbunCDlLG5VWVMcyqeQc6iK65o231nfNAcuGrKoqUQfJFUtBI+QlShk5HKIHxwebBtUl3rEfA==", "integrity": "sha512-3fVOW4cB02R066PUSwn8O4TJyz9NckQl7F81blIbaq9jKBm2tSmOKky7ZHIXzGvkVPXCSn6jkpziBPZdGFs8Tw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"bin": { "bin": {
"dotenv": "dist/index.cjs" "dotenv": "dist/index.cjs"
@@ -2748,9 +2756,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.8", "version": "3.1.9",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -2988,9 +2996,9 @@
} }
}, },
"node_modules/gaxios": { "node_modules/gaxios": {
"version": "8.3.1", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.1.tgz", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-8.3.2.tgz",
"integrity": "sha512-h4/R7ay7JFXU5kmAkp/Y4Y4Vmm7m83tD579GUgfQQsRRGQ4TzSedyHla6XgTWohQuVffT3ldN4FF6uCqlRGaEA==", "integrity": "sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"extend": "^3.0.2", "extend": "^3.0.2",
@@ -3002,9 +3010,9 @@
} }
}, },
"node_modules/gcp-metadata": { "node_modules/gcp-metadata": {
"version": "9.2.1", "version": "9.2.2",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.2.1.tgz", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.2.2.tgz",
"integrity": "sha512-5/LvxarRKbGCA5LJ+ha2aV1iVm7/aALIW4PzeXnQR5dyXiNMWbeA4UOBnqERInLfH/xSFp0ZREcxodUzxhEw2Q==", "integrity": "sha512-hlaHpCBoNCzoUjMnBZq0LbUiYqPAhXFaAgyNwQqwWU44TZIYJK3RD3o2wkNdvPs8nnUXqJjTudnodLJkLMBjow==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"gaxios": "^8.3.0", "gaxios": "^8.3.0",
@@ -3926,9 +3934,9 @@
} }
}, },
"node_modules/jose": { "node_modules/jose": {
"version": "6.2.12", "version": "6.2.13",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz",
"integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
@@ -4439,9 +4447,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/multer": { "node_modules/multer": {
"version": "2.4.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.4.0.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-2.5.0.tgz",
"integrity": "sha512-7dqa0ZcFfzbefdTuIkzOSMvZWC0J7FLqBOjJUZvDCXShIURWKxAyTT1wHhnE5q19c7jOJf43IYYKjBmZVZmvhg==", "integrity": "sha512-BsqOzYGVq49JcW2IzoSAxeKYRevb+dGGPPWleOR8Lrlk4igwxu1/9pwSXlByhCb7MVZDrNJFC0IBP5/O5nLYRw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
@@ -4535,9 +4543,9 @@
} }
}, },
"node_modules/node-abi": { "node_modules/node-abi": {
"version": "3.99.0", "version": "3.101.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.99.0.tgz", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.101.0.tgz",
"integrity": "sha512-lLdwkeg2XUVPTHqWjPltTxH81zbQNbUWC+AEb9ni50KjxkUzAAx1HGtMT21sbntzfUGfIphhcklvs3imJeqDXw==", "integrity": "sha512-bkb/RkuWCE64eWu1PCh3Fu2pGc9mKRs+kOE+FU2bGHRYlQa86/D321DSATV54AsCQMKz3/xSt9sRgraZTpqdhQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"semver": "^7.3.5" "semver": "^7.3.5"
@@ -4653,15 +4661,19 @@
} }
}, },
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ee-first": "1.1.1" "ee-first": "1.1.1"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/once": { "node_modules/once": {
@@ -4865,9 +4877,9 @@
} }
}, },
"node_modules/pkijs": { "node_modules/pkijs": {
"version": "3.4.1", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.1.tgz", "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.3.tgz",
"integrity": "sha512-Oo/NZcSWccq8KyoG7gLE9fnltgHns+pNCjCAp/WmjsUySi+sX7y4z4Xqu4fVb42CDHzRPl33fjzT15V1wvcyhA==", "integrity": "sha512-V9kOzaItwDgqlryhXysQsAXUEKi5O7kqWUaP3dXI7MLvkPqWWMb4wVb1QAtmEDCKI+XIAINsZoNKMFF7sZb1eg==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"workspaces": [ "workspaces": [
"website" "website"
@@ -5727,10 +5739,8 @@
} }
}, },
"node_modules/skelehash-wasm": { "node_modules/skelehash-wasm": {
"version": "0.1.0", "resolved": "../skelehash-wasm",
"resolved": "https://registry.npmjs.org/skelehash-wasm/-/skelehash-wasm-0.1.0.tgz", "link": true
"integrity": "sha512-QdxF9YsJvKlT93O70dXFaWnsKVX7KDD0RgYf9cwgOT6kGnSMOdsO0rsdge+5Tg+vv6BjivbJ/2o5P1KcQsavEw==",
"license": "MIT"
}, },
"node_modules/slice-ansi": { "node_modules/slice-ansi": {
"version": "9.0.1", "version": "9.0.1",
@@ -6000,9 +6010,9 @@
} }
}, },
"node_modules/swagger-ui-dist": { "node_modules/swagger-ui-dist": {
"version": "5.33.3", "version": "5.34.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.33.3.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.34.0.tgz",
"integrity": "sha512-7xJKxAjf+/ajlW/V/dMfbiY+NC5BWtbvzzdI/AsX5UzMtOMudmFz/tjuKN0jN7QVe5drKKnx01CdYEJvTFvQlw==", "integrity": "sha512-H6tAIjr3XqE31PbkZl0OFya1ziRLDfTwh3J6OHdumPtPCLfjTnyLeagl/55kFNZcTOz9a9QI9WP0FXldwiGJag==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@scarf/scarf": "=1.4.0" "@scarf/scarf": "=1.4.0"

View File

@@ -40,7 +40,7 @@
"react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor", "react-canvas-label-editor": "github:sebseb7/react-canvas-label-editor",
"selfsigned": "^5.5.0", "selfsigned": "^5.5.0",
"sharp": "^0.35.5", "sharp": "^0.35.5",
"skelehash-wasm": "^0.1.0", "skelehash-wasm": "file:../skelehash-wasm",
"swagger-ui-dist": "^5.33.3", "swagger-ui-dist": "^5.33.3",
"vm2": "^3.12.2", "vm2": "^3.12.2",
"zod": "^4.6.7" "zod": "^4.6.7"
@@ -60,4 +60,4 @@
"uuid": "^11.1.1" "uuid": "^11.1.1"
} }
} }
} }

View File

@@ -1461,7 +1461,7 @@
* $ref: '#/components/schemas/DeliveryFile' * $ref: '#/components/schemas/DeliveryFile'
* state: * state:
* type: string * type: string
* enum: [new, delivered, returned] * enum: [new, delivered]
* description: Delivery state * description: Delivery state
* ShipmentSummary: * ShipmentSummary:
* type: object * type: object
@@ -1508,7 +1508,7 @@
* nullable: true * nullable: true
* state: * state:
* type: string * type: string
* enum: [new, delivered, returned] * enum: [new, delivered, cancelled]
* Package: * Package:
* type: object * type: object
* required: [id, shippingId, entityaddressId, number, type, state] * required: [id, shippingId, entityaddressId, number, type, state]

View File

@@ -602,6 +602,7 @@ export function register(app, basePath = '') {
* summary: Get article history by SKU * summary: Get article history by SKU
* description: | * description: |
* Returns `articlehistory` rows for the article, newest first (`date` DESC, `id` DESC). * Returns `articlehistory` rows for the article, newest first (`date` DESC, `id` DESC).
* Supports pagination, search filter, and date cutoff via `seekMonths`.
* Auth like GET by SKU. * Auth like GET by SKU.
* security: * security:
* - cookieAuth: [] * - cookieAuth: []
@@ -614,29 +615,51 @@ export function register(app, basePath = '') {
* type: string * type: string
* description: Article SKU * description: Article SKU
* - in: query * - in: query
* name: offset
* schema:
* type: integer
* minimum: 0
* default: 0
* description: Paging offset
* - in: query
* name: limit * name: limit
* required: false
* schema: * schema:
* type: integer * type: integer
* minimum: 1 * minimum: 1
* maximum: 1000 * maximum: 1000
* description: Maximum number of entries to return (newest first). Omit for all entries. * default: 50
* description: Page size
* - in: query
* name: search
* schema:
* type: string
* description: Optional search filter for history text
* - in: query
* name: seekMonths
* schema:
* type: integer
* description: Negative integer cutoff months ago (e.g. -13)
* responses: * responses:
* 200: * 200:
* description: Article history (newest first) * description: Paged article history list
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: object * type: object
* required: [history, truncated] * required: [history, total, unfilteredTotal, offset, limit]
* properties: * properties:
* history: * history:
* type: array * type: array
* items: * items:
* $ref: '#/components/schemas/ArticleHistoryEntry' * $ref: '#/components/schemas/ArticleHistoryEntry'
* truncated: * total:
* type: boolean * type: integer
* description: True when a limit was applied and the result set was exactly that size (more entries may exist). * unfilteredTotal:
* type: integer
* offset:
* type: integer
* limit:
* type: integer
* example: * example:
* history: * history:
* - id: 42 * - id: 42
@@ -645,8 +668,12 @@ export function register(app, basePath = '') {
* - id: 41 * - id: 41
* date: '2026-08-01T09:00:00.000Z' * date: '2026-08-01T09:00:00.000Z'
* text: 'Name geändert von "" auf "Tee"' * text: 'Name geändert von "" auf "Tee"'
* total: 2
* unfilteredTotal: 2
* offset: 0
* limit: 50
* 400: * 400:
* description: Missing sku * description: Missing sku or invalid query parameters
* content: * content:
* application/json: * application/json:
* schema: * schema:
@@ -678,8 +705,19 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: 'sku is required' }); return res.status(400).json({ error: 'sku is required' });
} }
const rawOffset = req.query.offset;
const rawLimit = req.query.limit; const rawLimit = req.query.limit;
let limit = null;
let offset = 0;
let limit = 50;
if (rawOffset !== undefined) {
offset = parseInt(rawOffset, 10);
if (!Number.isInteger(offset) || offset < 0) {
return res.status(400).json({ error: 'offset must be an integer >= 0' });
}
}
if (rawLimit !== undefined) { if (rawLimit !== undefined) {
limit = parseInt(rawLimit, 10); limit = parseInt(rawLimit, 10);
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) { if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
@@ -687,10 +725,26 @@ export function register(app, basePath = '') {
} }
} }
const search = typeof req.query.search === 'string' && req.query.search.trim() !== ''
? req.query.search.trim()
: undefined;
let seekMonths = undefined;
if (req.query.seekMonths !== undefined) {
seekMonths = parseInt(req.query.seekMonths, 10);
if (!Number.isInteger(seekMonths)) {
return res.status(400).json({ error: 'seekMonths must be an integer' });
}
}
try { try {
const history = await db.getHistoryBySku(req.user.id, decodeURIComponent(sku), limit != null ? { limit } : undefined); const result = await db.getHistoryBySku(req.user.id, decodeURIComponent(sku), {
const truncated = limit != null && history.length === limit; offset,
res.status(200).json({ history, truncated }); limit,
search,
seekMonths,
});
res.status(200).json(result);
} catch (error) { } catch (error) {
const status = articleErrorStatus(error); const status = articleErrorStatus(error);
if (status === 500) console.error('Get article history by sku failed:', error); if (status === 500) console.error('Get article history by sku failed:', error);

View File

@@ -321,7 +321,7 @@ export function register(app, basePath = '') {
* name: state * name: state
* schema: * schema:
* type: string * type: string
* enum: [new, delivered, returned, cancelled, all] * enum: [new, delivered, cancelled, all]
* description: Optional filter by delivery state (cancelled shipments hidden by default) * description: Optional filter by delivery state (cancelled shipments hidden by default)
* - in: query * - in: query
* name: seekMonths * name: seekMonths
@@ -403,7 +403,7 @@ export function register(app, basePath = '') {
let state = undefined; let state = undefined;
if (typeof req.query.state === 'string' && req.query.state.trim() !== '') { if (typeof req.query.state === 'string' && req.query.state.trim() !== '') {
const trimmedState = req.query.state.trim().toLowerCase(); const trimmedState = req.query.state.trim().toLowerCase();
if (!['new', 'delivered', 'returned', 'cancelled', 'all'].includes(trimmedState)) { if (!['new', 'delivered', 'cancelled', 'all'].includes(trimmedState)) {
return res.status(400).json({ error: 'Invalid state' }); return res.status(400).json({ error: 'Invalid state' });
} }
state = trimmedState; state = trimmedState;
@@ -513,7 +513,7 @@ export function register(app, basePath = '') {
* tags: [Shipment] * tags: [Shipment]
* summary: Update shipment state or metadata * summary: Update shipment state or metadata
* description: | * description: |
* Updates delivery state (`new`, `delivered`, `returned`) for the shipment. * Updates delivery state (`new`, `delivered`) for the shipment.
* Emits SSE events (`shipment`, `order`) so connected clients rerender in real time. * Emits SSE events (`shipment`, `order`) so connected clients rerender in real time.
* security: * security:
* - cookieAuth: [] * - cookieAuth: []
@@ -534,7 +534,7 @@ export function register(app, basePath = '') {
* properties: * properties:
* state: * state:
* type: string * type: string
* enum: [new, delivered, returned, cancelled] * enum: [new, delivered, cancelled]
* description: Delivery state * description: Delivery state
* example: * example:
* state: delivered * state: delivered
@@ -582,7 +582,7 @@ export function register(app, basePath = '') {
const { state } = req.body ?? {}; const { state } = req.body ?? {};
if (state !== undefined) { if (state !== undefined) {
if (typeof state !== 'string' || !['new', 'delivered', 'returned'].includes(state.trim().toLowerCase())) { if (typeof state !== 'string' || !['new', 'delivered'].includes(state.trim().toLowerCase())) {
return res.status(400).json({ error: 'Invalid state' }); return res.status(400).json({ error: 'Invalid state' });
} }
} }

View File

@@ -261,7 +261,6 @@ export function register(app, basePath = '') {
* Books shipment for a shipment manifest item on the ledger (warehouse_id = null). * Books shipment for a shipment manifest item on the ledger (warehouse_id = null).
* If the connected article has stock tracking enabled, an adjustment entry is also * If the connected article has stock tracking enabled, an adjustment entry is also
* booked to deduct physical stock from the target warehouse. * booked to deduct physical stock from the target warehouse.
* If line amount is negative, books as a customer retoure (incoming).
* security: * security:
* - cookieAuth: [] * - cookieAuth: []
* x-signals: [stock, ledger, articles, shipping] * x-signals: [stock, ledger, articles, shipping]