Compare commits

..

13 Commits

Author SHA1 Message Date
seb
d7f7d669f9 u 2026-10-16 22:09:15 +02:00
seb
188cd71f63 u 2026-10-16 18:11:28 +02:00
seb
de6c4987b9 u 2026-10-16 18:04:42 +02:00
seb
d05ba9276f u 2026-10-16 17:34:38 +02:00
seb
75925ab35c u 2026-10-16 17:32:31 +02:00
seb
52f49fb651 u 2026-10-16 17:23:59 +02:00
seb
2f0c4aced1 u 2026-10-16 17:11:09 +02:00
seb
99ac40e6fa u 2026-10-16 17:05:43 +02:00
seb
7f28a05ed3 u 2026-10-16 16:36:32 +02:00
seb
3b875c9f9a u 2026-10-16 16:24:58 +02:00
seb
f506dd35be u 2026-10-16 15:38:47 +02:00
seb
5ed634990e u 2026-10-16 15:22:13 +02:00
seb
bed19f8b10 u 2026-10-16 13:59:02 +02:00
42 changed files with 1240 additions and 1893 deletions

View File

@@ -159,8 +159,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/labeltemplate/get.md`](routes/labeltemplate/get.md) | `GET /labeltemplate/{id}` | | [`routes/labeltemplate/get.md`](routes/labeltemplate/get.md) | `GET /labeltemplate/{id}` |
| [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PATCH /labeltemplate/{id}` | | [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PATCH /labeltemplate/{id}` |
| [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PUT /labeltemplate/{id}` | | [`routes/labeltemplate/update.md`](routes/labeltemplate/update.md) | `PUT /labeltemplate/{id}` |
| [`routes/ledger.get.md`](routes/ledger.get.md) | `GET /ledger` | | [`routes/ledger.md`](routes/ledger.md) | `GET /ledger` |
| [`routes/ledger.post.md`](routes/ledger.post.md) | `POST /ledger` |
| [`routes/ledger/manifestarticle/_manifestarticleId.md`](routes/ledger/manifestarticle/_manifestarticleId.md) | `GET /ledger/manifestarticle/{manifestarticleId}` | | [`routes/ledger/manifestarticle/_manifestarticleId.md`](routes/ledger/manifestarticle/_manifestarticleId.md) | `GET /ledger/manifestarticle/{manifestarticleId}` |
| [`routes/ledger/sku/_sku.md`](routes/ledger/sku/_sku.md) | `GET /ledger/sku/{sku}` | | [`routes/ledger/sku/_sku.md`](routes/ledger/sku/_sku.md) | `GET /ledger/sku/{sku}` |
| [`routes/mcp.md`](routes/mcp.md) | `POST /mcp` | | [`routes/mcp.md`](routes/mcp.md) | `POST /mcp` |
@@ -248,7 +247,9 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/shipping/_id/package.get.md`](routes/shipping/_id/package.get.md) | `GET /shipping/{id}/package` | | [`routes/shipping/_id/package.get.md`](routes/shipping/_id/package.get.md) | `GET /shipping/{id}/package` |
| [`routes/shipping/_id/package.post.md`](routes/shipping/_id/package.post.md) | `POST /shipping/{id}/package` | | [`routes/shipping/_id/package.post.md`](routes/shipping/_id/package.post.md) | `POST /shipping/{id}/package` |
| [`routes/smallthumb/get.md`](routes/smallthumb/get.md) | `GET /smallthumb/{fullHash}` | | [`routes/smallthumb/get.md`](routes/smallthumb/get.md) | `GET /smallthumb/{fullHash}` |
| [`routes/stock/accept.md`](routes/stock/accept.md) | `POST /stock/accept` |
| [`routes/stock/adjust.md`](routes/stock/adjust.md) | `POST /stock/adjust` | | [`routes/stock/adjust.md`](routes/stock/adjust.md) | `POST /stock/adjust` |
| [`routes/stock/ship.md`](routes/stock/ship.md) | `POST /stock/ship` |
| [`routes/supplier/list.md`](routes/supplier/list.md) | `GET /supplier` | | [`routes/supplier/list.md`](routes/supplier/list.md) | `GET /supplier` |
| [`routes/supplier/create.md`](routes/supplier/create.md) | `POST /supplier` | | [`routes/supplier/create.md`](routes/supplier/create.md) | `POST /supplier` |
| [`routes/supplier/delete.md`](routes/supplier/delete.md) | `DELETE /supplier/{id}` | | [`routes/supplier/delete.md`](routes/supplier/delete.md) | `DELETE /supplier/{id}` |
@@ -265,9 +266,10 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/pos/pimage.md`](routes/pos/pimage.md) | `GET /v1/pimage` | | [`routes/pos/pimage.md`](routes/pos/pimage.md) | `GET /v1/pimage` |
| [`routes/v1/product.md`](routes/v1/product.md) | `GET /v1/product` | | [`routes/v1/product.md`](routes/v1/product.md) | `GET /v1/product` |
| [`routes/vfs/list.md`](routes/vfs/list.md) | `GET /vfs` | | [`routes/vfs/list.md`](routes/vfs/list.md) | `GET /vfs` |
| [`routes/vfs/drafts/response.md`](routes/vfs/drafts/response.md) | `POST /vfs/drafts/response` | | [`routes/vfs/applets-restore.md`](routes/vfs/applets-restore.md) | `POST /vfs/applets/restore` |
| [`routes/vfs/mkdir.md`](routes/vfs/mkdir.md) | `POST /vfs/mkdir` | | [`routes/vfs/mkdir.md`](routes/vfs/mkdir.md) | `POST /vfs/mkdir` |
| [`routes/vfs/rename.md`](routes/vfs/rename.md) | `POST /vfs/rename` | | [`routes/vfs/rename.md`](routes/vfs/rename.md) | `POST /vfs/rename` |
| [`routes/vfs/skills-restore.md`](routes/vfs/skills-restore.md) | `POST /vfs/skills/restore` |
| [`routes/vfs/upload.md`](routes/vfs/upload.md) | `POST /vfs/upload` | | [`routes/vfs/upload.md`](routes/vfs/upload.md) | `POST /vfs/upload` |
| [`routes/vfs/delete.md`](routes/vfs/delete.md) | `DELETE /vfs/{*path}` | | [`routes/vfs/delete.md`](routes/vfs/delete.md) | `DELETE /vfs/{*path}` |
| [`routes/vfs/_*path.md`](routes/vfs/_*path.md) | `GET /vfs/{*path}` | | [`routes/vfs/_*path.md`](routes/vfs/_*path.md) | `GET /vfs/{*path}` |
@@ -290,13 +292,13 @@ Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). A
| Signal | Emitted by | | Signal | Emitted by |
|--------|------------| |--------|------------|
| `article-<sku>` | `PUT /article/{sku}/brand`, `PUT /article/{sku}/categories`, `PUT /article/{sku}/eans`, `POST /article/{sku}/image`, `DELETE /article/{sku}/image/{fullHash}`, `PUT /article/{sku}/images/order`, `PUT /article/{sku}/prices`, `POST /article/{sku}/supplier`, `PUT /article/{sku}/supplier/order`, `DELETE /article/{sku}/supplier/{id}`, `PATCH /article/{sku}/supplier/{id}`, `PUT /article/{sku}/track_stock`, `PUT /article/{sku}/weight`, `POST /stock/adjust` | | `article-<sku>` | `PUT /article/{sku}/brand`, `PUT /article/{sku}/categories`, `PUT /article/{sku}/eans`, `POST /article/{sku}/image`, `DELETE /article/{sku}/image/{fullHash}`, `PUT /article/{sku}/images/order`, `PUT /article/{sku}/prices`, `POST /article/{sku}/supplier`, `PUT /article/{sku}/supplier/order`, `DELETE /article/{sku}/supplier/{id}`, `PATCH /article/{sku}/supplier/{id}`, `PUT /article/{sku}/track_stock`, `PUT /article/{sku}/weight`, `POST /stock/adjust` |
| `articles` | `POST /article`, `DELETE /article/{sku}`, `PATCH /article/{sku}`, `DELETE /category/{id}`, `POST /stock/adjust` | | `articles` | `POST /article`, `DELETE /article/{sku}`, `PATCH /article/{sku}`, `DELETE /category/{id}`, `POST /stock/accept`, `POST /stock/adjust`, `POST /stock/ship` |
| `attributename` | `POST /attributename`, `PUT /attributename/order`, `DELETE /attributename/{id}`, `PATCH /attributename/{id}`, `DELETE /attributevalue/{id}/image`, `POST /attributevalue/{id}/image` | | `attributename` | `POST /attributename`, `PUT /attributename/order`, `DELETE /attributename/{id}`, `PATCH /attributename/{id}`, `DELETE /attributevalue/{id}/image`, `POST /attributevalue/{id}/image` |
| `brand` | `POST /brand`, `DELETE /brand/{id}`, `PATCH /brand/{id}`, `DELETE /brand/{id}/image`, `POST /brand/{id}/image` | | `brand` | `POST /brand`, `DELETE /brand/{id}`, `PATCH /brand/{id}`, `DELETE /brand/{id}/image`, `POST /brand/{id}/image` |
| `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image` | | `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image` |
| `delivery` | `DELETE /delivery/{id}/file/{fileId}`, `POST /delivery/{id}/pdf` | | `delivery` | `DELETE /delivery/{id}/file/{fileId}`, `POST /delivery/{id}/pdf`, `POST /stock/accept` |
| `labeltemplate` | `POST /labeltemplate`, `DELETE /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `PUT /labeltemplate/{id}` | | `labeltemplate` | `POST /labeltemplate`, `DELETE /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `PUT /labeltemplate/{id}` |
| `ledger` | `POST /stock/adjust` | | `ledger` | `POST /stock/accept`, `POST /stock/adjust`, `POST /stock/ship` |
| `modelconversation` | `POST /modelconversation`, `DELETE /modelconversation/{id}` | | `modelconversation` | `POST /modelconversation`, `DELETE /modelconversation/{id}` |
| `modelconversation-<id>` | `POST /modelconversation/{id}/message` | | `modelconversation-<id>` | `POST /modelconversation/{id}/message` |
| `order` | `DELETE /order/{id}/file/{fileId}`, `POST /order/{id}/pdf`, `PATCH /shipment/{id}`, `DELETE /shipment/{id}/file/{fileId}` | | `order` | `DELETE /order/{id}/file/{fileId}`, `POST /order/{id}/pdf`, `PATCH /shipment/{id}`, `DELETE /shipment/{id}/file/{fileId}` |
@@ -304,8 +306,9 @@ Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). A
| `propertyname` | `POST /propertyname`, `PUT /propertyname/order`, `DELETE /propertyname/{id}`, `PATCH /propertyname/{id}` | | `propertyname` | `POST /propertyname`, `PUT /propertyname/order`, `DELETE /propertyname/{id}`, `PATCH /propertyname/{id}` |
| `quote` | `DELETE /quote/{id}/file/{fileId}`, `POST /quote/{id}/pdf` | | `quote` | `DELETE /quote/{id}/file/{fileId}`, `POST /quote/{id}/pdf` |
| `shipment` | `PATCH /shipment/{id}`, `DELETE /shipment/{id}/file/{fileId}`, `POST /shipment/{id}/pdf` | | `shipment` | `PATCH /shipment/{id}`, `DELETE /shipment/{id}/file/{fileId}`, `POST /shipment/{id}/pdf` |
| `stock` | `POST /stock/adjust` | | `shipping` | `POST /stock/ship` |
| `vfs` | `POST /vfs/mkdir`, `POST /vfs/rename`, `POST /vfs/upload`, `DELETE /vfs/{*path}`, `POST /vfs/{*path}`, `PUT /vfs/{*path}` | | `stock` | `POST /stock/accept`, `POST /stock/adjust`, `POST /stock/ship` |
| `vfs` | `POST /vfs/applets/restore`, `POST /vfs/mkdir`, `POST /vfs/rename`, `POST /vfs/skills/restore`, `POST /vfs/upload`, `DELETE /vfs/{*path}`, `POST /vfs/{*path}`, `PUT /vfs/{*path}` |
| `warehouse` | `POST /warehouse`, `PUT /warehouse/order`, `DELETE /warehouse/{id}`, `PUT /warehouse/{id}` | | `warehouse` | `POST /warehouse`, `PUT /warehouse/order`, `DELETE /warehouse/{id}`, `PUT /warehouse/{id}` |
## DESIGN ## DESIGN

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview (All) # Dash API Routes Overview (All)
Overview of all 248 API endpoints across all modules. Overview of all 250 API endpoints across all modules.
### Notation ### Notation
@@ -10,15 +10,15 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types ### Reusable types
- `T1` = `{ order: T2 }` - `T1` = `{ order: T2 }`
- `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T30[], externalNumber?: str|null, createdAt?: str, shipping?: T38|null, shippings?: T38[], payment?: T44|null, payments?: T44[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T6[], files?: T16[] }` - `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T29[], externalNumber?: str|null, createdAt?: str, shipping?: T36|null, shippings?: T36[], payment?: T42|null, payments?: T42[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T6[], files?: T16[] }`
- `T3` = `{ article: T4 }` - `T3` = `{ article: T4 }`
- `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T41, brand: T39|null, categoryIds: int[], photos: T36[], eans: str[], prices: T48[], suppliers: T18[] }` - `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T39, brand: T37|null, categoryIds: int[], photos: T34[], eans: str[], prices: T46[], suppliers: T18[] }`
- `T5` = `{ shipment: T6 }` - `T5` = `{ shipment: T6 }`
- `T6` = `{ id: int, number: str, type: str, date: str, lines: T30[], shippings: T38[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T16[], state?: str }` - `T6` = `{ id: int, number: str, type: str, date: str, lines: T29[], shippings: T36[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T16[], state?: str }`
- `T7` = `{ quote: T8 }` - `T7` = `{ quote: T8 }`
- `T8` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T30[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T16[] }` - `T8` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T29[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T16[] }`
- `T9` = `{ delivery: T10 }` - `T9` = `{ delivery: T10 }`
- `T10` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T30[], files: T16[], externalNumber?: str|null, createdAt?: str, ledger?: T52 }` - `T10` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T29[], files: T16[], externalNumber?: str|null, createdAt?: str, ledger?: T50 }`
- `T11` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T12|null }` - `T11` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T12|null }`
- `T12` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T19, totalSizeBytes: int, durationMs: int }` - `T12` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T19, totalSizeBytes: int, durationMs: int }`
- `T13` = `{ labeltemplate: T15 }` - `T13` = `{ labeltemplate: T15 }`
@@ -26,46 +26,44 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- `T15` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T26, thumb?: str|null }` - `T15` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T26, thumb?: str|null }`
- `T16` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }` - `T16` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
- `T17` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }` - `T17` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }`
- `T18` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T55|null }` - `T18` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T53|null }`
- `T19` = `{ vfs: T53, storage: T53, database: T46 }` - `T19` = `{ vfs: T51, storage: T51, database: T44 }`
- `T20` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - `T20` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- `T21` = `{ ledger: T22[] }` - `T21` = `{ ledger: T22[] }`
- `T22` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }` - `T22` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
- `T23` = `{ id: int, name: str, sort: int, exclusive: bool, values: T42[], articleCount?: int }` - `T23` = `{ id: int, name: str, sort: int, exclusive: bool, values: T40[], articleCount?: int }`
- `T24` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: integer,null, first_message?: string,null }` - `T24` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: int|null, first_message?: str|null }`
- `T25` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }` - `T25` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }`
- `T26` = `{ objects: T31[] }` - `T26` = `{ objects: T30[] }`
- `T27` = `{ objects: T32[] }` - `T27` = `{ printer: T32 }`
- `T28` = `{ printer: T34 }` - `T28` = `{ entity: T35 }`
- `T29` = `{ entity: T37 }` - `T29` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
- `T30` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }` - `T30` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T31` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }` - `T31` = `{ shipping: T36 }`
- `T32` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }` - `T32` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
- `T33` = `{ shipping: T38 }` - `T33` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }`
- `T34` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }` - `T34` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }`
- `T35` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }` - `T35` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T36` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }` - `T36` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
- `T37` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }` - `T37` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
- `T38` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }` - `T38` = `{ article: T43 }`
- `T39` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }` - `T39` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T40` = `{ article: T45 }` - `T40` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
- `T41` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }` - `T41` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
- `T42` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }` - `T42` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
- `T43` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }` - `T43` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
- `T44` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }` - `T44` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }`
- `T45` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }` - `T45` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
- `T46` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }` - `T46` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
- `T47` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }` - `T47` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T48` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }` - `T48` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T49` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }` - `T49` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
- `T50` = `{ id: int, entityId: int, contactType: str, contact: str }` - `T50` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T51` = `{ id: int, name: str, createdAt: str, updatedAt: str }` - `T51` = `{ name: str, sizeBytes: int, sourceExists: bool }`
- `T52` = `{ totalPositions: int, completelyOnLedgerPositions: int }` - `T52` = `{ warehouse: T54 }`
- `T53` = `{ name: str, sizeBytes: int, sourceExists: bool }` - `T53` = `{ net: num, taxId: int, taxRate?: num|null }`
- `T54` = `{ warehouse: T56 }` - `T54` = `{ id: int, name: str, sort: int }`
- `T55` = `{ net: num, taxId: int, taxRate?: num|null }` - `T55` = `{ id: int, categoryIds: int[] }`
- `T56` = `{ id: int, name: str, sort: int }`
- `T57` = `{ id: int, categoryIds: int[] }`
### Routes ### Routes
@@ -95,7 +93,7 @@ _none_
#### Create article in category — `POST /article` #### Create article in category — `POST /article`
- Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }` - Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }`
- Returns: `201 T40` - Returns: `201 T38`
#### Export articles as ZIP — `GET /article/export.zip` #### Export articles as ZIP — `GET /article/export.zip`
- Params: `query: categoryId?: int` - Params: `query: categoryId?: int`
- Returns: `200 zip` - Returns: `200 zip`
@@ -131,10 +129,10 @@ _none_
- Returns: `200 { children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }` - Returns: `200 { children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
#### Create and attach a new child article to a parent article — `POST /article/{sku}/children` #### Create and attach a new child article to a parent article — `POST /article/{sku}/children`
- Params: `sku: str, body: { attributeValueId?: int, value?: str, name?: str }` - Params: `sku: str, body: { attributeValueId?: int, value?: str, name?: str }`
- Returns: `201 T40` - Returns: `201 T38`
#### Attach an existing article as a child to a parent article — `PUT /article/{sku}/children` #### 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 }` - Params: `sku: str, body: { sku: str, attributeValueId?: int, value?: str, reparent?: bool }`
- Returns: `200 { article: T45, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }` - Returns: `200 { article: T43, 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}` #### Remove a child article from a parent article — `DELETE /article/{sku}/children/{childSku}`
- Params: `sku: str, childSku: str, query: deleteArticle?: bool` - Params: `sku: str, childSku: str, query: deleteArticle?: bool`
- Returns: `200 { success: bool, deleted: bool }` - Returns: `200 { success: bool, deleted: bool }`
@@ -161,7 +159,7 @@ _none_
- Returns: `200 { options: { id: int, articleId: int, sku: str, name: str, count: int, sort: int, mandated: bool }[] }` - 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` #### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }` - Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `200 T40` - Returns: `200 T38`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices` #### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices`
- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }` - Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }`
- Returns: `200 T3` - Returns: `200 T3`
@@ -212,10 +210,10 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### List values for an attribute — `GET /attributename/{attributeId}/value` #### List values for an attribute — `GET /attributename/{attributeId}/value`
- Params: `attributeId: int` - Params: `attributeId: int`
- Returns: `200 { values: T42[] }` - Returns: `200 { values: T40[] }`
#### Create value for an attribute — `POST /attributename/{attributeId}/value` #### Create value for an attribute — `POST /attributename/{attributeId}/value`
- Params: `attributeId: int, body: { value: str, sort?: int }` - Params: `attributeId: int, body: { value: str, sort?: int }`
- Returns: `201 { attributevalue: T42 }` - Returns: `201 { attributevalue: T40 }`
#### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order` #### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order`
- Params: `attributeId: int, body: { attributevalueIds: int[] }` - Params: `attributeId: int, body: { attributevalueIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
@@ -233,28 +231,28 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update attribute value — `PATCH /attributevalue/{id}` #### Update attribute value — `PATCH /attributevalue/{id}`
- Params: `id: int, body: { value?: str, sort?: int }` - Params: `id: int, body: { value?: str, sort?: int }`
- Returns: `200 { attributevalue: T42 }` - Returns: `200 { attributevalue: T40 }`
#### Delete attribute value image — `DELETE /attributevalue/{id}/image` #### Delete attribute value image — `DELETE /attributevalue/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T49[] } }` - Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T47[] } }`
#### Add or replace attribute value image — `POST /attributevalue/{id}/image` #### Add or replace attribute value image — `POST /attributevalue/{id}/image`
- Params: `id: int, image/avif` - Params: `id: int, image/avif`
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T49[] } }` - Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T47[] } }`
### Section: /auth ### Section: /auth
#### Login with Google ID token — `POST /auth/login` #### Login with Google ID token — `POST /auth/login`
- Params: `body: { idToken: str }` - Params: `body: { idToken: str }`
- Returns: `200 { message: str, user: T47 }` - Returns: `200 { message: str, user: T45 }`
#### Logout — `POST /auth/logout` #### Logout — `POST /auth/logout`
_none_ _none_
- Returns: `200 { message: str }` - Returns: `200 { message: str }`
#### Current user — `GET /auth/me` #### Current user — `GET /auth/me`
_none_ _none_
- Returns: `200 { user: T47, googleClientId: str }` - Returns: `200 { user: T45, googleClientId: str }`
#### Set selected company — `PUT /auth/selected-company` #### Set selected company — `PUT /auth/selected-company`
- Params: `body: { companyId: int }` - Params: `body: { companyId: int }`
- Returns: `200 { user: T47 }` - Returns: `200 { user: T45 }`
### Section: /backup ### Section: /backup
@@ -275,7 +273,7 @@ _none_
- Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }` - Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }`
#### Create brand — `POST /brand` #### Create brand — `POST /brand`
- Params: `body: { name: str, gsprpinfo?: str }` - Params: `body: { name: str, gsprpinfo?: str }`
- Returns: `201 { brand: { id: int, name: str, pics: T49[], gsprpinfo?: str|null } }` - Returns: `201 { brand: { id: int, name: str, pics: T47[], gsprpinfo?: str|null } }`
#### Delete brand — `DELETE /brand/{id}` #### Delete brand — `DELETE /brand/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
@@ -284,10 +282,10 @@ _none_
- Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }` - Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }`
#### Delete brand image — `DELETE /brand/{id}/image` #### Delete brand image — `DELETE /brand/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T49[] } }` - Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T47[] } }`
#### Add or replace brand image — `POST /brand/{id}/image` #### Add or replace brand image — `POST /brand/{id}/image`
- Params: `id: int, image/avif` - Params: `id: int, image/avif`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T49[] } }` - Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T47[] } }`
### Section: /category ### Section: /category
@@ -311,7 +309,7 @@ _none_
- Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }` - Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }`
#### Articles in category — `GET /category/{id}/articles` #### Articles in category — `GET /category/{id}/articles`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T39|null }[] }` - Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T37|null }[] }`
#### List category attribute values — `GET /category/{id}/attributevalue` #### List category attribute values — `GET /category/{id}/attributevalue`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }` - Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }`
@@ -341,7 +339,7 @@ _none_
- Returns: `200 { companies: T25[] }` - Returns: `200 { companies: T25[] }`
#### Create company — `POST /company` #### Create company — `POST /company`
- Params: `body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }` - Params: `body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- Returns: `201 { company: T25, user: T47 }` - Returns: `201 { company: T25, user: T45 }`
#### Export company catalog as zip archive (SSE) — `GET /company/export` #### Export company catalog as zip archive (SSE) — `GET /company/export`
- Params: `query: stream?: str` - Params: `query: stream?: str`
- Returns: `200 SSE` - Returns: `200 SSE`
@@ -380,7 +378,7 @@ _none_
- Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }` - Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }`
#### Create customer group — `POST /customergroup` #### Create customer group — `POST /customergroup`
- Params: `body: { name: str }` - Params: `body: { name: str }`
- Returns: `201 { customergroup: T56 }` - Returns: `201 { customergroup: T54 }`
#### Reorder customer groups — `PUT /customergroup/order` #### Reorder customer groups — `PUT /customergroup/order`
- Params: `body: { customergroupIds: int[] }` - Params: `body: { customergroupIds: int[] }`
- Returns: `200 { customergroupIds: int[] }` - Returns: `200 { customergroupIds: int[] }`
@@ -389,15 +387,15 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Update customer group — `PATCH /customergroup/{id}` #### Update customer group — `PATCH /customergroup/{id}`
- Params: `id: int, body: { name: str }` - Params: `id: int, body: { name: str }`
- Returns: `200 { customergroup: T56 }` - Returns: `200 { customergroup: T54 }`
### Section: /delivery ### Section: /delivery
#### List deliveries — `GET /delivery` #### List deliveries — `GET /delivery`
_none_ - Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T52 }[] }` - Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T50 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create delivery — `POST /delivery` #### Create delivery — `POST /delivery`
- Params: `body: { externalNumber: str, date: str, lines: T35[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Params: `body: { externalNumber: str, date: str, lines: T33[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 T9` - Returns: `201 T9`
#### Delete delivery — `DELETE /delivery/{id}` #### Delete delivery — `DELETE /delivery/{id}`
- Params: `id: int` - Params: `id: int`
@@ -406,7 +404,7 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 T9` - Returns: `200 T9`
#### Update delivery — `PATCH /delivery/{id}` #### Update delivery — `PATCH /delivery/{id}`
- Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T35[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T33[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `200 T9` - Returns: `200 T9`
#### Delete delivery file attachment — `DELETE /delivery/{id}/file/{fileId}` #### Delete delivery file attachment — `DELETE /delivery/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
@@ -422,10 +420,10 @@ _none_
#### List entities — `GET /entity` #### List entities — `GET /entity`
- Params: `query: customergroupId?: int, role?: str` - Params: `query: customergroupId?: int, role?: str`
- Returns: `200 { entities: T37[] }` - Returns: `200 { entities: T35[] }`
#### Create entity — `POST /entity` #### Create entity — `POST /entity`
- Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }` - Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }`
- Returns: `201 T29` - Returns: `201 T28`
#### Peek next customer number — `GET /entity/next-number` #### Peek next customer number — `GET /entity/next-number`
_none_ _none_
- Returns: `200 { number: str }` - Returns: `200 { number: str }`
@@ -443,25 +441,25 @@ _none_
- Returns: `200 { address: T20 }` - Returns: `200 { address: T20 }`
#### List entity contacts — `GET /entity/{entityId}/contact` #### List entity contacts — `GET /entity/{entityId}/contact`
- Params: `entityId: int` - Params: `entityId: int`
- Returns: `200 { contacts: T50[] }` - Returns: `200 { contacts: T48[] }`
#### Create entity contact — `POST /entity/{entityId}/contact` #### Create entity contact — `POST /entity/{entityId}/contact`
- Params: `entityId: int, body: { contactType: str, contact: str }` - Params: `entityId: int, body: { contactType: str, contact: str }`
- Returns: `201 { contact: T50 }` - Returns: `201 { contact: T48 }`
#### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}` #### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int` - Params: `entityId: int, id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update entity contact — `PATCH /entity/{entityId}/contact/{id}` #### Update entity contact — `PATCH /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }` - Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }`
- Returns: `200 { contact: T50 }` - Returns: `200 { contact: T48 }`
#### Delete entity — `DELETE /entity/{id}` #### Delete entity — `DELETE /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get entity — `GET /entity/{id}` #### Get entity — `GET /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 T29` - Returns: `200 T28`
#### Update entity — `PATCH /entity/{id}` #### Update entity — `PATCH /entity/{id}`
- Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }` - Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }`
- Returns: `200 T29` - Returns: `200 T28`
### Section: /events ### Section: /events
@@ -484,8 +482,6 @@ _none_
- `modelconversation`: `{}` - `modelconversation`: `{}`
- `modelconversation-<id>`: `{}` - `modelconversation-<id>`: `{}`
- `brand`: `{ sender, brandId }` - `brand`: `{ sender, brandId }`
- `draft`: `{ sender, action, name, content }`
- `draft:request`: `{ requestId, action, name, target, sender }`
### Section: /health ### Section: /health
@@ -505,7 +501,7 @@ _none_
_none_ _none_
- Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }` - Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }`
#### Create label template — `POST /labeltemplate` #### Create label template — `POST /labeltemplate`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: T27 }` - Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: T26 }`
- Returns: `201 T13` - Returns: `201 T13`
#### Delete label template — `DELETE /labeltemplate/{id}` #### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`
@@ -514,10 +510,10 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 T13` - Returns: `200 T13`
#### Update label template (partial) — `PATCH /labeltemplate/{id}` #### Update label template (partial) — `PATCH /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T27 }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T26 }`
- Returns: `200 T13` - Returns: `200 T13`
#### Update label template — `PUT /labeltemplate/{id}` #### Update label template — `PUT /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T27 }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T26 }`
- Returns: `200 T13` - Returns: `200 T13`
### Section: /ledger ### Section: /ledger
@@ -525,9 +521,6 @@ _none_
#### Query ledger by manifest article ID or SKU — `GET /ledger` #### Query ledger by manifest article ID or SKU — `GET /ledger`
- Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str` - Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str`
- Returns: `200 T21` - Returns: `200 T21`
#### Add ledger entry — `POST /ledger`
- Params: `body: { manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str }`
- Returns: `201 { ledger: T22 }`
#### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}` #### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}`
- Params: `manifestarticleId: int, query: transaction_type?: str` - Params: `manifestarticleId: int, query: transaction_type?: str`
- Returns: `200 T21` - Returns: `200 T21`
@@ -613,7 +606,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
- Returns: `200 { orders: T2[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { orders: T2[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create order — `POST /order` #### Create order — `POST /order`
- Params: `body: { number: str, date: str, lines: T35[], entityId?: int|null, externalNumber?: str|null }` - Params: `body: { number: str, date: str, lines: T33[], entityId?: int|null, externalNumber?: str|null }`
- Returns: `201 T1` - Returns: `201 T1`
#### Delete order — `DELETE /order/{id}` #### Delete order — `DELETE /order/{id}`
- Params: `id: int` - Params: `id: int`
@@ -622,7 +615,7 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 T1` - Returns: `200 T1`
#### Update order — `PATCH /order/{id}` #### Update order — `PATCH /order/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T35[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T33[] }`
- Returns: `200 T1` - Returns: `200 T1`
#### Delete order file attachment — `DELETE /order/{id}/file/{fileId}` #### Delete order file attachment — `DELETE /order/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
@@ -632,10 +625,10 @@ _none_
- Returns: `200 str` - Returns: `200 str`
#### List payments of an order — `GET /order/{id}/payment` #### List payments of an order — `GET /order/{id}/payment`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { payments: T44[] }` - Returns: `200 { payments: T42[] }`
#### Add payment to an order — `POST /order/{id}/payment` #### Add payment to an order — `POST /order/{id}/payment`
- Params: `id: int, body: { accountId: int, type: str, amount: num }` - Params: `id: int, body: { accountId: int, type: str, amount: num }`
- Returns: `201 { payment: T44 }` - Returns: `201 { payment: T42 }`
#### Generate and attach order PDF — `POST /order/{id}/pdf` #### Generate and attach order PDF — `POST /order/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 T14` - Returns: `201 T14`
@@ -647,10 +640,10 @@ _none_
- Returns: `201 T5` - Returns: `201 T5`
#### List shippings of an order — `GET /order/{id}/shipping` #### List shippings of an order — `GET /order/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T38[] }` - Returns: `200 { shippings: T36[] }`
#### Create shipping for an order — `POST /order/{id}/shipping` #### Create shipping for an order — `POST /order/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, warehouseId?: int }` - Params: `id: int, body: { type: str, state: str, warehouseId?: int }`
- Returns: `201 T33` - Returns: `201 T31`
### Section: /package ### Section: /package
@@ -680,25 +673,25 @@ _none_
- Returns: `200 { posterminal: T17 }` - Returns: `200 { posterminal: T17 }`
#### Disable all categories on terminal — `DELETE /posterminal/{id}/all` #### Disable all categories on terminal — `DELETE /posterminal/{id}/all`
- Params: `id: int` - Params: `id: int`
- Returns: `200 T57` - Returns: `200 T55`
#### Enable all categories on terminal — `PUT /posterminal/{id}/all` #### Enable all categories on terminal — `PUT /posterminal/{id}/all`
- Params: `id: int` - Params: `id: int`
- Returns: `200 T57` - Returns: `200 T55`
#### Disable category on terminal — `DELETE /posterminal/{id}/category/{categoryId}` #### Disable category on terminal — `DELETE /posterminal/{id}/category/{categoryId}`
- Params: `id: int, categoryId: int` - Params: `id: int, categoryId: int`
- Returns: `200 T57` - Returns: `200 T55`
#### Enable category on terminal — `PUT /posterminal/{id}/category/{categoryId}` #### Enable category on terminal — `PUT /posterminal/{id}/category/{categoryId}`
- Params: `id: int, categoryId: int` - Params: `id: int, categoryId: int`
- Returns: `200 T57` - Returns: `200 T55`
### Section: /printer ### Section: /printer
#### List printers — `GET /printer` #### List printers — `GET /printer`
_none_ _none_
- Returns: `200 { printers: T34[] }` - Returns: `200 { printers: T32[] }`
#### Create printer — `POST /printer` #### Create printer — `POST /printer`
- Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `201 T28` - Returns: `201 T27`
#### Reorder printers — `PUT /printer/order` #### Reorder printers — `PUT /printer/order`
- Params: `body: { printerIds: int[] }` - Params: `body: { printerIds: int[] }`
- Returns: `200 { printerIds: int[] }` - Returns: `200 { printerIds: int[] }`
@@ -707,22 +700,22 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Get printer by ID — `GET /printer/{id}` #### Get printer by ID — `GET /printer/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 T28` - Returns: `200 T27`
#### Update printer (partial) — `PATCH /printer/{id}` #### Update printer (partial) — `PATCH /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 T28` - Returns: `200 T27`
#### Update printer — `PUT /printer/{id}` #### Update printer — `PUT /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 T28` - Returns: `200 T27`
### Section: /propertyname ### Section: /propertyname
#### List property names — `GET /propertyname` #### List property names — `GET /propertyname`
_none_ _none_
- Returns: `200 { propertynames: T43[] }` - Returns: `200 { propertynames: T41[] }`
#### Create property name — `POST /propertyname` #### Create property name — `POST /propertyname`
- Params: `body: { name: str, sort?: int, exclusive?: bool }` - Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { propertyname: T43 }` - Returns: `201 { propertyname: T41 }`
#### Reorder property names — `PUT /propertyname/order` #### Reorder property names — `PUT /propertyname/order`
- Params: `body: { propertynameIds: int[] }` - Params: `body: { propertynameIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
@@ -731,19 +724,19 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update property name — `PATCH /propertyname/{id}` #### Update property name — `PATCH /propertyname/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }` - Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { propertyname: T43 }` - Returns: `200 { propertyname: T41 }`
### Section: /public ### Section: /public
#### Receipt crash dump — `POST /public/telemetry/crash` #### Receipt crash dump — `POST /public/telemetry/crash`
- Params: `body: { content: any }` - Params: `body: { content: str|{} }`
- Returns: `200 { ok: bool, filename: str }` - Returns: `200 { ok: bool, filename: str }`
#### List public articles — `GET /public/{companyId}/articles` #### List public articles — `GET /public/{companyId}/articles`
- Params: `companyId: int, query: categoryId?: int, search?: str, limit?: int, offset?: int` - 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|null, taxRate: num|null, gross: num|null, trackStock: bool, brand: { id: int|null, name: str }, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }` - Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num|null, taxRate: num|null, gross: num|null, trackStock: bool, brand: { id: int|null, name: str }, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }`
#### Get public article detail — `GET /public/{companyId}/articles/{sku}` #### Get public article detail — `GET /public/{companyId}/articles/{sku}`
- Params: `companyId: int, sku: str` - Params: `companyId: int, sku: str`
- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, description: str, unitamount: num|null, weight: num|null, trackStock: bool, unit: {}|null, brand: {}|null, price: {}|null, prices: {}[], categories: {}[], photos: {}[], eans: str[], isParent: bool, parentSku: str|null, variants: {}[] } }` - Returns: `200 { article: { id: int, sku: str, name: str, slug: str, description: str, unitamount: num|null, weight: num|null, trackStock: bool, unit: T39|null, brand: T37|null, price: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }, prices: { minmultiplier: num, net: num|null, taxRate: num|null, gross: num|null }[], categories: { id: int, name: str, slug: str }[], photos: T34[], eans: str[], isParent: bool, parentSku: str|null, variants: { id: int, sku: str, sort: int, name: str, slug: str, attributeValueId: int|null, attributeValue: str|null, unitamount: num|null, weight: num|null, trackStock: bool }[] } }`
#### List public categories — `GET /public/{companyId}/categories` #### List public categories — `GET /public/{companyId}/categories`
- Params: `companyId: int` - Params: `companyId: int`
- Returns: `200 { categories: { id: int, parentId: int|null, sort: int, name: str, slug: str, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }` - Returns: `200 { categories: { id: int, parentId: int|null, sort: int, name: str, slug: str, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }`
@@ -754,7 +747,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { quotes: T8[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { quotes: T8[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create quote — `POST /quote` #### Create quote — `POST /quote`
- Params: `body: { number: str, date: str, lines: T35[], entityId?: int|null, externalNumber?: str|null }` - Params: `body: { number: str, date: str, lines: T33[], entityId?: int|null, externalNumber?: str|null }`
- Returns: `201 T7` - Returns: `201 T7`
#### Delete quote — `DELETE /quote/{id}` #### Delete quote — `DELETE /quote/{id}`
- Params: `id: int` - Params: `id: int`
@@ -763,7 +756,7 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 T7` - Returns: `200 T7`
#### Update quote — `PATCH /quote/{id}` #### Update quote — `PATCH /quote/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T35[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T33[] }`
- Returns: `200 T7` - Returns: `200 T7`
#### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}` #### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
@@ -772,7 +765,7 @@ _none_
- Params: `id: int, fileId: int, query: download?: bool` - Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str` - Returns: `200 str`
#### Create order from quote — `POST /quote/{id}/order` #### Create order from quote — `POST /quote/{id}/order`
- Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T35[] }` - Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T33[] }`
- Returns: `201 T1` - Returns: `201 T1`
#### Generate and attach quote PDF — `POST /quote/{id}/pdf` #### Generate and attach quote PDF — `POST /quote/{id}/pdf`
- Params: `id: int` - Params: `id: int`
@@ -818,10 +811,10 @@ _none_
- Returns: `201 T14` - Returns: `201 T14`
#### List packages of a shipment — `GET /shipment/{id}/shipping` #### List packages of a shipment — `GET /shipment/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T38[] }` - Returns: `200 { shippings: T36[] }`
#### Create package for a shipment — `POST /shipment/{id}/shipping` #### Create package for a shipment — `POST /shipment/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, trackingCode?: str, carrier?: str, warehouseId?: int }` - Params: `id: int, body: { type: str, state: str, trackingCode?: str, carrier?: str, warehouseId?: int }`
- Returns: `201 T33` - Returns: `201 T31`
### Section: /shipping ### Section: /shipping
@@ -830,7 +823,7 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Update shipping — `PATCH /shipping/{id}` #### Update shipping — `PATCH /shipping/{id}`
- Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrier?: str, warehouseId?: int }` - Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrier?: str, warehouseId?: int }`
- Returns: `200 T33` - Returns: `200 T31`
#### List packages of a shipping — `GET /shipping/{id}/package` #### List packages of a shipping — `GET /shipping/{id}/package`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }` - Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }`
@@ -846,30 +839,36 @@ _none_
### Section: /stock ### Section: /stock
#### Accept delivery manifest item(s) — `POST /stock/accept`
- Params: `body: { manifestarticleId?: int, warehouseId?: int, items?: { manifestarticleId: int, warehouseId?: int }[] }`
- Returns: `200`
#### Adjust stock manually — `POST /stock/adjust` #### Adjust stock manually — `POST /stock/adjust`
- Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }` - Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }`
- 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: T22[] }` - 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: T22[] }`
#### Ship shipment manifest item(s) — `POST /stock/ship`
- Params: `body: { manifestarticleId?: int, warehouseId?: int, items?: { manifestarticleId: int, warehouseId?: int }[] }`
- Returns: `200`
### Section: /supplier ### Section: /supplier
#### List suppliers — `GET /supplier` #### List suppliers — `GET /supplier`
_none_ _none_
- Returns: `200 { suppliers: T51[] }` - Returns: `200 { suppliers: T49[] }`
#### Create supplier — `POST /supplier` #### Create supplier — `POST /supplier`
- Params: `body: { name: str }` - Params: `body: { name: str }`
- Returns: `201 { supplier: T51 }` - Returns: `201 { supplier: T49 }`
#### Delete supplier — `DELETE /supplier/{id}` #### Delete supplier — `DELETE /supplier/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update supplier — `PATCH /supplier/{id}` #### Update supplier — `PATCH /supplier/{id}`
- Params: `id: int, body: { name: str }` - Params: `id: int, body: { name: str }`
- Returns: `200 { supplier: T51 }` - Returns: `200 { supplier: T49 }`
#### List supplier articles by SKUs — `GET /supplier/{id}/articles` #### List supplier articles by SKUs — `GET /supplier/{id}/articles`
- Params: `id: int, query: skus?: string` - Params: `id: int, query: skus?: str`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T55|null }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T53|null }[] }`
#### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles` #### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles`
- Params: `id: int, body?: { skus?: str[] }` - Params: `id: int, body?: { skus?: str[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T55|null }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T53|null }[] }`
### Section: /thumb ### Section: /thumb
@@ -884,10 +883,10 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### List units — `GET /unit` #### List units — `GET /unit`
_none_ _none_
- Returns: `200 { units: T41[] }` - Returns: `200 { units: T39[] }`
#### Upsert unit — `PUT /unit` #### Upsert unit — `PUT /unit`
- Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }` - Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }`
- Returns: `200 { unit: T41 }` - Returns: `200 { unit: T39 }`
### Section: /v1 ### Section: /v1
@@ -912,15 +911,18 @@ _none_
#### List virtual filesystem files — `GET /vfs` #### List virtual filesystem files — `GET /vfs`
_none_ _none_
- Returns: `200 { files: { name: str, size: int, mime: str, updated_at: str, created_at: str, isText: bool, isImage: bool, isPdf: bool }[] }` - Returns: `200 { files: { name: str, size: int, mime: str, updated_at: str, created_at: str, isText: bool, isImage: bool, isPdf: bool }[] }`
#### Respond to a frontend draft request — `POST /vfs/drafts/response` #### Restore system default applets — `POST /vfs/applets/restore`
- Params: `body: { requestId: str, content?: any, error?: str, files?: any[] }` _none_
- Returns: `200 { success: bool }` - Returns: `200 { success: bool, restored: str[], count: int }`
#### Create directory in virtual filesystem — `POST /vfs/mkdir` #### Create directory in virtual filesystem — `POST /vfs/mkdir`
- Params: `body: { path: str }` - Params: `body: { path: str }`
- Returns: `200 { success: bool, dir: str }` - Returns: `200 { success: bool, dir: str }`
#### Rename or move a virtual filesystem file or directory — `POST /vfs/rename` #### Rename or move a virtual filesystem file or directory — `POST /vfs/rename`
- Params: `body: { oldPath: str, newPath: str }` - Params: `body: { oldPath: str, newPath: str }`
- Returns: `200 { success: bool, oldPath: str, newPath: str }` - Returns: `200 { success: bool, oldPath: str, newPath: str }`
#### Restore system default skills — `POST /vfs/skills/restore`
_none_
- Returns: `200 { success: bool, restored: str[], count: int }`
#### Upload files to virtual filesystem — `POST /vfs/upload` #### Upload files to virtual filesystem — `POST /vfs/upload`
- Params: `form: { files: str[], targetDir?: str }` - Params: `form: { files: str[], targetDir?: str }`
- Returns: `200 { success: bool, uploaded: { name: str, size: int, mime: str }[] }` - Returns: `200 { success: bool, uploaded: { name: str, size: int, mime: str }[] }`
@@ -941,10 +943,10 @@ _none_
#### List warehouses — `GET /warehouse` #### List warehouses — `GET /warehouse`
_none_ _none_
- Returns: `200 { warehouses: T56[] }` - Returns: `200 { warehouses: T54[] }`
#### Create warehouse — `POST /warehouse` #### Create warehouse — `POST /warehouse`
- Params: `body: { name: str, sort?: int }` - Params: `body: { name: str, sort?: int }`
- Returns: `201 T54` - Returns: `201 T52`
#### Reorder warehouses — `PUT /warehouse/order` #### Reorder warehouses — `PUT /warehouse/order`
- Params: `body: { warehouseIds: int[] }` - Params: `body: { warehouseIds: int[] }`
- Returns: `200 { warehouseIds: int[] }` - Returns: `200 { warehouseIds: int[] }`
@@ -953,10 +955,10 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Get warehouse by ID — `GET /warehouse/{id}` #### Get warehouse by ID — `GET /warehouse/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 T54` - Returns: `200 T52`
#### Update warehouse (partial) — `PATCH /warehouse/{id}` #### Update warehouse (partial) — `PATCH /warehouse/{id}`
- Params: `id: int, body: { name?: str, sort?: int }` - Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 T54` - Returns: `200 T52`
#### Update warehouse — `PUT /warehouse/{id}` #### Update warehouse — `PUT /warehouse/{id}`
- Params: `id: int, body: { name?: str, sort?: int }` - Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 T54` - Returns: `200 T52`

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview # Dash API Routes Overview
Overview of all 174 API endpoints across all modules. Overview of all 175 API endpoints across all modules.
### Notation ### Notation
@@ -10,52 +10,50 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types ### Reusable types
- `T1` = `{ order: T2 }` - `T1` = `{ order: T2 }`
- `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T24[], externalNumber?: str|null, createdAt?: str, shipping?: T32|null, shippings?: T32[], payment?: T38|null, payments?: T38[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T6[], files?: T14[] }` - `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T23[], externalNumber?: str|null, createdAt?: str, shipping?: T30|null, shippings?: T30[], payment?: T36|null, payments?: T36[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T6[], files?: T14[] }`
- `T3` = `{ article: T4 }` - `T3` = `{ article: T4 }`
- `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T35, brand: T33|null, categoryIds: int[], photos: T30[], eans: str[], prices: T40[], suppliers: T15[] }` - `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T33, brand: T31|null, categoryIds: int[], photos: T28[], eans: str[], prices: T38[], suppliers: T15[] }`
- `T5` = `{ shipment: T6 }` - `T5` = `{ shipment: T6 }`
- `T6` = `{ id: int, number: str, type: str, date: str, lines: T24[], shippings: T32[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T14[], state?: str }` - `T6` = `{ id: int, number: str, type: str, date: str, lines: T23[], shippings: T30[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T14[], state?: str }`
- `T7` = `{ quote: T8 }` - `T7` = `{ quote: T8 }`
- `T8` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T24[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T14[] }` - `T8` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T23[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T14[] }`
- `T9` = `{ delivery: T10 }` - `T9` = `{ delivery: T10 }`
- `T10` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T24[], files: T14[], externalNumber?: str|null, createdAt?: str, ledger?: T44 }` - `T10` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T23[], files: T14[], externalNumber?: str|null, createdAt?: str, ledger?: T42 }`
- `T11` = `{ labeltemplate: T13 }` - `T11` = `{ labeltemplate: T13 }`
- `T12` = `{ attachment: T14, fileUrl: str }` - `T12` = `{ attachment: T14, fileUrl: str }`
- `T13` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T20, thumb?: str|null }` - `T13` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T20, thumb?: str|null }`
- `T14` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }` - `T14` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
- `T15` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T46|null }` - `T15` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T44|null }`
- `T16` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - `T16` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- `T17` = `{ ledger: T18[] }` - `T17` = `{ ledger: T18[] }`
- `T18` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }` - `T18` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
- `T19` = `{ id: int, name: str, sort: int, exclusive: bool, values: T36[], articleCount?: int }` - `T19` = `{ id: int, name: str, sort: int, exclusive: bool, values: T34[], articleCount?: int }`
- `T20` = `{ objects: T25[] }` - `T20` = `{ objects: T24[] }`
- `T21` = `{ objects: T26[] }` - `T21` = `{ printer: T26 }`
- `T22` = `{ printer: T28 }` - `T22` = `{ entity: T29 }`
- `T23` = `{ entity: T31 }` - `T23` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
- `T24` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }` - `T24` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T25` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }` - `T25` = `{ shipping: T30 }`
- `T26` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }` - `T26` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
- `T27` = `{ shipping: T32 }` - `T27` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }`
- `T28` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }` - `T28` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }`
- `T29` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }` - `T29` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T30` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }` - `T30` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
- `T31` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }` - `T31` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
- `T32` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }` - `T32` = `{ article: T37 }`
- `T33` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }` - `T33` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T34` = `{ article: T39 }` - `T34` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
- `T35` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }` - `T35` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
- `T36` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }` - `T36` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
- `T37` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }` - `T37` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
- `T38` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }` - `T38` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
- `T39` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }` - `T39` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T40` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }` - `T40` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T41` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }` - `T41` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
- `T42` = `{ id: int, entityId: int, contactType: str, contact: str }` - `T42` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T43` = `{ id: int, name: str, createdAt: str, updatedAt: str }` - `T43` = `{ warehouse: T45 }`
- `T44` = `{ totalPositions: int, completelyOnLedgerPositions: int }` - `T44` = `{ net: num, taxId: int, taxRate?: num|null }`
- `T45` = `{ warehouse: T47 }` - `T45` = `{ id: int, name: str, sort: int }`
- `T46` = `{ net: num, taxId: int, taxRate?: num|null }`
- `T47` = `{ id: int, name: str, sort: int }`
### Routes ### Routes
@@ -64,7 +62,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
#### Create article in category — `POST /article` #### Create article in category — `POST /article`
- Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }` - Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }`
- Returns: `201 T34` - Returns: `201 T32`
#### Delete article by SKU — `DELETE /article/{sku}` #### Delete article by SKU — `DELETE /article/{sku}`
- Params: `sku: str, query: cascade?: bool` - Params: `sku: str, query: cascade?: bool`
- Returns: `204 void` - Returns: `204 void`
@@ -97,10 +95,10 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `200 { children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }` - Returns: `200 { children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
#### Create and attach a new child article to a parent article — `POST /article/{sku}/children` #### Create and attach a new child article to a parent article — `POST /article/{sku}/children`
- Params: `sku: str, body: { attributeValueId?: int, value?: str, name?: str }` - Params: `sku: str, body: { attributeValueId?: int, value?: str, name?: str }`
- Returns: `201 T34` - Returns: `201 T32`
#### Attach an existing article as a child to a parent article — `PUT /article/{sku}/children` #### 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 }` - Params: `sku: str, body: { sku: str, attributeValueId?: int, value?: str, reparent?: bool }`
- Returns: `200 { article: T39, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }` - Returns: `200 { article: T37, 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}` #### Remove a child article from a parent article — `DELETE /article/{sku}/children/{childSku}`
- Params: `sku: str, childSku: str, query: deleteArticle?: bool` - Params: `sku: str, childSku: str, query: deleteArticle?: bool`
- Returns: `200 { success: bool, deleted: bool }` - Returns: `200 { success: bool, deleted: bool }`
@@ -124,7 +122,7 @@ 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 }[] }` - 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` #### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }` - Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `200 T34` - Returns: `200 T32`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices` #### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices`
- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }` - Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }`
- Returns: `200 T3` - Returns: `200 T3`
@@ -175,10 +173,10 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### List values for an attribute — `GET /attributename/{attributeId}/value` #### List values for an attribute — `GET /attributename/{attributeId}/value`
- Params: `attributeId: int` - Params: `attributeId: int`
- Returns: `200 { values: T36[] }` - Returns: `200 { values: T34[] }`
#### Create value for an attribute — `POST /attributename/{attributeId}/value` #### Create value for an attribute — `POST /attributename/{attributeId}/value`
- Params: `attributeId: int, body: { value: str, sort?: int }` - Params: `attributeId: int, body: { value: str, sort?: int }`
- Returns: `201 { attributevalue: T36 }` - Returns: `201 { attributevalue: T34 }`
#### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order` #### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order`
- Params: `attributeId: int, body: { attributevalueIds: int[] }` - Params: `attributeId: int, body: { attributevalueIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
@@ -196,13 +194,13 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update attribute value — `PATCH /attributevalue/{id}` #### Update attribute value — `PATCH /attributevalue/{id}`
- Params: `id: int, body: { value?: str, sort?: int }` - Params: `id: int, body: { value?: str, sort?: int }`
- Returns: `200 { attributevalue: T36 }` - Returns: `200 { attributevalue: T34 }`
#### Delete attribute value image — `DELETE /attributevalue/{id}/image` #### Delete attribute value image — `DELETE /attributevalue/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T41[] } }` - Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T39[] } }`
#### Add or replace attribute value image — `POST /attributevalue/{id}/image` #### Add or replace attribute value image — `POST /attributevalue/{id}/image`
- Params: `id: int, image/avif` - Params: `id: int, image/avif`
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T41[] } }` - Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T39[] } }`
### Section: /brand ### Section: /brand
@@ -211,7 +209,7 @@ _none_
- Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }` - Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }`
#### Create brand — `POST /brand` #### Create brand — `POST /brand`
- Params: `body: { name: str, gsprpinfo?: str }` - Params: `body: { name: str, gsprpinfo?: str }`
- Returns: `201 { brand: { id: int, name: str, pics: T41[], gsprpinfo?: str|null } }` - Returns: `201 { brand: { id: int, name: str, pics: T39[], gsprpinfo?: str|null } }`
#### Delete brand — `DELETE /brand/{id}` #### Delete brand — `DELETE /brand/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
@@ -220,10 +218,10 @@ _none_
- Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }` - Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }`
#### Delete brand image — `DELETE /brand/{id}/image` #### Delete brand image — `DELETE /brand/{id}/image`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T41[] } }` - Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T39[] } }`
#### Add or replace brand image — `POST /brand/{id}/image` #### Add or replace brand image — `POST /brand/{id}/image`
- Params: `id: int, image/avif` - Params: `id: int, image/avif`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T41[] } }` - Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T39[] } }`
### Section: /category ### Section: /category
@@ -244,7 +242,7 @@ _none_
- Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }` - Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }`
#### Articles in category — `GET /category/{id}/articles` #### Articles in category — `GET /category/{id}/articles`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T33|null }[] }` - Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T31|null }[] }`
#### List category attribute values — `GET /category/{id}/attributevalue` #### List category attribute values — `GET /category/{id}/attributevalue`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }` - Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }`
@@ -298,7 +296,7 @@ _none_
- Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }` - Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }`
#### Create customer group — `POST /customergroup` #### Create customer group — `POST /customergroup`
- Params: `body: { name: str }` - Params: `body: { name: str }`
- Returns: `201 { customergroup: T47 }` - Returns: `201 { customergroup: T45 }`
#### Reorder customer groups — `PUT /customergroup/order` #### Reorder customer groups — `PUT /customergroup/order`
- Params: `body: { customergroupIds: int[] }` - Params: `body: { customergroupIds: int[] }`
- Returns: `200 { customergroupIds: int[] }` - Returns: `200 { customergroupIds: int[] }`
@@ -307,15 +305,15 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Update customer group — `PATCH /customergroup/{id}` #### Update customer group — `PATCH /customergroup/{id}`
- Params: `id: int, body: { name: str }` - Params: `id: int, body: { name: str }`
- Returns: `200 { customergroup: T47 }` - Returns: `200 { customergroup: T45 }`
### Section: /delivery ### Section: /delivery
#### List deliveries — `GET /delivery` #### List deliveries — `GET /delivery`
_none_ - Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T44 }[] }` - Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T42 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create delivery — `POST /delivery` #### Create delivery — `POST /delivery`
- Params: `body: { externalNumber: str, date: str, lines: T29[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Params: `body: { externalNumber: str, date: str, lines: T27[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 T9` - Returns: `201 T9`
#### Delete delivery — `DELETE /delivery/{id}` #### Delete delivery — `DELETE /delivery/{id}`
- Params: `id: int` - Params: `id: int`
@@ -324,7 +322,7 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 T9` - Returns: `200 T9`
#### Update delivery — `PATCH /delivery/{id}` #### Update delivery — `PATCH /delivery/{id}`
- Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T29[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T27[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `200 T9` - Returns: `200 T9`
#### Delete delivery file attachment — `DELETE /delivery/{id}/file/{fileId}` #### Delete delivery file attachment — `DELETE /delivery/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
@@ -337,10 +335,10 @@ _none_
#### List entities — `GET /entity` #### List entities — `GET /entity`
- Params: `query: customergroupId?: int, role?: str` - Params: `query: customergroupId?: int, role?: str`
- Returns: `200 { entities: T31[] }` - Returns: `200 { entities: T29[] }`
#### Create entity — `POST /entity` #### Create entity — `POST /entity`
- Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }` - Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }`
- Returns: `201 T23` - Returns: `201 T22`
#### Peek next customer number — `GET /entity/next-number` #### Peek next customer number — `GET /entity/next-number`
_none_ _none_
- Returns: `200 { number: str }` - Returns: `200 { number: str }`
@@ -358,25 +356,25 @@ _none_
- Returns: `200 { address: T16 }` - Returns: `200 { address: T16 }`
#### List entity contacts — `GET /entity/{entityId}/contact` #### List entity contacts — `GET /entity/{entityId}/contact`
- Params: `entityId: int` - Params: `entityId: int`
- Returns: `200 { contacts: T42[] }` - Returns: `200 { contacts: T40[] }`
#### Create entity contact — `POST /entity/{entityId}/contact` #### Create entity contact — `POST /entity/{entityId}/contact`
- Params: `entityId: int, body: { contactType: str, contact: str }` - Params: `entityId: int, body: { contactType: str, contact: str }`
- Returns: `201 { contact: T42 }` - Returns: `201 { contact: T40 }`
#### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}` #### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int` - Params: `entityId: int, id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update entity contact — `PATCH /entity/{entityId}/contact/{id}` #### Update entity contact — `PATCH /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }` - Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }`
- Returns: `200 { contact: T42 }` - Returns: `200 { contact: T40 }`
#### Delete entity — `DELETE /entity/{id}` #### Delete entity — `DELETE /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Get entity — `GET /entity/{id}` #### Get entity — `GET /entity/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 T23` - Returns: `200 T22`
#### Update entity — `PATCH /entity/{id}` #### Update entity — `PATCH /entity/{id}`
- Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }` - Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }`
- Returns: `200 T23` - Returns: `200 T22`
### Section: /labeltemplate ### Section: /labeltemplate
@@ -384,7 +382,7 @@ _none_
_none_ _none_
- Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }` - Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }`
#### Create label template — `POST /labeltemplate` #### Create label template — `POST /labeltemplate`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: T21 }` - Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: T20 }`
- Returns: `201 T11` - Returns: `201 T11`
#### Delete label template — `DELETE /labeltemplate/{id}` #### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int` - Params: `id: int`
@@ -393,10 +391,10 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 T11` - Returns: `200 T11`
#### Update label template (partial) — `PATCH /labeltemplate/{id}` #### Update label template (partial) — `PATCH /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T21 }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T20 }`
- Returns: `200 T11` - Returns: `200 T11`
#### Update label template — `PUT /labeltemplate/{id}` #### Update label template — `PUT /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T21 }` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T20 }`
- Returns: `200 T11` - Returns: `200 T11`
### Section: /ledger ### Section: /ledger
@@ -404,9 +402,6 @@ _none_
#### Query ledger by manifest article ID or SKU — `GET /ledger` #### Query ledger by manifest article ID or SKU — `GET /ledger`
- Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str` - Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str`
- Returns: `200 T17` - Returns: `200 T17`
#### Add ledger entry — `POST /ledger`
- Params: `body: { manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str }`
- Returns: `201 { ledger: T18 }`
#### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}` #### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}`
- Params: `manifestarticleId: int, query: transaction_type?: str` - Params: `manifestarticleId: int, query: transaction_type?: str`
- Returns: `200 T17` - Returns: `200 T17`
@@ -429,7 +424,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
- Returns: `200 { orders: T2[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { orders: T2[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create order — `POST /order` #### Create order — `POST /order`
- Params: `body: { number: str, date: str, lines: T29[], entityId?: int|null, externalNumber?: str|null }` - Params: `body: { number: str, date: str, lines: T27[], entityId?: int|null, externalNumber?: str|null }`
- Returns: `201 T1` - Returns: `201 T1`
#### Delete order — `DELETE /order/{id}` #### Delete order — `DELETE /order/{id}`
- Params: `id: int` - Params: `id: int`
@@ -438,17 +433,17 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 T1` - Returns: `200 T1`
#### Update order — `PATCH /order/{id}` #### Update order — `PATCH /order/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T29[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T27[] }`
- Returns: `200 T1` - Returns: `200 T1`
#### Delete order file attachment — `DELETE /order/{id}/file/{fileId}` #### Delete order file attachment — `DELETE /order/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
- Returns: `204 void` - Returns: `204 void`
#### List payments of an order — `GET /order/{id}/payment` #### List payments of an order — `GET /order/{id}/payment`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { payments: T38[] }` - Returns: `200 { payments: T36[] }`
#### Add payment to an order — `POST /order/{id}/payment` #### Add payment to an order — `POST /order/{id}/payment`
- Params: `id: int, body: { accountId: int, type: str, amount: num }` - Params: `id: int, body: { accountId: int, type: str, amount: num }`
- Returns: `201 { payment: T38 }` - Returns: `201 { payment: T36 }`
#### Generate and attach order PDF — `POST /order/{id}/pdf` #### Generate and attach order PDF — `POST /order/{id}/pdf`
- Params: `id: int` - Params: `id: int`
- Returns: `201 T12` - Returns: `201 T12`
@@ -460,10 +455,10 @@ _none_
- Returns: `201 T5` - Returns: `201 T5`
#### List shippings of an order — `GET /order/{id}/shipping` #### List shippings of an order — `GET /order/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T32[] }` - Returns: `200 { shippings: T30[] }`
#### Create shipping for an order — `POST /order/{id}/shipping` #### Create shipping for an order — `POST /order/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, warehouseId?: int }` - Params: `id: int, body: { type: str, state: str, warehouseId?: int }`
- Returns: `201 T27` - Returns: `201 T25`
### Section: /package ### Section: /package
@@ -481,10 +476,10 @@ _none_
#### List printers — `GET /printer` #### List printers — `GET /printer`
_none_ _none_
- Returns: `200 { printers: T28[] }` - Returns: `200 { printers: T26[] }`
#### Create printer — `POST /printer` #### Create printer — `POST /printer`
- Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `201 T22` - Returns: `201 T21`
#### Reorder printers — `PUT /printer/order` #### Reorder printers — `PUT /printer/order`
- Params: `body: { printerIds: int[] }` - Params: `body: { printerIds: int[] }`
- Returns: `200 { printerIds: int[] }` - Returns: `200 { printerIds: int[] }`
@@ -493,22 +488,22 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Get printer by ID — `GET /printer/{id}` #### Get printer by ID — `GET /printer/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 T22` - Returns: `200 T21`
#### Update printer (partial) — `PATCH /printer/{id}` #### Update printer (partial) — `PATCH /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 T22` - Returns: `200 T21`
#### Update printer — `PUT /printer/{id}` #### Update printer — `PUT /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }` - Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 T22` - Returns: `200 T21`
### Section: /propertyname ### Section: /propertyname
#### List property names — `GET /propertyname` #### List property names — `GET /propertyname`
_none_ _none_
- Returns: `200 { propertynames: T37[] }` - Returns: `200 { propertynames: T35[] }`
#### Create property name — `POST /propertyname` #### Create property name — `POST /propertyname`
- Params: `body: { name: str, sort?: int, exclusive?: bool }` - Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { propertyname: T37 }` - Returns: `201 { propertyname: T35 }`
#### Reorder property names — `PUT /propertyname/order` #### Reorder property names — `PUT /propertyname/order`
- Params: `body: { propertynameIds: int[] }` - Params: `body: { propertynameIds: int[] }`
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
@@ -517,7 +512,7 @@ _none_
- Returns: `200 { ok: bool }` - Returns: `200 { ok: bool }`
#### Update property name — `PATCH /propertyname/{id}` #### Update property name — `PATCH /propertyname/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }` - Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { propertyname: T37 }` - Returns: `200 { propertyname: T35 }`
### Section: /quote ### Section: /quote
@@ -525,7 +520,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { quotes: T8[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { quotes: T8[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create quote — `POST /quote` #### Create quote — `POST /quote`
- Params: `body: { number: str, date: str, lines: T29[], entityId?: int|null, externalNumber?: str|null }` - Params: `body: { number: str, date: str, lines: T27[], entityId?: int|null, externalNumber?: str|null }`
- Returns: `201 T7` - Returns: `201 T7`
#### Delete quote — `DELETE /quote/{id}` #### Delete quote — `DELETE /quote/{id}`
- Params: `id: int` - Params: `id: int`
@@ -534,13 +529,13 @@ _none_
- Params: `id: int` - Params: `id: int`
- Returns: `200 T7` - Returns: `200 T7`
#### Update quote — `PATCH /quote/{id}` #### Update quote — `PATCH /quote/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T29[] }` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, externalNumber?: str|null, lines?: T27[] }`
- Returns: `200 T7` - Returns: `200 T7`
#### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}` #### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}`
- Params: `id: int, fileId: int` - Params: `id: int, fileId: int`
- Returns: `204 void` - Returns: `204 void`
#### Create order from quote — `POST /quote/{id}/order` #### Create order from quote — `POST /quote/{id}/order`
- Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T29[] }` - Params: `id: int, body?: { number?: str, date?: str, createdAt?: str, entityId?: int|null, externalNumber?: str|null, lines?: T27[] }`
- Returns: `201 T1` - Returns: `201 T1`
#### Generate and attach quote PDF — `POST /quote/{id}/pdf` #### Generate and attach quote PDF — `POST /quote/{id}/pdf`
- Params: `id: int` - Params: `id: int`
@@ -568,10 +563,10 @@ _none_
- Returns: `201 T12` - Returns: `201 T12`
#### List packages of a shipment — `GET /shipment/{id}/shipping` #### List packages of a shipment — `GET /shipment/{id}/shipping`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { shippings: T32[] }` - Returns: `200 { shippings: T30[] }`
#### Create package for a shipment — `POST /shipment/{id}/shipping` #### Create package for a shipment — `POST /shipment/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, trackingCode?: str, carrier?: str, warehouseId?: int }` - Params: `id: int, body: { type: str, state: str, trackingCode?: str, carrier?: str, warehouseId?: int }`
- Returns: `201 T27` - Returns: `201 T25`
### Section: /shipping ### Section: /shipping
@@ -580,7 +575,7 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Update shipping — `PATCH /shipping/{id}` #### Update shipping — `PATCH /shipping/{id}`
- Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrier?: str, warehouseId?: int }` - Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrier?: str, warehouseId?: int }`
- Returns: `200 T27` - Returns: `200 T25`
#### List packages of a shipping — `GET /shipping/{id}/package` #### List packages of a shipping — `GET /shipping/{id}/package`
- Params: `id: int` - Params: `id: int`
- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }` - Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }`
@@ -596,30 +591,36 @@ _none_
### Section: /stock ### Section: /stock
#### Accept delivery manifest item(s) — `POST /stock/accept`
- Params: `body: { manifestarticleId?: int, warehouseId?: int, items?: { manifestarticleId: int, warehouseId?: int }[] }`
- Returns: `200`
#### Adjust stock manually — `POST /stock/adjust` #### Adjust stock manually — `POST /stock/adjust`
- Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }` - Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }`
- 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: T18[] }` - 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: T18[] }`
#### Ship shipment manifest item(s) — `POST /stock/ship`
- Params: `body: { manifestarticleId?: int, warehouseId?: int, items?: { manifestarticleId: int, warehouseId?: int }[] }`
- Returns: `200`
### Section: /supplier ### Section: /supplier
#### List suppliers — `GET /supplier` #### List suppliers — `GET /supplier`
_none_ _none_
- Returns: `200 { suppliers: T43[] }` - Returns: `200 { suppliers: T41[] }`
#### Create supplier — `POST /supplier` #### Create supplier — `POST /supplier`
- Params: `body: { name: str }` - Params: `body: { name: str }`
- Returns: `201 { supplier: T43 }` - Returns: `201 { supplier: T41 }`
#### Delete supplier — `DELETE /supplier/{id}` #### Delete supplier — `DELETE /supplier/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `204 void` - Returns: `204 void`
#### Update supplier — `PATCH /supplier/{id}` #### Update supplier — `PATCH /supplier/{id}`
- Params: `id: int, body: { name: str }` - Params: `id: int, body: { name: str }`
- Returns: `200 { supplier: T43 }` - Returns: `200 { supplier: T41 }`
#### List supplier articles by SKUs — `GET /supplier/{id}/articles` #### List supplier articles by SKUs — `GET /supplier/{id}/articles`
- Params: `id: int, query: skus?: string` - Params: `id: int, query: skus?: str`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T46|null }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T44|null }[] }`
#### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles` #### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles`
- Params: `id: int, body?: { skus?: str[] }` - Params: `id: int, body?: { skus?: str[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T46|null }[] }` - Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T44|null }[] }`
### Section: /unit ### Section: /unit
@@ -628,19 +629,19 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### List units — `GET /unit` #### List units — `GET /unit`
_none_ _none_
- Returns: `200 { units: T35[] }` - Returns: `200 { units: T33[] }`
#### Upsert unit — `PUT /unit` #### Upsert unit — `PUT /unit`
- Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }` - Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }`
- Returns: `200 { unit: T35 }` - Returns: `200 { unit: T33 }`
### Section: /warehouse ### Section: /warehouse
#### List warehouses — `GET /warehouse` #### List warehouses — `GET /warehouse`
_none_ _none_
- Returns: `200 { warehouses: T47[] }` - Returns: `200 { warehouses: T45[] }`
#### Create warehouse — `POST /warehouse` #### Create warehouse — `POST /warehouse`
- Params: `body: { name: str, sort?: int }` - Params: `body: { name: str, sort?: int }`
- Returns: `201 T45` - Returns: `201 T43`
#### Reorder warehouses — `PUT /warehouse/order` #### Reorder warehouses — `PUT /warehouse/order`
- Params: `body: { warehouseIds: int[] }` - Params: `body: { warehouseIds: int[] }`
- Returns: `200 { warehouseIds: int[] }` - Returns: `200 { warehouseIds: int[] }`
@@ -649,13 +650,13 @@ _none_
- Returns: `204 void` - Returns: `204 void`
#### Get warehouse by ID — `GET /warehouse/{id}` #### Get warehouse by ID — `GET /warehouse/{id}`
- Params: `id: int` - Params: `id: int`
- Returns: `200 T45` - Returns: `200 T43`
#### Update warehouse (partial) — `PATCH /warehouse/{id}` #### Update warehouse (partial) — `PATCH /warehouse/{id}`
- Params: `id: int, body: { name?: str, sort?: int }` - Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 T45` - Returns: `200 T43`
#### Update warehouse — `PUT /warehouse/{id}` #### Update warehouse — `PUT /warehouse/{id}`
- Params: `id: int, body: { name?: str, sort?: int }` - Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 T45` - Returns: `200 T43`
### Binary & streaming routes (reachable only via `fetch()` in javascript_execute) ### 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: 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:

View File

@@ -5,12 +5,23 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
List deliveries List deliveries
Lists goods receipts (`manifest` rows with `type = 'delivery'`) Lists goods receipts (`manifest` rows with `type = 'delivery'`)
for the authenticated user's selected company. for the authenticated user's selected company. Supports pagination and filtering.
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `offset` | no | integer | Number of records to skip |
| `limit` | no | integer | Number of records to return (1-1000) |
| `search` | no | string | Substring match for delivery number, external number, or supplier name |
| `onlyUnbooked` | no | boolean | When true, returns only deliveries with unbooked stock items |
| `supplierId` | no | integer | Filter by supplier entity id |
| `seekMonths` | no | integer | Negative integer cutoff months ago (e.g. -1) |
## Response `200` ## Response `200`
Delivery list Delivery list (windowed when pagination/filters are used)
```json ```json
{ {
@@ -28,7 +39,11 @@ Delivery list
"completelyOnLedgerPositions": 1 "completelyOnLedgerPositions": 1
} }
} }
] ],
"total": 1,
"unfilteredTotal": 1,
"offset": 1,
"limit": 1
} }
``` ```

View File

@@ -44,8 +44,6 @@ richer payload.
| `modelconversation` | `{}` | `POST /modelconversation`, `DELETE /modelconversation/{id}` | | `modelconversation` | `{}` | `POST /modelconversation`, `DELETE /modelconversation/{id}` |
| `modelconversation-<id>` | `{}` | Conversation-specific updates on the same model conversation thread; the concrete ID is used in the event name | | `modelconversation-<id>` | `{}` | Conversation-specific updates on the same model conversation thread; the concrete ID is used in the event name |
| `brand` | `{ sender, brandId }` | `POST /brand`, `PATCH /brand/{id}`, `DELETE /brand/{id}`, `POST /brand/{id}/image`, `DELETE /brand/{id}/image` | | `brand` | `{ sender, brandId }` | `POST /brand`, `PATCH /brand/{id}`, `DELETE /brand/{id}`, `POST /brand/{id}/image`, `DELETE /brand/{id}/image` |
| `draft` | `{ sender, action, name, content }` | Draft update on `drafts/quote`, `drafts/order`, `drafts/delivery` (`sender` echoes the caller's `x-client-id`) |
| `draft:request` | `{ requestId, action, name, target, sender }` | Server request to frontend to read a draft or list drafts |
The `brand` payload is `{ sender, brandId }`: `sender` echoes the The `brand` payload is `{ sender, brandId }`: `sender` echoes the
caller's `x-client-id` header (empty string if absent) so the sender caller's `x-client-id` header (empty string if absent) so the sender

View File

@@ -1,63 +0,0 @@
# `POST /ledger`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Add ledger entry
Appends a new entry to the append-only ledger table.
`hash_version` is fixed to 1. `prev_hash` is cryptographically chained:
for the genesis row it defaults to `TODO_TSE_BASED_INITHASH`, and for subsequent rows
it is the SHA-512 hex hash of the canonical JSON representation of the previous highest-id entry.
Requires authentication and membership in the company owning the referenced manifest article.
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `manifestarticle_id` | yes | integer | References manifestarticle.id |
| `transaction_type` | yes | string | Transaction identifier / type (max 100 chars) |
| `amount` | yes | number | Movement amount |
| `transaction_text` | yes | string | Transaction memo / details |
```json
{
"manifestarticle_id": 1,
"transaction_type": "…",
"amount": 1,
"transaction_text": "…"
}
```
## Response `201`
Ledger entry added successfully
```json
{
"ledger": {
"id": 1,
"manifestarticle_id": 1,
"transaction_type": "…",
"amount": 1,
"transaction_text": "…",
"hash_version": 1,
"prev_hash": "…",
"sku": "…",
"created_at": "2026-08-10T00:00:00.000Z"
}
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid ledger entry or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Manifest article or company not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -21,7 +21,7 @@ List of conversations
"created_at": "…", "created_at": "…",
"updated_at": "…", "updated_at": "…",
"state": "idle", "state": "idle",
"started_by": "…", "started_by": 1,
"first_message": "…" "first_message": "…"
} }
] ]

