From 1860f38640894391224d5f9a17b4f787433978d8 Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 6 Aug 2026 05:32:48 +0200 Subject: [PATCH] Add delivery and supplier HTTP APIs with expanded response docs. Wire store-backed CRUD for goods receipts and suppliers, and teach the API doc generator to expand $ref/allOf/array samples so route markdown shows full JSON shapes. --- docs/api/README.md | 9 + docs/api/openapi.json | 966 ++++++++++++++++++++++ docs/api/routes/article/add-image.md | 16 +- docs/api/routes/article/create.md | 7 +- docs/api/routes/article/get.md | 2 +- docs/api/routes/article/order-images.md | 4 +- docs/api/routes/article/set-brand.md | 2 +- docs/api/routes/article/set-categories.md | 2 +- docs/api/routes/article/set-eans.md | 2 +- docs/api/routes/article/set-prices.md | 2 +- docs/api/routes/article/set-weight.md | 2 +- docs/api/routes/article/update.md | 2 +- docs/api/routes/articleIndex.md | 14 +- docs/api/routes/auth/login.md | 7 +- docs/api/routes/auth/me.md | 7 +- docs/api/routes/auth/selected-company.md | 7 +- docs/api/routes/brand/list.md | 8 +- docs/api/routes/brand/upsert.md | 6 +- docs/api/routes/category/add-image.md | 7 +- docs/api/routes/category/articles.md | 4 +- docs/api/routes/category/create.md | 7 +- docs/api/routes/category/delete-image.md | 7 +- docs/api/routes/category/order.md | 4 +- docs/api/routes/category/tree.md | 8 +- docs/api/routes/category/update.md | 8 +- docs/api/routes/company/create.md | 15 +- docs/api/routes/company/list.md | 10 +- docs/api/routes/company/update.md | 8 +- docs/api/routes/delivery/create.md | 70 ++ docs/api/routes/delivery/delete.md | 33 + docs/api/routes/delivery/get.md | 55 ++ docs/api/routes/delivery/list.md | 40 + docs/api/routes/delivery/update.md | 66 ++ docs/api/routes/posterminal/list.md | 4 +- docs/api/routes/posterminal/setOff.md | 4 +- docs/api/routes/posterminal/setOffAll.md | 4 +- docs/api/routes/posterminal/setOn.md | 4 +- docs/api/routes/posterminal/setOnAll.md | 4 +- docs/api/routes/posterminal/upsert.md | 2 +- docs/api/routes/supplier/create.md | 44 + docs/api/routes/supplier/delete.md | 34 + docs/api/routes/supplier/list.md | 39 + docs/api/routes/supplier/update.md | 49 ++ docs/api/routes/unit/list.md | 10 +- docs/api/routes/unit/upsert.md | 8 +- scripts/generateApiDocs.js | 90 +- src/api/openapi.js | 90 ++ src/api/routes/delivery.js | 429 ++++++++++ src/api/routes/supplier.js | 310 +++++++ src/db/delivery.js | 26 + src/db/store.js | 2 + src/db/supplier.js | 21 + 52 files changed, 2515 insertions(+), 66 deletions(-) create mode 100644 docs/api/routes/delivery/create.md create mode 100644 docs/api/routes/delivery/delete.md create mode 100644 docs/api/routes/delivery/get.md create mode 100644 docs/api/routes/delivery/list.md create mode 100644 docs/api/routes/delivery/update.md create mode 100644 docs/api/routes/supplier/create.md create mode 100644 docs/api/routes/supplier/delete.md create mode 100644 docs/api/routes/supplier/list.md create mode 100644 docs/api/routes/supplier/update.md create mode 100644 src/api/routes/delivery.js create mode 100644 src/api/routes/supplier.js create mode 100644 src/db/delivery.js create mode 100644 src/db/supplier.js diff --git a/docs/api/README.md b/docs/api/README.md index ede72c9..cf886da 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -65,6 +65,11 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/company/create.md`](routes/company/create.md) | `POST /company` | | [`routes/company/delete.md`](routes/company/delete.md) | `DELETE /company/{id}` | | [`routes/company/update.md`](routes/company/update.md) | `PUT /company/{id}` | +| [`routes/delivery/list.md`](routes/delivery/list.md) | `GET /delivery` | +| [`routes/delivery/create.md`](routes/delivery/create.md) | `POST /delivery` | +| [`routes/delivery/delete.md`](routes/delivery/delete.md) | `DELETE /delivery/{id}` | +| [`routes/delivery/get.md`](routes/delivery/get.md) | `GET /delivery/{id}` | +| [`routes/delivery/update.md`](routes/delivery/update.md) | `PATCH /delivery/{id}` | | [`routes/events.md`](routes/events.md) | `GET /events` | | [`routes/health.md`](routes/health.md) | `GET /health` | | [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` | @@ -75,6 +80,10 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/posterminal/setOnAll.md`](routes/posterminal/setOnAll.md) | `PUT /posterminal/{id}/all` | | [`routes/posterminal/setOff.md`](routes/posterminal/setOff.md) | `DELETE /posterminal/{id}/category/{categoryId}` | | [`routes/posterminal/setOn.md`](routes/posterminal/setOn.md) | `PUT /posterminal/{id}/category/{categoryId}` | +| [`routes/supplier/list.md`](routes/supplier/list.md) | `GET /supplier` | +| [`routes/supplier/create.md`](routes/supplier/create.md) | `POST /supplier` | +| [`routes/supplier/delete.md`](routes/supplier/delete.md) | `DELETE /supplier/{id}` | +| [`routes/supplier/update.md`](routes/supplier/update.md) | `PATCH /supplier/{id}` | | [`routes/thumb/get.md`](routes/thumb/get.md) | `GET /thumb/{fullHash}` | | [`routes/unit/delete.md`](routes/unit/delete.md) | `DELETE /unit` | | [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index e6bdb05..b2a9417 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -44,9 +44,21 @@ "name": "Brand", "description": "Company-scoped brands" }, + { + "name": "Supplier", + "description": "Company-scoped suppliers (Lieferanten)" + }, + { + "name": "Delivery", + "description": "Company-scoped goods receipts (Lieferungen)" + }, { "name": "Image", "description": "Content-addressed AVIF by fullHash" + }, + { + "name": "Posterminal", + "description": "Company-scoped POS terminals" } ], "paths": { @@ -314,6 +326,343 @@ } } }, + "/supplier": { + "get": { + "tags": [ + "Supplier" + ], + "summary": "List suppliers", + "description": "Lists suppliers (`entity` rows in the company `Lieferanten` customergroup)\nfor the authenticated user's selected company.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "supplier/list.md", + "responses": { + "200": { + "description": "Supplier list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "suppliers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Supplier" + } + } + } + } + } + } + }, + "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" + } + } + } + }, + "404": { + "description": "Company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Supplier" + ], + "summary": "Create supplier", + "description": "Creates a supplier in the company `Lieferanten` group (created on first use).\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "supplier/create.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "example": { + "name": "Großhandel Müller" + } + } + } + } + }, + "responses": { + "201": { + "description": "Created supplier", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "supplier": { + "$ref": "#/components/schemas/Supplier" + } + } + } + } + } + }, + "400": { + "description": "Name required 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": "Company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/supplier/{id}": { + "patch": { + "tags": [ + "Supplier" + ], + "summary": "Update supplier", + "description": "Renames a supplier for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "supplier/update.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + }, + "example": { + "name": "Großhandel Müller GmbH" + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated supplier", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "supplier": { + "$ref": "#/components/schemas/Supplier" + } + } + } + } + } + }, + "400": { + "description": "Name required, invalid id, 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": "Supplier or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Supplier" + ], + "summary": "Delete supplier", + "description": "Deletes the supplier entity for the selected company.\nRelated `entityhistory` cascades; `manifest.entity_id` is set null.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "supplier/delete.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "No company selected or invalid id", + "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": "Supplier not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/posterminal": { "get": { "tags": [ @@ -1202,6 +1551,461 @@ } } }, + "/delivery": { + "get": { + "tags": [ + "Delivery" + ], + "summary": "List deliveries", + "description": "Lists goods receipts (`manifest` rows with `type = 'delivery'`)\nfor the authenticated user's selected company.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "delivery/list.md", + "responses": { + "200": { + "description": "Delivery list", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deliveries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeliverySummary" + } + } + } + } + } + } + }, + "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" + } + } + } + }, + "404": { + "description": "Company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Delivery" + ], + "summary": "Create delivery", + "description": "Creates a goods receipt with header and lines in one transaction.\nEach line requires `sku`, `amount`, `net`, and `tax` (rate).\n`rebate` defaults to `0`; `transactedAmount` defaults to `amount`.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "delivery/create.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "number", + "date", + "lines" + ], + "properties": { + "number": { + "type": "string" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "supplierId": { + "type": "integer", + "nullable": true + }, + "lines": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeliveryLineInput" + } + } + }, + "example": { + "number": "DHL-123", + "date": "2026-08-10", + "supplierId": 1, + "lines": [ + { + "sku": "1", + "amount": 10, + "net": 99.5, + "tax": 19 + } + ] + } + } + } + } + }, + "responses": { + "201": { + "description": "Created delivery", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "delivery": { + "$ref": "#/components/schemas/Delivery" + } + } + } + } + } + }, + "400": { + "description": "Invalid delivery, invalid lines, 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": "Company, supplier, or article not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/delivery/{id}": { + "get": { + "tags": [ + "Delivery" + ], + "summary": "Get delivery", + "description": "Returns a goods receipt with lines for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "delivery/get.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Delivery", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "delivery": { + "$ref": "#/components/schemas/Delivery" + } + } + } + } + } + }, + "400": { + "description": "No company selected or invalid id", + "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": "Delivery or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Delivery" + ], + "summary": "Update delivery", + "description": "Patches header fields that are present. If `lines` is present, replaces\nall lines; if omitted, leaves lines unchanged. Returns the full delivery.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "delivery/update.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "number": { + "type": "string" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "supplierId": { + "type": "integer", + "nullable": true + }, + "lines": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeliveryLineInput" + } + } + }, + "example": { + "number": "DHL-124", + "lines": [] + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated delivery", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "delivery": { + "$ref": "#/components/schemas/Delivery" + } + } + } + } + } + }, + "400": { + "description": "Invalid delivery, invalid lines, invalid id, 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": "Delivery, supplier, article, or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Delivery" + ], + "summary": "Delete delivery", + "description": "Deletes the goods receipt (`manifestarticle` rows cascade).\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "delivery/delete.md", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "No company selected or invalid id", + "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": "Delivery not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/company": { "get": { "tags": [ @@ -4564,6 +5368,168 @@ } } }, + "Supplier": { + "type": "object", + "required": [ + "id", + "name", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "DeliverySummary": { + "type": "object", + "required": [ + "id", + "number", + "type", + "supplierId", + "date" + ], + "properties": { + "id": { + "type": "integer" + }, + "number": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "delivery" + ] + }, + "supplierId": { + "type": "integer", + "nullable": true, + "description": "manifest.entity_id" + }, + "date": { + "type": "string", + "format": "date-time" + } + } + }, + "DeliveryLine": { + "type": "object", + "required": [ + "id", + "articleId", + "sku", + "amount", + "net", + "transactedAmount", + "rebate", + "tax", + "comment" + ], + "properties": { + "id": { + "type": "integer" + }, + "articleId": { + "type": "integer", + "nullable": true + }, + "sku": { + "type": "string", + "nullable": true + }, + "amount": { + "type": "number" + }, + "net": { + "type": "number" + }, + "transactedAmount": { + "type": "number" + }, + "rebate": { + "type": "number" + }, + "tax": { + "type": "number", + "description": "tax.rate" + }, + "comment": { + "type": "string", + "nullable": true + } + } + }, + "DeliveryLineInput": { + "type": "object", + "required": [ + "sku", + "amount", + "net", + "tax" + ], + "properties": { + "sku": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "net": { + "type": "number" + }, + "tax": { + "type": "number", + "description": "Tax rate (resolved via tax.ensure)" + }, + "rebate": { + "type": "number", + "description": "Defaults to 0" + }, + "transactedAmount": { + "type": "number", + "description": "Defaults to amount" + }, + "comment": { + "type": "string", + "nullable": true + } + } + }, + "Delivery": { + "allOf": [ + { + "$ref": "#/components/schemas/DeliverySummary" + }, + { + "type": "object", + "required": [ + "lines" + ], + "properties": { + "lines": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeliveryLine" + } + } + } + } + ] + }, "ArticleImage": { "type": "object", "required": [ diff --git a/docs/api/routes/article/add-image.md b/docs/api/routes/article/add-image.md index 38d8a2e..98ec30d 100644 --- a/docs/api/routes/article/add-image.md +++ b/docs/api/routes/article/add-image.md @@ -30,7 +30,21 @@ Image stored and linked ```json { - "image": { … } + "image": { + "picId": 1, + "sort": 1, + "full": { + "w": 1, + "h": 1, + "size": 1 + }, + "thumb": { + "w": 1, + "h": 1 + }, + "hash": "…", + "thumbHash": "…" + } } ``` diff --git a/docs/api/routes/article/create.md b/docs/api/routes/article/create.md index 9f4dae6..2cf45fd 100644 --- a/docs/api/routes/article/create.md +++ b/docs/api/routes/article/create.md @@ -25,7 +25,12 @@ Created ```json { - "article": { … } + "article": { + "id": 1, + "parentId": 1, + "name": "…", + "sku": "…" + } } ``` diff --git a/docs/api/routes/article/get.md b/docs/api/routes/article/get.md index 3f4e82d..fdb72c1 100644 --- a/docs/api/routes/article/get.md +++ b/docs/api/routes/article/get.md @@ -18,7 +18,7 @@ Article detail ```json { - "article": { … } + "article": {} } ``` diff --git a/docs/api/routes/article/order-images.md b/docs/api/routes/article/order-images.md index c6445a8..f28382b 100644 --- a/docs/api/routes/article/order-images.md +++ b/docs/api/routes/article/order-images.md @@ -35,7 +35,9 @@ New order ```json { - "fullHashes": [] + "fullHashes": [ + "…" + ] } ``` diff --git a/docs/api/routes/article/set-brand.md b/docs/api/routes/article/set-brand.md index fa66108..27d062c 100644 --- a/docs/api/routes/article/set-brand.md +++ b/docs/api/routes/article/set-brand.md @@ -32,7 +32,7 @@ Updated article detail ```json { - "article": { … } + "article": {} } ``` diff --git a/docs/api/routes/article/set-categories.md b/docs/api/routes/article/set-categories.md index e91eb11..d8b7a10 100644 --- a/docs/api/routes/article/set-categories.md +++ b/docs/api/routes/article/set-categories.md @@ -36,7 +36,7 @@ Updated article detail ```json { - "article": { … } + "article": {} } ``` diff --git a/docs/api/routes/article/set-eans.md b/docs/api/routes/article/set-eans.md index c1e028a..4a8b2d7 100644 --- a/docs/api/routes/article/set-eans.md +++ b/docs/api/routes/article/set-eans.md @@ -34,7 +34,7 @@ Updated article detail ```json { - "article": { … } + "article": {} } ``` diff --git a/docs/api/routes/article/set-prices.md b/docs/api/routes/article/set-prices.md index 60ce9cb..a131eb3 100644 --- a/docs/api/routes/article/set-prices.md +++ b/docs/api/routes/article/set-prices.md @@ -43,7 +43,7 @@ Updated article detail ```json { - "article": { … } + "article": {} } ``` diff --git a/docs/api/routes/article/set-weight.md b/docs/api/routes/article/set-weight.md index 2c1d35d..6b9dd60 100644 --- a/docs/api/routes/article/set-weight.md +++ b/docs/api/routes/article/set-weight.md @@ -32,7 +32,7 @@ Updated article detail ```json { - "article": { … } + "article": {} } ``` diff --git a/docs/api/routes/article/update.md b/docs/api/routes/article/update.md index 132f5dd..1869eba 100644 --- a/docs/api/routes/article/update.md +++ b/docs/api/routes/article/update.md @@ -33,7 +33,7 @@ Updated article detail ```json { - "article": { … } + "article": {} } ``` diff --git a/docs/api/routes/articleIndex.md b/docs/api/routes/articleIndex.md index a191958..3dda5ae 100644 --- a/docs/api/routes/articleIndex.md +++ b/docs/api/routes/articleIndex.md @@ -15,7 +15,19 @@ Search index ```json { - "articles": [] + "articles": [ + { + "id": 1, + "sku": "…", + "name": "…", + "eans": [ + "…" + ], + "categoryIds": [ + 1 + ] + } + ] } ``` diff --git a/docs/api/routes/auth/login.md b/docs/api/routes/auth/login.md index 4b88b2d..07af993 100644 --- a/docs/api/routes/auth/login.md +++ b/docs/api/routes/auth/login.md @@ -24,7 +24,12 @@ Login successful; `token` cookie set ```json { "message": "Login successful", - "user": { … } + "user": { + "id": 1, + "email": "user@example.com", + "selectedCompanyId": 1, + "picture": "…" + } } ``` diff --git a/docs/api/routes/auth/me.md b/docs/api/routes/auth/me.md index e0d808e..ec37c91 100644 --- a/docs/api/routes/auth/me.md +++ b/docs/api/routes/auth/me.md @@ -15,7 +15,12 @@ Authenticated ```json { - "user": { … }, + "user": { + "id": 1, + "email": "user@example.com", + "selectedCompanyId": 1, + "picture": "…" + }, "googleClientId": "…" } ``` diff --git a/docs/api/routes/auth/selected-company.md b/docs/api/routes/auth/selected-company.md index a8e17b1..ebb9c0a 100644 --- a/docs/api/routes/auth/selected-company.md +++ b/docs/api/routes/auth/selected-company.md @@ -20,7 +20,12 @@ Updated user ```json { - "user": { … } + "user": { + "id": 1, + "email": "user@example.com", + "selectedCompanyId": 1, + "picture": "…" + } } ``` diff --git a/docs/api/routes/brand/list.md b/docs/api/routes/brand/list.md index 9dff3d8..368c9de 100644 --- a/docs/api/routes/brand/list.md +++ b/docs/api/routes/brand/list.md @@ -12,7 +12,13 @@ Brand list ```json { - "brands": [] + "brands": [ + { + "id": 1, + "name": "…", + "gsprpinfo": "…" + } + ] } ``` diff --git a/docs/api/routes/brand/upsert.md b/docs/api/routes/brand/upsert.md index 0a02c21..a82d922 100644 --- a/docs/api/routes/brand/upsert.md +++ b/docs/api/routes/brand/upsert.md @@ -22,7 +22,11 @@ Upserted brand ```json { - "brand": { … } + "brand": { + "id": 1, + "name": "…", + "gsprpinfo": "…" + } } ``` diff --git a/docs/api/routes/category/add-image.md b/docs/api/routes/category/add-image.md index 658f9ec..07bde77 100644 --- a/docs/api/routes/category/add-image.md +++ b/docs/api/routes/category/add-image.md @@ -30,7 +30,12 @@ Image stored and linked ```json { - "category": { … } + "category": { + "id": 1, + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + } } ``` diff --git a/docs/api/routes/category/articles.md b/docs/api/routes/category/articles.md index d8ad6b1..f6b1cec 100644 --- a/docs/api/routes/category/articles.md +++ b/docs/api/routes/category/articles.md @@ -18,7 +18,9 @@ Articles list ```json { - "articles": [] + "articles": [ + {} + ] } ``` diff --git a/docs/api/routes/category/create.md b/docs/api/routes/category/create.md index 06da01a..a93f4a2 100644 --- a/docs/api/routes/category/create.md +++ b/docs/api/routes/category/create.md @@ -25,7 +25,12 @@ Created ```json { - "category": { … } + "category": { + "id": 1, + "parentId": 1, + "sort": 1, + "name": "…" + } } ``` diff --git a/docs/api/routes/category/delete-image.md b/docs/api/routes/category/delete-image.md index cddf5ec..7c39ef8 100644 --- a/docs/api/routes/category/delete-image.md +++ b/docs/api/routes/category/delete-image.md @@ -24,7 +24,12 @@ Image removed ```json { - "category": { … } + "category": { + "id": 1, + "thumbW": 1, + "thumbH": 1, + "thumbHash": "…" + } } ``` diff --git a/docs/api/routes/category/order.md b/docs/api/routes/category/order.md index fa1c63c..a9fb2b0 100644 --- a/docs/api/routes/category/order.md +++ b/docs/api/routes/category/order.md @@ -34,7 +34,9 @@ New sibling order ```json { - "categoryIds": [] + "categoryIds": [ + 1 + ] } ``` diff --git a/docs/api/routes/category/tree.md b/docs/api/routes/category/tree.md index b31cfd8..24a0e2b 100644 --- a/docs/api/routes/category/tree.md +++ b/docs/api/routes/category/tree.md @@ -13,8 +13,12 @@ Tree and articles for the selected company ```json { "languageId": 1, - "tree": [], - "articles": [] + "tree": [ + {} + ], + "articles": [ + {} + ] } ``` diff --git a/docs/api/routes/category/update.md b/docs/api/routes/category/update.md index 70e7588..3fdb18b 100644 --- a/docs/api/routes/category/update.md +++ b/docs/api/routes/category/update.md @@ -34,7 +34,13 @@ Updated category (renamed and/or moved) ```json { - "category": { … } + "category": { + "id": 1, + "name": "…", + "slug": "…", + "parentId": 1, + "sort": 1 + } } ``` diff --git a/docs/api/routes/company/create.md b/docs/api/routes/company/create.md index 6c81323..0b1d9df 100644 --- a/docs/api/routes/company/create.md +++ b/docs/api/routes/company/create.md @@ -23,8 +23,19 @@ Created; selected company updated ```json { - "company": { … }, - "user": { … } + "company": { + "id": 1, + "iso639": "…", + "name": "…", + "taxrate": 1, + "reducedtaxrate": 1 + }, + "user": { + "id": 1, + "email": "user@example.com", + "selectedCompanyId": 1, + "picture": "…" + } } ``` diff --git a/docs/api/routes/company/list.md b/docs/api/routes/company/list.md index c375dd5..86f3c65 100644 --- a/docs/api/routes/company/list.md +++ b/docs/api/routes/company/list.md @@ -12,7 +12,15 @@ Company list ```json { - "companies": [] + "companies": [ + { + "id": 1, + "iso639": "…", + "name": "…", + "taxrate": 1, + "reducedtaxrate": 1 + } + ] } ``` diff --git a/docs/api/routes/company/update.md b/docs/api/routes/company/update.md index 0ab81ea..ca11a5a 100644 --- a/docs/api/routes/company/update.md +++ b/docs/api/routes/company/update.md @@ -29,7 +29,13 @@ Updated ```json { - "company": { … } + "company": { + "id": 1, + "iso639": "…", + "name": "…", + "taxrate": 1, + "reducedtaxrate": 1 + } } ``` diff --git a/docs/api/routes/delivery/create.md b/docs/api/routes/delivery/create.md new file mode 100644 index 0000000..7fb09c9 --- /dev/null +++ b/docs/api/routes/delivery/create.md @@ -0,0 +1,70 @@ +# `POST /delivery` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create delivery + +Creates a goods receipt with header and lines in one transaction. +Each line requires `sku`, `amount`, `net`, and `tax` (rate). +`rebate` defaults to `0`; `transactedAmount` defaults to `amount`. + + +## Body (JSON) + +```json +{ + "number": "DHL-123", + "date": "2026-08-10", + "supplierId": 1, + "lines": [ + { + "sku": "1", + "amount": 10, + "net": 99.5, + "tax": 19 + } + ] +} +``` + +## Response `201` + +Created delivery + +```json +{ + "delivery": { + "id": 1, + "number": "…", + "type": "delivery", + "supplierId": 1, + "date": "2026-08-10T00:00:00.000Z", + "lines": [ + { + "id": 1, + "articleId": 1, + "sku": "…", + "amount": 1, + "net": 1, + "transactedAmount": 1, + "rebate": 1, + "tax": 1, + "comment": "…" + } + ] + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid delivery, invalid lines, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company, supplier, or article not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/delivery/delete.md b/docs/api/routes/delivery/delete.md new file mode 100644 index 0000000..9674e21 --- /dev/null +++ b/docs/api/routes/delivery/delete.md @@ -0,0 +1,33 @@ +# `DELETE /delivery/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete delivery + +Deletes the goods receipt (`manifestarticle` rows cascade). + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Response `204` + +Deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid id — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Delivery not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/delivery/get.md b/docs/api/routes/delivery/get.md new file mode 100644 index 0000000..d7773c7 --- /dev/null +++ b/docs/api/routes/delivery/get.md @@ -0,0 +1,55 @@ +# `GET /delivery/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Get delivery + +Returns a goods receipt with lines for the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Response `200` + +Delivery + +```json +{ + "delivery": { + "id": 1, + "number": "…", + "type": "delivery", + "supplierId": 1, + "date": "2026-08-10T00:00:00.000Z", + "lines": [ + { + "id": 1, + "articleId": 1, + "sku": "…", + "amount": 1, + "net": 1, + "transactedAmount": 1, + "rebate": 1, + "tax": 1, + "comment": "…" + } + ] + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid id — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Delivery or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/delivery/list.md b/docs/api/routes/delivery/list.md new file mode 100644 index 0000000..c0d9a3a --- /dev/null +++ b/docs/api/routes/delivery/list.md @@ -0,0 +1,40 @@ +# `GET /delivery` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List deliveries + +Lists goods receipts (`manifest` rows with `type = 'delivery'`) +for the authenticated user's selected company. + + +## Response `200` + +Delivery list + +```json +{ + "deliveries": [ + { + "id": 1, + "number": "…", + "type": "delivery", + "supplierId": 1, + "date": "2026-08-10T00:00:00.000Z" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/delivery/update.md b/docs/api/routes/delivery/update.md new file mode 100644 index 0000000..4fa9c7a --- /dev/null +++ b/docs/api/routes/delivery/update.md @@ -0,0 +1,66 @@ +# `PATCH /delivery/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update delivery + +Patches header fields that are present. If `lines` is present, replaces +all lines; if omitted, leaves lines unchanged. Returns the full delivery. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Body (JSON) + +```json +{ + "number": "DHL-124", + "lines": [] +} +``` + +## Response `200` + +Updated delivery + +```json +{ + "delivery": { + "id": 1, + "number": "…", + "type": "delivery", + "supplierId": 1, + "date": "2026-08-10T00:00:00.000Z", + "lines": [ + { + "id": 1, + "articleId": 1, + "sku": "…", + "amount": 1, + "net": 1, + "transactedAmount": 1, + "rebate": 1, + "tax": 1, + "comment": "…" + } + ] + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid delivery, invalid lines, invalid id, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Delivery, supplier, article, or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/posterminal/list.md b/docs/api/routes/posterminal/list.md index ade516c..54ab43e 100644 --- a/docs/api/routes/posterminal/list.md +++ b/docs/api/routes/posterminal/list.md @@ -12,7 +12,9 @@ Posterminal list ```json { - "posterminals": [] + "posterminals": [ + {} + ] } ``` diff --git a/docs/api/routes/posterminal/setOff.md b/docs/api/routes/posterminal/setOff.md index 1decee9..8caf70f 100644 --- a/docs/api/routes/posterminal/setOff.md +++ b/docs/api/routes/posterminal/setOff.md @@ -20,7 +20,9 @@ Updated category IDs for this terminal ```json { "id": 1, - "categoryIds": [] + "categoryIds": [ + 1 + ] } ``` diff --git a/docs/api/routes/posterminal/setOffAll.md b/docs/api/routes/posterminal/setOffAll.md index 261c47d..93d7306 100644 --- a/docs/api/routes/posterminal/setOffAll.md +++ b/docs/api/routes/posterminal/setOffAll.md @@ -19,7 +19,9 @@ Updated category IDs for this terminal ```json { "id": 1, - "categoryIds": [] + "categoryIds": [ + 1 + ] } ``` diff --git a/docs/api/routes/posterminal/setOn.md b/docs/api/routes/posterminal/setOn.md index 4a9578c..4e12405 100644 --- a/docs/api/routes/posterminal/setOn.md +++ b/docs/api/routes/posterminal/setOn.md @@ -20,7 +20,9 @@ Updated category IDs for this terminal ```json { "id": 1, - "categoryIds": [] + "categoryIds": [ + 1 + ] } ``` diff --git a/docs/api/routes/posterminal/setOnAll.md b/docs/api/routes/posterminal/setOnAll.md index 6f2577d..2288af5 100644 --- a/docs/api/routes/posterminal/setOnAll.md +++ b/docs/api/routes/posterminal/setOnAll.md @@ -21,7 +21,9 @@ Updated category IDs for this terminal ```json { "id": 1, - "categoryIds": [] + "categoryIds": [ + 1 + ] } ``` diff --git a/docs/api/routes/posterminal/upsert.md b/docs/api/routes/posterminal/upsert.md index 13d147a..78632b6 100644 --- a/docs/api/routes/posterminal/upsert.md +++ b/docs/api/routes/posterminal/upsert.md @@ -22,7 +22,7 @@ Created posterminal with generated pairingpin ```json { - "posterminal": { … } + "posterminal": {} } ``` diff --git a/docs/api/routes/supplier/create.md b/docs/api/routes/supplier/create.md new file mode 100644 index 0000000..93606ac --- /dev/null +++ b/docs/api/routes/supplier/create.md @@ -0,0 +1,44 @@ +# `POST /supplier` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create supplier + +Creates a supplier in the company `Lieferanten` group (created on first use). + + +## Body (JSON) + +```json +{ + "name": "Großhandel Müller" +} +``` + +## Response `201` + +Created supplier + +```json +{ + "supplier": { + "id": 1, + "name": "…", + "createdAt": "2026-08-10T00:00:00.000Z", + "updatedAt": "2026-08-10T00:00:00.000Z" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Name required or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/supplier/delete.md b/docs/api/routes/supplier/delete.md new file mode 100644 index 0000000..2352bd8 --- /dev/null +++ b/docs/api/routes/supplier/delete.md @@ -0,0 +1,34 @@ +# `DELETE /supplier/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete supplier + +Deletes the supplier entity for the selected company. +Related `entityhistory` cascades; `manifest.entity_id` is set null. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Response `204` + +Deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid id — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Supplier not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/supplier/list.md b/docs/api/routes/supplier/list.md new file mode 100644 index 0000000..b6d13e5 --- /dev/null +++ b/docs/api/routes/supplier/list.md @@ -0,0 +1,39 @@ +# `GET /supplier` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List suppliers + +Lists suppliers (`entity` rows in the company `Lieferanten` customergroup) +for the authenticated user's selected company. + + +## Response `200` + +Supplier list + +```json +{ + "suppliers": [ + { + "id": 1, + "name": "…", + "createdAt": "2026-08-10T00:00:00.000Z", + "updatedAt": "2026-08-10T00:00:00.000Z" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/supplier/update.md b/docs/api/routes/supplier/update.md new file mode 100644 index 0000000..b79dcfe --- /dev/null +++ b/docs/api/routes/supplier/update.md @@ -0,0 +1,49 @@ +# `PATCH /supplier/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update supplier + +Renames a supplier for the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | | + +## Body (JSON) + +```json +{ + "name": "Großhandel Müller GmbH" +} +``` + +## Response `200` + +Updated supplier + +```json +{ + "supplier": { + "id": 1, + "name": "…", + "createdAt": "2026-08-10T00:00:00.000Z", + "updatedAt": "2026-08-10T00:00:00.000Z" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Name required, invalid id, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Supplier or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/unit/list.md b/docs/api/routes/unit/list.md index 16eb417..f1d0c8d 100644 --- a/docs/api/routes/unit/list.md +++ b/docs/api/routes/unit/list.md @@ -12,7 +12,15 @@ Unit list ```json { - "units": [] + "units": [ + { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + } + ] } ``` diff --git a/docs/api/routes/unit/upsert.md b/docs/api/routes/unit/upsert.md index e3a038e..f7dc657 100644 --- a/docs/api/routes/unit/upsert.md +++ b/docs/api/routes/unit/upsert.md @@ -26,7 +26,13 @@ Upserted unit ```json { - "unit": { … } + "unit": { + "si": "…", + "basesi": "…", + "basesifactor": 1, + "symbol": "…", + "name": "…" + } } ``` diff --git a/scripts/generateApiDocs.js b/scripts/generateApiDocs.js index 843415c..d8ecfc2 100644 --- a/scripts/generateApiDocs.js +++ b/scripts/generateApiDocs.js @@ -36,7 +36,10 @@ const options = { { name: 'Company', description: 'Company CRUD' }, { name: 'Unit', description: 'Company-scoped units of measure' }, { name: 'Brand', description: 'Company-scoped brands' }, + { name: 'Supplier', description: 'Company-scoped suppliers (Lieferanten)' }, + { name: 'Delivery', description: 'Company-scoped goods receipts (Lieferungen)' }, { name: 'Image', description: 'Content-addressed AVIF by fullHash' }, + { name: 'Posterminal', description: 'Company-scoped POS terminals' }, ], }, apis: [ @@ -64,41 +67,68 @@ function schemaRefName(schema) { return null; } -function formatSchema(schema, components, indent = 0) { - if (!schema) return '_none_'; +/** + * Build a sample JSON value from an OpenAPI schema (expands $ref / allOf / array items). + * @param {object} schema + * @param {object} [components] + * @param {number} [depth] + * @returns {*} + */ +function toExample(schema, components, depth = 0) { + if (!schema || depth > 8) return null; + + if (schema.example !== undefined) return schema.example; + if (schema.$ref) { - const name = schemaRefName(schema); - const resolved = components?.schemas?.[name]; - if (resolved) { - return formatSchema({ ...resolved, title: name }, components, indent); + const resolved = components?.schemas?.[schemaRefName(schema)]; + return resolved ? toExample(resolved, components, depth) : {}; + } + + if (Array.isArray(schema.allOf) && schema.allOf.length) { + const out = {}; + for (const part of schema.allOf) { + const sample = toExample(part, components, depth + 1); + if (sample && typeof sample === 'object' && !Array.isArray(sample)) { + Object.assign(out, sample); + } } - return `\`${name}\``; - } - if (schema.example !== undefined) { - return '```json\n' + JSON.stringify(schema.example, null, 2) + '\n```'; - } - if (schema.type === 'object' && schema.properties) { - const lines = ['```json', '{']; - const keys = Object.keys(schema.properties); - keys.forEach((key, i) => { - const prop = schema.properties[key]; - const comma = i < keys.length - 1 ? ',' : ''; - let value = '…'; - if (prop.type === 'string') value = prop.format === 'email' ? '"user@example.com"' : '"…"'; - else if (prop.type === 'integer' || prop.type === 'number') value = prop.nullable ? 'null' : '1'; - else if (prop.type === 'boolean') value = 'true'; - else if (prop.type === 'array') value = '[]'; - else if (prop.type === 'object' || prop.$ref) value = '{ … }'; - if (prop.example !== undefined) value = JSON.stringify(prop.example); - lines.push(` "${key}": ${value}${comma}`); - }); - lines.push('}', '```'); - return lines.join('\n'); + return out; } + if (schema.type === 'array') { - return '```json\n[]\n```'; + if (schema.items) { + return [toExample(schema.items, components, depth + 1)]; + } + return []; } - return '```json\n' + JSON.stringify(schema, null, 2) + '\n```'; + + if (schema.type === 'object' || schema.properties) { + const out = {}; + for (const [key, prop] of Object.entries(schema.properties || {})) { + out[key] = toExample(prop, components, depth + 1); + } + return out; + } + + if (schema.type === 'string') { + if (schema.enum?.length) return schema.enum[0]; + if (schema.format === 'email') return 'user@example.com'; + if (schema.format === 'date-time') return '2026-08-10T00:00:00.000Z'; + if (schema.format === 'date') return '2026-08-10'; + return '…'; + } + + if (schema.type === 'integer') return 1; + if (schema.type === 'number') return 1; + if (schema.type === 'boolean') return true; + if (schema.nullable) return null; + return '…'; +} + +function formatSchema(schema, components) { + if (!schema) return '_none_'; + const example = toExample(schema, components); + return '```json\n' + JSON.stringify(example, null, 2) + '\n```'; } function contentSchema(content) { diff --git a/src/api/openapi.js b/src/api/openapi.js index 45cf55c..975da3b 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -110,6 +110,96 @@ * gsprpinfo: * type: string * nullable: true + * Supplier: + * type: object + * required: [id, name, createdAt, updatedAt] + * properties: + * id: + * type: integer + * name: + * type: string + * createdAt: + * type: string + * format: date-time + * updatedAt: + * type: string + * format: date-time + * DeliverySummary: + * type: object + * required: [id, number, type, supplierId, date] + * properties: + * id: + * type: integer + * number: + * type: string + * type: + * type: string + * enum: [delivery] + * supplierId: + * type: integer + * nullable: true + * description: manifest.entity_id + * date: + * type: string + * format: date-time + * DeliveryLine: + * type: object + * required: [id, articleId, sku, amount, net, transactedAmount, rebate, tax, comment] + * properties: + * id: + * type: integer + * articleId: + * type: integer + * nullable: true + * sku: + * type: string + * nullable: true + * amount: + * type: number + * net: + * type: number + * transactedAmount: + * type: number + * rebate: + * type: number + * tax: + * type: number + * description: tax.rate + * comment: + * type: string + * nullable: true + * DeliveryLineInput: + * type: object + * required: [sku, amount, net, tax] + * properties: + * sku: + * type: string + * amount: + * type: number + * net: + * type: number + * tax: + * type: number + * description: Tax rate (resolved via tax.ensure) + * rebate: + * type: number + * description: Defaults to 0 + * transactedAmount: + * type: number + * description: Defaults to amount + * comment: + * type: string + * nullable: true + * Delivery: + * allOf: + * - $ref: '#/components/schemas/DeliverySummary' + * - type: object + * required: [lines] + * properties: + * lines: + * type: array + * items: + * $ref: '#/components/schemas/DeliveryLine' * ArticleImage: * type: object * required: [picId, sort, full, thumb, hash, thumbHash] diff --git a/src/api/routes/delivery.js b/src/api/routes/delivery.js new file mode 100644 index 0000000..f27f2cb --- /dev/null +++ b/src/api/routes/delivery.js @@ -0,0 +1,429 @@ +import * as db from '../../db/delivery.js'; + +function requireSelectedCompany(req) { + const companyId = req.user.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function deliveryErrorStatus(error) { + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Delivery not found') return 404; + if (error.message === 'Supplier not found') return 404; + if (error.message === 'Article not found') return 404; + if (error.message === 'Invalid delivery') return 400; + if (error.message === 'Invalid lines') return 400; + return 500; +} + +/** + * Registers delivery (goods receipt) routes for the authenticated user's selected company. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /delivery: + * get: + * tags: [Delivery] + * summary: List deliveries + * description: | + * Lists goods receipts (`manifest` rows with `type = 'delivery'`) + * for the authenticated user's selected company. + * security: + * - cookieAuth: [] + * x-markdown: delivery/list.md + * responses: + * 200: + * description: Delivery list + * content: + * application/json: + * schema: + * type: object + * properties: + * deliveries: + * type: array + * items: + * $ref: '#/components/schemas/DeliverySummary' + * 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' + * 404: + * description: Company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * post: + * tags: [Delivery] + * summary: Create delivery + * description: | + * Creates a goods receipt with header and lines in one transaction. + * Each line requires `sku`, `amount`, `net`, and `tax` (rate). + * `rebate` defaults to `0`; `transactedAmount` defaults to `amount`. + * security: + * - cookieAuth: [] + * x-markdown: delivery/create.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [number, date, lines] + * properties: + * number: + * type: string + * date: + * type: string + * format: date-time + * supplierId: + * type: integer + * nullable: true + * lines: + * type: array + * items: + * $ref: '#/components/schemas/DeliveryLineInput' + * example: + * number: DHL-123 + * date: '2026-08-10' + * supplierId: 1 + * lines: + * - sku: '1' + * amount: 10 + * net: 99.5 + * tax: 19 + * responses: + * 201: + * description: Created delivery + * content: + * application/json: + * schema: + * type: object + * properties: + * delivery: + * $ref: '#/components/schemas/Delivery' + * 400: + * description: Invalid delivery, invalid lines, 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: Company, supplier, or article not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/delivery`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const deliveries = await db.list(req.user.id, selected.companyId); + res.status(200).json({ deliveries }); + } catch (error) { + const status = deliveryErrorStatus(error); + if (status === 500) console.error('List deliveries failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.post(`${basePath}/delivery`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const { number, date, supplierId, lines } = req.body ?? {}; + + try { + const delivery = await db.create(req.user.id, selected.companyId, { + number, + date, + supplierId, + lines, + }); + res.status(201).json({ delivery }); + } catch (error) { + const status = deliveryErrorStatus(error); + if (status === 500) console.error('Create delivery failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /delivery/{id}: + * get: + * tags: [Delivery] + * summary: Get delivery + * description: Returns a goods receipt with lines for the selected company. + * security: + * - cookieAuth: [] + * x-markdown: delivery/get.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Delivery + * content: + * application/json: + * schema: + * type: object + * properties: + * delivery: + * $ref: '#/components/schemas/Delivery' + * 400: + * description: No company selected or invalid id + * 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: Delivery or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * patch: + * tags: [Delivery] + * summary: Update delivery + * description: | + * Patches header fields that are present. If `lines` is present, replaces + * all lines; if omitted, leaves lines unchanged. Returns the full delivery. + * security: + * - cookieAuth: [] + * x-markdown: delivery/update.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * number: + * type: string + * date: + * type: string + * format: date-time + * supplierId: + * type: integer + * nullable: true + * lines: + * type: array + * items: + * $ref: '#/components/schemas/DeliveryLineInput' + * example: + * number: DHL-124 + * lines: [] + * responses: + * 200: + * description: Updated delivery + * content: + * application/json: + * schema: + * type: object + * properties: + * delivery: + * $ref: '#/components/schemas/Delivery' + * 400: + * description: Invalid delivery, invalid lines, invalid id, 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: Delivery, supplier, article, or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * delete: + * tags: [Delivery] + * summary: Delete delivery + * description: | + * Deletes the goods receipt (`manifestarticle` rows cascade). + * security: + * - cookieAuth: [] + * x-markdown: delivery/delete.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 204: + * description: Deleted + * 400: + * description: No company selected or invalid id + * 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: Delivery not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/delivery/:id`, 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' }); + } + + try { + const delivery = await db.getById(req.user.id, selected.companyId, id); + res.status(200).json({ delivery }); + } catch (error) { + const status = deliveryErrorStatus(error); + if (status === 500) console.error('Get delivery failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.patch(`${basePath}/delivery/:id`, 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 body = req.body ?? {}; + const patch = {}; + if (Object.prototype.hasOwnProperty.call(body, 'number')) { + patch.number = body.number; + } + if (Object.prototype.hasOwnProperty.call(body, 'date')) { + patch.date = body.date; + } + if (Object.prototype.hasOwnProperty.call(body, 'supplierId')) { + patch.supplierId = body.supplierId; + } + if (Object.prototype.hasOwnProperty.call(body, 'lines')) { + patch.lines = body.lines; + } + + try { + const delivery = await db.update(req.user.id, selected.companyId, id, patch); + res.status(200).json({ delivery }); + } catch (error) { + const status = deliveryErrorStatus(error); + if (status === 500) console.error('Update delivery failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/delivery/:id`, 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' }); + } + + try { + await db.remove(req.user.id, selected.companyId, id); + res.sendStatus(204); + } catch (error) { + const status = deliveryErrorStatus(error); + if (status === 500) console.error('Delete delivery failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/supplier.js b/src/api/routes/supplier.js new file mode 100644 index 0000000..cdb8c41 --- /dev/null +++ b/src/api/routes/supplier.js @@ -0,0 +1,310 @@ +import * as db from '../../db/supplier.js'; + +function requireSelectedCompany(req) { + const companyId = req.user.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function supplierErrorStatus(error) { + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Supplier not found') return 404; + if (error.message === 'Name required') return 400; + return 500; +} + +/** + * Registers supplier routes for the authenticated user's selected company. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /supplier: + * get: + * tags: [Supplier] + * summary: List suppliers + * description: | + * Lists suppliers (`entity` rows in the company `Lieferanten` customergroup) + * for the authenticated user's selected company. + * security: + * - cookieAuth: [] + * x-markdown: supplier/list.md + * responses: + * 200: + * description: Supplier list + * content: + * application/json: + * schema: + * type: object + * properties: + * suppliers: + * type: array + * items: + * $ref: '#/components/schemas/Supplier' + * 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' + * 404: + * description: Company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * post: + * tags: [Supplier] + * summary: Create supplier + * description: | + * Creates a supplier in the company `Lieferanten` group (created on first use). + * security: + * - cookieAuth: [] + * x-markdown: supplier/create.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [name] + * properties: + * name: + * type: string + * example: + * name: Großhandel Müller + * responses: + * 201: + * description: Created supplier + * content: + * application/json: + * schema: + * type: object + * properties: + * supplier: + * $ref: '#/components/schemas/Supplier' + * 400: + * description: Name required 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: Company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/supplier`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const suppliers = await db.list(req.user.id, selected.companyId); + res.status(200).json({ suppliers }); + } catch (error) { + const status = supplierErrorStatus(error); + if (status === 500) console.error('List suppliers failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.post(`${basePath}/supplier`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const { name } = req.body ?? {}; + + try { + const supplier = await db.create(req.user.id, selected.companyId, { name }); + res.status(201).json({ supplier }); + } catch (error) { + const status = supplierErrorStatus(error); + if (status === 500) console.error('Create supplier failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /supplier/{id}: + * patch: + * tags: [Supplier] + * summary: Update supplier + * description: Renames a supplier for the selected company. + * security: + * - cookieAuth: [] + * x-markdown: supplier/update.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [name] + * properties: + * name: + * type: string + * example: + * name: Großhandel Müller GmbH + * responses: + * 200: + * description: Updated supplier + * content: + * application/json: + * schema: + * type: object + * properties: + * supplier: + * $ref: '#/components/schemas/Supplier' + * 400: + * description: Name required, invalid id, 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: Supplier or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * delete: + * tags: [Supplier] + * summary: Delete supplier + * description: | + * Deletes the supplier entity for the selected company. + * Related `entityhistory` cascades; `manifest.entity_id` is set null. + * security: + * - cookieAuth: [] + * x-markdown: supplier/delete.md + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * responses: + * 204: + * description: Deleted + * 400: + * description: No company selected or invalid id + * 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: Supplier not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.patch(`${basePath}/supplier/:id`, 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 { name } = req.body ?? {}; + + try { + const supplier = await db.update(req.user.id, selected.companyId, id, { name }); + res.status(200).json({ supplier }); + } catch (error) { + const status = supplierErrorStatus(error); + if (status === 500) console.error('Update supplier failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/supplier/:id`, 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' }); + } + + try { + await db.remove(req.user.id, selected.companyId, id); + res.sendStatus(204); + } catch (error) { + const status = supplierErrorStatus(error); + if (status === 500) console.error('Delete supplier failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/db/delivery.js b/src/db/delivery.js new file mode 100644 index 0000000..7432381 --- /dev/null +++ b/src/db/delivery.js @@ -0,0 +1,26 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId, companyId) { + const store = await getStore(); + return store.delivery.list(googleAuthId, companyId); +} + +export async function getById(googleAuthId, companyId, id) { + const store = await getStore(); + return store.delivery.getById(googleAuthId, companyId, id); +} + +export async function create(googleAuthId, companyId, data) { + const store = await getStore(); + return store.delivery.create(googleAuthId, companyId, data); +} + +export async function update(googleAuthId, companyId, id, data) { + const store = await getStore(); + return store.delivery.update(googleAuthId, companyId, id, data); +} + +export async function remove(googleAuthId, companyId, id) { + const store = await getStore(); + return store.delivery.delete(googleAuthId, companyId, id); +} diff --git a/src/db/store.js b/src/db/store.js index a7be2be..af7cb3a 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -19,6 +19,8 @@ const STORE_NAMESPACES = [ 'tax', 'google_auth', 'posterminal', + 'supplier', + 'delivery', ]; /** diff --git a/src/db/supplier.js b/src/db/supplier.js new file mode 100644 index 0000000..546f608 --- /dev/null +++ b/src/db/supplier.js @@ -0,0 +1,21 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId, companyId) { + const store = await getStore(); + return store.supplier.list(googleAuthId, companyId); +} + +export async function create(googleAuthId, companyId, data) { + const store = await getStore(); + return store.supplier.create(googleAuthId, companyId, data); +} + +export async function update(googleAuthId, companyId, id, data) { + const store = await getStore(); + return store.supplier.update(googleAuthId, companyId, id, data); +} + +export async function remove(googleAuthId, companyId, id) { + const store = await getStore(); + return store.supplier.delete(googleAuthId, companyId, id); +}