From c06a5e3413030f96c26a4065ff1c298fa1d12b07 Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 18 Oct 2026 20:29:48 +0200 Subject: [PATCH] u --- docs/api/routes-all.md | 12 ++++++------ docs/api/routes.md | 12 ++++++------ docs/api/routes/delivery/create.md | 2 +- docs/api/routes/order/create.md | 4 ++-- src/api/openapi.js | 4 +--- src/api/routes/delivery.js | 11 +++++++++-- src/api/routes/order.js | 17 ++++++++++++----- src/api/routes/quote.js | 1 + src/api/routes/shipment.js | 1 + 9 files changed, 39 insertions(+), 25 deletions(-) diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index 462cfa0..12f0f9d 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -9,11 +9,11 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). ### Reusable types -- `T1` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T19|null, shippings?: T19[], payment?: T30|null, payments?: T30[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T9[] }` +- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T19|null, shippings?: T19[], payment?: T30|null, payments?: T30[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T9[] }` - `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T27, brand: T26|null, categoryIds: int[], photos: T24[], eans: str[], prices: T35[], suppliers: T11[] }` - `T3` = `{ id: int, number: str, type: str, date: str, lines: T20[], shippings: T19[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T9[], state?: str }` -- `T4` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }` -- `T5` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T20[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T39 }` +- `T4` = `{ id: int, number: str, type: str, date: str, lines: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }` +- `T5` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T20[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T39 }` - `T6` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T7|null }` - `T7` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T12, totalSizeBytes: int, durationMs: int }` - `T8` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T21[] }, thumb?: str|null }` @@ -403,9 +403,9 @@ _none_ #### List deliveries — `GET /delivery` - 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?: T39 }[], total: int, unfilteredTotal: int, offset: int, limit: int }` +- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T39 }[], total: int, unfilteredTotal: int, offset: int, limit: int }` #### Create delivery — `POST /delivery` -- Params: `body: { externalNumber: str, date: str, lines: T23[], 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, supplierId: int, lines: T23[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Returns: `201 { delivery: T5 }` #### Delete delivery — `DELETE /delivery/{id}` - Params: `id: int` @@ -619,7 +619,7 @@ _none_ - Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }` #### Create order — `POST /order` -- Params: `body: { number: str, date: str, lines: T23[], entityId?: int|null, externalNumber?: str|null }` +- Params: `body: { number: str, date: str, entityId: int, lines: T23[], externalNumber?: str|null }` - Returns: `201 { order: T1 }` #### Delete order — `DELETE /order/{id}` - Params: `id: int` diff --git a/docs/api/routes.md b/docs/api/routes.md index 3c442aa..9bad7ea 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -9,11 +9,11 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). ### Reusable types -- `T1` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T14[], externalNumber?: str|null, createdAt?: str, shipping?: T13|null, shippings?: T13[], payment?: T24|null, payments?: T24[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T7[] }` +- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T14[], externalNumber?: str|null, createdAt?: str, shipping?: T13|null, shippings?: T13[], payment?: T24|null, payments?: T24[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T7[] }` - `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T21, brand: T20|null, categoryIds: int[], photos: T18[], eans: str[], prices: T27[], suppliers: T8[] }` - `T3` = `{ id: int, number: str, type: str, date: str, lines: T14[], shippings: T13[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T7[], state?: str }` -- `T4` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T14[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T7[] }` -- `T5` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T14[], files: T7[], externalNumber?: str|null, createdAt?: str, ledger?: T31 }` +- `T4` = `{ id: int, number: str, type: str, date: str, lines: T14[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T7[] }` +- `T5` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T14[], files: T7[], externalNumber?: str|null, createdAt?: str, ledger?: T31 }` - `T6` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T15[] }, thumb?: str|null }` - `T7` = `{ 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 }` - `T8` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T32|null }` @@ -316,9 +316,9 @@ _none_ #### List deliveries — `GET /delivery` - 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?: T31 }[], total: int, unfilteredTotal: int, offset: int, limit: int }` +- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T31 }[], total: int, unfilteredTotal: int, offset: int, limit: int }` #### Create delivery — `POST /delivery` -- Params: `body: { externalNumber: str, date: str, lines: T17[], 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, supplierId: int, lines: T17[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Returns: `201 { delivery: T5 }` #### Delete delivery — `DELETE /delivery/{id}` - Params: `id: int` @@ -432,7 +432,7 @@ _none_ - Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }` #### Create order — `POST /order` -- Params: `body: { number: str, date: str, lines: T17[], entityId?: int|null, externalNumber?: str|null }` +- Params: `body: { number: str, date: str, entityId: int, lines: T17[], externalNumber?: str|null }` - Returns: `201 { order: T1 }` #### Delete order — `DELETE /order/{id}` - Params: `id: int` diff --git a/docs/api/routes/delivery/create.md b/docs/api/routes/delivery/create.md index dd8731c..82a4289 100644 --- a/docs/api/routes/delivery/create.md +++ b/docs/api/routes/delivery/create.md @@ -17,7 +17,7 @@ The manifest `number` is allocated automatically via |-------|----------|------|-------------| | `externalNumber` | yes | string | User-supplied reference number | | `date` | yes | string | | -| `supplierId` | no | integer | | +| `supplierId` | yes | integer | | | `lines` | yes | DeliveryLineInput[] | | | `files` | no | DeliveryFileInput[] | Attached documents (pdf or txt) stored with the delivery and returned in `files` on every delivery response. Thumbnails (AVIF) are generated server-side. | diff --git a/docs/api/routes/order/create.md b/docs/api/routes/order/create.md index ac15158..7b0676c 100644 --- a/docs/api/routes/order/create.md +++ b/docs/api/routes/order/create.md @@ -6,7 +6,7 @@ Create order Creates a sales order with header and lines in one transaction. `date` is the document date (expected delivery). -`entityId` must be a company customer (not Lieferanten), or null. +`entityId` must be a company customer (not Lieferanten). Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. @@ -16,7 +16,7 @@ Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` |-------|----------|------|-------------| | `number` | yes | string | | | `date` | yes | string | Document date / expected delivery | -| `entityId` | no | integer | Customer entity (manifest.entity_id) | +| `entityId` | yes | integer | Customer entity (manifest.entity_id) | | `externalNumber` | no | string | External customer reference number (manifest.external_number) | | `lines` | yes | ManifestLineInput[] | | diff --git a/src/api/openapi.js b/src/api/openapi.js index 9932f56..4e73fb1 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -746,7 +746,6 @@ * enum: [delivery] * supplierId: * type: integer - * nullable: true * description: manifest.entity_id * externalNumber: * type: string @@ -980,7 +979,7 @@ * nullable: true * QuoteSummary: * type: object - * required: [id, number, type, entityId, date] + * required: [id, number, type, date] * properties: * id: * type: integer @@ -1044,7 +1043,6 @@ * enum: [order] * entityId: * type: integer - * nullable: true * description: Customer (manifest.entity_id) * externalNumber: * type: string diff --git a/src/api/routes/delivery.js b/src/api/routes/delivery.js index 55ce252..97ce358 100644 --- a/src/api/routes/delivery.js +++ b/src/api/routes/delivery.js @@ -169,8 +169,10 @@ function deliveryErrorStatus(error) { if (error.message === 'File not found') return 404; if (error.message === 'Supplier not found') return 404; if (error.message === 'Warehouse not found') return 404; + if (error.message === 'No supplier selected') return 400; if (error.message === 'Invalid supplierId') return 400; if (error.message === 'Article not found') return 404; + if (error.message === 'Delivery number already exists') return 409; if (error.message === 'Invalid delivery') return 400; if (error.message === 'Invalid lines') return 400; if (error.message === 'Delivery has no lines') return 400; @@ -305,7 +307,7 @@ export function register(app, basePath = '') { * application/json: * schema: * type: object - * required: [externalNumber, date, lines] + * required: [externalNumber, date, supplierId, lines] * properties: * externalNumber: * type: string @@ -315,7 +317,6 @@ export function register(app, basePath = '') { * format: date-time * supplierId: * type: integer - * nullable: true * lines: * type: array * items: @@ -497,6 +498,9 @@ export function register(app, basePath = '') { try { const { externalNumber, date, createdAt, supplierId, lines, files } = parseDeliveryPayload(req); + if (supplierId === undefined || supplierId === null) { + return res.status(400).json({ error: 'No supplier selected' }); + } logDeliveryFiles(files, 'create'); const delivery = await db.create(req.user.id, { externalNumber, @@ -795,6 +799,9 @@ export function register(app, basePath = '') { patch.createdAt = parsed.createdAt; } if (Object.prototype.hasOwnProperty.call(parsed.payload, 'supplierId')) { + if (parsed.supplierId === null) { + return res.status(400).json({ error: 'No supplier selected' }); + } patch.supplierId = parsed.supplierId; } if (Object.prototype.hasOwnProperty.call(parsed.payload, 'lines')) { diff --git a/src/api/routes/order.js b/src/api/routes/order.js index 2969595..06d115a 100644 --- a/src/api/routes/order.js +++ b/src/api/routes/order.js @@ -21,6 +21,8 @@ function orderErrorStatus(error) { if (error.message === 'Attachment not found') return 404; if (error.message === 'Entity not found') return 404; if (error.message === 'Article not found') return 404; + if (error.message === 'No entity selected') return 400; + if (error.message === 'Order number already exists') return 409; if (error.message === 'Invalid entityId') return 400; if (error.message === 'Invalid order') return 400; if (error.message === 'Invalid lines') return 400; @@ -148,7 +150,7 @@ export function register(app, basePath = '') { * description: | * Creates a sales order with header and lines in one transaction. * `date` is the document date (expected delivery). - * `entityId` must be a company customer (not Lieferanten), or null. + * `entityId` must be a company customer (not Lieferanten). * Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. * security: * - cookieAuth: [] @@ -159,7 +161,7 @@ export function register(app, basePath = '') { * application/json: * schema: * type: object - * required: [number, date, lines] + * required: [number, date, entityId, lines] * properties: * number: * type: string @@ -169,7 +171,6 @@ export function register(app, basePath = '') { * description: Document date / expected delivery * entityId: * type: integer - * nullable: true * description: Customer entity (manifest.entity_id) * externalNumber: * type: string @@ -297,7 +298,10 @@ export function register(app, basePath = '') { } const { number, date, createdAt, entityId, externalNumber, lines } = req.body ?? {}; - if (entityId !== undefined && entityId !== null && (typeof entityId !== 'number' || !Number.isInteger(entityId) || entityId <= 0)) { + if (entityId === undefined || entityId === null) { + return res.status(400).json({ error: 'No entity selected' }); + } + if (typeof entityId !== 'number' || !Number.isInteger(entityId) || entityId <= 0) { return res.status(400).json({ error: 'Invalid entityId' }); } @@ -538,7 +542,10 @@ export function register(app, basePath = '') { patch.createdAt = body.createdAt; } if (Object.prototype.hasOwnProperty.call(body, 'entityId')) { - if (body.entityId !== null && (typeof body.entityId !== 'number' || !Number.isInteger(body.entityId) || body.entityId <= 0)) { + if (body.entityId === null) { + return res.status(400).json({ error: 'No entity selected' }); + } + if (typeof body.entityId !== 'number' || !Number.isInteger(body.entityId) || body.entityId <= 0) { return res.status(400).json({ error: 'Invalid entityId' }); } patch.entityId = body.entityId; diff --git a/src/api/routes/quote.js b/src/api/routes/quote.js index e939edf..e98df48 100644 --- a/src/api/routes/quote.js +++ b/src/api/routes/quote.js @@ -26,6 +26,7 @@ function quoteErrorStatus(error) { if (error.message === 'Entity not found') return 404; if (error.message === 'Article not found') return 404; if (error.message === 'Invalid entityId') return 400; + if (error.message === 'Quote number already exists') return 409; if (error.message === 'Invalid quote') return 400; if (error.message === 'Invalid order') return 400; if (error.message === 'Invalid lines') return 400; diff --git a/src/api/routes/shipment.js b/src/api/routes/shipment.js index a894dc5..9323941 100644 --- a/src/api/routes/shipment.js +++ b/src/api/routes/shipment.js @@ -37,6 +37,7 @@ function shipmentErrorStatus(error) { } if ( error.message?.startsWith('Shipment amount exceeds') || + error.message === 'Shipment number already exists' || error.message?.includes('cannot be deleted') || error.message?.includes('cannot be undone') || error.message?.includes('already shipped')