View File

@@ -38,7 +38,7 @@ Created conversation
"created_at": "…", "created_at": "…",
"updated_at": "…", "updated_at": "…",
"state": "idle", "state": "idle",
"started_by": "…", "started_by": 1,
"first_message": "…" "first_message": "…"
} }
} }

View File

@@ -26,7 +26,7 @@ Conversation details
"created_at": "…", "created_at": "…",
"updated_at": "…", "updated_at": "…",
"state": "idle", "state": "idle",
"started_by": "…", "started_by": 1,
"first_message": "…" "first_message": "…"
} }
} }

View File

@@ -28,17 +28,53 @@ Article detail
"unitamount": 1, "unitamount": 1,
"weight": 1, "weight": 1,
"trackStock": true, "trackStock": true,
"unit": {}, "unit": {
"brand": {}, "si": "…",
"price": {}, "basesi": "…",
"basesifactor": 1,
"symbol": "…",
"name": "…"
},
"brand": {
"id": 1,
"name": "…",
"fullHash": "…",
"thumbW": 1,
"thumbH": 1,
"thumbHash": "…"
},
"price": {
"minmultiplier": 1,
"net": 1,
"taxRate": 1,
"gross": 1
},
"prices": [ "prices": [
{} {
"minmultiplier": 1,
"net": 1,
"taxRate": 1,
"gross": 1
}
], ],
"categories": [ "categories": [
{} {
"id": 1,
"name": "…",
"slug": "…"
}
], ],
"photos": [ "photos": [
{} {
"sort": 1,
"fullHash": "…",
"fullW": 1,
"fullH": 1,
"fullSize": 1,
"thumbW": 1,
"thumbH": 1,
"thumbHash": "…"
}
], ],
"eans": [ "eans": [
"…" "…"
@@ -46,7 +82,18 @@ Article detail
"isParent": true, "isParent": true,
"parentSku": "…", "parentSku": "…",
"variants": [ "variants": [
{} {
"id": 1,
"sku": "…",
"sort": 1,
"name": "…",
"slug": "…",
"attributeValueId": 1,
"attributeValue": "…",
"unitamount": 1,
"weight": 1,
"trackStock": true
}
] ]
} }
} }

