From e462a3d250e3afcf37daa4b3c51c252bcfb26b16 Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 15 Oct 2026 15:55:01 +0200 Subject: [PATCH] u --- docs/api/README.md | 1 + docs/api/routes-all.md | 5 +- docs/api/routes.md | 5 +- docs/api/routes/quote/order.md | 210 +++++++++++++++++++++++++++++++++ src/api/routes/quote.js | 131 ++++++++++++++++++++ src/db/order.js | 6 + 6 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 docs/api/routes/quote/order.md diff --git a/docs/api/README.md b/docs/api/README.md index 76cac1b..a87bfee 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -231,6 +231,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/quote/update.md`](routes/quote/update.md) | `PATCH /quote/{id}` | | [`routes/quote/_id/file/_fileId.delete.md`](routes/quote/_id/file/_fileId.delete.md) | `DELETE /quote/{id}/file/{fileId}` | | [`routes/quote/_id/file/_fileId.get.md`](routes/quote/_id/file/_fileId.get.md) | `GET /quote/{id}/file/{fileId}` | +| [`routes/quote/order.md`](routes/quote/order.md) | `POST /quote/{id}/order` | | [`routes/quote/_id/pdf.md`](routes/quote/_id/pdf.md) | `POST /quote/{id}/pdf` | | [`routes/serper/image.md`](routes/serper/image.md) | `POST /serper/image` | | [`routes/serper/images.md`](routes/serper/images.md) | `POST /serper/images` | diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index 31e7868..2a460b3 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -1,6 +1,6 @@ # Dash API Routes Overview (All) -Overview of all 246 API endpoints across all modules. +Overview of all 247 API endpoints across all modules. ### Notation @@ -782,6 +782,9 @@ _none_ #### Get quote attachment file content — `GET /quote/{id}/file/{fileId}` - Params: `id: int, fileId: int, query: download?: bool` - Returns: `200 str` +#### 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?: T34[] }` +- Returns: `201 T1` #### Generate and attach quote PDF — `POST /quote/{id}/pdf` - Params: `id: int` - Returns: `201 T13` diff --git a/docs/api/routes.md b/docs/api/routes.md index 12cf92b..f962076 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -1,6 +1,6 @@ # Dash API Routes Overview -Overview of all 171 API endpoints across all modules. +Overview of all 172 API endpoints across all modules. ### Notation @@ -538,6 +538,9 @@ _none_ #### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}` - Params: `id: int, fileId: int` - Returns: `204 void` +#### 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?: T28[] }` +- Returns: `201 T1` #### Generate and attach quote PDF — `POST /quote/{id}/pdf` - Params: `id: int` - Returns: `201 T11` diff --git a/docs/api/routes/quote/order.md b/docs/api/routes/quote/order.md new file mode 100644 index 0000000..4ba6b72 --- /dev/null +++ b/docs/api/routes/quote/order.md @@ -0,0 +1,210 @@ +# `POST /quote/{id}/order` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create order from quote + +Creates a sales order from an existing quote for the authenticated user's selected company. +Copies lines, customer entity, and external number from the quote unless explicitly overridden. +If the order number is omitted, it is auto-allocated from the sequence. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Quote ID | + +## Body (JSON) — _optional_ + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `number` | no | string | Optional sales order number. Auto-allocated from sequence if omitted. | +| `date` | no | string | Optional order date (ISO format). Defaults to current date. | +| `createdAt` | no | string | Optional creation timestamp. | +| `entityId` | no | integer | Customer entity ID override. Defaults to quote's entityId. | +| `externalNumber` | no | string | External customer reference override. Defaults to quote's externalNumber. | +| `lines` | no | ManifestLineInput[] | Optional line items override. Defaults to quote's lines. | + +```json +{ + "number": "…", + "date": "…", + "createdAt": "2026-08-10T00:00:00.000Z", + "entityId": 1, + "externalNumber": "…", + "lines": [ + { + "sku": "…", + "name": "…", + "pu": 1, + "amount": 1, + "net": 1, + "taxrate": 1, + "rebate": 1, + "comment": "…" + } + ] +} +``` + +## Response `201` + +Created order + +```json +{ + "order": { + "id": 1, + "number": "…", + "type": "order", + "entityId": 1, + "externalNumber": "…", + "date": "2026-08-10T00:00:00.000Z", + "createdAt": "2026-08-10T00:00:00.000Z", + "lines": [ + { + "id": 1, + "sku": "…", + "name": "…", + "pu": 1, + "amount": 1, + "net": 1, + "rebate": 1, + "taxrate": 1, + "comment": "…" + } + ], + "shipping": { + "id": 1, + "manifestId": 1, + "number": "…", + "trackingCode": "…", + "carrier": "…", + "type": "…", + "state": "…" + }, + "shippings": [ + { + "id": 1, + "manifestId": 1, + "number": "…", + "trackingCode": "…", + "carrier": "…", + "type": "…", + "state": "…" + } + ], + "payment": { + "id": 1, + "accountId": 1, + "manifestId": 1, + "type": "…", + "amount": 1 + }, + "payments": [ + { + "id": 1, + "accountId": 1, + "manifestId": 1, + "type": "…", + "amount": 1 + } + ], + "paid": true, + "shipped": true, + "delivered": true, + "orderTotal": 1, + "paidTotal": 1, + "orderedQuantity": 1, + "shippedQuantity": 1, + "shipments": [ + { + "id": 1, + "number": "…", + "type": "shipment", + "entityId": 1, + "externalNumber": "…", + "date": "…", + "createdAt": "…", + "lineCount": 1, + "packageCount": 1, + "lines": [ + { + "id": 1, + "sku": "…", + "name": "…", + "pu": 1, + "amount": 1, + "net": 1, + "rebate": 1, + "taxrate": 1, + "comment": "…" + } + ], + "shippings": [ + { + "id": 1, + "manifestId": 1, + "number": "…", + "trackingCode": "…", + "carrier": "…", + "type": "…", + "state": "…" + } + ], + "files": [ + { + "id": 1, + "manifestId": 1, + "fileId": 1, + "sort": 1, + "hash": "…", + "mimetype": "…", + "name": "…", + "size": 1, + "created": "2026-08-10T00:00:00.000Z", + "modified": "2026-08-10T00:00:00.000Z", + "skeledash": "…", + "thumbx": 1, + "thumby": 1, + "meta": {} + } + ] + } + ], + "files": [ + { + "id": 1, + "manifestId": 1, + "fileId": 1, + "sort": 1, + "hash": "…", + "mimetype": "…", + "name": "…", + "size": 1, + "created": "2026-08-10T00:00:00.000Z", + "modified": "2026-08-10T00:00:00.000Z", + "skeledash": "…", + "thumbx": 1, + "thumby": 1, + "meta": {} + } + ] + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected, invalid quote id, or invalid order data — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized for company — `{"error":"…"}` | +| 404 | Quote not found or entity not found — `{"error":"…"}` | +| 500 | Server error — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/src/api/routes/quote.js b/src/api/routes/quote.js index d35e71c..b342f68 100644 --- a/src/api/routes/quote.js +++ b/src/api/routes/quote.js @@ -1,4 +1,5 @@ import * as db from '../../db/quote.js'; +import * as orderDb from '../../db/order.js'; import { createAndAttachManifestPdf } from '../../services/manifestPdf.js'; import { emit } from '../sse.js'; @@ -17,12 +18,14 @@ function quoteErrorStatus(error) { if (error.message === 'Not authorized') return 403; if (error.message === 'Company not found') return 404; if (error.message === 'Quote not found') return 404; + if (error.message === 'Order not found') return 404; if (error.message === 'Manifest not found') return 404; 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 === 'Invalid entityId') return 400; if (error.message === 'Invalid quote') return 400; + if (error.message === 'Invalid order') return 400; if (error.message === 'Invalid lines') return 400; if (error.message === 'File name required') return 400; if (error.message === 'Mimetype required') return 400; @@ -570,6 +573,134 @@ export function register(app, basePath = '') { // #endregion + /** + * @openapi + * /quote/{id}/order: + * post: + * tags: [Quote] + * summary: Create order from quote + * description: | + * Creates a sales order from an existing quote for the authenticated user's selected company. + * Copies lines, customer entity, and external number from the quote unless explicitly overridden. + * If the order number is omitted, it is auto-allocated from the sequence. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Quote ID + * requestBody: + * required: false + * content: + * application/json: + * schema: + * type: object + * properties: + * number: + * type: string + * description: Optional sales order number. Auto-allocated from sequence if omitted. + * date: + * type: string + * description: Optional order date (ISO format). Defaults to current date. + * createdAt: + * type: string + * format: date-time + * description: Optional creation timestamp. + * entityId: + * type: integer + * nullable: true + * description: Customer entity ID override. Defaults to quote's entityId. + * externalNumber: + * type: string + * nullable: true + * description: External customer reference override. Defaults to quote's externalNumber. + * lines: + * type: array + * description: Optional line items override. Defaults to quote's lines. + * items: + * $ref: '#/components/schemas/ManifestLineInput' + * x-markdown: quote/order.md + * responses: + * 201: + * description: Created order + * content: + * application/json: + * schema: + * type: object + * properties: + * order: + * $ref: '#/components/schemas/Order' + * 400: + * description: No company selected, invalid quote id, or invalid order data + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized for company + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Quote not found or entity not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 500: + * description: Server error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + // #region POST /quote/:id/order + + app.post(`${basePath}/quote/:id/order`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const id = Number(req.params.id); + if (!Number.isInteger(id) || id <= 0) { + return res.status(400).json({ error: 'id is required' }); + } + + const { number, date, createdAt, entityId, externalNumber, lines } = req.body ?? {}; + if (entityId !== undefined && entityId !== null && (typeof entityId !== 'number' || !Number.isInteger(entityId) || entityId <= 0)) { + return res.status(400).json({ error: 'Invalid entityId' }); + } + + try { + const order = await orderDb.createFromQuote(req.user.id, id, { + number, + date, + createdAt, + entityId, + externalNumber: externalNumber !== undefined && externalNumber !== null ? String(externalNumber) : externalNumber, + lines, + }); + res.status(201).json({ order }); + } catch (error) { + const status = quoteErrorStatus(error); + if (status === 500) console.error('Create order from quote failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + // #endregion + /** * @openapi * /quote/{id}/pdf: diff --git a/src/db/order.js b/src/db/order.js index cf2e836..98b5095 100644 --- a/src/db/order.js +++ b/src/db/order.js @@ -149,6 +149,12 @@ export async function create(googleAuthId, data) { return enrichSingleOrder(store, googleAuthId, order); } +export async function createFromQuote(googleAuthId, quoteId, data) { + const store = await getStore(); + const order = await store.order.createFromQuote(googleAuthId, quoteId, data); + return enrichSingleOrder(store, googleAuthId, order); +} + export async function update(googleAuthId, id, data) { const store = await getStore(); const order = await store.order.update(googleAuthId, id, data);