From 69f27a29424eeef5c6d59b809b2dc1293f1b2188 Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 26 Sep 2026 23:06:09 +0200 Subject: [PATCH] u --- docs/api/README.md | 8 + docs/api/openapi.json | 841 +++++++++++++++++++++++++++++ docs/api/routes/printer/:id.put.md | 58 ++ docs/api/routes/printer/create.md | 53 ++ docs/api/routes/printer/delete.md | 32 ++ docs/api/routes/printer/get.md | 45 ++ docs/api/routes/printer/list.md | 40 ++ docs/api/routes/printer/order.md | 43 ++ docs/api/routes/printer/update.md | 58 ++ docs/api/routes/stock/adjust.md | 73 +++ scripts/generateApiDocs.js | 2 + src/api/openapi.js | 129 +++++ src/api/routes/printer.js | 529 ++++++++++++++++++ src/api/routes/stock.js | 118 ++++ src/api/routes/warehouse.js | 136 +++++ src/db/printer.js | 31 ++ src/db/stock.js | 6 + src/db/store.js | 3 + src/db/warehouse.js | 26 + 19 files changed, 2231 insertions(+) create mode 100644 docs/api/routes/printer/:id.put.md create mode 100644 docs/api/routes/printer/create.md create mode 100644 docs/api/routes/printer/delete.md create mode 100644 docs/api/routes/printer/get.md create mode 100644 docs/api/routes/printer/list.md create mode 100644 docs/api/routes/printer/order.md create mode 100644 docs/api/routes/printer/update.md create mode 100644 docs/api/routes/stock/adjust.md create mode 100644 src/api/routes/printer.js create mode 100644 src/api/routes/stock.js create mode 100644 src/api/routes/warehouse.js create mode 100644 src/db/printer.js create mode 100644 src/db/stock.js create mode 100644 src/db/warehouse.js diff --git a/docs/api/README.md b/docs/api/README.md index 2434c54..201a015 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -120,11 +120,19 @@ 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/printer/list.md`](routes/printer/list.md) | `GET /printer` | +| [`routes/printer/create.md`](routes/printer/create.md) | `POST /printer` | +| [`routes/printer/order.md`](routes/printer/order.md) | `PUT /printer/order` | +| [`routes/printer/delete.md`](routes/printer/delete.md) | `DELETE /printer/{id}` | +| [`routes/printer/get.md`](routes/printer/get.md) | `GET /printer/{id}` | +| [`routes/printer/update.md`](routes/printer/update.md) | `PATCH /printer/{id}` | +| [`routes/printer/:id.put.md`](routes/printer/:id.put.md) | `PUT /printer/{id}` | | [`routes/quote/list.md`](routes/quote/list.md) | `GET /quote` | | [`routes/quote/create.md`](routes/quote/create.md) | `POST /quote` | | [`routes/quote/delete.md`](routes/quote/delete.md) | `DELETE /quote/{id}` | | [`routes/quote/get.md`](routes/quote/get.md) | `GET /quote/{id}` | | [`routes/quote/update.md`](routes/quote/update.md) | `PATCH /quote/{id}` | +| [`routes/stock/adjust.md`](routes/stock/adjust.md) | `POST /stock/adjust` | | [`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}` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 90ab992..6001285 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -80,9 +80,17 @@ "name": "Posterminal", "description": "Company-scoped POS terminals" }, + { + "name": "Printer", + "description": "Company-scoped receipt and label printers" + }, { "name": "Ledger", "description": "Append-only ledger transactions" + }, + { + "name": "Stock", + "description": "Manual stock additions and removals" } ], "paths": { @@ -901,6 +909,83 @@ } } }, + "/stock/adjust": { + "post": { + "tags": [ + "Stock" + ], + "summary": "Adjust stock manually", + "description": "Writes signed (+ or -) entries to the append-only ledger under an implicit manifest\nof type \"adjustment\", allocating a number from the \"adjustment\" number scheme.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "stock/adjust.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StockAdjustmentInput" + } + } + } + }, + "responses": { + "201": { + "description": "Stock adjusted successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StockAdjustmentResult" + } + } + } + }, + "400": { + "description": "Missing fields, invalid amount, 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": "Article or warehouse not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/quote": { "get": { "tags": [ @@ -1361,6 +1446,560 @@ } } }, + "/printer": { + "get": { + "tags": [ + "Printer" + ], + "summary": "List printers", + "description": "Lists printers for the selected company, ordered by sort and id.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "printer/list.md", + "responses": { + "200": { + "description": "List of printers", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "printers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Printer" + } + } + } + } + } + } + }, + "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" + } + } + } + } + } + }, + "post": { + "tags": [ + "Printer" + ], + "summary": "Create printer", + "description": "Creates a new printer for the selected company.\n`type` must be `\"esc/pos\"` (lines) or `\"zpl\"` (pages).\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "printer/create.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrinterCreateInput" + } + } + } + }, + "responses": { + "201": { + "description": "Created printer", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "printer": { + "$ref": "#/components/schemas/Printer" + } + } + } + } + } + }, + "400": { + "description": "Invalid input or missing fields", + "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" + } + } + } + } + } + } + }, + "/printer/order": { + "put": { + "tags": [ + "Printer" + ], + "summary": "Reorder printers", + "description": "Reorders printers for the selected company.\n`printerIds` must include all existing printer IDs exactly once.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "printer/order.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "printerIds" + ], + "properties": { + "printerIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Applied printer order", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "printerIds": { + "type": "array", + "items": { + "type": "integer" + } + } + } + } + } + } + }, + "400": { + "description": "Invalid printer order 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" + } + } + } + } + } + } + }, + "/printer/{id}": { + "get": { + "tags": [ + "Printer" + ], + "summary": "Get printer by ID", + "description": "Retrieves a single printer for the authenticated user's selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Printer ID" + } + ], + "x-markdown": "printer/get.md", + "responses": { + "200": { + "description": "Printer details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "printer": { + "$ref": "#/components/schemas/Printer" + } + } + } + } + } + }, + "400": { + "description": "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": "Printer not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Printer" + ], + "summary": "Update printer (partial)", + "description": "Updates specified fields of an existing printer for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Printer ID" + } + ], + "x-markdown": "printer/update.md", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrinterUpdateInput" + } + } + } + }, + "responses": { + "200": { + "description": "Updated printer", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "printer": { + "$ref": "#/components/schemas/Printer" + } + } + } + } + } + }, + "400": { + "description": "Invalid input", + "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": "Printer not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "put": { + "tags": [ + "Printer" + ], + "summary": "Update printer", + "description": "Updates specified fields of an existing printer for the selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Printer ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrinterUpdateInput" + } + } + } + }, + "responses": { + "200": { + "description": "Updated printer", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "printer": { + "$ref": "#/components/schemas/Printer" + } + } + } + } + } + }, + "400": { + "description": "Invalid input", + "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": "Printer not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Printer" + ], + "summary": "Delete printer", + "description": "Deletes a printer for the authenticated user's selected company.", + "security": [ + { + "cookieAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "Printer ID" + } + ], + "x-markdown": "printer/delete.md", + "responses": { + "204": { + "description": "Printer deleted" + }, + "400": { + "description": "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": "Printer not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/posterminal": { "get": { "tags": [ @@ -11071,6 +11710,208 @@ "description": "Transaction memo / details" } } + }, + "Printer": { + "type": "object", + "required": [ + "id", + "company_id", + "address", + "type", + "sort" + ], + "properties": { + "id": { + "type": "integer" + }, + "company_id": { + "type": "integer" + }, + "address": { + "type": "string", + "description": "Printer network address or hostname" + }, + "type": { + "type": "string", + "enum": [ + "esc/pos", + "zpl" + ], + "description": "Printer type (\"esc/pos\" for lines or \"zpl\" for pages)" + }, + "model": { + "type": "string", + "nullable": true, + "description": "Printer model name" + }, + "serial": { + "type": "string", + "nullable": true, + "description": "Printer serial number" + }, + "sort": { + "type": "integer", + "description": "Sort order" + }, + "desc": { + "type": "string", + "nullable": true, + "description": "Printer description" + } + } + }, + "PrinterCreateInput": { + "type": "object", + "required": [ + "address", + "type" + ], + "properties": { + "address": { + "type": "string", + "description": "Printer network address or hostname" + }, + "type": { + "type": "string", + "enum": [ + "esc/pos", + "zpl" + ], + "description": "Printer type (\"esc/pos\" for lines or \"zpl\" for pages)" + }, + "model": { + "type": "string", + "nullable": true + }, + "serial": { + "type": "string", + "nullable": true + }, + "sort": { + "type": "integer" + }, + "desc": { + "type": "string", + "nullable": true + } + } + }, + "PrinterUpdateInput": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "esc/pos", + "zpl" + ] + }, + "model": { + "type": "string", + "nullable": true + }, + "serial": { + "type": "string", + "nullable": true + }, + "sort": { + "type": "integer" + }, + "desc": { + "type": "string", + "nullable": true + } + } + }, + "StockAdjustmentInput": { + "type": "object", + "properties": { + "sku": { + "type": "string", + "description": "Article SKU (for single-item adjustment)" + }, + "warehouseId": { + "type": "integer", + "description": "Target warehouse ID" + }, + "amount": { + "type": "number", + "description": "Signed adjustment quantity (+ to add, - to remove)" + }, + "text": { + "type": "string", + "nullable": true, + "description": "Reason or memo" + }, + "lines": { + "type": "array", + "description": "Batch adjustment lines", + "items": { + "type": "object", + "required": [ + "sku", + "amount" + ], + "properties": { + "sku": { + "type": "string" + }, + "warehouseId": { + "type": "integer" + }, + "amount": { + "type": "number" + }, + "text": { + "type": "string" + } + } + } + } + } + }, + "StockAdjustmentResult": { + "type": "object", + "properties": { + "manifest": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "number": { + "type": "string" + }, + "type": { + "type": "string", + "example": "adjustment" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + }, + "lines": { + "type": "array", + "items": { + "type": "object" + } + }, + "ledger": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LedgerEntry" + } + } + } } } } diff --git a/docs/api/routes/printer/:id.put.md b/docs/api/routes/printer/:id.put.md new file mode 100644 index 0000000..ba67446 --- /dev/null +++ b/docs/api/routes/printer/:id.put.md @@ -0,0 +1,58 @@ +# `PUT /printer/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update printer + +Updates specified fields of an existing printer for the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Printer ID | + +## Body (JSON) + +```json +{ + "address": "…", + "type": "esc/pos", + "model": "…", + "serial": "…", + "sort": 1, + "desc": "…" +} +``` + +## Response `200` + +Updated printer + +```json +{ + "printer": { + "id": 1, + "company_id": 1, + "address": "…", + "type": "esc/pos", + "model": "…", + "serial": "…", + "sort": 1, + "desc": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid input — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Printer not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/printer/create.md b/docs/api/routes/printer/create.md new file mode 100644 index 0000000..07dc2e3 --- /dev/null +++ b/docs/api/routes/printer/create.md @@ -0,0 +1,53 @@ +# `POST /printer` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create printer + +Creates a new printer for the selected company. +`type` must be `"esc/pos"` (lines) or `"zpl"` (pages). + + +## Body (JSON) + +```json +{ + "address": "…", + "type": "esc/pos", + "model": "…", + "serial": "…", + "sort": 1, + "desc": "…" +} +``` + +## Response `201` + +Created printer + +```json +{ + "printer": { + "id": 1, + "company_id": 1, + "address": "…", + "type": "esc/pos", + "model": "…", + "serial": "…", + "sort": 1, + "desc": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid input or missing fields — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/printer/delete.md b/docs/api/routes/printer/delete.md new file mode 100644 index 0000000..5852742 --- /dev/null +++ b/docs/api/routes/printer/delete.md @@ -0,0 +1,32 @@ +# `DELETE /printer/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete printer + +Deletes a printer for the authenticated user's selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Printer ID | + +## Response `204` + +Printer deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid ID or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Printer not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/printer/get.md b/docs/api/routes/printer/get.md new file mode 100644 index 0000000..8bb1d4e --- /dev/null +++ b/docs/api/routes/printer/get.md @@ -0,0 +1,45 @@ +# `GET /printer/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Get printer by ID + +Retrieves a single printer for the authenticated user's selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Printer ID | + +## Response `200` + +Printer details + +```json +{ + "printer": { + "id": 1, + "company_id": 1, + "address": "…", + "type": "esc/pos", + "model": "…", + "serial": "…", + "sort": 1, + "desc": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid ID or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Printer not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/printer/list.md b/docs/api/routes/printer/list.md new file mode 100644 index 0000000..6ecca88 --- /dev/null +++ b/docs/api/routes/printer/list.md @@ -0,0 +1,40 @@ +# `GET /printer` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List printers + +Lists printers for the selected company, ordered by sort and id. + +## Response `200` + +List of printers + +```json +{ + "printers": [ + { + "id": 1, + "company_id": 1, + "address": "…", + "type": "esc/pos", + "model": "…", + "serial": "…", + "sort": 1, + "desc": "…" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/printer/order.md b/docs/api/routes/printer/order.md new file mode 100644 index 0000000..47b9feb --- /dev/null +++ b/docs/api/routes/printer/order.md @@ -0,0 +1,43 @@ +# `PUT /printer/order` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Reorder printers + +Reorders printers for the selected company. +`printerIds` must include all existing printer IDs exactly once. + + +## Body (JSON) + +```json +{ + "printerIds": [ + 1 + ] +} +``` + +## Response `200` + +Applied printer order + +```json +{ + "printerIds": [ + 1 + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid printer order or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/printer/update.md b/docs/api/routes/printer/update.md new file mode 100644 index 0000000..d78c6ab --- /dev/null +++ b/docs/api/routes/printer/update.md @@ -0,0 +1,58 @@ +# `PATCH /printer/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update printer (partial) + +Updates specified fields of an existing printer for the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Printer ID | + +## Body (JSON) + +```json +{ + "address": "…", + "type": "esc/pos", + "model": "…", + "serial": "…", + "sort": 1, + "desc": "…" +} +``` + +## Response `200` + +Updated printer + +```json +{ + "printer": { + "id": 1, + "company_id": 1, + "address": "…", + "type": "esc/pos", + "model": "…", + "serial": "…", + "sort": 1, + "desc": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid input — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Printer not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/stock/adjust.md b/docs/api/routes/stock/adjust.md new file mode 100644 index 0000000..d085ea3 --- /dev/null +++ b/docs/api/routes/stock/adjust.md @@ -0,0 +1,73 @@ +# `POST /stock/adjust` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Adjust stock manually + +Writes signed (+ or -) entries to the append-only ledger under an implicit manifest +of type "adjustment", allocating a number from the "adjustment" number scheme. + + +## Body (JSON) + +```json +{ + "sku": "…", + "warehouseId": 1, + "amount": 1, + "text": "…", + "lines": [ + { + "sku": "…", + "warehouseId": 1, + "amount": 1, + "text": "…" + } + ] +} +``` + +## Response `201` + +Stock adjusted successfully + +```json +{ + "manifest": { + "id": 1, + "number": "…", + "type": "adjustment", + "date": "2026-08-10T00:00:00.000Z", + "createdAt": "2026-08-10T00:00:00.000Z" + }, + "lines": [ + {} + ], + "ledger": [ + { + "id": 1, + "manifestarticle_id": 1, + "transaction_type": "…", + "amount": 1, + "transaction_text": "…", + "hash_version": 1, + "prev_hash": "…", + "sku": "…", + "created_at": "2026-08-10T00:00:00.000Z" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Missing fields, invalid amount, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Article or warehouse not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/scripts/generateApiDocs.js b/scripts/generateApiDocs.js index c94f86a..b5c7fbb 100644 --- a/scripts/generateApiDocs.js +++ b/scripts/generateApiDocs.js @@ -45,7 +45,9 @@ const options = { { name: 'Entity', description: 'Company-scoped entities, addresses, and contacts' }, { name: 'Image', description: 'Content-addressed AVIF by fullHash' }, { name: 'Posterminal', description: 'Company-scoped POS terminals' }, + { name: 'Printer', description: 'Company-scoped receipt and label printers' }, { name: 'Ledger', description: 'Append-only ledger transactions' }, + { name: 'Stock', description: 'Manual stock additions and removals' }, ], }, apis: [ diff --git a/src/api/openapi.js b/src/api/openapi.js index 4b6345d..7d62cdd 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -730,6 +730,135 @@ * transaction_text: * type: string * description: Transaction memo / details + * Printer: + * type: object + * required: [id, company_id, address, type, sort] + * properties: + * id: + * type: integer + * company_id: + * type: integer + * address: + * type: string + * description: Printer network address or hostname + * type: + * type: string + * enum: [esc/pos, zpl] + * description: Printer type ("esc/pos" for lines or "zpl" for pages) + * model: + * type: string + * nullable: true + * description: Printer model name + * serial: + * type: string + * nullable: true + * description: Printer serial number + * sort: + * type: integer + * description: Sort order + * desc: + * type: string + * nullable: true + * description: Printer description + * PrinterCreateInput: + * type: object + * required: [address, type] + * properties: + * address: + * type: string + * description: Printer network address or hostname + * type: + * type: string + * enum: [esc/pos, zpl] + * description: Printer type ("esc/pos" for lines or "zpl" for pages) + * model: + * type: string + * nullable: true + * serial: + * type: string + * nullable: true + * sort: + * type: integer + * desc: + * type: string + * nullable: true + * PrinterUpdateInput: + * type: object + * properties: + * address: + * type: string + * type: + * type: string + * enum: [esc/pos, zpl] + * model: + * type: string + * nullable: true + * serial: + * type: string + * nullable: true + * sort: + * type: integer + * desc: + * type: string + * nullable: true + * StockAdjustmentInput: + * type: object + * properties: + * sku: + * type: string + * description: Article SKU (for single-item adjustment) + * warehouseId: + * type: integer + * description: Target warehouse ID + * amount: + * type: number + * description: Signed adjustment quantity (+ to add, - to remove) + * text: + * type: string + * nullable: true + * description: Reason or memo + * lines: + * type: array + * description: Batch adjustment lines + * items: + * type: object + * required: [sku, amount] + * properties: + * sku: + * type: string + * warehouseId: + * type: integer + * amount: + * type: number + * text: + * type: string + * StockAdjustmentResult: + * type: object + * properties: + * manifest: + * type: object + * properties: + * id: + * type: integer + * number: + * type: string + * type: + * type: string + * example: adjustment + * date: + * type: string + * format: date-time + * createdAt: + * type: string + * format: date-time + * lines: + * type: array + * items: + * type: object + * ledger: + * type: array + * items: + * $ref: '#/components/schemas/LedgerEntry' */ export {}; diff --git a/src/api/routes/printer.js b/src/api/routes/printer.js new file mode 100644 index 0000000..c12e6c4 --- /dev/null +++ b/src/api/routes/printer.js @@ -0,0 +1,529 @@ +import * as db from '../../db/printer.js'; +import { emit } from '../sse.js'; + +function requireSelectedCompany(req) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function parsePositiveInt(value, field) { + const id = Number(value); + if (!Number.isInteger(id) || id <= 0) { + return { error: `${field} is required` }; + } + return { id }; +} + +function printerErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Printer not found') return 404; + if (error.message === 'Address required') return 400; + if (error.message?.startsWith('Invalid printer type')) return 400; + if (error.message === 'Invalid sort') return 400; + if (error.message === 'Invalid printer order') return 400; + return 500; +} + +/** + * Registers printer routes for the authenticated user's selected company. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /printer: + * get: + * tags: [Printer] + * summary: List printers + * description: Lists printers for the selected company, ordered by sort and id. + * security: + * - cookieAuth: [] + * x-markdown: printer/list.md + * responses: + * 200: + * description: List of printers + * content: + * application/json: + * schema: + * type: object + * properties: + * printers: + * type: array + * items: + * $ref: '#/components/schemas/Printer' + * 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' + */ + app.get(`${basePath}/printer`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const printers = await db.list(req.user.id); + res.status(200).json({ printers }); + } catch (error) { + const status = printerErrorStatus(error); + if (status === 500) console.error('List printers failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /printer/order: + * put: + * tags: [Printer] + * summary: Reorder printers + * description: | + * Reorders printers for the selected company. + * `printerIds` must include all existing printer IDs exactly once. + * security: + * - cookieAuth: [] + * x-markdown: printer/order.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [printerIds] + * properties: + * printerIds: + * type: array + * items: + * type: integer + * responses: + * 200: + * description: Applied printer order + * content: + * application/json: + * schema: + * type: object + * properties: + * printerIds: + * type: array + * items: + * type: integer + * 400: + * description: Invalid printer order 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' + */ + app.put(`${basePath}/printer/order`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const { printerIds } = req.body ?? {}; + + try { + const result = await db.order(req.user.id, { printerIds }); + emit('printer', { companyId: selected.companyId }); + res.status(200).json(result); + } catch (error) { + const status = printerErrorStatus(error); + if (status === 500) console.error('Order printers failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /printer/{id}: + * get: + * tags: [Printer] + * summary: Get printer by ID + * description: Retrieves a single printer for the authenticated user's selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Printer ID + * x-markdown: printer/get.md + * responses: + * 200: + * description: Printer details + * content: + * application/json: + * schema: + * type: object + * properties: + * printer: + * $ref: '#/components/schemas/Printer' + * 400: + * description: 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: Printer not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/printer/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const parsedId = parsePositiveInt(req.params.id, 'id'); + if (parsedId.error) { + return res.status(400).json({ error: parsedId.error }); + } + + try { + const printer = await db.getById(req.user.id, parsedId.id); + if (!printer) { + return res.status(404).json({ error: 'Printer not found' }); + } + res.status(200).json({ printer }); + } catch (error) { + const status = printerErrorStatus(error); + if (status === 500) console.error('Get printer failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /printer: + * post: + * tags: [Printer] + * summary: Create printer + * description: | + * Creates a new printer for the selected company. + * `type` must be `"esc/pos"` (lines) or `"zpl"` (pages). + * security: + * - cookieAuth: [] + * x-markdown: printer/create.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PrinterCreateInput' + * responses: + * 201: + * description: Created printer + * content: + * application/json: + * schema: + * type: object + * properties: + * printer: + * $ref: '#/components/schemas/Printer' + * 400: + * description: Invalid input or missing fields + * 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' + */ + app.post(`${basePath}/printer`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const { address, type, model, serial, sort, desc } = req.body ?? {}; + + try { + const printer = await db.create(req.user.id, { + address, + type, + model, + serial, + sort, + desc, + }); + emit('printer', { companyId: selected.companyId }); + res.status(201).json({ printer }); + } catch (error) { + const status = printerErrorStatus(error); + if (status === 500) console.error('Create printer failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + async function handleUpdate(req, res) { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const parsedId = parsePositiveInt(req.params.id, 'id'); + if (parsedId.error) { + return res.status(400).json({ error: parsedId.error }); + } + + const { address, type, model, serial, sort, desc } = req.body ?? {}; + + try { + const printer = await db.update(req.user.id, parsedId.id, { + address, + type, + model, + serial, + sort, + desc, + }); + emit('printer', { companyId: selected.companyId }); + res.status(200).json({ printer }); + } catch (error) { + const status = printerErrorStatus(error); + if (status === 500) console.error('Update printer failed:', error); + res.status(status).json({ error: error.message }); + } + } + + /** + * @openapi + * /printer/{id}: + * patch: + * tags: [Printer] + * summary: Update printer (partial) + * description: Updates specified fields of an existing printer for the selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Printer ID + * x-markdown: printer/update.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PrinterUpdateInput' + * responses: + * 200: + * description: Updated printer + * content: + * application/json: + * schema: + * type: object + * properties: + * printer: + * $ref: '#/components/schemas/Printer' + * 400: + * description: Invalid input + * 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: Printer not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * put: + * tags: [Printer] + * summary: Update printer + * description: Updates specified fields of an existing printer for the selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Printer ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PrinterUpdateInput' + * responses: + * 200: + * description: Updated printer + * content: + * application/json: + * schema: + * type: object + * properties: + * printer: + * $ref: '#/components/schemas/Printer' + * 400: + * description: Invalid input + * 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: Printer not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.patch(`${basePath}/printer/:id`, handleUpdate); + app.put(`${basePath}/printer/:id`, handleUpdate); + + /** + * @openapi + * /printer/{id}: + * delete: + * tags: [Printer] + * summary: Delete printer + * description: Deletes a printer for the authenticated user's selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Printer ID + * x-markdown: printer/delete.md + * responses: + * 204: + * description: Printer deleted + * 400: + * description: 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: Printer not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.delete(`${basePath}/printer/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const parsedId = parsePositiveInt(req.params.id, 'id'); + if (parsedId.error) { + return res.status(400).json({ error: parsedId.error }); + } + + try { + await db.remove(req.user.id, parsedId.id); + emit('printer', { companyId: selected.companyId }); + res.sendStatus(204); + } catch (error) { + const status = printerErrorStatus(error); + if (status === 500) console.error('Delete printer failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/stock.js b/src/api/routes/stock.js new file mode 100644 index 0000000..e9cdda9 --- /dev/null +++ b/src/api/routes/stock.js @@ -0,0 +1,118 @@ +import * as db from '../../db/stock.js'; +import { emit } from '../sse.js'; + +function requireSelectedCompany(req) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function stockErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Article not found') return 404; + if (error.message === 'Warehouse not found') return 404; + if (error.message?.includes('SKU required')) return 400; + if (error.message?.includes('Warehouse required')) return 400; + if (error.message?.includes('Amount must be')) return 400; + if (error.message?.includes('Invalid amount')) return 400; + if (error.message === 'No items to adjust') return 400; + return 500; +} + +function emitStockEvents(companyId, body, result) { + emit('stock', { companyId }); + emit('ledger', { companyId }); + emit('articles', { companyId }); + + if (body.sku) { + emit(`article-${body.sku}`, { companyId }); + } + if (Array.isArray(body.lines)) { + for (const line of body.lines) { + if (line?.sku) { + emit(`article-${line.sku}`, { companyId }); + } + } + } +} + +/** + * Registers stock adjustment routes for the authenticated user's selected company. + * All operations append to the append-only ledger under an implicit "adjustment" manifest. + * + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + + /** + * @openapi + * /stock/adjust: + * post: + * tags: [Stock] + * summary: Adjust stock manually + * description: | + * Writes signed (+ or -) entries to the append-only ledger under an implicit manifest + * of type "adjustment", allocating a number from the "adjustment" number scheme. + * security: + * - cookieAuth: [] + * x-markdown: stock/adjust.md + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/StockAdjustmentInput' + * responses: + * 201: + * description: Stock adjusted successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/StockAdjustmentResult' + * 400: + * description: Missing fields, invalid amount, 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: Article or warehouse not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.post(`${basePath}/stock/adjust`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const result = await db.adjust(req.user.id, req.body ?? {}); + emitStockEvents(selected.companyId, req.body ?? {}, result); + res.status(201).json(result); + } catch (error) { + const status = stockErrorStatus(error); + if (status === 500) console.error('Stock adjust failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/warehouse.js b/src/api/routes/warehouse.js new file mode 100644 index 0000000..fc7ada3 --- /dev/null +++ b/src/api/routes/warehouse.js @@ -0,0 +1,136 @@ +import * as db from '../../db/warehouse.js'; +import { emit } from '../sse.js'; + +function requireSelectedCompany(req) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function warehouseErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Warehouse not found') return 404; + if (error.message === 'Name required') return 400; + if (error.message === 'Warehouse already exists') return 409; + if (error.message === 'Warehouse in use in ledger') return 409; + return 500; +} + +/** + * Registers warehouse routes for the authenticated user's selected company. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + app.get(`${basePath}/warehouse`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const warehouses = await db.list(req.user.id); + res.status(200).json({ warehouses }); + } catch (error) { + const status = warehouseErrorStatus(error); + if (status === 500) console.error('List warehouses failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.get(`${basePath}/warehouse/: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 warehouse = await db.getById(req.user.id, id); + if (!warehouse) { + return res.status(404).json({ error: 'Warehouse not found' }); + } + res.status(200).json({ warehouse }); + } catch (error) { + const status = warehouseErrorStatus(error); + if (status === 500) console.error('Get warehouse failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.post(`${basePath}/warehouse`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const { name } = req.body ?? {}; + + try { + const warehouse = await db.create(req.user.id, { name }); + emit('warehouse', { companyId: selected.companyId }); + res.status(201).json({ warehouse }); + } catch (error) { + const status = warehouseErrorStatus(error); + if (status === 500) console.error('Create warehouse failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + async function handleUpdate(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 warehouse = await db.update(req.user.id, id, { name }); + emit('warehouse', { companyId: selected.companyId }); + res.status(200).json({ warehouse }); + } catch (error) { + const status = warehouseErrorStatus(error); + if (status === 500) console.error('Update warehouse failed:', error); + res.status(status).json({ error: error.message }); + } + } + + app.patch(`${basePath}/warehouse/:id`, handleUpdate); + app.put(`${basePath}/warehouse/:id`, handleUpdate); + + app.delete(`${basePath}/warehouse/: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, id); + emit('warehouse', { companyId: selected.companyId }); + res.sendStatus(204); + } catch (error) { + const status = warehouseErrorStatus(error); + if (status === 500) console.error('Delete warehouse failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/db/printer.js b/src/db/printer.js new file mode 100644 index 0000000..2955ec9 --- /dev/null +++ b/src/db/printer.js @@ -0,0 +1,31 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId) { + const store = await getStore(); + return store.printer.list(googleAuthId); +} + +export async function getById(googleAuthId, id) { + const store = await getStore(); + return store.printer.getById(googleAuthId, id); +} + +export async function create(googleAuthId, data) { + const store = await getStore(); + return store.printer.create(googleAuthId, data); +} + +export async function update(googleAuthId, id, data) { + const store = await getStore(); + return store.printer.update(googleAuthId, id, data); +} + +export async function order(googleAuthId, data) { + const store = await getStore(); + return store.printer.order(googleAuthId, data); +} + +export async function remove(googleAuthId, id) { + const store = await getStore(); + return store.printer.delete(googleAuthId, id); +} diff --git a/src/db/stock.js b/src/db/stock.js new file mode 100644 index 0000000..31864e0 --- /dev/null +++ b/src/db/stock.js @@ -0,0 +1,6 @@ +import { getStore } from './store.js'; + +export async function adjust(googleAuthId, data) { + const store = await getStore(); + return store.stock.adjust(googleAuthId, data); +} diff --git a/src/db/store.js b/src/db/store.js index af11762..7a1f6bd 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -33,6 +33,9 @@ const STORE_NAMESPACES = [ 'articlesupplier', 'manifestattachment', 'ledger', + 'warehouse', + 'printer', + 'stock', ]; /** diff --git a/src/db/warehouse.js b/src/db/warehouse.js new file mode 100644 index 0000000..1fbde1e --- /dev/null +++ b/src/db/warehouse.js @@ -0,0 +1,26 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId) { + const store = await getStore(); + return store.warehouse.list(googleAuthId); +} + +export async function getById(googleAuthId, id) { + const store = await getStore(); + return store.warehouse.getById(googleAuthId, id); +} + +export async function create(googleAuthId, data) { + const store = await getStore(); + return store.warehouse.create(googleAuthId, data); +} + +export async function update(googleAuthId, id, data) { + const store = await getStore(); + return store.warehouse.update(googleAuthId, id, data); +} + +export async function remove(googleAuthId, id) { + const store = await getStore(); + return store.warehouse.delete(googleAuthId, id); +}