View File

@@ -0,0 +1,54 @@
# `POST /stock/accept`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Accept delivery manifest item(s)
Books acceptance for a delivery manifest item on the ledger (warehouse_id = null).
If the connected article has stock tracking enabled, an adjustment entry is also
booked to adjust physical stock in the target warehouse.
If line amount is negative, books as a delivery return (outgoing).
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `manifestarticleId` | no | integer | Manifest article ID of the delivery line |
| `warehouseId` | no | integer | Optional warehouse ID for inventory intake |
| `items` | no | object[] | |
| `items[].manifestarticleId` | yes | integer | |
| `items[].warehouseId` | no | integer | |
```json
{
"manifestarticleId": 1,
"warehouseId": 1,
"items": [
{
"manifestarticleId": 1,
"warehouseId": 1
}
]
}
```
## Signals
On success, emits SSE signals: `stock`, `ledger`, `articles`, `delivery` (see [`GET /events`](../events.md)).
## Response `200`
Delivery item(s) accepted successfully
## Errors
| Status | Description |
|--------|-------------|
| 400 | Validation error or no company selected |
| 404 | Manifest article or warehouse not found |
| 409 | Delivery line already accepted on ledger |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,54 @@
# `POST /stock/ship`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Ship shipment manifest item(s)
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
booked to deduct physical stock from the target warehouse.
If line amount is negative, books as a customer retoure (incoming).
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `manifestarticleId` | no | integer | Manifest article ID of the shipment line |
| `warehouseId` | no | integer | Optional warehouse ID for inventory deduction |
| `items` | no | object[] | |
| `items[].manifestarticleId` | yes | integer | |
| `items[].warehouseId` | no | integer | |
```json
{
"manifestarticleId": 1,
"warehouseId": 1,
"items": [
{
"manifestarticleId": 1,
"warehouseId": 1
}
]
}
```
## Signals
On success, emits SSE signals: `stock`, `ledger`, `articles`, `shipping` (see [`GET /events`](../events.md)).
## Response `200`
Shipment item(s) shipped successfully
## Errors
| Status | Description |
|--------|-------------|
| 400 | Validation error or no company selected |
| 404 | Manifest article or warehouse not found |
| 409 | Shipment line already shipped on ledger |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -19,7 +19,7 @@ Includes linked article details (name, slug, photos, eans).
| Param | Required | Type | Description | | Param | Required | Type | Description |
|-------|----------|------|-------------| |-------|----------|------|-------------|
| `skus` | no | | Optional SKU or comma-separated SKUs to filter by | | `skus` | no | string | Optional SKU or comma-separated SKUs to filter by |
## Response `200` ## Response `200`

