This commit is contained in:
seb
2026-10-15 15:55:01 +02:00
parent 529b142341
commit e462a3d250
6 changed files with 356 additions and 2 deletions

View File

@@ -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` |

View File

@@ -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`

View File

@@ -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`

View File

@@ -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`._

View File

@@ -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:

View File

@@ -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);