From 7d537aaa6bb856c58e220e93cd2ffeba4559d3b3 Mon Sep 17 00:00:00 2001 From: seb Date: Wed, 7 Oct 2026 21:23:10 +0200 Subject: [PATCH] u --- docs/api/routes-all.md | 141 +++++++-------- docs/api/routes.md | 115 ++++++------ .../routes/article/_sku/articlelist.put.md | 9 +- docs/api/routes/article/_sku/children.put.md | 9 +- .../routes/article/_sku/optionslist.put.md | 10 +- docs/api/routes/article/_sku/track_stock.md | 17 +- docs/api/routes/article/get.md | 17 +- docs/api/routes/article/order-images.md | 4 +- docs/api/routes/article/set-brand.md | 17 +- docs/api/routes/article/set-categories.md | 17 +- docs/api/routes/article/set-eans.md | 17 +- docs/api/routes/article/set-prices.md | 37 ++-- docs/api/routes/article/set-weight.md | 17 +- docs/api/routes/article/update.md | 17 +- docs/api/routes/brand/list.md | 11 +- docs/api/routes/brand/upsert.md | 11 +- docs/api/routes/category/articles.md | 11 +- docs/api/routes/category/tree.md | 42 ++++- docs/api/routes/image/get.md | 4 +- docs/api/routes/stock/adjust.md | 9 +- docs/api/routes/thumb/get.md | 2 +- docs/api/routes/vfs/delete.md | 2 +- docs/api/routes/vfs/upload.md | 2 +- docs/api/routes/vfs/write.md | 2 +- scripts/generateApiDocs.js | 14 +- src/api/openapi.js | 170 +++++++++++++++++- src/api/routes/article.js | 94 ++++------ src/api/routes/category.js | 49 +---- src/api/routes/pic.js | 18 +- src/api/routes/vfs.js | 6 +- src/tools/analyze-image.js | 6 +- src/tools/edit-image.js | 2 +- src/tools/vfs.js | 26 ++- 33 files changed, 598 insertions(+), 327 deletions(-) diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index 0e82162..801ca8a 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -4,24 +4,24 @@ Overview of all 189 API endpoints across all modules. ### Notation -Types: `str` string, `int` integer, `num` number, `bool` boolean, `obj` object; `T[]` array; `?` optional. +Types: `str` string, `int` integer, `num` number, `bool` boolean, `{}` object; `T[]` array; `?` optional. Params: path values first, then `query:` and `body:` (`body?` = optional body). Responses: status code + JSON shape; `Tn` refers to a reusable type below. ### Reusable types - `T1` = `{ article: T2 }` -- `T2` = `{ id: int, sku: str, name: str, slug: str, track_stock: bool, unitamount: num, weight: num, unit: obj, brand: obj, categoryIds: int[], photos: T17[], eans: str[], prices: T28[], suppliers: T9[] }` +- `T2` = `{ id: int, sku: str, name: str, slug: str, track_stock: bool, unitamount: num, weight: num, unit: T28, brand: T24, categoryIds: int[], photos: T17[], eans: str[], prices: T29[], suppliers: T9[] }` - `T3` = `{ delivery: T4 }` -- `T4` = `{ id: int, number: str, type: str, supplierId: int, externalNumber: str, date: str, createdAt: str, ledger: T33, lines: T18[], files: T8[] }` +- `T4` = `{ id: int, number: str, type: str, supplierId: int, externalNumber: str, date: str, createdAt: str, ledger: T34, lines: T18[], files: T8[] }` - `T5` = `{ order: T7 }` - `T6` = `{ quote: T7 }` - `T7` = `{ id: int, number: str, type: str, entityId: int, date: str, createdAt: str, lines: T18[] }` -- `T8` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created: str, modified: str, thumbavif: str, thumbx: int, thumby: int, meta: obj }` -- `T9` = `{ id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T36 }` +- `T8` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created: str, modified: str, thumbavif: str, thumbx: int, thumby: int, meta: { [k]: str } }` +- `T9` = `{ id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T37 }` - `T10` = `{ ledger: T11[] }` - `T11` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash: str, sku: str, created_at: str }` - `T12` = `{ id: int, entityId: int, firstName: str, name: str, street: str, streetNumber: str, zip: str, city: str, county: str, addressType: str }` -- `T13` = `{ id: int, name: str, sort: int, exclusive: bool, values: T31[] }` +- `T13` = `{ id: int, name: str, sort: int, exclusive: bool, values: T32[] }` - `T14` = `{ entity: T20 }` - `T15` = `{ printer: T21 }` - `T16` = `{ labeltemplate: T23 }` @@ -31,22 +31,23 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - `T20` = `{ id: int, customergroupId: int, firstName: str, name: str, number: str, createdAt: str, updatedAt: str }` - `T21` = `{ id: int, company_id: int, address: str, type: str, model: str, serial: str, sort: int, desc: str }` - `T22` = `{ amount: num, net: num, taxrate: num, sku?: str, name?: str, pu?: num, rebate?: num, comment?: str }` -- `T23` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: obj, thumb: str }` -- `T24` = `{ id: int, sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }` +- `T23` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: {}, thumb: str }` +- `T24` = `{ id: int, name: str, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }` - `T25` = `{ article: T27 }` - `T26` = `{ id: int, iso639: str, name: str, taxrate: num, reducedtaxrate: num }` - `T27` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }` -- `T28` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }` -- `T29` = `{ id: int, email: str, selectedCompanyId: int, picture: str }` -- `T30` = `{ id: int, entityId: int, contactType: str, contact: str }` -- `T31` = `{ id: int, attributeId: int, value: str, sort: int }` -- `T32` = `{ id: int, name: str, createdAt: str, updatedAt: str }` -- `T33` = `{ totalPositions: int, completelyOnLedgerPositions: int }` -- `T34` = `{ id: int, name: str, sort: int, exclusive: bool }` -- `T35` = `{ warehouse: T37 }` -- `T36` = `{ net: num, taxId: int, taxRate: num }` -- `T37` = `{ id: int, name: str, sort: int }` -- `T38` = `{ id: int, categoryIds: int[] }` +- `T28` = `{ si: str, basesi: str, basesifactor: num, symbol: str, name: str }` +- `T29` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }` +- `T30` = `{ id: int, email: str, selectedCompanyId: int, picture: str }` +- `T31` = `{ id: int, entityId: int, contactType: str, contact: str }` +- `T32` = `{ id: int, attributeId: int, value: str, sort: int }` +- `T33` = `{ id: int, name: str, createdAt: str, updatedAt: str }` +- `T34` = `{ totalPositions: int, completelyOnLedgerPositions: int }` +- `T35` = `{ id: int, name: str, sort: int, exclusive: bool }` +- `T36` = `{ warehouse: T38 }` +- `T37` = `{ net: num, taxId: int, taxRate: num }` +- `T38` = `{ id: int, name: str, sort: int }` +- `T39` = `{ id: int, categoryIds: int[] }` ### Routes @@ -70,7 +71,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - Returns: `200 { items: { id: int, articleId: int, sku: str, name: str, count: int, sort: int }[] }` #### Set Stückliste (bundle items) for an article — `PUT /article/{sku}/articlelist` - Params: `sku: str, body: { items: { articleId?: int, sku?: str, count?: int, sort?: int }[] }` -- Returns: `200 { items: obj[] }` +- Returns: `200 { items: { id: int, articleId: int, sku: str, name: str, count: int, sort: int }[] }` #### List article attribute values — `GET /article/{sku}/attributevalue` - Params: `sku: str` - Returns: `200 { attributevalues: { id: int, articleId: int, attributeId: int, attributeName: str, exclusive: bool, attributeValueId: int, value: str, sort: int }[] }` @@ -91,7 +92,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - Returns: `201 T25` #### Attach an existing article as a child to a parent article — `PUT /article/{sku}/children` - Params: `sku: str, body: { sku: str, attributeValueId?: int, value?: str, reparent?: bool }` -- Returns: `200 { article: T27, children: obj[] }` +- Returns: `200 { article: T27, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }` #### Remove a child article from a parent article — `DELETE /article/{sku}/children/{childSku}` - Params: `sku: str, childSku: str, query: deleteArticle?: bool` - Returns: `200 { success: bool, deleted: bool }` @@ -115,12 +116,12 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - Returns: `200 { options: { id: int, articleId: int, sku: str, name: str, count: int, sort: int, mandated: bool }[] }` #### Set options list for an article — `PUT /article/{sku}/optionslist` - Params: `sku: str, body: { options: { articleId?: int, sku?: str, count?: int, sort?: int, mandated?: bool }[] }` -- Returns: `200 { options: obj[] }` +- Returns: `200 { options: { id: int, articleId: int, sku: str, name: str, count: int, sort: int, mandated: bool }[] }` #### Toggle parent mode for an article — `POST /article/{sku}/parent` - Params: `sku: str, body: { isParent: bool, attributeNameId?: int }` - Returns: `200 T25` #### Replace POS prices by SKU — `PUT /article/{sku}/prices` -- Params: `sku: str, body: { prices: { price: num, tax: num, minmultiplier?: num }[] }` +- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }` - Returns: `200 T1` #### Print article label — `POST /article/{sku}/print` - Params: `sku: str, body: { templateId: int, printerId: int, quantity: int }` @@ -166,10 +167,10 @@ _none_ - Returns: `200` #### List values for an attribute — `GET /attributename/{attributeId}/value` - Params: `attributeId: int` -- Returns: `200 { values: T31[] }` +- Returns: `200 { values: T32[] }` #### Create value for an attribute — `POST /attributename/{attributeId}/value` - Params: `attributeId: int, body: { value: str, sort?: int }` -- Returns: `201 { attributevalue: T31 }` +- Returns: `201 { attributevalue: T32 }` #### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order` - Params: `attributeId: int, body: { attributevalueIds: int[] }` - Returns: `200` @@ -184,34 +185,34 @@ _none_ - Returns: `200` #### Update attribute value — `PATCH /attributevalue/{id}` - Params: `id: int, body: { value?: str, sort?: int }` -- Returns: `200 { attributevalue: T31 }` +- Returns: `200 { attributevalue: T32 }` #### Login with Google ID token — `POST /auth/login` - Params: `body: { idToken: str }` -- Returns: `200 { message: str, user: T29 }` +- Returns: `200 { message: str, user: T30 }` #### Logout — `POST /auth/logout` _none_ - Returns: `200 { message: str }` #### Current user — `GET /auth/me` _none_ -- Returns: `200 { user: T29, googleClientId: str }` +- Returns: `200 { user: T30, googleClientId: str }` #### Set selected company — `PUT /auth/selected-company` - Params: `body: { companyId: int }` -- Returns: `200 { user: T29 }` +- Returns: `200 { user: T30 }` #### List brands — `GET /brand` _none_ -- Returns: `200 { brands: { id: int, name: str, gsprpinfo: str, pics: T24[] }[] }` +- Returns: `200 { brands: { id: int, name: str, gsprpinfo: str, pics: any[] }[] }` #### Upsert brand — `PUT /brand` - Params: `body: { name: str, gsprpinfo?: str }` -- Returns: `200 { brand: { id: int, name: str, gsprpinfo: str, pics: T24[] } }` +- Returns: `200 { brand: { id: int, name: str, gsprpinfo: str, pics: any[] } }` #### Delete brand — `DELETE /brand/{id}` - Params: `id: int` - Returns: `204 void` #### Delete brand image — `DELETE /brand/{id}/image` - Params: `id: int` -- Returns: `200 { brand: { id: int, thumbW: int, thumbH: int, thumbHash: str, pics: T24[] } }` +- Returns: `200 { brand: { id: int, thumbW: int, thumbH: int, thumbHash: str, pics: { id: int, sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[] } }` #### Add or replace brand image — `POST /brand/{id}/image` - Params: `id: int, image/avif` -- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T24[] } }` +- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: { id: int, sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[] } }` #### Create category — `POST /category` - Params: `body: { parentId: int, name: str }` - Returns: `201 { category: { id: int, parentId: int, sort: int, name: str } }` @@ -223,7 +224,7 @@ _none_ - Returns: `200 { categoryIds: int[] }` #### Category tree — `GET /category/tree` _none_ -- Returns: `200 { languageId: int, tree: obj[], articles: obj[] }` +- Returns: `200 { languageId: int, tree: { id: int, parentId: int, sort: int, name: str, slug: str, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, posterminalIds: int[] }[], articles: { id: int, sku: str, name: str, slug: str, net: num, taxRate: num, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, parentArticleId: int, parentSku: str, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, brand: T24 }[] }` #### Delete category — `DELETE /category/{id}` - Params: `id: int` - Returns: `204 void` @@ -232,7 +233,7 @@ _none_ - Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }` #### Articles in category — `GET /category/{id}/articles` - Params: `id: int` -- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num, taxRate: num, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, brand: obj }[] }` +- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num, taxRate: num, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, parentArticleId: int, parentSku: str, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, brand: T24 }[] }` #### List category attribute values — `GET /category/{id}/attributevalue` - Params: `id: int` - Returns: `200 { attributevalues: { id: int, categoryId: int, attributeId: int, attributeName: str, exclusive: bool, attributeValueId: int, value: str, sort: int }[] }` @@ -259,7 +260,7 @@ _none_ - Returns: `200 { companies: T26[] }` #### Create company — `POST /company` - Params: `body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }` -- Returns: `201 { company: T26, user: T29 }` +- Returns: `201 { company: T26, user: T30 }` #### Get company history — `GET /company/history` - Params: `query: limit?: int` - Returns: `200 { history: { id: int, companyuserId: int, date: str, text: str }[], truncated: bool }` @@ -286,7 +287,7 @@ _none_ - Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }` #### Create customer group — `POST /customergroup` - Params: `body: { name: str }` -- Returns: `201 { customergroup: T37 }` +- Returns: `201 { customergroup: T38 }` #### Reorder customer groups — `PUT /customergroup/order` - Params: `body: { customergroupIds: int[] }` - Returns: `200 { customergroupIds: int[] }` @@ -295,12 +296,12 @@ _none_ - Returns: `204 void` #### Update customer group — `PATCH /customergroup/{id}` - Params: `id: int, body: { name: str }` -- Returns: `200 { customergroup: T37 }` +- Returns: `200 { customergroup: T38 }` #### List deliveries — `GET /delivery` _none_ -- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, externalNumber: str, date: str, createdAt: str, ledger: T33 }[] }` +- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, externalNumber: str, date: str, createdAt: str, ledger: T34 }[] }` #### Create delivery — `POST /delivery` -- Params: `body: { externalNumber: str, date: str, lines: T22[], supplierId?: int, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: obj }[] }` +- Params: `body: { externalNumber: str, date: str, lines: T22[], supplierId?: int, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Returns: `201 T3` #### Parse document (SSE) — `POST /delivery/parse` - Params: `form: { file: str }` @@ -312,7 +313,7 @@ _none_ - Params: `id: int` - Returns: `200 T3` #### Update delivery — `PATCH /delivery/{id}` -- Params: `id: int, body: { externalNumber?: str, date?: str, supplierId?: int, lines?: T22[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: obj }[] }` +- Params: `id: int, body: { externalNumber?: str, date?: str, supplierId?: int, lines?: T22[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Returns: `200 T3` #### Get delivery attachment file content — `GET /delivery/{id}/file/{fileId}` - Params: `id: int, fileId: int, query: download?: bool` @@ -340,16 +341,16 @@ _none_ - Returns: `200 { address: T12 }` #### List entity contacts — `GET /entity/{entityId}/contact` - Params: `entityId: int` -- Returns: `200 { contacts: T30[] }` +- Returns: `200 { contacts: T31[] }` #### Create entity contact — `POST /entity/{entityId}/contact` - Params: `entityId: int, body: { contactType: str, contact: str }` -- Returns: `201 { contact: T30 }` +- Returns: `201 { contact: T31 }` #### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}` - Params: `entityId: int, id: int` - Returns: `204 void` #### Update entity contact — `PATCH /entity/{entityId}/contact/{id}` - Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }` -- Returns: `200 { contact: T30 }` +- Returns: `200 { contact: T31 }` #### Delete entity — `DELETE /entity/{id}` - Params: `id: int` - Returns: `204 void` @@ -372,7 +373,7 @@ _none_ _none_ - Returns: `200 { labeltemplates: T23[] }` #### Create label template — `POST /labeltemplate` -- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: obj }` +- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: {} }` - Returns: `201 T16` #### Delete label template — `DELETE /labeltemplate/{id}` - Params: `id: int` @@ -381,10 +382,10 @@ _none_ - Params: `id: int` - Returns: `200 T16` #### Update label template (partial) — `PATCH /labeltemplate/{id}` -- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: obj }` +- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: {} }` - Returns: `200 T16` #### Update label template — `PUT /labeltemplate/{id}` -- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: obj }` +- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: {} }` - Returns: `200 T16` #### Query ledger by manifest article ID or SKU — `GET /ledger` - Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str` @@ -451,16 +452,16 @@ _none_ - Returns: `204 void` #### Disable all categories on terminal — `DELETE /posterminal/{id}/all` - Params: `id: int` -- Returns: `200 T38` +- Returns: `200 T39` #### Enable all categories on terminal — `PUT /posterminal/{id}/all` - Params: `id: int` -- Returns: `200 T38` +- Returns: `200 T39` #### Disable category on terminal — `DELETE /posterminal/{id}/category/{categoryId}` - Params: `id: int, categoryId: int` -- Returns: `200 T38` +- Returns: `200 T39` #### Enable category on terminal — `PUT /posterminal/{id}/category/{categoryId}` - Params: `id: int, categoryId: int` -- Returns: `200 T38` +- Returns: `200 T39` #### List printers — `GET /printer` _none_ - Returns: `200 { printers: T21[] }` @@ -484,10 +485,10 @@ _none_ - Returns: `200 T15` #### List property names — `GET /propertyname` _none_ -- Returns: `200 { propertynames: T34[] }` +- Returns: `200 { propertynames: T35[] }` #### Create property name — `POST /propertyname` - Params: `body: { name: str, sort?: int, exclusive?: bool }` -- Returns: `201 { propertyname: T34 }` +- Returns: `201 { propertyname: T35 }` #### Reorder property names — `PUT /propertyname/order` - Params: `body: { propertynameIds: int[] }` - Returns: `200` @@ -496,13 +497,13 @@ _none_ - Returns: `200` #### Update property name — `PATCH /propertyname/{id}` - Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }` -- Returns: `200 { propertyname: T34 }` +- Returns: `200 { propertyname: T35 }` #### List public articles — `GET /public/{companyId}/articles` - Params: `companyId: int, query: categoryId?: int, search?: str, limit?: int, offset?: int` - Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num, taxRate: num, gross: num, trackStock: bool, brand: { id: int, name: str }, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[] }` #### Get public article detail — `GET /public/{companyId}/articles/{sku}` - Params: `companyId: int, sku: str` -- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, description: str, unitamount: num, weight: num, trackStock: bool, unit: obj, brand: obj, price: obj, prices: obj[], categories: obj[], photos: obj[], eans: str[], isParent: bool, parentSku: str, variants: obj[] } }` +- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, description: str, unitamount: num, weight: num, trackStock: bool, unit: {}, brand: {}, price: {}, prices: {}[], categories: {}[], photos: {}[], eans: str[], isParent: bool, parentSku: str, variants: {}[] } }` #### List public categories — `GET /public/{companyId}/categories` - Params: `companyId: int` - Returns: `200 { categories: { id: int, parentId: int, sort: int, name: str, slug: str, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[] }` @@ -529,25 +530,25 @@ _none_ - Returns: `200 SSE` #### Adjust stock manually — `POST /stock/adjust` - Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }` -- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: obj[], ledger: T11[] }` +- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T11[] }` #### List suppliers — `GET /supplier` _none_ -- Returns: `200 { suppliers: T32[] }` +- Returns: `200 { suppliers: T33[] }` #### Create supplier — `POST /supplier` - Params: `body: { name: str }` -- Returns: `201 { supplier: T32 }` +- Returns: `201 { supplier: T33 }` #### Delete supplier — `DELETE /supplier/{id}` - Params: `id: int` - Returns: `204 void` #### Update supplier — `PATCH /supplier/{id}` - Params: `id: int, body: { name: str }` -- Returns: `200 { supplier: T32 }` +- Returns: `200 { supplier: T33 }` #### List supplier articles by SKUs — `GET /supplier/{id}/articles` - Params: `id: int, query: skus?: string` -- Returns: `200 { articles: { id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T36, article: { id: int, sku: str, name: str, slug: str, photos: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[] } }[] }` +- Returns: `200 { articles: { id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T37, article: { id: int, sku: str, name: str, slug: str, photos: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[] } }[] }` #### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles` - Params: `id: int, body?: { skus?: str[] }` -- Returns: `200 { articles: { id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T36, article: { id: int, sku: str, name: str, slug: str, photos: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[] } }[] }` +- Returns: `200 { articles: { id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T37, article: { id: int, sku: str, name: str, slug: str, photos: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[] } }[] }` #### Thumb image by hash — `GET /thumb/{fullHash}` - Params: `fullHash: str` - Returns: `200 avif` @@ -556,13 +557,13 @@ _none_ - Returns: `204 void` #### List units — `GET /unit` _none_ -- Returns: `200 { units: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }[] }` +- Returns: `200 { units: T28[] }` #### Upsert unit — `PUT /unit` - Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }` -- Returns: `200 { unit: { si: str, basesi: str, basesifactor: num, symbol: str, name: str } }` +- Returns: `200 { unit: T28 }` #### JTL-POS category synchronization — `GET /v1/category` - Params: `query: lastChangedCategory?: str, limit?: int` -- Returns: `200 obj[]` +- Returns: `200 {}[]` #### POS discovery and pairing — `GET /v1/client` - Params: `query: authCode: str, name?: str` - Returns: `200 { authCode: str, authToken: str, certificateFingerprint: str, certificateSerialNumber: str, mandantId: str, mandantName: str, mandantDatabase: str, serverFingerprint: str, name: str, serverTimestamp: str }` @@ -574,7 +575,7 @@ _none_ - Returns: `200 jpeg` #### JTL-POS product catalog synchronization — `GET /v1/product` - Params: `query: lastChangedProduct?: str, limit?: int` -- Returns: `200 obj[]` +- Returns: `200 {}[]` #### List virtual filesystem files — `GET /vfs` _none_ - Returns: `200 { files: { name: str, size: int, mime: str, updated_at: str, created_at: str, isText: bool, isImage: bool, isPdf: bool }[] }` @@ -598,10 +599,10 @@ _none_ - Returns: `200 { success: bool, file: { name: str, size: int, mime: str, updated_at: str } }` #### List warehouses — `GET /warehouse` _none_ -- Returns: `200 { warehouses: T37[] }` +- Returns: `200 { warehouses: T38[] }` #### Create warehouse — `POST /warehouse` - Params: `body: { name: str, sort?: int }` -- Returns: `201 T35` +- Returns: `201 T36` #### Reorder warehouses — `PUT /warehouse/order` - Params: `body: { warehouseIds: int[] }` - Returns: `200 { warehouseIds: int[] }` @@ -610,10 +611,10 @@ _none_ - Returns: `204 void` #### Get warehouse by ID — `GET /warehouse/{id}` - Params: `id: int` -- Returns: `200 T35` +- Returns: `200 T36` #### Update warehouse (partial) — `PATCH /warehouse/{id}` - Params: `id: int, body: { name?: str, sort?: int }` -- Returns: `200 T35` +- Returns: `200 T36` #### Update warehouse — `PUT /warehouse/{id}` - Params: `id: int, body: { name?: str, sort?: int }` -- Returns: `200 T35` +- Returns: `200 T36` diff --git a/docs/api/routes.md b/docs/api/routes.md index 12be455..9b1419e 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -4,24 +4,24 @@ Overview of all 145 API endpoints across all modules. ### Notation -Types: `str` string, `int` integer, `num` number, `bool` boolean, `obj` object; `T[]` array; `?` optional. +Types: `str` string, `int` integer, `num` number, `bool` boolean, `{}` object; `T[]` array; `?` optional. Params: path values first, then `query:` and `body:` (`body?` = optional body). Responses: status code + JSON shape; `Tn` refers to a reusable type below. ### Reusable types - `T1` = `{ article: T2 }` -- `T2` = `{ id: int, sku: str, name: str, slug: str, track_stock: bool, unitamount: num, weight: num, unit: obj, brand: obj, categoryIds: int[], photos: T17[], eans: str[], prices: T26[], suppliers: T9[] }` +- `T2` = `{ id: int, sku: str, name: str, slug: str, track_stock: bool, unitamount: num, weight: num, unit: T26, brand: T23, categoryIds: int[], photos: T17[], eans: str[], prices: T27[], suppliers: T9[] }` - `T3` = `{ delivery: T4 }` -- `T4` = `{ id: int, number: str, type: str, supplierId: int, externalNumber: str, date: str, createdAt: str, ledger: T30, lines: T18[], files: T8[] }` +- `T4` = `{ id: int, number: str, type: str, supplierId: int, externalNumber: str, date: str, createdAt: str, ledger: T31, lines: T18[], files: T8[] }` - `T5` = `{ order: T7 }` - `T6` = `{ quote: T7 }` - `T7` = `{ id: int, number: str, type: str, entityId: int, date: str, createdAt: str, lines: T18[] }` -- `T8` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created: str, modified: str, thumbavif: str, thumbx: int, thumby: int, meta: obj }` -- `T9` = `{ id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T33 }` +- `T8` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created: str, modified: str, thumbavif: str, thumbx: int, thumby: int, meta: { [k]: str } }` +- `T9` = `{ id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T34 }` - `T10` = `{ ledger: T11[] }` - `T11` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash: str, sku: str, created_at: str }` - `T12` = `{ id: int, entityId: int, firstName: str, name: str, street: str, streetNumber: str, zip: str, city: str, county: str, addressType: str }` -- `T13` = `{ id: int, name: str, sort: int, exclusive: bool, values: T28[] }` +- `T13` = `{ id: int, name: str, sort: int, exclusive: bool, values: T29[] }` - `T14` = `{ entity: T19 }` - `T15` = `{ printer: T20 }` - `T16` = `{ labeltemplate: T22 }` @@ -30,19 +30,20 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - `T19` = `{ id: int, customergroupId: int, firstName: str, name: str, number: str, createdAt: str, updatedAt: str }` - `T20` = `{ id: int, company_id: int, address: str, type: str, model: str, serial: str, sort: int, desc: str }` - `T21` = `{ amount: num, net: num, taxrate: num, sku?: str, name?: str, pu?: num, rebate?: num, comment?: str }` -- `T22` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: obj, thumb: str }` -- `T23` = `{ id: int, sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }` +- `T22` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: {}, thumb: str }` +- `T23` = `{ id: int, name: str, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }` - `T24` = `{ article: T25 }` - `T25` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }` -- `T26` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }` -- `T27` = `{ id: int, entityId: int, contactType: str, contact: str }` -- `T28` = `{ id: int, attributeId: int, value: str, sort: int }` -- `T29` = `{ id: int, name: str, createdAt: str, updatedAt: str }` -- `T30` = `{ totalPositions: int, completelyOnLedgerPositions: int }` -- `T31` = `{ id: int, name: str, sort: int, exclusive: bool }` -- `T32` = `{ warehouse: T34 }` -- `T33` = `{ net: num, taxId: int, taxRate: num }` -- `T34` = `{ id: int, name: str, sort: int }` +- `T26` = `{ si: str, basesi: str, basesifactor: num, symbol: str, name: str }` +- `T27` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }` +- `T28` = `{ id: int, entityId: int, contactType: str, contact: str }` +- `T29` = `{ id: int, attributeId: int, value: str, sort: int }` +- `T30` = `{ id: int, name: str, createdAt: str, updatedAt: str }` +- `T31` = `{ totalPositions: int, completelyOnLedgerPositions: int }` +- `T32` = `{ id: int, name: str, sort: int, exclusive: bool }` +- `T33` = `{ warehouse: T35 }` +- `T34` = `{ net: num, taxId: int, taxRate: num }` +- `T35` = `{ id: int, name: str, sort: int }` ### Routes @@ -63,7 +64,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - Returns: `200 { items: { id: int, articleId: int, sku: str, name: str, count: int, sort: int }[] }` #### Set Stückliste (bundle items) for an article — `PUT /article/{sku}/articlelist` - Params: `sku: str, body: { items: { articleId?: int, sku?: str, count?: int, sort?: int }[] }` -- Returns: `200 { items: obj[] }` +- Returns: `200 { items: { id: int, articleId: int, sku: str, name: str, count: int, sort: int }[] }` #### List article attribute values — `GET /article/{sku}/attributevalue` - Params: `sku: str` - Returns: `200 { attributevalues: { id: int, articleId: int, attributeId: int, attributeName: str, exclusive: bool, attributeValueId: int, value: str, sort: int }[] }` @@ -84,7 +85,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - Returns: `201 T24` #### Attach an existing article as a child to a parent article — `PUT /article/{sku}/children` - Params: `sku: str, body: { sku: str, attributeValueId?: int, value?: str, reparent?: bool }` -- Returns: `200 { article: T25, children: obj[] }` +- Returns: `200 { article: T25, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }` #### Remove a child article from a parent article — `DELETE /article/{sku}/children/{childSku}` - Params: `sku: str, childSku: str, query: deleteArticle?: bool` - Returns: `200 { success: bool, deleted: bool }` @@ -105,12 +106,12 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). - Returns: `200 { options: { id: int, articleId: int, sku: str, name: str, count: int, sort: int, mandated: bool }[] }` #### Set options list for an article — `PUT /article/{sku}/optionslist` - Params: `sku: str, body: { options: { articleId?: int, sku?: str, count?: int, sort?: int, mandated?: bool }[] }` -- Returns: `200 { options: obj[] }` +- Returns: `200 { options: { id: int, articleId: int, sku: str, name: str, count: int, sort: int, mandated: bool }[] }` #### Toggle parent mode for an article — `POST /article/{sku}/parent` - Params: `sku: str, body: { isParent: bool, attributeNameId?: int }` - Returns: `200 T24` #### Replace POS prices by SKU — `PUT /article/{sku}/prices` -- Params: `sku: str, body: { prices: { price: num, tax: num, minmultiplier?: num }[] }` +- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }` - Returns: `200 T1` #### Print article label — `POST /article/{sku}/print` - Params: `sku: str, body: { templateId: int, printerId: int, quantity: int }` @@ -156,10 +157,10 @@ _none_ - Returns: `200` #### List values for an attribute — `GET /attributename/{attributeId}/value` - Params: `attributeId: int` -- Returns: `200 { values: T28[] }` +- Returns: `200 { values: T29[] }` #### Create value for an attribute — `POST /attributename/{attributeId}/value` - Params: `attributeId: int, body: { value: str, sort?: int }` -- Returns: `201 { attributevalue: T28 }` +- Returns: `201 { attributevalue: T29 }` #### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order` - Params: `attributeId: int, body: { attributevalueIds: int[] }` - Returns: `200` @@ -174,7 +175,7 @@ _none_ - Returns: `200` #### Update attribute value — `PATCH /attributevalue/{id}` - Params: `id: int, body: { value?: str, sort?: int }` -- Returns: `200 { attributevalue: T28 }` +- Returns: `200 { attributevalue: T29 }` #### Login with Google ID token — `POST /auth/login` - Params: `body: { idToken: str }` - Returns: `200 { message: str, user: { id: int, email: str, selectedCompanyId: int, picture: str } }` @@ -183,19 +184,19 @@ _none_ - Returns: `200 { message: str }` #### List brands — `GET /brand` _none_ -- Returns: `200 { brands: { id: int, name: str, gsprpinfo: str, pics: T23[] }[] }` +- Returns: `200 { brands: { id: int, name: str, gsprpinfo: str, pics: any[] }[] }` #### Upsert brand — `PUT /brand` - Params: `body: { name: str, gsprpinfo?: str }` -- Returns: `200 { brand: { id: int, name: str, gsprpinfo: str, pics: T23[] } }` +- Returns: `200 { brand: { id: int, name: str, gsprpinfo: str, pics: any[] } }` #### Delete brand — `DELETE /brand/{id}` - Params: `id: int` - Returns: `204 void` #### Delete brand image — `DELETE /brand/{id}/image` - Params: `id: int` -- Returns: `200 { brand: { id: int, thumbW: int, thumbH: int, thumbHash: str, pics: T23[] } }` +- Returns: `200 { brand: { id: int, thumbW: int, thumbH: int, thumbHash: str, pics: { id: int, sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[] } }` #### Add or replace brand image — `POST /brand/{id}/image` - Params: `id: int, image/avif` -- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T23[] } }` +- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: { id: int, sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[] } }` #### Create category — `POST /category` - Params: `body: { parentId: int, name: str }` - Returns: `201 { category: { id: int, parentId: int, sort: int, name: str } }` @@ -204,7 +205,7 @@ _none_ - Returns: `200 { categoryIds: int[] }` #### Category tree — `GET /category/tree` _none_ -- Returns: `200 { languageId: int, tree: obj[], articles: obj[] }` +- Returns: `200 { languageId: int, tree: { id: int, parentId: int, sort: int, name: str, slug: str, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, posterminalIds: int[] }[], articles: { id: int, sku: str, name: str, slug: str, net: num, taxRate: num, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, parentArticleId: int, parentSku: str, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, brand: T23 }[] }` #### Delete category — `DELETE /category/{id}` - Params: `id: int` - Returns: `204 void` @@ -213,7 +214,7 @@ _none_ - Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }` #### Articles in category — `GET /category/{id}/articles` - Params: `id: int` -- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num, taxRate: num, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, brand: obj }[] }` +- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num, taxRate: num, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, parentArticleId: int, parentSku: str, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, brand: T23 }[] }` #### List category attribute values — `GET /category/{id}/attributevalue` - Params: `id: int` - Returns: `200 { attributevalues: { id: int, categoryId: int, attributeId: int, attributeName: str, exclusive: bool, attributeValueId: int, value: str, sort: int }[] }` @@ -258,7 +259,7 @@ _none_ - Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }` #### Create customer group — `POST /customergroup` - Params: `body: { name: str }` -- Returns: `201 { customergroup: T34 }` +- Returns: `201 { customergroup: T35 }` #### Reorder customer groups — `PUT /customergroup/order` - Params: `body: { customergroupIds: int[] }` - Returns: `200 { customergroupIds: int[] }` @@ -267,12 +268,12 @@ _none_ - Returns: `204 void` #### Update customer group — `PATCH /customergroup/{id}` - Params: `id: int, body: { name: str }` -- Returns: `200 { customergroup: T34 }` +- Returns: `200 { customergroup: T35 }` #### List deliveries — `GET /delivery` _none_ -- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, externalNumber: str, date: str, createdAt: str, ledger: T30 }[] }` +- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, externalNumber: str, date: str, createdAt: str, ledger: T31 }[] }` #### Create delivery — `POST /delivery` -- Params: `body: { externalNumber: str, date: str, lines: T21[], supplierId?: int, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: obj }[] }` +- Params: `body: { externalNumber: str, date: str, lines: T21[], supplierId?: int, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Returns: `201 T3` #### Delete delivery — `DELETE /delivery/{id}` - Params: `id: int` @@ -281,7 +282,7 @@ _none_ - Params: `id: int` - Returns: `200 T3` #### Update delivery — `PATCH /delivery/{id}` -- Params: `id: int, body: { externalNumber?: str, date?: str, supplierId?: int, lines?: T21[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: obj }[] }` +- Params: `id: int, body: { externalNumber?: str, date?: str, supplierId?: int, lines?: T21[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Returns: `200 T3` #### Get delivery attachment file content — `GET /delivery/{id}/file/{fileId}` - Params: `id: int, fileId: int, query: download?: bool` @@ -309,16 +310,16 @@ _none_ - Returns: `200 { address: T12 }` #### List entity contacts — `GET /entity/{entityId}/contact` - Params: `entityId: int` -- Returns: `200 { contacts: T27[] }` +- Returns: `200 { contacts: T28[] }` #### Create entity contact — `POST /entity/{entityId}/contact` - Params: `entityId: int, body: { contactType: str, contact: str }` -- Returns: `201 { contact: T27 }` +- Returns: `201 { contact: T28 }` #### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}` - Params: `entityId: int, id: int` - Returns: `204 void` #### Update entity contact — `PATCH /entity/{entityId}/contact/{id}` - Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }` -- Returns: `200 { contact: T27 }` +- Returns: `200 { contact: T28 }` #### Delete entity — `DELETE /entity/{id}` - Params: `id: int` - Returns: `204 void` @@ -332,7 +333,7 @@ _none_ _none_ - Returns: `200 { labeltemplates: T22[] }` #### Create label template — `POST /labeltemplate` -- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: obj }` +- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: {} }` - Returns: `201 T16` #### Delete label template — `DELETE /labeltemplate/{id}` - Params: `id: int` @@ -341,10 +342,10 @@ _none_ - Params: `id: int` - Returns: `200 T16` #### Update label template (partial) — `PATCH /labeltemplate/{id}` -- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: obj }` +- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: {} }` - Returns: `200 T16` #### Update label template — `PUT /labeltemplate/{id}` -- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: obj }` +- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: {} }` - Returns: `200 T16` #### Query ledger by manifest article ID or SKU — `GET /ledger` - Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str` @@ -402,10 +403,10 @@ _none_ - Returns: `200 T15` #### List property names — `GET /propertyname` _none_ -- Returns: `200 { propertynames: T31[] }` +- Returns: `200 { propertynames: T32[] }` #### Create property name — `POST /propertyname` - Params: `body: { name: str, sort?: int, exclusive?: bool }` -- Returns: `201 { propertyname: T31 }` +- Returns: `201 { propertyname: T32 }` #### Reorder property names — `PUT /propertyname/order` - Params: `body: { propertynameIds: int[] }` - Returns: `200` @@ -414,7 +415,7 @@ _none_ - Returns: `200` #### Update property name — `PATCH /propertyname/{id}` - Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }` -- Returns: `200 { propertyname: T31 }` +- Returns: `200 { propertyname: T32 }` #### List quotes — `GET /quote` _none_ - Returns: `200 { quotes: { id: int, number: str, type: str, entityId: int, date: str, createdAt: str }[] }` @@ -432,40 +433,40 @@ _none_ - Returns: `200 T6` #### Adjust stock manually — `POST /stock/adjust` - Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }` -- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: obj[], ledger: T11[] }` +- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T11[] }` #### List suppliers — `GET /supplier` _none_ -- Returns: `200 { suppliers: T29[] }` +- Returns: `200 { suppliers: T30[] }` #### Create supplier — `POST /supplier` - Params: `body: { name: str }` -- Returns: `201 { supplier: T29 }` +- Returns: `201 { supplier: T30 }` #### Delete supplier — `DELETE /supplier/{id}` - Params: `id: int` - Returns: `204 void` #### Update supplier — `PATCH /supplier/{id}` - Params: `id: int, body: { name: str }` -- Returns: `200 { supplier: T29 }` +- Returns: `200 { supplier: T30 }` #### List supplier articles by SKUs — `GET /supplier/{id}/articles` - Params: `id: int, query: skus?: string` -- Returns: `200 { articles: { id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T33, article: { id: int, sku: str, name: str, slug: str, photos: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[] } }[] }` +- Returns: `200 { articles: { id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T34, article: { id: int, sku: str, name: str, slug: str, photos: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[] } }[] }` #### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles` - Params: `id: int, body?: { skus?: str[] }` -- Returns: `200 { articles: { id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T33, article: { id: int, sku: str, name: str, slug: str, photos: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[] } }[] }` +- Returns: `200 { articles: { id: int, articleId: int, entityId: int, supplierName: str, sku: str, pu: num, stock: num, sort: int, priceId: int, price: T34, article: { id: int, sku: str, name: str, slug: str, photos: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[] } }[] }` #### Delete unit — `DELETE /unit` - Params: `query: si: str` - Returns: `204 void` #### List units — `GET /unit` _none_ -- Returns: `200 { units: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }[] }` +- Returns: `200 { units: T26[] }` #### Upsert unit — `PUT /unit` - Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }` -- Returns: `200 { unit: { si: str, basesi: str, basesifactor: num, symbol: str, name: str } }` +- Returns: `200 { unit: T26 }` #### List warehouses — `GET /warehouse` _none_ -- Returns: `200 { warehouses: T34[] }` +- Returns: `200 { warehouses: T35[] }` #### Create warehouse — `POST /warehouse` - Params: `body: { name: str, sort?: int }` -- Returns: `201 T32` +- Returns: `201 T33` #### Reorder warehouses — `PUT /warehouse/order` - Params: `body: { warehouseIds: int[] }` - Returns: `200 { warehouseIds: int[] }` @@ -474,13 +475,13 @@ _none_ - Returns: `204 void` #### Get warehouse by ID — `GET /warehouse/{id}` - Params: `id: int` -- Returns: `200 T32` +- Returns: `200 T33` #### Update warehouse (partial) — `PATCH /warehouse/{id}` - Params: `id: int, body: { name?: str, sort?: int }` -- Returns: `200 T32` +- Returns: `200 T33` #### Update warehouse — `PUT /warehouse/{id}` - Params: `id: int, body: { name?: str, sort?: int }` -- Returns: `200 T32` +- Returns: `200 T33` ### Binary & streaming routes (reachable only via `fetch()` in javascript_execute) These routes are NOT in the table above because their responses are binary or long-lived streams, which the `dashback_request` tool cannot return usefully. They ARE reachable from inside `javascript_execute` with `fetch("/route")`, where you can decode the bytes, write them to the VFS and embed them in your reply: diff --git a/docs/api/routes/article/_sku/articlelist.put.md b/docs/api/routes/article/_sku/articlelist.put.md index 139becd..a88f936 100644 --- a/docs/api/routes/article/_sku/articlelist.put.md +++ b/docs/api/routes/article/_sku/articlelist.put.md @@ -42,7 +42,14 @@ Updated Stückliste items ```json { "items": [ - {} + { + "id": 1, + "articleId": 1, + "sku": "…", + "name": "…", + "count": 1, + "sort": 1 + } ] } ``` diff --git a/docs/api/routes/article/_sku/children.put.md b/docs/api/routes/article/_sku/children.put.md index 105af3b..eb8d71c 100644 --- a/docs/api/routes/article/_sku/children.put.md +++ b/docs/api/routes/article/_sku/children.put.md @@ -44,7 +44,14 @@ Attached child article and list of children "track_stock": true }, "children": [ - {} + { + "id": 1, + "sku": "…", + "name": "…", + "parentsort": 1, + "attributeValueId": 1, + "attributeValue": "…" + } ] } ``` diff --git a/docs/api/routes/article/_sku/optionslist.put.md b/docs/api/routes/article/_sku/optionslist.put.md index cc49bf7..c526d70 100644 --- a/docs/api/routes/article/_sku/optionslist.put.md +++ b/docs/api/routes/article/_sku/optionslist.put.md @@ -44,7 +44,15 @@ Updated options list ```json { "options": [ - {} + { + "id": 1, + "articleId": 1, + "sku": "…", + "name": "…", + "count": 1, + "sort": 1, + "mandated": true + } ] } ``` diff --git a/docs/api/routes/article/_sku/track_stock.md b/docs/api/routes/article/_sku/track_stock.md index 3148499..4160979 100644 --- a/docs/api/routes/article/_sku/track_stock.md +++ b/docs/api/routes/article/_sku/track_stock.md @@ -43,8 +43,21 @@ Updated article detail "track_stock": true, "unitamount": 1, "weight": 1, - "unit": {}, - "brand": {}, + "unit": { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + }, + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + }, "categoryIds": [ 1 ], diff --git a/docs/api/routes/article/get.md b/docs/api/routes/article/get.md index 4af2dba..f3ab6a9 100644 --- a/docs/api/routes/article/get.md +++ b/docs/api/routes/article/get.md @@ -26,8 +26,21 @@ Article detail "track_stock": true, "unitamount": 1, "weight": 1, - "unit": {}, - "brand": {}, + "unit": { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + }, + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + }, "categoryIds": [ 1 ], diff --git a/docs/api/routes/article/order-images.md b/docs/api/routes/article/order-images.md index 8f1f905..3e76c91 100644 --- a/docs/api/routes/article/order-images.md +++ b/docs/api/routes/article/order-images.md @@ -23,8 +23,8 @@ Sets `articlepic.sort` to `0..n-1` in the given order. ```json { "fullHashes": [ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + "aB1cD2eF", + "gH3iJ4kL" ] } ``` diff --git a/docs/api/routes/article/set-brand.md b/docs/api/routes/article/set-brand.md index 4301474..5b27b1c 100644 --- a/docs/api/routes/article/set-brand.md +++ b/docs/api/routes/article/set-brand.md @@ -44,8 +44,21 @@ Updated article detail "track_stock": true, "unitamount": 1, "weight": 1, - "unit": {}, - "brand": {}, + "unit": { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + }, + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + }, "categoryIds": [ 1 ], diff --git a/docs/api/routes/article/set-categories.md b/docs/api/routes/article/set-categories.md index c138135..269a1ad 100644 --- a/docs/api/routes/article/set-categories.md +++ b/docs/api/routes/article/set-categories.md @@ -48,8 +48,21 @@ Updated article detail "track_stock": true, "unitamount": 1, "weight": 1, - "unit": {}, - "brand": {}, + "unit": { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + }, + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + }, "categoryIds": [ 1 ], diff --git a/docs/api/routes/article/set-eans.md b/docs/api/routes/article/set-eans.md index 711f554..93f897e 100644 --- a/docs/api/routes/article/set-eans.md +++ b/docs/api/routes/article/set-eans.md @@ -46,8 +46,21 @@ Updated article detail "track_stock": true, "unitamount": 1, "weight": 1, - "unit": {}, - "brand": {}, + "unit": { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + }, + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + }, "categoryIds": [ 1 ], diff --git a/docs/api/routes/article/set-prices.md b/docs/api/routes/article/set-prices.md index 70c3fb9..07d4f84 100644 --- a/docs/api/routes/article/set-prices.md +++ b/docs/api/routes/article/set-prices.md @@ -5,7 +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` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1). +Each entry accepts `net` OR `gross` price (or legacy `price`), `taxRate` OR `tax` (percentage, e.g. 19 for 19%, ensured via tax.ensure), optional `minmultiplier` (default 1). +When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`. Empty `prices` clears all POS prices. Returns the same shape as GET by SKU (article `prices` array contains `{ minmultiplier, net, taxRate, customergroupId }`). @@ -20,20 +21,23 @@ Empty `prices` clears all POS prices. Returns the same shape as GET by SKU (arti | Field | Required | Type | Description | |-------|----------|------|-------------| | `prices` | yes | object[] | | -| `prices[].price` | yes | number | Net price | -| `prices[].tax` | yes | number | Tax rate (e.g. 19) | -| `prices[].minmultiplier` | no | number | Defaults to 1; must be unique per request | +| `prices[].net` | no | number | Net price (excl. VAT). Either `net`, `gross`, or legacy `price` is required. | +| `prices[].gross` | no | number | Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`. | +| `prices[].price` | no | number | Legacy alias for `net`. | +| `prices[].taxRate` | no | number | Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%). | +| `prices[].tax` | no | number | Alias for `taxRate` percentage. | +| `prices[].minmultiplier` | no | number | Defaults to 1; must be unique per request. | ```json { "prices": [ { - "price": 10.5, - "tax": 19 + "net": 10.5, + "taxRate": 19 }, { - "price": 9.9, - "tax": 19, + "gross": 11.9, + "taxRate": 19, "minmultiplier": 10 } ] @@ -58,8 +62,21 @@ Updated article detail "track_stock": true, "unitamount": 1, "weight": 1, - "unit": {}, - "brand": {}, + "unit": { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + }, + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + }, "categoryIds": [ 1 ], diff --git a/docs/api/routes/article/set-weight.md b/docs/api/routes/article/set-weight.md index 8e981c1..e79b6ef 100644 --- a/docs/api/routes/article/set-weight.md +++ b/docs/api/routes/article/set-weight.md @@ -44,8 +44,21 @@ Updated article detail "track_stock": true, "unitamount": 1, "weight": 1, - "unit": {}, - "brand": {}, + "unit": { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + }, + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + }, "categoryIds": [ 1 ], diff --git a/docs/api/routes/article/update.md b/docs/api/routes/article/update.md index 57e1af3..7f309c4 100644 --- a/docs/api/routes/article/update.md +++ b/docs/api/routes/article/update.md @@ -48,8 +48,21 @@ Updated article detail "track_stock": true, "unitamount": 1, "weight": 1, - "unit": {}, - "brand": {}, + "unit": { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + }, + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + }, "categoryIds": [ 1 ], diff --git a/docs/api/routes/brand/list.md b/docs/api/routes/brand/list.md index 8dd1ed8..0f14560 100644 --- a/docs/api/routes/brand/list.md +++ b/docs/api/routes/brand/list.md @@ -17,16 +17,7 @@ Brand list "id": 1, "name": "…", "gsprpinfo": "…", - "pics": [ - { - "id": 1, - "sort": 1, - "fullHash": "…", - "thumbW": 1, - "thumbH": 1, - "thumbHash": "…" - } - ] + "pics": [] } ] } diff --git a/docs/api/routes/brand/upsert.md b/docs/api/routes/brand/upsert.md index 933803a..3a2216a 100644 --- a/docs/api/routes/brand/upsert.md +++ b/docs/api/routes/brand/upsert.md @@ -31,16 +31,7 @@ Upserted brand "id": 1, "name": "…", "gsprpinfo": "…", - "pics": [ - { - "id": 1, - "sort": 1, - "fullHash": "…", - "thumbW": 1, - "thumbH": 1, - "thumbHash": "…" - } - ] + "pics": [] } } ``` diff --git a/docs/api/routes/category/articles.md b/docs/api/routes/category/articles.md index c156c02..45f8b3a 100644 --- a/docs/api/routes/category/articles.md +++ b/docs/api/routes/category/articles.md @@ -30,11 +30,20 @@ Articles list "optionsCount": 1, "articleListCount": 1, "childCount": 1, + "parentArticleId": 1, + "parentSku": "…", "fullHash": "…", "thumbW": 1, "thumbH": 1, "thumbHash": "…", - "brand": {} + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + } } ] } diff --git a/docs/api/routes/category/tree.md b/docs/api/routes/category/tree.md index 24a0e2b..2fb0877 100644 --- a/docs/api/routes/category/tree.md +++ b/docs/api/routes/category/tree.md @@ -14,10 +14,48 @@ Tree and articles for the selected company { "languageId": 1, "tree": [ - {} + { + "id": 1, + "parentId": 1, + "sort": 1, + "name": "…", + "slug": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…", + "posterminalIds": [ + 1 + ] + } ], "articles": [ - {} + { + "id": 1, + "sku": "…", + "name": "…", + "slug": "…", + "net": 1, + "taxRate": 1, + "track_stock": true, + "optionsCount": 1, + "articleListCount": 1, + "childCount": 1, + "parentArticleId": 1, + "parentSku": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…", + "brand": { + "id": 1, + "name": "…", + "fullHash": "…", + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + } + } ] } ``` diff --git a/docs/api/routes/image/get.md b/docs/api/routes/image/get.md index 577e6bd..9574cbd 100644 --- a/docs/api/routes/image/get.md +++ b/docs/api/routes/image/get.md @@ -4,7 +4,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`). Full image by hash -Returns the full AVIF by `pic.hash` (SHA-256 hex). +Returns the full AVIF by `pic.hash` (8-char alphanumeric Base62). Auth via article category access. Browser-cacheable (`private`, `immutable`, `ETag`). @@ -12,7 +12,7 @@ Auth via article category access. Browser-cacheable (`private`, `immutable`, `ET | Param | Required | Type | Description | |-------|----------|------|-------------| -| `fullHash` | yes | string | SHA-256 hex of the full image bytes | +| `fullHash` | yes | string | 8-char alphanumeric hash of the full image bytes | ## Response `200` diff --git a/docs/api/routes/stock/adjust.md b/docs/api/routes/stock/adjust.md index 54a2873..b3c53e9 100644 --- a/docs/api/routes/stock/adjust.md +++ b/docs/api/routes/stock/adjust.md @@ -55,7 +55,14 @@ Stock adjusted successfully "createdAt": "2026-08-10T00:00:00.000Z" }, "lines": [ - {} + { + "manifestarticle_id": 1, + "sku": "…", + "name": "…", + "warehouse_id": 1, + "amount": 1, + "text": "…" + } ], "ledger": [ { diff --git a/docs/api/routes/thumb/get.md b/docs/api/routes/thumb/get.md index 825d48d..f0e4fb8 100644 --- a/docs/api/routes/thumb/get.md +++ b/docs/api/routes/thumb/get.md @@ -12,7 +12,7 @@ Auth via article category access. Browser-cacheable (`private`, `immutable`, `ET | Param | Required | Type | Description | |-------|----------|------|-------------| -| `fullHash` | yes | string | SHA-256 hex of the full image bytes | +| `fullHash` | yes | string | 8-char alphanumeric hash of the full image bytes | ## Response `200` diff --git a/docs/api/routes/vfs/delete.md b/docs/api/routes/vfs/delete.md index 395a5cd..badc821 100644 --- a/docs/api/routes/vfs/delete.md +++ b/docs/api/routes/vfs/delete.md @@ -5,7 +5,7 @@ Protected. Requires a valid session cookie (`credentials: 'include'`). Delete a virtual filesystem file Deletes a file from both the on-disk virtual filesystem and database for the authenticated user's selected company. -Deleting read-only database files (`image/*` or filenames containing 64-character hex hashes) is blocked with HTTP 403. +Deleting read-only database files (`image/*` or filenames containing 8-character image hashes) is blocked with HTTP 403. ## Path parameters diff --git a/docs/api/routes/vfs/upload.md b/docs/api/routes/vfs/upload.md index 0a66654..e164ddd 100644 --- a/docs/api/routes/vfs/upload.md +++ b/docs/api/routes/vfs/upload.md @@ -8,7 +8,7 @@ Uploads one or more files to the virtual filesystem for the authenticated user's Files are written to the company's VFS root directory and saved to the database. An optional `targetDir` form field specifies a subfolder within the VFS. -Writing to read-only database paths (`image/*` or filenames containing 64-character hex hashes) is +Writing to read-only database paths (`image/*` or filenames containing 8-character image hashes) is forbidden and automatically skipped during upload. diff --git a/docs/api/routes/vfs/write.md b/docs/api/routes/vfs/write.md index 2643ccc..ab01952 100644 --- a/docs/api/routes/vfs/write.md +++ b/docs/api/routes/vfs/write.md @@ -10,7 +10,7 @@ Supports multiple request payload formats: - Binary body: raw binary buffer with optional `Content-Type` header - Plain text: raw UTF-8 string -Modifying read-only database paths (`image/*` or filenames containing 64-character hex hashes) is blocked with HTTP 403. +Modifying read-only database paths (`image/*` or filenames containing 8-character image hashes) is blocked with HTTP 403. ## Path parameters diff --git a/scripts/generateApiDocs.js b/scripts/generateApiDocs.js index ed4234a..04cb883 100644 --- a/scripts/generateApiDocs.js +++ b/scripts/generateApiDocs.js @@ -531,7 +531,7 @@ function summarizeSchema( skipRootAlias = false, ) { if (!schema) return ''; - if (depth > 6) return aliases?.compactTypes ? 'obj' : 'object'; + if (depth > 6) return aliases?.compactTypes ? '{}' : 'object'; const resolved = resolveSchema(schema, components); if (!resolved) return schemaRefName(schema) || ''; @@ -549,7 +549,13 @@ function summarizeSchema( const required = new Set(resolved.required || []); let entries = Object.entries(resolved.properties || {}); - if (entries.length === 0) return aliases?.compactTypes ? 'obj' : 'object'; + if (entries.length === 0) { + if (resolved.additionalProperties && typeof resolved.additionalProperties === 'object') { + const valType = summarizeSchema(resolved.additionalProperties, components, depth + 1, markOptional, aliases) || 'any'; + return `{ [k]: ${valType} }`; + } + return aliases?.compactTypes ? '{}' : 'object'; + } // Required props first, then optional ones (stable within each group). if (markOptional) { @@ -574,7 +580,7 @@ function summarizeSchema( integer: 'int', number: 'num', boolean: 'bool', - object: 'obj', + object: '{}', }[resolved.type] || resolved.type; } return resolved.type; @@ -741,7 +747,7 @@ function renderRoutesOverview(operations, title = 'Dash API Routes Overview') { lines.push(''); lines.push('### Notation'); lines.push(''); - lines.push('Types: `str` string, `int` integer, `num` number, `bool` boolean, `obj` object; `T[]` array; `?` optional.'); + lines.push('Types: `str` string, `int` integer, `num` number, `bool` boolean, `{}` object; `T[]` array; `?` optional.'); lines.push('Params: path values first, then `query:` and `body:` (`body?` = optional body). Responses: status code + JSON shape; `Tn` refers to a reusable type below.'); if (definitions.length) { lines.push(''); diff --git a/src/api/openapi.js b/src/api/openapi.js index d0f3311..6252fa2 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -134,9 +134,9 @@ * type: number * nullable: true * unit: - * type: object + * $ref: '#/components/schemas/Unit' * brand: - * type: object + * $ref: '#/components/schemas/ArticleBrand' * nullable: true * categoryIds: * type: array @@ -353,7 +353,157 @@ * type: array * description: Brand pictures sorted by sort * items: - * $ref: '#/components/schemas/BrandPic' + * ArticleBrand: + * type: object + * required: [name] + * properties: + * id: + * type: integer + * nullable: true + * name: + * type: string + * fullHash: + * type: string + * nullable: true + * thumbW: + * type: integer + * nullable: true + * thumbH: + * type: integer + * nullable: true + * thumbHash: + * type: string + * nullable: true + * StuecklisteItem: + * type: object + * required: [id, articleId, sku, name, count, sort] + * properties: + * id: + * type: integer + * articleId: + * type: integer + * sku: + * type: string + * name: + * type: string + * count: + * type: integer + * sort: + * type: integer + * ArticleChild: + * type: object + * required: [id, sku, name, parentsort, attributeValueId, attributeValue] + * properties: + * id: + * type: integer + * sku: + * type: string + * name: + * type: string + * parentsort: + * type: integer + * attributeValueId: + * type: integer + * attributeValue: + * type: string + * ArticleOptionItem: + * type: object + * required: [id, articleId, sku, name, count, sort, mandated] + * properties: + * id: + * type: integer + * articleId: + * type: integer + * sku: + * type: string + * name: + * type: string + * count: + * type: integer + * sort: + * type: integer + * mandated: + * type: boolean + * CategoryTreeNode: + * type: object + * required: [id, sort, posterminalIds] + * properties: + * id: + * type: integer + * parentId: + * type: integer + * nullable: true + * sort: + * type: integer + * name: + * type: string + * nullable: true + * slug: + * type: string + * nullable: true + * fullHash: + * type: string + * nullable: true + * thumbW: + * type: integer + * nullable: true + * thumbH: + * type: integer + * nullable: true + * thumbHash: + * type: string + * nullable: true + * posterminalIds: + * type: array + * items: + * type: integer + * CategoryArticle: + * type: object + * required: [id, sku, name, slug, track_stock, optionsCount, articleListCount, childCount] + * properties: + * id: + * type: integer + * sku: + * type: string + * name: + * type: string + * slug: + * type: string + * net: + * type: number + * nullable: true + * taxRate: + * type: number + * nullable: true + * track_stock: + * type: boolean + * optionsCount: + * type: integer + * articleListCount: + * type: integer + * childCount: + * type: integer + * parentArticleId: + * type: integer + * nullable: true + * parentSku: + * type: string + * nullable: true + * fullHash: + * type: string + * nullable: true + * thumbW: + * type: integer + * nullable: true + * thumbH: + * type: integer + * nullable: true + * thumbHash: + * type: string + * nullable: true + * brand: + * $ref: '#/components/schemas/ArticleBrand' + * nullable: true * NumberSequence: * type: object * required: [id, type, prefix, padding, next] @@ -1151,6 +1301,20 @@ * type: array * items: * type: object + * required: [manifestarticle_id, sku, name, warehouse_id, amount, text] + * properties: + * manifestarticle_id: + * type: integer + * sku: + * type: string + * name: + * type: string + * warehouse_id: + * type: integer + * amount: + * type: number + * text: + * type: string * ledger: * type: array * items: diff --git a/src/api/routes/article.js b/src/api/routes/article.js index 5841921..22c659f 100644 --- a/src/api/routes/article.js +++ b/src/api/routes/article.js @@ -11,7 +11,7 @@ import { emit } from '../sse.js'; import { buildArtikelCsv, toWindows1252 } from '../../util/artikelCsv.js'; const AVIF_BODY_LIMIT = '20mb'; -const FULL_HASH_RE = /^[a-fA-F0-9]{64}$/; +const FULL_HASH_RE = /^[a-zA-Z0-9]{8}$/; /** Notify SSE subscribers after create / rename-ish list-level article changes. */ function emitArticlesSignal(req) { @@ -44,9 +44,9 @@ function parseSku(value) { function parseFullHash(value) { const fullHash = typeof value === 'string' ? value.trim() : ''; if (!FULL_HASH_RE.test(fullHash)) { - return { error: 'fullHash must be a 64-char hex SHA-256' }; + return { error: 'fullHash must be an 8-char alphanumeric string' }; } - return { fullHash: fullHash.toLowerCase() }; + return { fullHash }; } function articleErrorStatus(error) { @@ -720,7 +720,8 @@ export function register(app, basePath = '') { * summary: Replace POS prices by SKU * description: | * Replace-all prices for the company POS customer group. - * Each entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1). + * Each entry accepts `net` OR `gross` price (or legacy `price`), `taxRate` OR `tax` (percentage, e.g. 19 for 19%, ensured via tax.ensure), optional `minmultiplier` (default 1). + * When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`. * Empty `prices` clears all POS prices. Returns the same shape as GET by SKU (article `prices` array contains `{ minmultiplier, net, taxRate, customergroupId }`). * security: * - cookieAuth: [] @@ -744,23 +745,31 @@ export function register(app, basePath = '') { * type: array * items: * type: object - * required: [price, tax] * properties: + * net: + * type: number + * description: Net price (excl. VAT). Either `net`, `gross`, or legacy `price` is required. + * gross: + * type: number + * description: Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`. * price: * type: number - * description: Net price + * description: Legacy alias for `net`. + * taxRate: + * type: number + * description: Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%). * tax: * type: number - * description: Tax rate (e.g. 19) + * description: Alias for `taxRate` percentage. * minmultiplier: * type: number - * description: Defaults to 1; must be unique per request + * description: Defaults to 1; must be unique per request. * example: * prices: - * - price: 10.5 - * tax: 19 - * - price: 9.9 - * tax: 19 + * - net: 10.5 + * taxRate: 19 + * - gross: 11.9 + * taxRate: 19 * minmultiplier: 10 * responses: * 200: @@ -936,7 +945,7 @@ export function register(app, basePath = '') { * required: true * schema: * type: string - * pattern: '^[a-fA-F0-9]{64}$' + * pattern: '^[a-zA-Z0-9]{8}$' * responses: * 204: * description: Deleted @@ -1017,11 +1026,11 @@ export function register(app, basePath = '') { * type: array * items: * type: string - * pattern: '^[a-fA-F0-9]{64}$' + * pattern: '^[a-zA-Z0-9]{8}$' * example: * fullHashes: - * - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - * - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + * - "aB1cD2eF" + * - "gH3iJ4kL" * responses: * 200: * description: New order @@ -1806,20 +1815,7 @@ export function register(app, basePath = '') { * children: * type: array * items: - * type: object - * properties: - * id: - * type: integer - * sku: - * type: string - * name: - * type: string - * parentsort: - * type: integer - * attributeValueId: - * type: integer - * attributeValue: - * type: string + * $ref: '#/components/schemas/ArticleChild' * 404: * description: Article not found */ @@ -1930,7 +1926,7 @@ export function register(app, basePath = '') { * children: * type: array * items: - * type: object + * $ref: '#/components/schemas/ArticleChild' * 400: * description: Missing sku or attribute value, or invalid input * 404: @@ -2080,20 +2076,7 @@ export function register(app, basePath = '') { * items: * type: array * items: - * type: object - * properties: - * id: - * type: integer - * articleId: - * type: integer - * sku: - * type: string - * name: - * type: string - * count: - * type: integer - * sort: - * type: integer + * $ref: '#/components/schemas/StuecklisteItem' * 404: * description: Article not found */ @@ -2161,7 +2144,7 @@ export function register(app, basePath = '') { * items: * type: array * items: - * type: object + * $ref: '#/components/schemas/StuecklisteItem' * 400: * description: Article is a parent, has options, or contains itself * 404: @@ -2216,22 +2199,7 @@ export function register(app, basePath = '') { * options: * type: array * items: - * type: object - * properties: - * id: - * type: integer - * articleId: - * type: integer - * sku: - * type: string - * name: - * type: string - * count: - * type: integer - * sort: - * type: integer - * mandated: - * type: boolean + * $ref: '#/components/schemas/ArticleOptionItem' * 404: * description: Article not found */ @@ -2302,7 +2270,7 @@ export function register(app, basePath = '') { * options: * type: array * items: - * type: object + * $ref: '#/components/schemas/ArticleOptionItem' * 400: * description: Article is a parent, a Stückliste, or contains itself * 404: diff --git a/src/api/routes/category.js b/src/api/routes/category.js index 9ac2644..6cd4ab2 100644 --- a/src/api/routes/category.js +++ b/src/api/routes/category.js @@ -74,13 +74,11 @@ export function register(app, basePath = '') { * tree: * type: array * items: - * type: object - * additionalProperties: true + * $ref: '#/components/schemas/CategoryTreeNode' * articles: * type: array * items: - * type: object - * additionalProperties: true + * $ref: '#/components/schemas/CategoryArticle' * 400: * description: No company selected * content: @@ -326,48 +324,7 @@ export function register(app, basePath = '') { * articles: * type: array * items: - * type: object - * properties: - * id: - * type: integer - * sku: - * type: string - * name: - * type: string - * slug: - * type: string - * net: - * type: number - * nullable: true - * taxRate: - * type: number - * nullable: true - * track_stock: - * type: boolean - * optionsCount: - * type: integer - * articleListCount: - * type: integer - * childCount: - * type: integer - * description: Number of child articles belonging to this article - * fullHash: - * type: string - * nullable: true - * thumbW: - * type: integer - * nullable: true - * thumbH: - * type: integer - * nullable: true - * thumbHash: - * type: string - * nullable: true - * brand: - * type: object - * nullable: true - * additionalProperties: true - * additionalProperties: true + * $ref: '#/components/schemas/CategoryArticle' * 400: * description: Invalid id * content: diff --git a/src/api/routes/pic.js b/src/api/routes/pic.js index 0ad2d10..8c0c2fe 100644 --- a/src/api/routes/pic.js +++ b/src/api/routes/pic.js @@ -1,15 +1,15 @@ import * as db from '../../db/pic.js'; -const FULL_HASH_RE = /^[a-fA-F0-9]{64}$/; -/** Content-addressed by SHA-256 — safe for long-lived browser cache (auth still required on first fetch). */ +const FULL_HASH_RE = /^[a-zA-Z0-9]{8}$/; +/** Content-addressed by SHA-256 (Base62, 8 chars) — safe for long-lived browser cache (auth still required on first fetch). */ const CACHE_CONTROL = 'private, max-age=31536000, immutable'; function parseFullHash(value) { const fullHash = typeof value === 'string' ? value.trim() : ''; if (!FULL_HASH_RE.test(fullHash)) { - return { error: 'fullHash must be a 64-char hex SHA-256' }; + return { error: 'fullHash must be an 8-char alphanumeric string' }; } - return { fullHash: fullHash.toLowerCase() }; + return { fullHash }; } function picErrorStatus(error) { @@ -47,7 +47,7 @@ export function register(app, basePath = '') { * tags: [Image] * summary: Full image by hash * description: | - * Returns the full AVIF by `pic.hash` (SHA-256 hex). + * Returns the full AVIF by `pic.hash` (8-char alphanumeric Base62). * Auth via article category access. Browser-cacheable (`private`, `immutable`, `ETag`). * security: * - cookieAuth: [] @@ -58,8 +58,8 @@ export function register(app, basePath = '') { * required: true * schema: * type: string - * pattern: '^[a-fA-F0-9]{64}$' - * description: SHA-256 hex of the full image bytes + * pattern: '^[a-zA-Z0-9]{8}$' + * description: 8-char alphanumeric hash of the full image bytes * responses: * 200: * description: AVIF image bytes @@ -143,8 +143,8 @@ export function register(app, basePath = '') { * required: true * schema: * type: string - * pattern: '^[a-fA-F0-9]{64}$' - * description: SHA-256 hex of the full image bytes + * pattern: '^[a-zA-Z0-9]{8}$' + * description: 8-char alphanumeric hash of the full image bytes * responses: * 200: * description: AVIF thumb bytes diff --git a/src/api/routes/vfs.js b/src/api/routes/vfs.js index a36a235..0408902 100644 --- a/src/api/routes/vfs.js +++ b/src/api/routes/vfs.js @@ -479,7 +479,7 @@ export function register(app, basePath = '') { * Files are written to the company's VFS root directory and saved to the database. * An optional `targetDir` form field specifies a subfolder within the VFS. * - * Writing to read-only database paths (`image/*` or filenames containing 64-character hex hashes) is + * Writing to read-only database paths (`image/*` or filenames containing 8-character image hashes) is * forbidden and automatically skipped during upload. * security: * - cookieAuth: [] @@ -813,7 +813,7 @@ export function register(app, basePath = '') { * - Binary body: raw binary buffer with optional `Content-Type` header * - Plain text: raw UTF-8 string * - * Modifying read-only database paths (`image/*` or filenames containing 64-character hex hashes) is blocked with HTTP 403. + * Modifying read-only database paths (`image/*` or filenames containing 8-character image hashes) is blocked with HTTP 403. * security: * - cookieAuth: [] * x-signals: [vfs] @@ -959,7 +959,7 @@ export function register(app, basePath = '') { * summary: Delete a virtual filesystem file * description: | * Deletes a file from both the on-disk virtual filesystem and database for the authenticated user's selected company. - * Deleting read-only database files (`image/*` or filenames containing 64-character hex hashes) is blocked with HTTP 403. + * Deleting read-only database files (`image/*` or filenames containing 8-character image hashes) is blocked with HTTP 403. * security: * - cookieAuth: [] * x-signals: [vfs] diff --git a/src/tools/analyze-image.js b/src/tools/analyze-image.js index 45d799d..ad26fc7 100644 --- a/src/tools/analyze-image.js +++ b/src/tools/analyze-image.js @@ -236,7 +236,7 @@ function extractImageInputs(args = {}) { /** * Fetches a database image by its content hash using store or authenticated local endpoint. - * @param {string} hash 64-char SHA-256 hex + * @param {string} hash 8-char SHA-256 Base62 * @param {number} googleAuthId * @returns {Promise} */ @@ -323,7 +323,7 @@ export function createAnalyzeImageTool({ googleAuthId, model }) { // ── Load the bytes ──────────────────────────────────────────── const items = []; for (const item of inputs) { - // Only a path that actually carries a 64-char content hash is a database image. + // Only a path that actually carries an 8-char content hash is a database image. // A bare "images/…" prefix is just the VFS folder, so it must NOT be treated as // a DB lookup or `images/candidates/x.avif` would be misread as a hash path. const hash = extractFullHash(item.path); @@ -355,7 +355,7 @@ export function createAnalyzeImageTool({ googleAuthId, model }) { throw new ToolError( `Image not found or empty: "${item.path}". ` + (hash - ? 'Database images must be addressed as "image/" with a 64-char hash.' + ? 'Database images must be addressed as "image/" with an 8-char hash.' : 'Store it first (search_images writes to "images/…") or check the path with find_files.') ); } diff --git a/src/tools/edit-image.js b/src/tools/edit-image.js index 7fcacb3..67c9795 100644 --- a/src/tools/edit-image.js +++ b/src/tools/edit-image.js @@ -134,7 +134,7 @@ function resolveImagesApiUrl() { } /** - * Fetches a database image by its 64-char content hash using store or authenticated local endpoint. + * Fetches a database image by its 8-char content hash using store or authenticated local endpoint. * @param {string} hash * @param {number} googleAuthId * @returns {Promise} diff --git a/src/tools/vfs.js b/src/tools/vfs.js index 3f363fe..5599862 100644 --- a/src/tools/vfs.js +++ b/src/tools/vfs.js @@ -4,7 +4,7 @@ import { getStore } from '../db/store.js'; import env from '../config/env.js'; import { getDefaultSkills } from './skill.js'; -const FULL_HASH_REGEX = /^[a-fA-F0-9]{64}$/; +const FULL_HASH_REGEX = /^[a-zA-Z0-9]{8}$/; const VIRTUAL_IMAGE_FOLDERS = new Set(['image', 'images']); export function getMimeType(filePath) { @@ -55,18 +55,26 @@ export function getMimeType(filePath) { export function extractFullHash(p) { if (typeof p !== 'string' || !p.trim()) return null; const clean = p.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, ''); - const parts = clean.split('/'); + const parts = clean.split('/').filter(Boolean); + if (parts.length === 0) return null; + + const first = parts[0].toLowerCase(); const last = parts[parts.length - 1]; + const ext = path.extname(last).toLowerCase(); const withoutExt = last.replace(/\.[a-zA-Z0-9]+$/, ''); - if (FULL_HASH_REGEX.test(withoutExt)) { - return withoutExt.toLowerCase(); - } - for (const part of parts) { - const seg = part.replace(/\.[a-zA-Z0-9]+$/, ''); - if (FULL_HASH_REGEX.test(seg)) { - return seg.toLowerCase(); + + // Under virtual image folder (e.g. image/aB1cD2eF, images/aB1cD2eF.avif) + if (VIRTUAL_IMAGE_FOLDERS.has(first)) { + if (parts.length === 2 && FULL_HASH_REGEX.test(withoutExt)) { + return withoutExt; } } + + // Direct hash (e.g. "aB1cD2eF" or "aB1cD2eF.avif") + if (parts.length === 1 && FULL_HASH_REGEX.test(withoutExt) && (!ext || ext === '.avif')) { + return withoutExt; + } + return null; }