View File

@@ -0,0 +1,40 @@
# `POST /vfs/applets/restore`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Restore system default applets
Restores all shipped system default example applets (.applet.js) into the active virtual
filesystem (VFS) for the authenticated user's selected company, overwriting existing system applets with their upstream definitions.
## Signals
On success, emits SSE signal: `vfs` (see [`GET /events`](../events.md)).
## Response `200`
System applets restored successfully
```json
{
"success": true,
"restored": [
"…"
],
"count": 1
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 500 | Restore failed — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -1,48 +0,0 @@
# `POST /vfs/drafts/response`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Respond to a frontend draft request
Used by the frontend to answer a server-initiated `draft:request` SSE event
when reading or listing virtual drafts (`drafts/quote`, `drafts/order`, `drafts/delivery`).
## Body (JSON)
| Field | Required | Type | Description |
|-------|----------|------|-------------|
| `requestId` | yes | string | Unique request ID from the draft:request SSE event |
| `content` | no | | File content when action was read |
| `error` | no | string | Error message if draft retrieval failed on frontend |
| `files` | no | any[] | File listing when action was list |
```json
{
"requestId": "…",
"content": "…",
"error": "…",
"files": []
}
```
## Response `200`
Response received and resolved
```json
{
"success": true
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Missing requestId or no company selected — `{"error":"…"}` |
| 404 | Request ID not found or already timed out — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,40 @@
# `POST /vfs/skills/restore`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Restore system default skills
Restores all shipped system default SKILL files into the active virtual filesystem (VFS)
for the authenticated user's selected company, overwriting existing system skills with their upstream definitions.
## Signals
On success, emits SSE signal: `vfs` (see [`GET /events`](../events.md)).
## Response `200`
System skills restored successfully
```json
{
"success": true,
"restored": [
"…"
],
"count": 1
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 500 | Restore failed — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -560,6 +560,16 @@ function summarizeSchema(
if (!resolved) return schemaRefName(schema) || ''; if (!resolved) return schemaRefName(schema) || '';
if (resolved.$ref) return schemaRefName(resolved) || ''; if (resolved.$ref) return schemaRefName(resolved) || '';
const union = resolved.oneOf || resolved.anyOf;
if (Array.isArray(union) && union.length) {
const parts = union
.map((s) => summarizeSchema(s, components, depth + 1, markOptional, aliases))
.filter(Boolean);
if (parts.length) {
return [...new Set(parts)].join('|');
}
}
if (resolved.type === 'array') { if (resolved.type === 'array') {
const item = summarizeSchema(resolved.items, components, depth + 1, markOptional, aliases) || 'any'; const item = summarizeSchema(resolved.items, components, depth + 1, markOptional, aliases) || 'any';
return `${item}[]`; return `${item}[]`;
@@ -567,7 +577,7 @@ function summarizeSchema(
if (resolved.type === 'object' || resolved.properties) { if (resolved.type === 'object' || resolved.properties) {
const shape = aliases && summarizeSchema(resolved, components, 0, markOptional); const shape = aliases && summarizeSchema(resolved, components, 0, markOptional);
const alias = shape && aliases.get(`${markOptional ? 1 : 0}\0${shape}`); const alias = shape && aliases.get(shape);
if (alias && !skipRootAlias) return alias; if (alias && !skipRootAlias) return alias;
const hasExplicitRequired = Array.isArray(resolved.required); const hasExplicitRequired = Array.isArray(resolved.required);
@@ -602,6 +612,22 @@ function summarizeSchema(
} }
if (resolved.type) { if (resolved.type) {
if (Array.isArray(resolved.type)) {
const types = resolved.type.map((raw) => {
if (raw === 'null') return 'null';
if (aliases?.compactTypes) {
return {
string: 'str',
integer: 'int',
number: 'num',
boolean: 'bool',
object: '{}',
}[raw] || raw;
}
return raw;
});
return types.join('|');
}
let t = resolved.type; let t = resolved.type;
if (aliases?.compactTypes) { if (aliases?.compactTypes) {
t = { t = {
@@ -633,12 +659,11 @@ function collectSchemaAliases(operations) {
if (resolved.type !== 'object' && !resolved.properties) return; if (resolved.type !== 'object' && !resolved.properties) return;
const shape = summarizeSchema(resolved, spec.components, 0, markOptional); const shape = summarizeSchema(resolved, spec.components, 0, markOptional);
const key = `${markOptional ? 1 : 0}\0${shape}`; const current = shapes.get(shape);
const current = shapes.get(key);
if (current) { if (current) {
current.count += 1; current.count += 1;
} else { } else {
shapes.set(key, { count: 1, shape, schema: resolved, markOptional }); shapes.set(shape, { count: 1, shape, schema: resolved, markOptional });
} }
for (const prop of Object.values(resolved.properties || {})) { for (const prop of Object.values(resolved.properties || {})) {
visit(prop, markOptional); visit(prop, markOptional);
@@ -664,9 +689,9 @@ function collectSchemaAliases(operations) {
.sort((a, b) => b[1].shape.length - a[1].shape.length || a[0].localeCompare(b[0])); .sort((a, b) => b[1].shape.length - a[1].shape.length || a[0].localeCompare(b[0]));
const aliases = new Map(); const aliases = new Map();
const definitions = []; const definitions = [];
for (const [key, item] of candidates) { for (const [, item] of candidates) {
const alias = `T${definitions.length + 1}`; const alias = `T${definitions.length + 1}`;
aliases.set(key, alias); aliases.set(item.shape, alias);
definitions.push({ alias, ...item }); definitions.push({ alias, ...item });
} }
aliases.compactTypes = true; aliases.compactTypes = true;
@@ -680,7 +705,7 @@ function collectSchemaAliases(operations) {
* @returns {string} * @returns {string}
*/ */
function paramType(param, aliases = null) { function paramType(param, aliases = null) {
return summarizeSchema(param?.schema, spec.components, 0, false, aliases) || 'string'; return summarizeSchema(param?.schema, spec.components, 0, false, aliases) || (aliases?.compactTypes ? 'str' : 'string');
} }
function summarizeOperationParams(operation, aliases = null) { function summarizeOperationParams(operation, aliases = null) {

View File

@@ -1176,10 +1176,12 @@
* enum: [idle, busy] * enum: [idle, busy]
* description: Messages can only be posted while `idle` * description: Messages can only be posted while `idle`
* started_by: * started_by:
* type: [integer, 'null'] * type: integer
* nullable: true
* description: Id of the conversation that started this one as a subagent, or null for a top-level conversation. Deleting a conversation also deletes every conversation below it in this chain. * description: Id of the conversation that started this one as a subagent, or null for a top-level conversation. Deleting a conversation also deletes every conversation below it in this chain.
* first_message: * first_message:
* type: [string, 'null'] * type: string
* nullable: true
* description: Plain text of the conversation's first user message (XML stripped, max 50 chars, ellipsis if cut), or null if there is none yet. * description: Plain text of the conversation's first user message (XML stripped, max 50 chars, ellipsis if cut), or null if there is none yet.
* ModelConversationCreateInput: * ModelConversationCreateInput:
* type: object * type: object

View File

@@ -168,10 +168,12 @@ function deliveryErrorStatus(error) {
if (error.message === 'Attachment not found') return 404; if (error.message === 'Attachment not found') return 404;
if (error.message === 'File not found') return 404; if (error.message === 'File not found') return 404;
if (error.message === 'Supplier not found') return 404; if (error.message === 'Supplier not found') return 404;
if (error.message === 'Warehouse not found') return 404;
if (error.message === 'Invalid supplierId') return 400; if (error.message === 'Invalid supplierId') return 400;
if (error.message === 'Article not found') return 404; if (error.message === 'Article not found') return 404;
if (error.message === 'Invalid delivery') return 400; if (error.message === 'Invalid delivery') return 400;
if (error.message === 'Invalid lines') return 400; if (error.message === 'Invalid lines') return 400;
if (error.message === 'Delivery has no lines') return 400;
if (error.message === 'Invalid file') return 400; if (error.message === 'Invalid file') return 400;
if (error.message === 'Invalid JSON in "data" field') return 400; if (error.message === 'Invalid JSON in "data" field') return 400;
if (error.message === 'Only PDF or text files are accepted as attachments') return 400; if (error.message === 'Only PDF or text files are accepted as attachments') return 400;
@@ -204,13 +206,46 @@ export function register(app, basePath = '') {
* summary: List deliveries * summary: List deliveries
* description: | * description: |
* Lists goods receipts (`manifest` rows with `type = 'delivery'`) * Lists goods receipts (`manifest` rows with `type = 'delivery'`)
* for the authenticated user's selected company. * for the authenticated user's selected company. Supports pagination and filtering.
* security: * security:
* - cookieAuth: [] * - cookieAuth: []
* x-markdown: delivery/list.md * x-markdown: delivery/list.md
* parameters:
* - name: offset
* in: query
* schema:
* type: integer
* default: 0
* description: Number of records to skip
* - name: limit
* in: query
* schema:
* type: integer
* default: 50
* description: Number of records to return (1-1000)
* - name: search
* in: query
* schema:
* type: string
* description: Substring match for delivery number, external number, or supplier name
* - name: onlyUnbooked
* in: query
* schema:
* type: boolean
* description: When true, returns only deliveries with unbooked stock items
* - name: supplierId
* in: query
* schema:
* type: integer
* description: Filter by supplier entity id
* - name: seekMonths
* in: query
* schema:
* type: integer
* description: Negative integer cutoff months ago (e.g. -1)
* responses: * responses:
* 200: * 200:
* description: Delivery list * description: Delivery list (windowed when pagination/filters are used)
* content: * content:
* application/json: * application/json:
* schema: * schema:
@@ -220,6 +255,14 @@ export function register(app, basePath = '') {
* type: array * type: array
* items: * items:
* $ref: '#/components/schemas/DeliverySummary' * $ref: '#/components/schemas/DeliverySummary'
* total:
* type: integer
* unfilteredTotal:
* type: integer
* offset:
* type: integer
* limit:
* type: integer
* 400: * 400:
* description: No company selected * description: No company selected
* content: * content:
@@ -358,9 +401,67 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: selected.error }); return res.status(400).json({ error: selected.error });
} }
const rawOffset = req.query.offset;
const rawLimit = req.query.limit;
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) {
limit = parseInt(rawLimit, 10);
if (!Number.isInteger(limit) || limit < 1 || limit > 1000) {
return res.status(400).json({ error: 'limit must be an integer between 1 and 1000' });
}
}
const search = typeof req.query.search === 'string' && req.query.search.trim() !== ''
? req.query.search.trim()
: undefined;
const onlyUnbooked = req.query.onlyUnbooked === 'true';
let supplierId = undefined;
if (req.query.supplierId !== undefined && req.query.supplierId !== '') {
const parsedSupplier = parseInt(req.query.supplierId, 10);
if (!Number.isInteger(parsedSupplier) || parsedSupplier <= 0) {
return res.status(400).json({ error: 'supplierId must be a positive integer' });
}
supplierId = parsedSupplier;
}
let seekMonths = undefined;
if (req.query.seekMonths !== undefined && req.query.seekMonths !== '') {
const parsedSeek = parseInt(req.query.seekMonths, 10);
if (!Number.isInteger(parsedSeek)) {
return res.status(400).json({ error: 'seekMonths must be an integer' });
}
seekMonths = parsedSeek;
}
const isPaged = rawOffset !== undefined || rawLimit !== undefined || search !== undefined || onlyUnbooked || supplierId !== undefined || seekMonths !== undefined;
try { try {
if (isPaged) {
const result = await db.list(req.user.id, {
offset,
limit,
search,
onlyUnbooked: onlyUnbooked ? true : undefined,
supplierId,
seekMonths,
});
res.status(200).json(result);
} else {
const deliveries = await db.list(req.user.id); const deliveries = await db.list(req.user.id);
res.status(200).json({ deliveries }); res.status(200).json({ deliveries: Array.isArray(deliveries) ? deliveries : (deliveries.deliveries || []) });
}
} catch (error) { } catch (error) {
res.locals.error = error; res.locals.error = error;
const status = deliveryErrorStatus(error); const status = deliveryErrorStatus(error);
@@ -369,6 +470,21 @@ export function register(app, basePath = '') {
} }
}); });
app.post(`${basePath}/delivery/rebuild-summaries`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const result = await db.rebuildSummaries(req.user.id);
res.status(200).json(result);
} catch (error) {
const status = deliveryErrorStatus(error);
if (status === 500) console.error('Rebuild delivery summaries failed:', error);
res.status(status).json({ error: status === 500 ? 'Internal server error' : error.message });
}
});
// #endregion // #endregion
// #region POST /delivery // #region POST /delivery
@@ -749,6 +865,8 @@ export function register(app, basePath = '') {
// #endregion // #endregion
/** /**
* @openapi * @openapi
* /delivery/{id}/file/{fileId}: * /delivery/{id}/file/{fileId}:

View File

@@ -53,8 +53,6 @@ export function register(app, basePath = '') {
* | `modelconversation` | `{}` | `POST /modelconversation`, `DELETE /modelconversation/{id}` | * | `modelconversation` | `{}` | `POST /modelconversation`, `DELETE /modelconversation/{id}` |
* | `modelconversation-<id>` | `{}` | Conversation-specific updates on the same model conversation thread; the concrete ID is used in the event name | * | `modelconversation-<id>` | `{}` | Conversation-specific updates on the same model conversation thread; the concrete ID is used in the event name |
* | `brand` | `{ sender, brandId }` | `POST /brand`, `PATCH /brand/{id}`, `DELETE /brand/{id}`, `POST /brand/{id}/image`, `DELETE /brand/{id}/image` | * | `brand` | `{ sender, brandId }` | `POST /brand`, `PATCH /brand/{id}`, `DELETE /brand/{id}`, `POST /brand/{id}/image`, `DELETE /brand/{id}/image` |
* | `draft` | `{ sender, action, name, content }` | Draft update on `drafts/quote`, `drafts/order`, `drafts/delivery` (`sender` echoes the caller's `x-client-id`) |
* | `draft:request` | `{ requestId, action, name, target, sender }` | Server request to frontend to read a draft or list drafts |
* *
* The `brand` payload is `{ sender, brandId }`: `sender` echoes the * The `brand` payload is `{ sender, brandId }`: `sender` echoes the
* caller's `x-client-id` header (empty string if absent) so the sender * caller's `x-client-id` header (empty string if absent) so the sender
@@ -97,10 +95,6 @@ export function register(app, basePath = '') {
* payload: "{}" * payload: "{}"
* - name: brand * - name: brand
* payload: "{ sender, brandId }" * payload: "{ sender, brandId }"
* - name: draft
* payload: "{ sender, action, name, content }"
* - name: draft:request
* payload: "{ requestId, action, name, target, sender }"
* responses: * responses:
* 200: * 200:
* description: | * description: |

View File

@@ -30,85 +30,6 @@ function ledgerErrorStatus(error) {
* @param {string} [basePath=''] * @param {string} [basePath='']
*/ */
export function register(app, basePath = '') { export function register(app, basePath = '') {
/**
* @openapi
* /ledger:
* post:
* tags: [Ledger]
* summary: Add ledger entry
* description: |
* Appends a new entry to the append-only ledger table.
* `hash_version` is fixed to 1. `prev_hash` is cryptographically chained:
* for the genesis row it defaults to `TODO_TSE_BASED_INITHASH`, and for subsequent rows
* it is the SHA-512 hex hash of the canonical JSON representation of the previous highest-id entry.
* Requires authentication and membership in the company owning the referenced manifest article.
* security:
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/LedgerEntryInput'
* example:
* manifestarticle_id: 1
* transaction_type: 'PAYMENT'
* amount: 150.00
* transaction_text: 'Payment received for invoice'
* responses:
* 201:
* description: Ledger entry added successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* ledger:
* $ref: '#/components/schemas/LedgerEntry'
* 400:
* description: Invalid ledger entry or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Manifest article or company not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /ledger
app.post(`${basePath}/ledger`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const ledger = await db.add(req.user.id, req.body ?? {});
res.status(201).json({ ledger });
} catch (error) {
const status = ledgerErrorStatus(error);
if (status === 500) console.error('Add ledger entry failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/** /**
* @openapi * @openapi

View File

@@ -306,26 +306,57 @@ export function register(app, basePath = '') {
* trackStock: * trackStock:
* type: boolean * type: boolean
* unit: * unit:
* type: object * $ref: '#/components/schemas/Unit'
* nullable: true * nullable: true
* brand: * brand:
* type: object * $ref: '#/components/schemas/ArticleBrand'
* nullable: true * nullable: true
* price: * price:
* type: object * type: object
* nullable: true * nullable: true
* properties:
* minmultiplier:
* type: number
* net:
* type: number
* nullable: true
* taxRate:
* type: number
* nullable: true
* gross:
* type: number
* nullable: true
* prices: * prices:
* type: array * type: array
* items: * items:
* type: object * type: object
* properties:
* minmultiplier:
* type: number
* net:
* type: number
* nullable: true
* taxRate:
* type: number
* nullable: true
* gross:
* type: number
* nullable: true
* categories: * categories:
* type: array * type: array
* items: * items:
* type: object * type: object
* properties:
* id:
* type: integer
* name:
* type: string
* slug:
* type: string
* photos: * photos:
* type: array * type: array
* items: * items:
* type: object * $ref: '#/components/schemas/ArticlePhoto'
* eans: * eans:
* type: array * type: array
* items: * items:
@@ -339,6 +370,31 @@ export function register(app, basePath = '') {
* type: array * type: array
* items: * items:
* type: object * type: object
* properties:
* id:
* type: integer
* sku:
* type: string
* sort:
* type: integer
* name:
* type: string
* slug:
* type: string
* attributeValueId:
* type: integer
* nullable: true
* attributeValue:
* type: string
* nullable: true
* unitamount:
* type: number
* nullable: true
* weight:
* type: number
* nullable: true
* trackStock:
* type: boolean
* 400: * 400:
* description: Invalid parameters * description: Invalid parameters
* content: * content:

View File

@@ -135,4 +135,146 @@ export function register(app, basePath = '') {
}); });
// #endregion // #endregion
/**
* @openapi
* /stock/accept:
* post:
* tags: [Stock]
* summary: Accept delivery manifest item(s)
* description: |
* Books acceptance for a delivery manifest item on the ledger (warehouse_id = null).
* If the connected article has stock tracking enabled, an adjustment entry is also
* booked to adjust physical stock in the target warehouse.
* If line amount is negative, books as a delivery return (outgoing).
* security:
* - cookieAuth: []
* x-signals: [stock, ledger, articles, delivery]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* manifestarticleId:
* type: integer
* description: Manifest article ID of the delivery line
* warehouseId:
* type: integer
* description: Optional warehouse ID for inventory intake
* items:
* type: array
* items:
* type: object
* required: [manifestarticleId]
* properties:
* manifestarticleId:
* type: integer
* warehouseId:
* type: integer
* responses:
* 200:
* description: Delivery item(s) accepted successfully
* 400:
* description: Validation error or no company selected
* 404:
* description: Manifest article or warehouse not found
* 409:
* description: Delivery line already accepted on ledger
*/
// #region POST /stock/accept
app.post(`${basePath}/stock/accept`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const result = await db.accept(req.user.id, req.body ?? {});
emit('stock', { companyId: selected.companyId });
emit('ledger', { companyId: selected.companyId });
emit('articles', { companyId: selected.companyId });
emit('delivery', { companyId: selected.companyId });
res.status(200).json(result);
} catch (error) {
const status = stockErrorStatus(error);
if (status === 500) console.error('Stock accept failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /stock/ship:
* post:
* tags: [Stock]
* summary: Ship shipment manifest item(s)
* description: |
* 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
* booked to deduct physical stock from the target warehouse.
* If line amount is negative, books as a customer retoure (incoming).
* security:
* - cookieAuth: []
* x-signals: [stock, ledger, articles, shipping]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* manifestarticleId:
* type: integer
* description: Manifest article ID of the shipment line
* warehouseId:
* type: integer
* description: Optional warehouse ID for inventory deduction
* items:
* type: array
* items:
* type: object
* required: [manifestarticleId]
* properties:
* manifestarticleId:
* type: integer
* warehouseId:
* type: integer
* responses:
* 200:
* description: Shipment item(s) shipped successfully
* 400:
* description: Validation error or no company selected
* 404:
* description: Manifest article or warehouse not found
* 409:
* description: Shipment line already shipped on ledger
*/
// #region POST /stock/ship
app.post(`${basePath}/stock/ship`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const result = await db.ship(req.user.id, req.body ?? {});
emit('stock', { companyId: selected.companyId });
emit('ledger', { companyId: selected.companyId });
emit('articles', { companyId: selected.companyId });
emit('shipping', { companyId: selected.companyId });
res.status(200).json(result);
} catch (error) {
const status = stockErrorStatus(error);
if (status === 500) console.error('Stock ship failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
} }

View File

@@ -384,11 +384,7 @@ export function register(app, basePath = '') {
* name: skus * name: skus
* required: false * required: false
* schema: * schema:
* oneOf:
* - type: array
* items:
* type: string * type: string
* - type: string
* description: Optional SKU or comma-separated SKUs to filter by * description: Optional SKU or comma-separated SKUs to filter by
* responses: * responses:
* 200: * 200:

View File

@@ -2,15 +2,15 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import multer from 'multer'; import multer from 'multer';
import { emit } from '../sse.js'; import { emit } from '../sse.js';
import { prepareVfs, resolveVfsRoot, isReadOnlyDatabasePath, getMimeType } from '../../tools/vfs.js';
import sharp from 'sharp';
import { import {
parseDraftPath, prepareVfs,
requestDraftRead, resolveVfsRoot,
requestDraftList, isReadOnlyDatabasePath,
handleDraftResponse, getMimeType,
notifyDraftUpdate, restoreDefaultSkills,
} from '../../services/draftManager.js'; restoreDefaultApplets,
} from '../../tools/vfs.js';
import sharp from 'sharp';
// #region Helpers // #region Helpers
@@ -472,18 +472,7 @@ export function register(app, basePath = '') {
} }
return list; return list;
} }
const diskFiles = walk(vfsRoot); const files = walk(vfsRoot).sort((a, b) => a.name.localeCompare(b.name));
let drafts = [];
try {
drafts = await requestDraftList({
companyId: selected.companyId,
userId: req.user.id,
clientId: req.headers?.['x-client-id'],
});
} catch (err) {
console.error('Failed to request drafts from frontend:', err);
}
const files = [...diskFiles, ...drafts].sort((a, b) => a.name.localeCompare(b.name));
return res.json({ files }); return res.json({ files });
} catch (error) { } catch (error) {
const status = vfsFileErrorStatus(error); const status = vfsFileErrorStatus(error);
@@ -699,9 +688,6 @@ export function register(app, basePath = '') {
if (isReadOnlyDatabasePath(parsed.name)) { if (isReadOnlyDatabasePath(parsed.name)) {
return res.status(403).json({ error: 'Cannot create directories in read-only database paths' }); return res.status(403).json({ error: 'Cannot create directories in read-only database paths' });
} }
if (parseDraftPath(parsed.name).isFolder || parseDraftPath(parsed.name).isDraft) {
return res.status(403).json({ error: 'Cannot create directories in virtual drafts folder' });
}
try { try {
const vfsRoot = await resolveVfsRoot(req.user.id); const vfsRoot = await resolveVfsRoot(req.user.id);
@@ -810,14 +796,6 @@ export function register(app, basePath = '') {
if (isReadOnlyDatabasePath(parsedOld.name) || isReadOnlyDatabasePath(parsedNew.name)) { if (isReadOnlyDatabasePath(parsedOld.name) || isReadOnlyDatabasePath(parsedNew.name)) {
return res.status(403).json({ error: 'Cannot rename read-only database paths' }); return res.status(403).json({ error: 'Cannot rename read-only database paths' });
} }
if (
parseDraftPath(parsedOld.name).isFolder ||
parseDraftPath(parsedOld.name).isDraft ||
parseDraftPath(parsedNew.name).isFolder ||
parseDraftPath(parsedNew.name).isDraft
) {
return res.status(403).json({ error: 'Cannot rename virtual draft files or folders' });
}
try { try {
const vfsRoot = await resolveVfsRoot(req.user.id); const vfsRoot = await resolveVfsRoot(req.user.id);
@@ -961,50 +939,6 @@ export function register(app, basePath = '') {
return res.status(403).json({ error: 'Cannot modify read-only database files' }); return res.status(403).json({ error: 'Cannot modify read-only database files' });
} }
const draftInfo = parseDraftPath(parsed.name);
if (draftInfo.isFolder) {
return res.status(400).json({ error: 'Cannot write directly to directory' });
}
if (draftInfo.isDraft) {
let strContent;
if (typeof req.body?.content === 'string') {
strContent = req.body.content;
} else if (req.body && typeof req.body === 'object' && !Buffer.isBuffer(req.body)) {
strContent = req.body.content !== undefined
? (typeof req.body.content === 'string' ? req.body.content : JSON.stringify(req.body.content))
: JSON.stringify(req.body);
} else if (Buffer.isBuffer(req.body)) {
strContent = req.body.toString('utf8');
} else if (typeof req.body === 'string') {
strContent = req.body;
} else {
return res.status(400).json({ error: 'No content provided' });
}
let parsedContent = strContent;
try {
parsedContent = JSON.parse(strContent);
} catch {}
const updated = notifyDraftUpdate({
companyId: selected.companyId,
userId: req.user.id,
clientId: req.headers?.['x-client-id'],
name: draftInfo.name,
content: parsedContent,
});
return res.json({
success: true,
file: {
name: `drafts/${draftInfo.name}`,
size: updated.size,
mime: 'application/json; charset=utf-8',
updated_at: updated.updatedAt,
},
});
}
let buffer; let buffer;
let mime = parsed.mime; let mime = parsed.mime;
if (typeof req.body?.content === 'string') { if (typeof req.body?.content === 'string') {
@@ -1117,10 +1051,6 @@ export function register(app, basePath = '') {
if (isReadOnlyDatabasePath(parsed.name)) { if (isReadOnlyDatabasePath(parsed.name)) {
return res.status(403).json({ error: 'Cannot delete read-only database files' }); return res.status(403).json({ error: 'Cannot delete read-only database files' });
} }
const draftDeleteInfo = parseDraftPath(parsed.name);
if (draftDeleteInfo.isFolder || draftDeleteInfo.isDraft) {
return res.status(403).json({ error: 'Cannot delete virtual draft files' });
}
try { try {
const vfsRoot = await resolveVfsRoot(req.user.id); const vfsRoot = await resolveVfsRoot(req.user.id);
@@ -1288,34 +1218,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: parsed.error }); return res.status(400).json({ error: parsed.error });
} }
const draftGetInfo = parseDraftPath(parsed.name);
if (draftGetInfo.isFolder) {
return res.status(400).json({ error: 'Cannot download a directory directly' });
}
if (draftGetInfo.isDraft) {
try {
const content = await requestDraftRead({
companyId: selected.companyId,
userId: req.user.id,
clientId: req.headers?.['x-client-id'],
name: draftGetInfo.name,
});
const strContent = typeof content === 'string' ? content : JSON.stringify(content ?? {}, null, 2);
const buffer = Buffer.from(strContent, 'utf8');
const effectiveMime = 'application/json; charset=utf-8';
const fallbackName = `${draftGetInfo.name}.json`;
res.setHeader('Content-Type', effectiveMime);
res.setHeader('Content-Length', String(buffer.length));
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Cache-Control', 'private, max-age=0, must-revalidate');
res.setHeader('Content-Disposition', `inline; filename="${fallbackName}"`);
return res.status(200).send(buffer);
} catch (err) {
const status = err.message?.includes('timed out') ? 504 : 404;
return res.status(status).json({ error: err.message });
}
}
try { try {
const vfsRoot = await resolveVfsRoot(req.user.id); const vfsRoot = await resolveVfsRoot(req.user.id);
@@ -1392,75 +1295,162 @@ export function register(app, basePath = '') {
// #endregion // #endregion
/** /**
* @openapi * @openapi
* /vfs/drafts/response: * /vfs/skills/restore:
* post: * post:
* tags: [Vfs] * tags: [Vfs]
* summary: Respond to a frontend draft request * summary: Restore system default skills
* description: | * description: |
* Used by the frontend to answer a server-initiated `draft:request` SSE event * Restores all shipped system default SKILL files into the active virtual filesystem (VFS)
* when reading or listing virtual drafts (`drafts/quote`, `drafts/order`, `drafts/delivery`). * for the authenticated user's selected company, overwriting existing system skills with their upstream definitions.
* security: * security:
* - cookieAuth: [] * - cookieAuth: []
* requestBody: * x-signals: [vfs]
* required: true * x-markdown: vfs/skills-restore.md
* content:
* application/json:
* schema:
* type: object
* required: [requestId]
* properties:
* requestId:
* type: string
* description: Unique request ID from the draft:request SSE event
* content:
* description: File content when action was read
* error:
* type: string
* description: Error message if draft retrieval failed on frontend
* files:
* type: array
* description: File listing when action was list
* responses: * responses:
* 200: * 200:
* description: Response received and resolved * description: System skills restored successfully
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: object * type: object
* required: [success, restored, count]
* properties: * properties:
* success: * success:
* type: boolean * type: boolean
* restored:
* type: array
* items:
* type: string
* count:
* type: integer
* 400: * 400:
* description: Missing requestId or no company selected * description: No company selected
* content: * content:
* application/json: * application/json:
* schema: * schema:
* $ref: '#/components/schemas/Error' * $ref: '#/components/schemas/Error'
* 404: * 401:
* description: Request ID not found or already timed out * description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Restore failed
* content: * content:
* application/json: * application/json:
* schema: * schema:
* $ref: '#/components/schemas/Error' * $ref: '#/components/schemas/Error'
*/ */
// #region POST /vfs/drafts/response // #region POST /vfs/skills/restore
app.post(`${basePath}/vfs/drafts/response`, async (req, res) => { app.post(`${basePath}/vfs/skills/restore`, async (req, res) => {
const selected = requireSelectedCompany(req); const selected = requireSelectedCompany(req);
if (selected.error) { if (selected.error) {
return res.status(400).json({ error: selected.error }); return res.status(400).json({ error: selected.error });
} }
const { requestId, content, error, files } = req.body || {}; try {
if (!requestId) { const restored = await restoreDefaultSkills({ googleAuthId: req.user.id });
return res.status(400).json({ error: 'requestId is required' }); emit('vfs', { companyId: selected.companyId });
return res.json({
success: true,
restored,
count: restored.length,
});
} catch (error) {
const status = vfsFileErrorStatus(error);
if (status === 500) console.error('Restore default skills failed:', error);
return res.status(status).json({ error: error.message });
} }
const result = handleDraftResponse({ requestId, content, error, files }); });
if (!result.handled) {
return res.status(404).json({ error: result.error || 'Request expired or not found' }); // #endregion
/**
* @openapi
* /vfs/applets/restore:
* post:
* tags: [Vfs]
* summary: Restore system default applets
* description: |
* Restores all shipped system default example applets (.applet.js) into the active virtual
* filesystem (VFS) for the authenticated user's selected company, overwriting existing system applets with their upstream definitions.
* security:
* - cookieAuth: []
* x-signals: [vfs]
* x-markdown: vfs/applets-restore.md
* responses:
* 200:
* description: System applets restored successfully
* content:
* application/json:
* schema:
* type: object
* required: [success, restored, count]
* properties:
* success:
* type: boolean
* restored:
* type: array
* items:
* type: string
* count:
* type: integer
* 400:
* description: No company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Restore failed
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /vfs/applets/restore
app.post(`${basePath}/vfs/applets/restore`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
try {
const restored = await restoreDefaultApplets({ googleAuthId: req.user.id });
emit('vfs', { companyId: selected.companyId });
return res.json({
success: true,
restored,
count: restored.length,
});
} catch (error) {
const status = vfsFileErrorStatus(error);
if (status === 500) console.error('Restore default applets failed:', error);
return res.status(status).json({ error: error.message });
} }
return res.json({ success: true });
}); });
// #endregion // #endregion

View File

@@ -10,7 +10,7 @@ import { parseVfsRef, vfsFileErrorStatus } from './vfs.js';
// VFS paths that are owned by their own literal route in `vfs.js`. They are registered as // VFS paths that are owned by their own literal route in `vfs.js`. They are registered as
// separate Express handlers, so this wildcard must defer to them instead of treating e.g. // separate Express handlers, so this wildcard must defer to them instead of treating e.g.
// `/vfs/upload` as an applet path. Deferring keeps the two files order-independent. // `/vfs/upload` as an applet path. Deferring keeps the two files order-independent.
const RESERVED_VFS_SEGMENTS = new Set(['upload', 'mkdir', 'rename']); const RESERVED_VFS_SEGMENTS = new Set(['upload', 'mkdir', 'rename', 'skills', 'applets']);
function requireSelectedCompany(req) { function requireSelectedCompany(req) {
const companyId = req.user?.selectedCompanyId; const companyId = req.user?.selectedCompanyId;

View File

@@ -75,7 +75,7 @@ export function hasActiveClient({ companyId, userId, clientId } = {}) {
* Broadcast or target a named signal to subscribers. * Broadcast or target a named signal to subscribers.
* If `targetClientId` or `targetUserId` is specified, sends only to matching clients of that user. * If `targetClientId` or `targetUserId` is specified, sends only to matching clients of that user.
* *
* @param {string} signal - SSE event name (e.g. `category`, `draft`, `draft:request`) * @param {string} signal - SSE event name (e.g. `category`, `articles`, `modelconversation`)
* @param {{ companyId: number, data?: string|object, targetClientId?: string, targetUserId?: number }} options * @param {{ companyId: number, data?: string|object, targetClientId?: string, targetUserId?: number }} options
*/ */
export function emit(signal, { companyId, data, targetClientId, targetUserId }) { export function emit(signal, { companyId, data, targetClientId, targetUserId }) {

View File

@@ -10,15 +10,27 @@ async function formatDelivery(delivery) {
return delivery; return delivery;
} }
export async function list(googleAuthId) { export async function list(googleAuthId, opts) {
const store = await getStore(); const store = await getStore();
const result = await store.delivery.list(googleAuthId); const result = await store.delivery.list(googleAuthId, opts);
if (Array.isArray(result)) { if (Array.isArray(result)) {
return Promise.all(result.map(formatDelivery)); return Promise.all(result.map(formatDelivery));
} }
if (result && Array.isArray(result.deliveries)) {
const deliveries = await Promise.all(result.deliveries.map(formatDelivery));
return {
...result,
deliveries,
};
}
return result; return result;
} }
export async function rebuildSummaries(googleAuthId) {
const store = await getStore();
return store.delivery.rebuildSummaries(googleAuthId);
}
export async function getById(googleAuthId, id) { export async function getById(googleAuthId, id) {
const store = await getStore(); const store = await getStore();
const delivery = await store.delivery.getById(googleAuthId, id); const delivery = await store.delivery.getById(googleAuthId, id);
@@ -42,6 +54,12 @@ export async function remove(googleAuthId, id) {
return store.delivery.delete(googleAuthId, id); return store.delivery.delete(googleAuthId, id);
} }
export async function book(googleAuthId, data) {
const store = await getStore();
const delivery = await store.delivery.book(googleAuthId, data);
return formatDelivery(delivery);
}
export async function getFile(googleAuthId, deliveryId, fileId) { export async function getFile(googleAuthId, deliveryId, fileId) {
const store = await getStore(); const store = await getStore();
return store.manifestattachment.getById(googleAuthId, deliveryId, fileId); return store.manifestattachment.getById(googleAuthId, deliveryId, fileId);
@@ -63,5 +81,3 @@ export async function removeAttachment(googleAuthId, deliveryId, fileId) {
const store = await getStore(); const store = await getStore();
return store.manifestattachment.remove(googleAuthId, deliveryId, fileId); return store.manifestattachment.remove(googleAuthId, deliveryId, fileId);
} }

View File

@@ -4,3 +4,13 @@ export async function adjust(googleAuthId, data) {
const store = await getStore(); const store = await getStore();
return store.stock.adjust(googleAuthId, data); return store.stock.adjust(googleAuthId, data);
} }
export async function accept(googleAuthId, data) {
const store = await getStore();
return store.stock.accept(googleAuthId, data);
}
export async function ship(googleAuthId, data) {
const store = await getStore();
return store.stock.ship(googleAuthId, data);
}

View File

@@ -1,382 +0,0 @@
VFS Drafts: Collaborative Quote and Order Editing
# Overview
The virtual directory `drafts/` in VFS allows the LLM agent and the frontend user to collaborate in real time on in-progress commercial documents.
Drafts are stateful forms maintained in the frontend application and synchronized with the backend.
There are two virtual draft files:
1. `drafts/quote` : Sales Quote / Angebot (Customer sales proposal)
2. `drafts/order` : Sales Order / Auftrag & Bestellung (Customer order confirmation)
---
# Draft JSON Schemas
## 1. Common Manifest Envelope Schema
Every draft file contains a root object with a `manifest` property conforming to this JSON Schema:
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DraftManifestEnvelope",
"type": "object",
"required": ["manifest"],
"properties": {
"manifest": {
"type": "object",
"required": ["kind", "mode", "lines"],
"properties": {
"kind": {
"type": "string",
"enum": ["quote", "order"],
"description": "Document type"
},
"mode": {
"type": "string",
"enum": ["create", "edit"],
"description": "Whether creating a new document or editing an existing saved one"
},
"id": {
"type": ["integer", "null"],
"description": "Database document ID (null for new drafts, integer when editing)"
},
"date": {
"type": ["string", "null"],
"format": "date",
"description": "ISO date string (YYYY-MM-DD) or null"
},
"partyId": {
"type": ["integer", "null"],
"description": "Customer entity.id for quotes and orders"
},
"number": {
"type": ["string", "null"],
"description": "Document identifier. Null for automatic numbering upon commit"
},
"externalNumber": {
"type": ["string", "null"],
"description": "External reference (e.g. customer PO number, supplier delivery slip number)"
},
"comment": {
"type": ["string", "null"],
"description": "Document-level remarks or notes"
},
"lines": {
"type": "array",
"description": "Ordered line items of the document"
}
}
}
}
}
```
---
## 2. Sales Quote Draft (`drafts/quote`)
Used for preparing customer sales proposals (Angebote).
### Complete Quote JSON Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QuoteDraft",
"type": "object",
"required": ["manifest"],
"properties": {
"manifest": {
"type": "object",
"required": ["kind", "mode", "lines"],
"properties": {
"kind": { "type": "string", "const": "quote" },
"mode": { "type": "string", "enum": ["create", "edit"] },
"id": { "type": ["integer", "null"] },
"date": {
"type": ["string", "null"],
"format": "date",
"description": "Offer validity expiration date (Gültig bis, YYYY-MM-DD)"
},
"partyId": {
"type": ["integer", "null"],
"description": "Customer entity ID (entity.id)"
},
"number": {
"type": ["string", "null"],
"description": "Quote number (null for auto-generation)"
},
"externalNumber": {
"type": ["string", "null"],
"description": "Customer project or RFQ reference"
},
"comment": {
"type": ["string", "null"],
"description": "Optional quotation remark"
},
"lines": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "amount", "net", "taxrate"],
"properties": {
"pos": {
"type": "integer",
"minimum": 1,
"description": "Position line number (1, 2, 3...)"
},
"sku": {
"type": ["string", "null"],
"description": "Catalogue article SKU, or null for custom service/text position"
},
"name": {
"type": "string",
"description": "Title or designation of the position"
},
"amount": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Quoted quantity"
},
"pu": {
"type": "number",
"minimum": 1,
"default": 1,
"description": "Packing unit / price unit (Preiseinheit, default 1)"
},
"net": {
"type": "number",
"minimum": 0,
"description": "Net unit price in EUR"
},
"gross": {
"type": ["number", "null"],
"description": "Gross unit price in EUR (net * (1 + taxrate/100))"
},
"taxrate": {
"type": "number",
"minimum": 0,
"description": "VAT rate percentage (e.g. 19 or 7)"
},
"rebate": {
"type": "number",
"minimum": 0,
"maximum": 100,
"default": 0,
"description": "Line discount percentage (0 to 100)"
},
"unit": {
"type": ["string", "null"],
"description": "Unit of measurement (e.g. 'Stück', 'm', 'kg', 'Pauschal')"
},
"comment": {
"type": ["string", "null"],
"description": "Optional line-specific comment or specification"
}
}
}
}
}
}
}
}
```
### Complete Quote Example
```json
{
"manifest": {
"kind": "quote",
"mode": "create",
"id": null,
"date": "2026-11-30",
"partyId": 104,
"number": null,
"externalNumber": "ANFRAGE-2026-441",
"comment": "Angebot freibleibend, Lieferung frei Haus ab 250 EUR.",
"lines": [
{
"pos": 1,
"sku": "42001257",
"name": "1 Pot 8L System inkl. 47L Wasserfass",
"amount": 2,
"pu": 1,
"net": 58.74,
"gross": 69.90,
"taxrate": 19,
"rebate": 0,
"unit": "Stück",
"comment": null
},
{
"pos": 2,
"sku": null,
"name": "Aufbau und Fachberatung vor Ort",
"amount": 1.5,
"pu": 1,
"net": 80.00,
"gross": 95.20,
"taxrate": 19,
"rebate": 10,
"unit": "Std",
"comment": "Inklusive Anfahrt Zone 1"
}
]
}
}
```
---
## 3. Sales Order Draft (`drafts/order`)
Used for customer order confirmations and sales orders (Aufträge & Bestellungen).
### Complete Order JSON Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "OrderDraft",
"type": "object",
"required": ["manifest"],
"properties": {
"manifest": {
"type": "object",
"required": ["kind", "mode", "lines"],
"properties": {
"kind": { "type": "string", "const": "order" },
"mode": { "type": "string", "enum": ["create", "edit"] },
"id": { "type": ["integer", "null"] },
"date": {
"type": ["string", "null"],
"format": "date",
"description": "Expected delivery date (Liefertermin, YYYY-MM-DD)"
},
"partyId": {
"type": ["integer", "null"],
"description": "Customer entity ID (entity.id)"
},
"number": {
"type": ["string", "null"],
"description": "Order number (null for auto-generation)"
},
"externalNumber": {
"type": ["string", "null"],
"description": "Customer purchase order / PO number (Kundenbestellnummer)"
},
"comment": {
"type": ["string", "null"],
"description": "Optional order remark or delivery instruction"
},
"lines": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "amount", "net", "taxrate"],
"properties": {
"pos": {
"type": "integer",
"minimum": 1,
"description": "Position line number (1, 2, 3...)"
},
"sku": {
"type": ["string", "null"],
"description": "Article SKU"
},
"name": {
"type": "string",
"description": "Line item description"
},
"amount": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Ordered quantity"
},
"pu": {
"type": "number",
"minimum": 1,
"default": 1,
"description": "Packaging unit / price unit (default 1)"
},
"net": {
"type": "number",
"minimum": 0,
"description": "Net unit price in EUR"
},
"gross": {
"type": ["number", "null"],
"description": "Gross unit price in EUR"
},
"taxrate": {
"type": "number",
"minimum": 0,
"description": "VAT percentage (19 or 7)"
},
"rebate": {
"type": "number",
"minimum": 0,
"maximum": 100,
"default": 0,
"description": "Line discount percentage"
},
"unit": {
"type": ["string", "null"],
"description": "Unit of measurement"
},
"comment": {
"type": ["string", "null"],
"description": "Line comment or customer note"
}
}
}
}
}
}
}
}
```
### Complete Order Example
```json
{
"manifest": {
"kind": "order",
"mode": "create",
"id": null,
"date": "2026-10-28",
"partyId": 104,
"number": null,
"externalNumber": "PO-2026-8841",
"comment": "Bitte Lieferung avisieren unter +49 171 1234567.",
"lines": [
{
"pos": 1,
"sku": "42001257",
"name": "1 Pot 8L System inkl. 47L Wasserfass",
"amount": 5,
"pu": 1,
"net": 58.74,
"gross": 69.90,
"taxrate": 19,
"rebate": 5,
"unit": "Stück",
"comment": null
},
{
"pos": 2,
"sku": "42000009",
"name": "Autopot Easy2Grow Erweiterungsset 2Pot",
"amount": 2,
"pu": 1,
"net": 39.50,
"gross": 47.01,
"taxrate": 19,
"rebate": 0,
"unit": "Stück",
"comment": "Farbe: Schwarz"
}
]
}
}
```

View File

@@ -1,305 +1,28 @@
Process Incoming Product and Invoices: Extraction & Delivery Drafts Process Incoming Product and Invoices: use draft_delivery_manifest tool to store parsed invoice
# Overview # Overview
This skill guides the automated and interactive processing of inbound supplier documents (invoices / *Rechnungen* and delivery notes / *Lieferscheine*). first task a subagent to find "lieferant_id using GET /supplier" using information in the data.
It covers:
1. **Document Data Extraction**: Extracting structured metadata, packaging units (VPE), units of measurement, line items, and notes from document text/images via the `output_manifest` tool.
2. **Goods Inbound Delivery Draft (`drafts/delivery`)**: Storing and maintaining the inbound receipt in the virtual file system (`drafts/delivery`) for real-time collaboration with the user in the UI.
--- then task a subagent to find existing links with articles using "`POST /supplier/{id}/articles` Params: `id: int, body?: { skus?: str[] }`" with all the SKUs of that supplieer we have in the data.
use the existing linked data to besser match vpe
# 1. Document Extraction Rules & Prompt Logic if the supplier entity does not exist, confirm with the user to create it.
When reading or processing an invoice or delivery note, follow these core rules: finally call draft_delivery_manifest
## General Extraction Rules - manifestType: "rechnung" or "lieferschein". For "lieferschein", prices and taxes must be null.
- **Document Type (`manifestType`)**: `"rechnung"` or `"lieferschein"`. For `"lieferschein"`, prices (`eknetto`) and taxes (`mwst`) must be `null`. - Exclude non-product lines (shipping, handling, etc.).
- **Exclude Non-Product Lines**: Exclude freight, shipping, postage, pallet charges, packaging disposal fees, and handling surcharges. Only include physical inventory products. - Line items: Count, vpe (default: 1), liefArtNr, mwst (7 or 19), and eknetto per Item(euro cents).
- **Line Items**: Extract `position`, `count`, `vpe` (default: 1), `unit`, `articleName`, `liefArtNr`, `mwst` (7, 19, or null), and `eknetto` per piece in euro cents (or null). - Match supplier against . For unknown suppliers, extract lieferantName and set lieferant_id to null.
- **Supplier Matching**: Match the supplier against known company suppliers to identify `lieferant_id` and `lieferantName`. For unknown suppliers, extract `lieferantName` as string and set `lieferant_id` to `null`. - Write notes strictly in German.
- **Notes Strictly in German**: Record all notes, ambiguities, packaging assumptions, or excluded line details in German. These notes assist a human reviewer in double-checking results.
- **Delta / Batching Updates**: The `output_manifest` tool consists of three optional components: `basedata`, `notes`, and `items`. Only include a component when it carries genuinely new or changed data; omit unchanged components.
- Include `basedata` ONLY when at least one field is currently null/empty or value actually changes.
- Include `items` ONLY when new product line items are being added.
- Include `notes` ONLY when there is a genuinely new noteworthy detail.
## VPE (Verpackungseinheit) Logic VPE (Verpackungseinheit) Logic:
VPE represents the number of individually sellable units inside a bulk item. If not explicitly split for individual sale, default to 1. VPE represents the number of individually sellable units inside a bulk item as sold by the store. If not explicitly split for individual sale, default to 1.
- `"20 Boxen á 10 Stück"` -> `vpe = 20`. - "20 Boxen á 10 Stück" -> VPE = 20.
- `"10 Gläser á 100 Filter"` -> `vpe = 10` (the 10 glasses are sold individually). - "10 Gläser á 100 Filter" -> VPE = 10 (we sell the 10 glasses individually).
- `"100 Stück"` -> `vpe = 1` (sold as one bulk pack). - "100 Stück" -> VPE = 1 (sold as one bulk pack).
- Typical sellable VPEs are containers like *Beutel*, *Gläser*, *Boxen*, *Hefte*, not loose sub-components. - We don't sell the individual filters or Pieces, but typical VPEs like Beutel, Glässer, Boxen, Hefte
- In `"Y Hefte á X Blatt"` the VPE is NOT X, but `Y`. - That means that in "Y Hefte á X Blatt" the VPE is NOT X but: Y
- A "Display" represents a VPE: `"20er Display"` means `vpe = 20`. - a so called "Display" also means VPE. So "20er Display" means VPE = 20
- `eknetto` is the price of one full VPE. - eknetto is the price of one full vpe
## Unit (ME) Rules
- The unit (ME) must ALWAYS refer to the individual sellable item (e.g. `Stk.`, `Beutel`, `Flasche`, `Box`, `Set`). It must **NEVER** be `"VE"` or `"VPE"`.
- A packaging size like `"VE100"` means the bulk pack contains 100 individual items: set `vpe = 100` and `unit = "Stk."` (or the relevant individual unit).
- Example: `"Einhängesiebe Stahl, VE100"` -> `vpe = 100`, `unit = "Stk."`, `count = 1` (one bulk pack of 100 sieves).
---
# 2. Extraction Tool Specification (`output_manifest`)
The extraction engine outputs structured findings via the `output_manifest` tool call.
```json
{
"name": "output_manifest",
"description": "Stores extracted invoice/delivery-note data: header metadata (basedata), notes, and product line items. Each component is strictly optional — only include the ones that carry new data.",
"strict": false,
"parameters": {
"type": "object",
"additionalProperties": false,
"required": [],
"properties": {
"basedata": {
"anyOf": [
{
"type": "object",
"additionalProperties": false,
"properties": {
"date": { "type": ["string", "null"], "description": "Document date (ISO 8601, YYYY-MM-DD)" },
"lieferant_id": { "type": ["number", "null"], "description": "Lieferant ID if matched, else null" },
"lieferantName": { "type": "string", "description": "Supplier name" },
"manifestNr": { "type": ["string", "null"], "description": "Lieferscheinnummer or Rechnungsnummer" },
"manifestType": {
"anyOf": [
{ "type": "string", "enum": ["rechnung", "lieferschein"] },
{ "type": "null" }
],
"description": "Document type"
}
}
},
{ "type": "null" }
],
"description": "Header/metadata. Only include fields that are currently missing or changed."
},
"notes": {
"anyOf": [
{ "type": "array", "items": { "type": "string" } },
{ "type": "null" }
],
"description": "New noteworthy details (in German), for a human to double-check the result."
},
"items": {
"anyOf": [
{
"type": "object",
"additionalProperties": false,
"properties": {
"numberOfItemsToAdd": { "type": "number", "description": "Number of NEW line items being added in this call." },
"items": {
"type": "array",
"description": "Invoice/delivery product line items",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["position", "count", "vpe", "unit", "articleName", "liefArtNr", "mwst", "eknetto"],
"properties": {
"position": { "type": "number", "description": "Position number" },
"count": { "type": "number", "description": "Quantity (Menge)" },
"vpe": { "type": "number", "description": "Packaging unit size (VPE). Default: 1." },
"unit": { "type": "string", "description": "Unit of the individual sellable item. NEVER 'VE' or 'VPE'." },
"articleName": { "type": "string", "description": "Article name" },
"liefArtNr": { "type": "string", "description": "Supplier article number" },
"mwst": {
"anyOf": [{ "type": "string", "enum": ["7", "19"] }, { "type": "null" }],
"description": "VAT rate percentage; null if delivery note or missing"
},
"eknetto": { "type": ["number", "null"], "description": "Net purchase price per VPE in euro cents; null if missing" }
}
}
}
}
},
{ "type": "null" }
],
"description": "New product line items."
}
}
}
}
```
---
# 3. Goods Inbound Delivery Draft (`drafts/delivery`)
Inbound deliveries are staged and edited as drafts in the virtual file system under `drafts/delivery`.
This allows the agent and frontend user to review, correct, and finalize stock receipts and purchase costs before committing them into the inventory ledger.
## Field Mapping from Extracted Manifest to Delivery Draft
- **`date`**: Document date (`YYYY-MM-DD`).
- **`partyId`**: Supplier ID (`lieferant_id` / `entity.id`).
- **`externalNumber`**: Supplier delivery slip number or invoice number (`manifestNr`).
- **`comment`**: Human-readable notes, inspection remarks, storage location, or extraction caveats.
- **`lines`**:
- `pos`: 1-based position line number.
- `sku`: Internal article SKU or matched supplier SKU (`liefArtNr`).
- `name`: Delivered product name (`articleName`).
- `amount`: Quantity delivered (`count`).
- `pu`: Packaging unit size (`vpe`, default `1`).
- `net`: Purchase cost net per unit in EUR (`eknetto / 100`, or `0` for delivery notes without prices).
- `gross`: Purchase cost gross per unit in EUR (`net * (1 + taxrate/100)`).
- `taxrate`: Input tax rate percentage (`19`, `7`, or `0`).
- `rebate`: Supplier discount percentage (default `0`).
- `unit`: Individual sellable unit (e.g. `Stück`, `Beutel`, `Flasche`).
- `comment`: Specific batch, lot, or packaging comment.
---
## Delivery Draft JSON Schema
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DeliveryDraft",
"type": "object",
"required": ["manifest"],
"properties": {
"manifest": {
"type": "object",
"required": ["kind", "mode", "lines"],
"properties": {
"kind": { "type": "string", "const": "delivery" },
"mode": { "type": "string", "enum": ["create", "edit"] },
"id": { "type": ["integer", "null"] },
"date": {
"type": ["string", "null"],
"format": "date",
"description": "Delivery arrival / receipt date (Lieferdatum, YYYY-MM-DD)"
},
"partyId": {
"type": ["integer", "null"],
"description": "Supplier ID (supplier.id / entity_id)"
},
"number": {
"type": ["string", "null"],
"description": "Delivery manifest number (allocated by system sequence upon creation)"
},
"externalNumber": {
"type": ["string", "null"],
"description": "Supplier delivery slip number (Lieferschein-Nr. des Lieferanten)"
},
"comment": {
"type": ["string", "null"],
"description": "Warehouse receipt remarks or gate notes"
},
"lines": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "amount", "net", "taxrate"],
"properties": {
"pos": {
"type": "integer",
"minimum": 1,
"description": "Position line number (1, 2, 3...)"
},
"sku": {
"type": ["string", "null"],
"description": "Article SKU (internal SKU or matched supplier SKU)"
},
"name": {
"type": "string",
"description": "Delivered article name"
},
"amount": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Delivered quantity received into warehouse"
},
"pu": {
"type": "number",
"minimum": 1,
"default": 1,
"description": "Packaging unit / VPE (default 1)"
},
"net": {
"type": "number",
"minimum": 0,
"description": "Purchase cost net per unit in EUR (EK netto)"
},
"gross": {
"type": ["number", "null"],
"description": "Purchase cost gross per unit in EUR (EK brutto)"
},
"taxrate": {
"type": "number",
"minimum": 0,
"description": "Input tax percentage (e.g. 19 or 7)"
},
"rebate": {
"type": "number",
"minimum": 0,
"maximum": 100,
"default": 0,
"description": "Supplier discount percentage"
},
"unit": {
"type": ["string", "null"],
"description": "Unit of measurement"
},
"comment": {
"type": ["string", "null"],
"description": "Batch number, lot, condition, or storage bin note"
}
}
}
}
}
}
}
}
```
---
## Delivery Draft Complete Example
```json
{
"manifest": {
"kind": "delivery",
"mode": "create",
"id": null,
"date": "2026-10-14",
"partyId": 12,
"number": null,
"externalNumber": "LS-SUP-99412",
"comment": "Ware unbeschädigt auf Palette 3 angeliefert.",
"lines": [
{
"pos": 1,
"sku": "42001257",
"name": "1 Pot 8L System inkl. 47L Wasserfass",
"amount": 20,
"pu": 1,
"net": 38.50,
"gross": 45.82,
"taxrate": 19,
"rebate": 0,
"unit": "Stück",
"comment": "Palette A-04"
},
{
"pos": 2,
"sku": "42000009",
"name": "Autopot Easy2Grow Erweiterungsset 2Pot",
"amount": 50,
"pu": 1,
"net": 24.00,
"gross": 28.56,
"taxrate": 19,
"rebate": 3,
"unit": "Stück",
"comment": "Karton 1 bis 5"
}
]
}
}
```

View File

@@ -21,6 +21,9 @@ process.on('SIGINT', () => {
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
shutdown().then(() => process.exit(0)); shutdown().then(() => process.exit(0));
}); });
process.on('unhandledRejection', (reason) => {
sysLog.error('Unhandled rejection:', reason);
});
// MARK: calls startApi from ./api/index.js // MARK: calls startApi from ./api/index.js
startApi().catch((err) => { startApi().catch((err) => {

View File

@@ -28,6 +28,7 @@ import { createSearchArticlesTool } from '../tools/search-articles.js';
import { createEanChecksumTool } from '../tools/ean-checksum.js'; import { createEanChecksumTool } from '../tools/ean-checksum.js';
import { createNetGrossCalcTool } from '../tools/net-gross-calc.js'; import { createNetGrossCalcTool } from '../tools/net-gross-calc.js';
import { createSetUserRouteTool } from '../tools/set-user-route.js'; import { createSetUserRouteTool } from '../tools/set-user-route.js';
import { createDraftDeliveryManifestTool } from '../tools/draft-delivery-manifest.js';
import { createSubagentTool } from '../tools/subagent.js'; import { createSubagentTool } from '../tools/subagent.js';
import { resolveOpenRouterKey, resolveDefaultModel, resolveModelReasoning, RETIRED_MODELS, hasConfiguredOpenRouterKey } from '../tools/llm-key.js'; import { resolveOpenRouterKey, resolveDefaultModel, resolveModelReasoning, RETIRED_MODELS, hasConfiguredOpenRouterKey } from '../tools/llm-key.js';
@@ -501,6 +502,7 @@ export class ConversationManager {
}); });
toolMsg = await db.addMessage(googleAuthId, this.conversationId, { message: payload, role: 'tool' }); toolMsg = await db.addMessage(googleAuthId, this.conversationId, { message: payload, role: 'tool' });
await db.setState(googleAuthId, this.conversationId, 'busy'); await db.setState(googleAuthId, this.conversationId, 'busy');
} catch (err) { } catch (err) {
this.isProcessing = false; this.isProcessing = false;
throw err; throw err;
@@ -758,6 +760,7 @@ export class ConversationManager {
this.broadcast('route', { route }); this.broadcast('route', { route });
}, },
}), }),
createDraftDeliveryManifestTool(),
createSubagentTool({ createSubagentTool({
googleAuthId, googleAuthId,
companyId: this.companyId, companyId: this.companyId,

View File

@@ -1,349 +0,0 @@
import { emit, hasActiveClient } from '../api/sse.js';
import { getStore } from '../db/store.js';
export const DRAFT_NAMES = new Set(['quote', 'order', 'delivery']);
/**
* Checks and parses a VFS path to determine if it points to the drafts directory or a draft file.
* Accepts paths like:
* - 'drafts', '/drafts', 'drafts/'
* - 'drafts/quote', 'drafts/quote.json', '/api/vfs/drafts/order', 'vfs://drafts/delivery'
*
* @param {string} filePath
* @returns {{ isFolder: boolean, isDraft: boolean, name?: 'quote'|'order'|'delivery', filename?: string }}
*/
export function parseDraftPath(filePath) {
if (typeof filePath !== 'string' || !filePath.trim()) {
return { isFolder: false, isDraft: false };
}
let clean = filePath.trim().replace(/^vfs:\/+/i, '').replace(/^([a-zA-Z]:|\/)+/, '').replace(/^\.\/+/, '');
clean = clean.replace(/^api\//i, '').replace(/^vfs\//i, '');
const segments = clean.split('/').filter(Boolean);
if (segments.length === 0) return { isFolder: false, isDraft: false };
if (segments[0].toLowerCase() === 'drafts') {
if (segments.length === 1) {
return { isFolder: true, isDraft: false };
}
if (segments.length === 2) {
const rawName = segments[1];
const lower = rawName.toLowerCase();
const withoutExt = lower.endsWith('.json') ? lower.slice(0, -5) : lower;
if (DRAFT_NAMES.has(withoutExt)) {
return { isFolder: false, isDraft: true, name: withoutExt, filename: rawName };
}
}
}
return { isFolder: false, isDraft: false };
}
/**
* In-memory cache for drafts: key: `${companyId}:${userId}:${name}` -> { content, updatedAt, size }
*/
const draftCache = new Map();
function cacheKey(companyId, userId, name) {
return `${companyId}:${userId}:${name}`;
}
export function getCachedDraft({ companyId, userId, name }) {
return draftCache.get(cacheKey(companyId, userId, name)) || null;
}
/**
* Normalizes draft content by deduplicating redundant `items` array when `lines` exists.
* @param {any} content
* @returns {any}
*/
export function normalizeDraftContent(content) {
if (!content) return content;
let parsed = content;
let wasString = false;
if (typeof content === 'string') {
try {
parsed = JSON.parse(content);
wasString = true;
} catch {
return content;
}
}
if (parsed && typeof parsed === 'object') {
const target = parsed.manifest && typeof parsed.manifest === 'object' ? parsed.manifest : parsed;
if (target && typeof target === 'object') {
if (Array.isArray(target.lines) && Array.isArray(target.items)) {
delete target.items;
} else if (!Array.isArray(target.lines) && Array.isArray(target.items)) {
target.lines = target.items;
delete target.items;
}
}
}
return wasString ? JSON.stringify(parsed, null, 2) : parsed;
}
export function setCachedDraft({ companyId, userId, name, content }) {
const normalized = normalizeDraftContent(content);
const updatedAt = new Date().toISOString();
const str = typeof normalized === 'string' ? normalized : JSON.stringify(normalized ?? {}, null, 2);
const entry = {
content: normalized,
updatedAt,
size: Buffer.byteLength(str, 'utf8'),
};
draftCache.set(cacheKey(companyId, userId, name), entry);
return entry;
}
/**
* Resolves the selected company ID for a given googleAuth ID if needed.
* @param {number} googleAuthId
* @returns {Promise<number|null>}
*/
export async function resolveCompanyForUser(googleAuthId) {
if (!googleAuthId) return null;
try {
const store = await getStore();
const row = await store.google_auth.getById(googleAuthId);
return row?.selectedCompanyId ?? null;
} catch {
return null;
}
}
/**
* Pending requests map: requestId -> { resolve, reject, timer, companyId, userId, action, name }
*/
const pendingRequests = new Map();
/**
* Request a draft content from the frontend via SSE and wait for its response.
*
* @param {{ companyId?: number, userId: number, clientId?: string, name: string, timeoutMs?: number }} options
* @returns {Promise<any>}
*/
export async function requestDraftRead({ companyId, userId, clientId, name, timeoutMs = 15000 }) {
const effectiveCompanyId = companyId != null ? companyId : await resolveCompanyForUser(userId);
if (effectiveCompanyId == null) {
throw new Error('No company selected');
}
// Check if client is active; if not and we have cache, return cache immediately
const hasClient = hasActiveClient({ companyId: effectiveCompanyId, userId, clientId });
if (!hasClient) {
const cached = getCachedDraft({ companyId: effectiveCompanyId, userId, name });
if (cached) {
return cached.content;
}
}
const requestId = `draft_req_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pendingRequests.delete(requestId);
// Fallback to cache if available
const cached = getCachedDraft({ companyId: effectiveCompanyId, userId, name });
if (cached) {
return resolve(cached.content);
}
reject(new Error(`Frontend draft response timed out for "${name}"`));
}, timeoutMs);
pendingRequests.set(requestId, {
resolve,
reject,
timer,
companyId: effectiveCompanyId,
userId,
action: 'read',
name,
});
// Emit request over SSE
emit('draft:request', {
companyId: effectiveCompanyId,
targetUserId: userId,
targetClientId: clientId,
data: {
requestId,
action: 'read',
name,
target: name,
sender: clientId || '',
},
});
});
}
/**
* Request draft listing from the frontend via SSE.
* If frontend doesn't respond or times out, returns default metadata for all 3 drafts.
*
* @param {{ companyId?: number, userId: number, clientId?: string, timeoutMs?: number }} options
* @returns {Promise<Array<object>>}
*/
export async function requestDraftList({ companyId, userId, clientId, timeoutMs = 2500 }) {
const effectiveCompanyId = companyId != null ? companyId : await resolveCompanyForUser(userId);
if (effectiveCompanyId == null) {
return [];
}
const defaultDrafts = () => {
const now = new Date().toISOString();
return Array.from(DRAFT_NAMES).map((name) => {
const cached = getCachedDraft({ companyId: effectiveCompanyId, userId, name });
return {
name: `drafts/${name}`,
size: cached ? cached.size : 0,
mime: 'application/json; charset=utf-8',
updated_at: cached ? cached.updatedAt : now,
created_at: cached ? cached.updatedAt : now,
isText: true,
isImage: false,
isPdf: false,
isDraft: true,
};
});
};
// If no client is connected, return defaults immediately without delaying listing
const hasClient = hasActiveClient({ companyId: effectiveCompanyId, userId, clientId });
if (!hasClient) {
return defaultDrafts();
}
const requestId = `draft_list_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
return new Promise((resolve) => {
const timer = setTimeout(() => {
pendingRequests.delete(requestId);
resolve(defaultDrafts());
}, timeoutMs);
pendingRequests.set(requestId, {
resolve: (filesOrList) => {
if (Array.isArray(filesOrList)) {
const now = new Date().toISOString();
const normalized = filesOrList.map((item) => {
const rawName = typeof item === 'string' ? item : (item.name || item.filename || '');
const parsed = parseDraftPath(rawName.startsWith('drafts/') ? rawName : `drafts/${rawName}`);
const nameKey = parsed.name || rawName;
const cached = getCachedDraft({ companyId: effectiveCompanyId, userId, name: nameKey });
return {
name: `drafts/${nameKey}`,
size: typeof item === 'object' && item.size != null ? item.size : (cached ? cached.size : 0),
mime: 'application/json; charset=utf-8',
updated_at: typeof item === 'object' && item.updated_at ? item.updated_at : (cached ? cached.updatedAt : now),
created_at: typeof item === 'object' && item.created_at ? item.created_at : (cached ? cached.updatedAt : now),
isText: true,
isImage: false,
isPdf: false,
isDraft: true,
};
});
resolve(normalized);
} else {
resolve(defaultDrafts());
}
},
reject: () => {
resolve(defaultDrafts());
},
timer,
companyId: effectiveCompanyId,
userId,
action: 'list',
});
emit('draft:request', {
companyId: effectiveCompanyId,
targetUserId: userId,
targetClientId: clientId,
data: {
requestId,
action: 'list',
sender: clientId || '',
},
});
});
}
/**
* Handle frontend response to a previous draft:request.
*
* @param {{ requestId: string, content?: any, error?: string, files?: any[] }} payload
* @returns {{ handled: boolean, error?: string }}
*/
export function handleDraftResponse({ requestId, content, error, files }) {
if (!requestId || !pendingRequests.has(requestId)) {
return { handled: false, error: 'Unknown or expired requestId' };
}
const pending = pendingRequests.get(requestId);
pendingRequests.delete(requestId);
clearTimeout(pending.timer);
if (error) {
pending.reject(new Error(error));
return { handled: true };
}
if (pending.action === 'list') {
pending.resolve(files !== undefined ? files : content);
return { handled: true };
}
// action === 'read'
const normalized = normalizeDraftContent(content);
setCachedDraft({
companyId: pending.companyId,
userId: pending.userId,
name: pending.name,
content: normalized,
});
pending.resolve(normalized);
return { handled: true };
}
/**
* Notify frontend about a draft update via the normal /events SSE endpoint.
*
* @param {{ companyId: number, userId: number, clientId?: string, name: string, content: any }} options
* @returns {{ name: string, size: number, updatedAt: string }}
*/
export function notifyDraftUpdate({ companyId, userId, clientId, name, content }) {
const entry = setCachedDraft({ companyId, userId, name, content });
emit('draft', {
companyId,
targetUserId: userId,
targetClientId: clientId,
data: {
sender: clientId || '',
action: 'update',
name,
content,
},
});
return {
name: `drafts/${name}`,
size: entry.size,
updatedAt: entry.updatedAt,
};
}
/**
* Synchronous/fire-and-forget version of notifyDraftUpdate for sync writes.
*
* @param {{ googleAuthId: number, clientId?: string, name: string, content: any }} options
*/
export function notifyDraftUpdateSync({ googleAuthId, clientId, name, content }) {
resolveCompanyForUser(googleAuthId).then((companyId) => {
if (companyId != null) {
notifyDraftUpdate({ companyId, userId: googleAuthId, clientId, name, content });
}
}).catch((err) => {
console.error('[draftManager] notifyDraftUpdateSync error:', err);
});
}

View File

@@ -0,0 +1,88 @@
import { defineTool } from 'libllmorbiter';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
/**
* Creates the draft_delivery_manifest tool.
* Stores extracted invoice/delivery-note data: header metadata (basedata), notes, and product line items.
*/
export function createDraftDeliveryManifestTool() {
const P = TOOL_PARAMETER_DESCRIPTIONS.draft_delivery_manifest;
return defineTool({
name: 'draft_delivery_manifest',
description: TOOL_DESCRIPTIONS.draft_delivery_manifest,
strict: false,
parameters: {
type: 'object',
additionalProperties: false,
required: [],
properties: {
basedata: {
anyOf: [
{
type: 'object',
additionalProperties: false,
properties: {
date: { type: ['string', 'null'], description: P.date },
lieferant_id: { type: ['number', 'null'], description: P.lieferant_id },
lieferantName: { type: 'string', description: P.lieferantName },
manifestNr: { type: ['string', 'null'], description: P.manifestNr },
manifestType: {
anyOf: [
{ type: 'string', enum: ['rechnung', 'lieferschein'] },
{ type: 'null' },
],
description: P.manifestType,
},
},
},
{ type: 'null' },
],
description: P.basedata,
},
notes: {
anyOf: [
{ type: 'array', items: { type: 'string' } },
{ type: 'null' },
],
description: P.notes,
},
items: {
anyOf: [
{
type: 'object',
additionalProperties: false,
properties: {
numberOfItemsToAdd: { type: 'number', description: P.numberOfItemsToAdd },
items: {
type: 'array',
description: P.items_list,
items: {
type: 'object',
additionalProperties: false,
required: ['position', 'count', 'vpe', 'articleName', 'liefArtNr', 'mwst', 'eknetto'],
properties: {
position: { type: 'number', description: P.position },
count: { type: 'number', description: P.count },
vpe: { type: 'number', description: P.vpe },
articleName: { type: 'string', description: P.articleName },
liefArtNr: { type: 'string', description: P.liefArtNr },
mwst: { // todo those needs to come from company settings.
anyOf: [{ type: 'string', enum: ['7', '19'] }, { type: 'null' }],
description: P.mwst,
},
eknetto: { type: ['number', 'null'], description: P.eknetto },
},
},
},
},
},
{ type: 'null' },
],
description: P.items,
},
},
},
invoke: async () => ({ success: true }),
});
}

View File

@@ -4,7 +4,6 @@ import { defineTool, ToolError } from 'libllmorbiter';
import { isSkillPath, listSkillFiles, SKILL_EXT } from './skill.js'; import { isSkillPath, listSkillFiles, SKILL_EXT } from './skill.js';
import { prepareVfs } from './vfs.js'; import { prepareVfs } from './vfs.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { DRAFT_NAMES } from '../services/draftManager.js';
/** /**
* find_files — lists the contents of the virtual filesystem (VFS). * find_files — lists the contents of the virtual filesystem (VFS).
@@ -174,30 +173,6 @@ export function createFindFilesTool({ googleAuthId }) {
); );
} }
if (cleanDir.replace(/\/+$/, '').toLowerCase() === 'drafts') {
const draftEntries = Array.from(DRAFT_NAMES).map((name) => ({
path: `drafts/${name}`,
name,
type: 'file',
size: null,
modified: null,
}));
const page = draftEntries.slice(seek, seek + vfsLimit);
const truncated = Math.max(0, draftEntries.length - (seek + page.length));
const result = {
kind: 'files',
dirname: 'drafts',
recursive: recursive === true,
total: draftEntries.length,
returned: page.length,
seek,
entries: page,
};
if (truncated > 0) {
result.truncated = truncated;
}
return result;
}
const absDir = path.resolve(vfsRoot, cleanDir); const absDir = path.resolve(vfsRoot, cleanDir);
const rel = path.relative(vfsRoot, absDir); const rel = path.relative(vfsRoot, absDir);
@@ -213,16 +188,6 @@ export function createFindFilesTool({ googleAuthId }) {
} }
const all = walkVfs(absDir, vfsRoot, recursive === true, 0, maxDepth); const all = walkVfs(absDir, vfsRoot, recursive === true, 0, maxDepth);
if (cleanDir === '.') {
if (!all.some((e) => e.path === 'drafts/')) {
all.push({ path: 'drafts/', name: 'drafts', type: 'dir', size: null, modified: null });
}
if (recursive === true) {
for (const name of DRAFT_NAMES) {
all.push({ path: `drafts/${name}`, name, type: 'file', size: null, modified: null });
}
}
}
// Directories first, then alphabetical — makes a truncated listing predictable // Directories first, then alphabetical — makes a truncated listing predictable
// instead of arbitrary. // instead of arbitrary.
all.sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === 'dir' ? -1 : 1)); all.sort((a, b) => (a.type === b.type ? a.path.localeCompare(b.path) : a.type === 'dir' ? -1 : 1));

View File

@@ -6,7 +6,6 @@ import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from '
import { prepareVfs, syncVfsToDb } from './vfs.js'; import { prepareVfs, syncVfsToDb } from './vfs.js';
import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { getStore } from '../db/store.js'; import { getStore } from '../db/store.js';
import { parseDraftPath, requestDraftRead, notifyDraftUpdate, getCachedDraft } from '../services/draftManager.js';
/** /**
* patch — applies edit operations to files in the virtual filesystem (VFS). * patch — applies edit operations to files in the virtual filesystem (VFS).
@@ -177,12 +176,7 @@ export function createPatchTool({ googleAuthId }) {
); );
} }
const draftInfo = parseDraftPath(clean); validated.push({ index: i, op, clean, abs, isSkill: looksSkillByExt, skillBody });
if (draftInfo.isFolder) {
throw new ToolError(`Operation at index ${i}: "${clean}" is a directory and cannot be patched.`);
}
validated.push({ index: i, op, clean, abs, isSkill: looksSkillByExt, skillBody, draftInfo });
} }
// ── Apply ────────────────────────────────────────────────────── // ── Apply ──────────────────────────────────────────────────────
@@ -190,56 +184,7 @@ export function createPatchTool({ googleAuthId }) {
const touchedSkills = new Set(); const touchedSkills = new Set();
for (const v of validated) { for (const v of validated) {
const { op, clean, abs, isSkill, draftInfo } = v; const { op, clean, abs, isSkill } = v;
if (draftInfo?.isDraft) {
const isCreate = op.search === null || op.search === undefined || op.search === '';
let original = '';
if (!isCreate) {
try {
const readVal = await requestDraftRead({ userId: googleAuthId, name: draftInfo.name });
original = typeof readVal === 'string' ? readVal : JSON.stringify(readVal ?? {}, null, 2);
} catch (err) {
const cached = getCachedDraft({ userId: googleAuthId, name: draftInfo.name });
if (cached) {
original = typeof cached.content === 'string' ? cached.content : JSON.stringify(cached.content ?? {}, null, 2);
} else {
throw new ToolError(`Cannot read draft "${clean}" before patching: ${err.message}`);
}
}
}
let draftUpdated;
if (isCreate) {
draftUpdated = typeof op.replace === 'string' ? op.replace : JSON.stringify(op.replace, null, 2);
} else {
const haystack = original.includes('\r\n') ? original.replace(/\r\n/g, '\n') : original;
const needle = typeof op.search === 'string' ? op.search.replace(/\r\n/g, '\n') : '';
if (!needle) {
throw new ToolError(`Operation at index ${v.index} ("${clean}") has an empty "search". Use "search": null to overwrite.`);
}
const at = haystack.indexOf(needle);
if (at === -1) {
throw new ToolError(`SEARCH/REPLACE failed in "${clean}": the search text was not found. Read the draft again with read_file.`);
}
const replacement = typeof op.replace === 'string' ? op.replace : JSON.stringify(op.replace, null, 2);
draftUpdated = haystack.slice(0, at) + replacement + haystack.slice(at + needle.length);
}
let parsed = draftUpdated;
try { parsed = JSON.parse(draftUpdated); } catch {}
const store = await getStore();
const row = await store.google_auth.getById(googleAuthId);
const companyId = row?.selectedCompanyId;
notifyDraftUpdate({
companyId,
userId: googleAuthId,
name: draftInfo.name,
content: parsed,
});
applied.push(`PATCH ${clean}`);
continue;
}
const isCreate = op.search === null || op.search === undefined || op.search === ''; const isCreate = op.search === null || op.search === undefined || op.search === '';
const exists = fs.existsSync(abs); const exists = fs.existsSync(abs);
@@ -317,10 +262,7 @@ export function createPatchTool({ googleAuthId }) {
if (isSkill) touchedSkills.add(clean); if (isSkill) touchedSkills.add(clean);
} }
// Persist immediately for disk files — unlike javascript_execute, there is no trailing sync, so a if (applied.length > 0) {
// text edit would otherwise live only in the on-disk cache until some other tool ran.
const hasDiskWrites = validated.some((v) => !v.draftInfo?.isDraft);
if (hasDiskWrites) {
try { try {
await syncVfsToDb({ googleAuthId, vfsRoot }); await syncVfsToDb({ googleAuthId, vfsRoot });
} catch (err) { } catch (err) {

View File

@@ -105,6 +105,9 @@ export const TOOL_DESCRIPTIONS = {
'Subagents can call all tools you can call except subagent, ask_user, and set_user_route. ' + 'Subagents can call all tools you can call except subagent, ask_user, and set_user_route. ' +
'To efficiently communicate with subagents, store the input and output data in VFS files ' + 'To efficiently communicate with subagents, store the input and output data in VFS files ' +
'and provide these paths in the subagent instructions.', 'and provide these paths in the subagent instructions.',
draft_delivery_manifest:
'Stores extracted invoice/delivery-note data: header metadata (basedata), notes, and product line items. Each component is strictly optional.',
}; };
// #endregion // #endregion
@@ -254,10 +257,30 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
subagent: { subagent: {
note: NOTE_DESC, note: NOTE_DESC,
task: 'Specific instructions and goal for the subagent.', task: 'Specific instructions and goal for the subagent.',
skills: 'Array of SKILL names or slugs to inject directly into the subagent system prompt (e.g. ["api-article", "label"]).', skills: 'Array of SKILLs (e.g. ["api-article.SKILL", "label.SKILL"]) to inject directly into the subagent system prompt.',
conversation_id: 'Existing subagent conversation ID to follow up on, or null to create a new one.', conversation_id: 'Existing subagent conversation ID to follow up on, or null to create a new one.',
max_iterations: 'Safety bound on tool loops (default 15, max 30).', max_iterations: 'Safety bound on tool loops (default 15, max 30).',
}, },
draft_delivery_manifest: {
basedata: 'Header/metadata. Only include fields that are currently missing or changed.',
date: 'Document date (ISO 8601).',
lieferant_id: 'Lieferant ID.',
lieferantName: 'Lieferant name.',
manifestNr: 'Lieferscheinnummer or Rechnungsnummer.',
manifestType: 'Document type.',
notes: 'New noteworthy details (in German), for a human to double-check the result.',
items: 'New product line items.',
numberOfItemsToAdd: 'Number of NEW line items being added in this call.',
items_list: 'Invoice/delivery product line items.',
position: 'Position.',
count: 'Quantity (Menge).',
vpe: 'Packaging unit size (VPE). Default: 1.',
articleName: 'Article name.',
liefArtNr: 'Supplier article number.',
mwst: 'VAT rate; null if missing.',
eknetto: 'Net price per vpe in euro cents; null if missing.',
},
}; };
export const PATCH_PARAMETER_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch; export const PATCH_PARAMETER_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch;
@@ -333,17 +356,17 @@ export async function buildSystemPrompt(vfsRoot, googleAuthId) {
} }
} }
const today = new Date().toISOString().split('T')[0]; const today = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' });
const basePrompt = [ const basePrompt = [
'QuixPOS Article Management System API Assistant', 'QuixPOS Article Management System API Assistant',
'[TESTSYSTEM: destructive commands allowed; primary goal is finding bugs]', '[TESTSYSTEM: destructive commands allowed; primary goal is finding bugs]',
'', '---',
SYSTEM_PROMPT_RULES, SYSTEM_PROMPT_RULES,
'', '---',
SYSTEM_PROMPT_ROUTE_MATRIX, SYSTEM_PROMPT_ROUTE_MATRIX,
'', '---',
`Date: ${today}`, `Date: ${today}`,
'', '---',
buildSkillIndex(vfsRoot), buildSkillIndex(vfsRoot),
].join('\n'); ].join('\n');

View File

@@ -5,7 +5,6 @@ import { isSkillPath, parseSkill, SKILL_EXT } from './skill.js';
import { getApiRouteSkill } from './routes-skills.js'; import { getApiRouteSkill } from './routes-skills.js';
import { prepareVfs } from './vfs.js'; import { prepareVfs } from './vfs.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { parseDraftPath, requestDraftRead } from '../services/draftManager.js';
const DEFAULT_LIMIT = 400; const DEFAULT_LIMIT = 400;
const MAX_LIMIT = 5000; const MAX_LIMIT = 5000;
@@ -109,26 +108,7 @@ export function createReadFileTool({ googleAuthId }) {
throw new ToolError(`Access denied: path "${filename}" escapes virtual filesystem boundary.`); throw new ToolError(`Access denied: path "${filename}" escapes virtual filesystem boundary.`);
} }
const draftInfo = parseDraftPath(clean); const virtualSkill = !fs.existsSync(absPath) && isSkillPath(clean) ? getApiRouteSkill(clean) : null;
if (draftInfo.isFolder) {
throw new ToolError('"drafts" is a directory. Use find_files to list its contents.');
}
let raw;
let virtualSkill = null;
if (draftInfo.isDraft) {
try {
const content = await requestDraftRead({
userId: googleAuthId,
name: draftInfo.name,
});
const str = typeof content === 'string' ? content : JSON.stringify(content ?? {}, null, 2);
raw = Buffer.from(str, 'utf8');
} catch (err) {
throw new ToolError(`Failed to read draft "${draftInfo.name}": ${err.message}`);
}
} else {
virtualSkill = !fs.existsSync(absPath) && isSkillPath(clean) ? getApiRouteSkill(clean) : null;
if (!virtualSkill && !fs.existsSync(absPath)) { if (!virtualSkill && !fs.existsSync(absPath)) {
throw new ToolError( throw new ToolError(
`File not found in virtual filesystem: "${clean}". ` + `File not found in virtual filesystem: "${clean}". ` +
@@ -138,8 +118,7 @@ export function createReadFileTool({ googleAuthId }) {
if (!virtualSkill && !fs.statSync(absPath).isFile()) { if (!virtualSkill && !fs.statSync(absPath).isFile()) {
throw new ToolError(`"${clean}" is not a file. Use find_files to list directories.`); throw new ToolError(`"${clean}" is not a file. Use find_files to list directories.`);
} }
raw = virtualSkill ? Buffer.from(virtualSkill.content, 'utf8') : fs.readFileSync(absPath); const raw = virtualSkill ? Buffer.from(virtualSkill.content, 'utf8') : fs.readFileSync(absPath);
}
if (looksBinary(raw)) { if (looksBinary(raw)) {
const ext = path.extname(clean).toLowerCase(); const ext = path.extname(clean).toLowerCase();
throw new ToolError( throw new ToolError(

View File

@@ -28,6 +28,7 @@ import { createApiSuggestionsOrIssuesTool } from './apisuggestionsorissues.js';
import { createSearchArticlesTool } from './search-articles.js'; import { createSearchArticlesTool } from './search-articles.js';
import { createEanChecksumTool } from './ean-checksum.js'; import { createEanChecksumTool } from './ean-checksum.js';
import { createNetGrossCalcTool } from './net-gross-calc.js'; import { createNetGrossCalcTool } from './net-gross-calc.js';
import { createDraftDeliveryManifestTool } from './draft-delivery-manifest.js';
/** /**
* subagent — delegates a self-contained task to an isolated LLMOrbiter run that lives in * subagent — delegates a self-contained task to an isolated LLMOrbiter run that lives in
@@ -179,6 +180,7 @@ function createSubagentTools({ googleAuthId, companyId, model }) {
createSearchArticlesTool({ googleAuthId, companyId }), createSearchArticlesTool({ googleAuthId, companyId }),
createEanChecksumTool(), createEanChecksumTool(),
createNetGrossCalcTool(), createNetGrossCalcTool(),
createDraftDeliveryManifestTool(),
]; ];
} }

View File

@@ -4,14 +4,6 @@ import { getStore } from '../db/store.js';
import env from '../config/env.js'; import env from '../config/env.js';
import { getDefaultSkills } from './skill.js'; import { getDefaultSkills } from './skill.js';
import { getDefaultApplets, DEFAULT_APPLETS_VERSION, APPLET_EXT } from './applet.js'; import { getDefaultApplets, DEFAULT_APPLETS_VERSION, APPLET_EXT } from './applet.js';
import {
parseDraftPath,
requestDraftRead,
notifyDraftUpdate,
notifyDraftUpdateSync,
getCachedDraft,
DRAFT_NAMES,
} from '../services/draftManager.js';
const FULL_HASH_REGEX = /^[a-zA-Z0-9]{8}$/; const FULL_HASH_REGEX = /^[a-zA-Z0-9]{8}$/;
const VIRTUAL_IMAGE_FOLDERS = new Set(['image', 'images']); const VIRTUAL_IMAGE_FOLDERS = new Set(['image', 'images']);
@@ -162,8 +154,8 @@ export async function loadDefaultSkills({ googleAuthId, store: passedStore, vfsR
if (!fs.existsSync(diskPath)) { if (!fs.existsSync(diskPath)) {
fs.mkdirSync(path.dirname(diskPath), { recursive: true }); fs.mkdirSync(path.dirname(diskPath), { recursive: true });
fs.writeFileSync(diskPath, skill.content, 'utf8'); fs.writeFileSync(diskPath, skill.content, 'utf8');
}
loaded.push(skill.name); loaded.push(skill.name);
}
} catch (err) { } catch (err) {
console.error(`[vfs] failed to write default skill "${skill.name}" to disk:`, err.message); console.error(`[vfs] failed to write default skill "${skill.name}" to disk:`, err.message);
} }
@@ -172,6 +164,34 @@ export async function loadDefaultSkills({ googleAuthId, store: passedStore, vfsR
return loaded; return loaded;
} }
/**
* Restores all shipped system default SKILL files into the company's VFS.
*
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
* @returns {Promise<Array<string>>} List of restored skill file names
*/
export async function restoreDefaultSkills({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
const store = passedStore || await getStore();
const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId);
fs.mkdirSync(vfsRoot, { recursive: true });
const defaultSkills = getDefaultSkills();
const restored = [];
for (const skill of defaultSkills) {
try {
const diskPath = path.resolve(vfsRoot, skill.name);
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
fs.writeFileSync(diskPath, skill.content, 'utf8');
restored.push(skill.name);
} catch (err) {
console.error(`[vfs] failed to restore default skill "${skill.name}":`, err.message);
}
}
return restored;
}
/** /**
* Loads the shipped example `.applet.js` files into the selected company's VFS. * Loads the shipped example `.applet.js` files into the selected company's VFS.
* *
@@ -194,8 +214,8 @@ export async function loadDefaultApplets({ googleAuthId, store: passedStore, vfs
const diskPath = path.resolve(vfsRoot, name); const diskPath = path.resolve(vfsRoot, name);
if (!fs.existsSync(diskPath)) { if (!fs.existsSync(diskPath)) {
fs.writeFileSync(diskPath, applet.content, 'utf8'); fs.writeFileSync(diskPath, applet.content, 'utf8');
}
seeded.push(name); seeded.push(name);
}
} catch (err) { } catch (err) {
console.error(`[vfs] failed to write default applet "${name}" to disk:`, err.message); console.error(`[vfs] failed to write default applet "${name}" to disk:`, err.message);
} }
@@ -204,6 +224,32 @@ export async function loadDefaultApplets({ googleAuthId, store: passedStore, vfs
return seeded; return seeded;
} }
/**
* Restores all shipped system default example applets into the company's VFS.
*
* @param {{ googleAuthId: number, store?: object, vfsRoot?: string }} ctx
* @returns {Promise<Array<string>>} List of restored applet file names
*/
export async function restoreDefaultApplets({ googleAuthId, store: passedStore, vfsRoot: passedVfsRoot } = {}) {
const store = passedStore || await getStore();
const vfsRoot = passedVfsRoot || await vfsRootFor(store, googleAuthId);
fs.mkdirSync(vfsRoot, { recursive: true });
const restored = [];
for (const applet of getDefaultApplets()) {
const name = `${DEFAULT_APPLETS_VERSION}-${applet.name}`;
try {
const diskPath = path.resolve(vfsRoot, name);
fs.writeFileSync(diskPath, applet.content, 'utf8');
restored.push(name);
} catch (err) {
console.error(`[vfs] failed to restore default applet "${name}":`, err.message);
}
}
return restored;
}
/** /**
* Initializes the VFS for the selected company with default SKILL files and example applets. * Initializes the VFS for the selected company with default SKILL files and example applets.
* *
@@ -308,16 +354,6 @@ export function createVfs(vfsRoot, options = {}) {
const vfs = { const vfs = {
readFileSync: (p, opt) => { readFileSync: (p, opt) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isDraft) {
const cached = getCachedDraft({ userId: googleAuthId, name: draftInfo.name });
if (cached) {
const str = typeof cached.content === 'string' ? cached.content : JSON.stringify(cached.content ?? {}, null, 2);
const encoding = typeof opt === 'string' ? opt : opt?.encoding;
return encoding ? str : Buffer.from(str, 'utf8');
}
throw new Error(`Virtual draft "${draftInfo.name}" is not cached in memory. Use vfs.promises.readFile() to fetch it asynchronously.`);
}
const hash = extractFullHash(p); const hash = extractFullHash(p);
if (hash) { if (hash) {
const buf = syncFetchImage(hash); const buf = syncFetchImage(hash);
@@ -360,19 +396,6 @@ export function createVfs(vfsRoot, options = {}) {
}, },
writeFileSync: (p, data, opt) => { writeFileSync: (p, data, opt) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isDraft) {
let str = typeof data === 'string' ? data : Buffer.isBuffer(data) ? data.toString('utf8') : JSON.stringify(data);
let parsed = str;
try { parsed = JSON.parse(str); } catch {}
notifyDraftUpdateSync({
googleAuthId,
clientId: options.clientId,
name: draftInfo.name,
content: parsed,
});
return;
}
assertNotReadOnlyDatabasePath(p); assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
fs.mkdirSync(path.dirname(diskPath), { recursive: true }); fs.mkdirSync(path.dirname(diskPath), { recursive: true });
@@ -413,8 +436,6 @@ export function createVfs(vfsRoot, options = {}) {
} }
}, },
existsSync: (p) => { existsSync: (p) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isFolder || draftInfo.isDraft) return true;
const hash = extractFullHash(p); const hash = extractFullHash(p);
if (hash) { if (hash) {
const buf = syncFetchImage(hash); const buf = syncFetchImage(hash);
@@ -429,31 +450,6 @@ export function createVfs(vfsRoot, options = {}) {
}, },
statSync: (p, opt) => { statSync: (p, opt) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isFolder) {
const now = new Date();
return {
isFile: () => false,
isDirectory: () => true,
size: 0,
mtime: now,
ctime: now,
atime: now,
};
}
if (draftInfo.isDraft) {
const cached = getCachedDraft({ userId: googleAuthId, name: draftInfo.name });
const now = cached?.updatedAt ? new Date(cached.updatedAt) : new Date();
const size = cached?.size ?? 0;
return {
isFile: () => true,
isDirectory: () => false,
size,
mtime: now,
ctime: now,
atime: now,
};
}
const hash = extractFullHash(p); const hash = extractFullHash(p);
if (hash) { if (hash) {
const buf = syncFetchImage(hash); const buf = syncFetchImage(hash);
@@ -476,29 +472,15 @@ export function createVfs(vfsRoot, options = {}) {
lstatSync: (p, opt) => vfs.statSync(p, opt), lstatSync: (p, opt) => vfs.statSync(p, opt),
readdirSync: (p = '.', opt) => { readdirSync: (p = '.', opt) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isFolder) {
return Array.from(DRAFT_NAMES);
}
const clean = typeof p === 'string' ? p.trim().replace(/^([a-zA-Z]:|\/)+/, '') : ''; const clean = typeof p === 'string' ? p.trim().replace(/^([a-zA-Z]:|\/)+/, '') : '';
if (VIRTUAL_IMAGE_FOLDERS.has(clean.toLowerCase())) { if (VIRTUAL_IMAGE_FOLDERS.has(clean.toLowerCase())) {
return []; return [];
} }
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
const entries = fs.readdirSync(diskPath, opt); return fs.readdirSync(diskPath, opt);
if ((clean === '' || clean === '.') && Array.isArray(entries)) {
if (!entries.some((e) => (typeof e === 'string' ? e : e.name) === 'drafts')) {
entries.push('drafts');
}
}
return entries;
}, },
mkdirSync: (p, opt) => { mkdirSync: (p, opt) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isFolder || draftInfo.isDraft) {
throw new Error(`Access denied: "${p}" is a virtual draft and cannot be created on disk.`);
}
assertNotReadOnlyDatabasePath(p); assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
try { try {
@@ -508,30 +490,18 @@ export function createVfs(vfsRoot, options = {}) {
} }
}, },
unlinkSync: (p) => { unlinkSync: (p) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isFolder || draftInfo.isDraft) {
throw new Error(`Access denied: "${p}" is a virtual draft and cannot be unlinked on disk.`);
}
assertNotReadOnlyDatabasePath(p); assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
return fs.unlinkSync(diskPath); return fs.unlinkSync(diskPath);
}, },
rmdirSync: (p, opt) => { rmdirSync: (p, opt) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isFolder || draftInfo.isDraft) {
throw new Error(`Access denied: "${p}" is a virtual draft and cannot be removed on disk.`);
}
assertNotReadOnlyDatabasePath(p); assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
return fs.rmdirSync(diskPath, opt); return fs.rmdirSync(diskPath, opt);
}, },
rmSync: (p, opt) => { rmSync: (p, opt) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isFolder || draftInfo.isDraft) {
throw new Error(`Access denied: "${p}" is a virtual draft and cannot be removed on disk.`);
}
assertNotReadOnlyDatabasePath(p); assertNotReadOnlyDatabasePath(p);
const diskPath = safeResolve(p); const diskPath = safeResolve(p);
return fs.rmSync(diskPath, opt); return fs.rmSync(diskPath, opt);
@@ -539,17 +509,6 @@ export function createVfs(vfsRoot, options = {}) {
promises: { promises: {
readFile: async (p, opt) => { readFile: async (p, opt) => {
const draftInfo = parseDraftPath(p);
if (draftInfo.isDraft) {
const content = await requestDraftRead({
userId: googleAuthId,
clientId: options.clientId,
name: draftInfo.name,
});
const str = typeof content === 'string' ? content : JSON.stringify(content ?? {}, null, 2);
const encoding = typeof opt === 'string' ? opt : opt?.encoding;
return encoding ? str : Buffer.from(str, 'utf8');
}
const hash = extractFullHash(p); const hash = extractFullHash(p);
if (hash) { if (hash) {
const buf = syncFetchImage(hash); const buf = syncFetchImage(hash);
@@ -577,29 +536,7 @@ export function createVfs(vfsRoot, options = {}) {
} }
return vfs.readFileSync(p, opt); return vfs.readFileSync(p, opt);
}, },
writeFile: async (p, data, opt) => { writeFile: async (p, data, opt) => vfs.writeFileSync(p, data, opt),
const draftInfo = parseDraftPath(p);
if (draftInfo.isDraft) {
let str = typeof data === 'string' ? data : Buffer.isBuffer(data) ? data.toString('utf8') : JSON.stringify(data);
let parsed = str;
try { parsed = JSON.parse(str); } catch {}
let companyId = options.companyId;
if (companyId == null && googleAuthId) {
const store = await getStore();
const row = await store.google_auth.getById(googleAuthId);
companyId = row?.selectedCompanyId;
}
notifyDraftUpdate({
companyId,
userId: googleAuthId,
clientId: options.clientId,
name: draftInfo.name,
content: parsed,
});
return;
}
return vfs.writeFileSync(p, data, opt);
},
appendFile: async (p, data, opt) => vfs.appendFileSync(p, data, opt), appendFile: async (p, data, opt) => vfs.appendFileSync(p, data, opt),
stat: async (p, opt) => vfs.statSync(p, opt), stat: async (p, opt) => vfs.statSync(p, opt),
readdir: async (p, opt) => vfs.readdirSync(p, opt), readdir: async (p, opt) => vfs.readdirSync(p, opt),