From 011d84c3c159a6882291d3ed268ffcc49c934a52 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 9 Oct 2026 10:41:21 +0200 Subject: [PATCH] u --- .gitignore | 3 +- README.md | 10 + docs/api/README.md | 12 + docs/api/routes-all.md | 38 ++- docs/api/routes.md | 38 ++- docs/api/routes/account.get.md | 35 +++ docs/api/routes/account.post.md | 48 ++++ docs/api/routes/account/_id.md | 32 +++ docs/api/routes/order/_id/payment.get.md | 43 ++++ docs/api/routes/order/_id/payment.post.md | 58 +++++ docs/api/routes/order/_id/shipping.get.md | 43 ++++ docs/api/routes/order/_id/shipping.post.md | 58 +++++ docs/api/routes/package/_id.md | 31 +++ docs/api/routes/payment/_id.md | 31 +++ docs/api/routes/shipping/_id.md | 31 +++ docs/api/routes/shipping/_id/package.get.md | 45 ++++ docs/api/routes/shipping/_id/package.post.md | 64 +++++ package-lock.json | 1 + package.json | 2 + scripts/extractFunctions.js | 133 ++++++++++ src/api/auth/meJson.js | 2 +- src/api/openapi.js | 61 +++++ src/api/routes/account.js | 213 ++++++++++++++++ src/api/routes/package.js | 252 +++++++++++++++++++ src/api/routes/payment.js | 249 ++++++++++++++++++ src/api/routes/shipping.js | 248 ++++++++++++++++++ src/db/account.js | 16 ++ src/db/package.js | 16 ++ src/db/payment.js | 16 ++ src/db/shipping.js | 16 ++ src/db/store.js | 4 + 31 files changed, 1845 insertions(+), 4 deletions(-) create mode 100644 docs/api/routes/account.get.md create mode 100644 docs/api/routes/account.post.md create mode 100644 docs/api/routes/account/_id.md create mode 100644 docs/api/routes/order/_id/payment.get.md create mode 100644 docs/api/routes/order/_id/payment.post.md create mode 100644 docs/api/routes/order/_id/shipping.get.md create mode 100644 docs/api/routes/order/_id/shipping.post.md create mode 100644 docs/api/routes/package/_id.md create mode 100644 docs/api/routes/payment/_id.md create mode 100644 docs/api/routes/shipping/_id.md create mode 100644 docs/api/routes/shipping/_id/package.get.md create mode 100644 docs/api/routes/shipping/_id/package.post.md create mode 100644 scripts/extractFunctions.js create mode 100644 src/api/routes/account.js create mode 100644 src/api/routes/package.js create mode 100644 src/api/routes/payment.js create mode 100644 src/api/routes/shipping.js create mode 100644 src/db/account.js create mode 100644 src/db/package.js create mode 100644 src/db/payment.js create mode 100644 src/db/shipping.js diff --git a/.gitignore b/.gitignore index bc61670..e1cbd51 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules/ data/ logs/ print_spool/ -certs/ \ No newline at end of file +certs/ +output/ diff --git a/README.md b/README.md index 3fec1cb..7b41fbd 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,16 @@ npm run docs:api That writes `docs/api/routes.md`, `docs/api/routes-all.md`, and per-route Markdown under `docs/api/routes/`. There is no generated `openapi.json` on disk — the interactive Swagger UI at `/docs/` builds the spec from the same `@openapi` JSDoc at runtime (`src/api/openapiSpec.js`). +### Function review files + +Create one JavaScript file per function in the project: + +```bash +npm run review:functions +``` + +The generated files are written to `output/` and include each function's source filename and line numbers. The same directory also contains `functions.txt`, an index of source filenames, line numbers, and function signatures. Existing generated function files and the index are replaced each time the command runs. Dependencies, runtime data, and the output directory itself are excluded from the scan. + ### Brands (company-scoped, `selectedCompanyId` aus der Session) | Methode | Route | Store-Methode | diff --git a/docs/api/README.md b/docs/api/README.md index db3feee..2822acc 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -32,6 +32,9 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | File | Endpoint | |------|----------| +| [`routes/account.get.md`](routes/account.get.md) | `GET /account` | +| [`routes/account.post.md`](routes/account.post.md) | `POST /account` | +| [`routes/account/_id.md`](routes/account/_id.md) | `DELETE /account/{id}` | | [`routes/article/create.md`](routes/article/create.md) | `POST /article` | | [`routes/article/export.zip.md`](routes/article/export.zip.md) | `GET /article/export.zip` | | [`routes/article/delete.md`](routes/article/delete.md) | `DELETE /article/{sku}` | @@ -163,6 +166,12 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/order/delete.md`](routes/order/delete.md) | `DELETE /order/{id}` | | [`routes/order/get.md`](routes/order/get.md) | `GET /order/{id}` | | [`routes/order/update.md`](routes/order/update.md) | `PATCH /order/{id}` | +| [`routes/order/_id/payment.get.md`](routes/order/_id/payment.get.md) | `GET /order/{id}/payment` | +| [`routes/order/_id/payment.post.md`](routes/order/_id/payment.post.md) | `POST /order/{id}/payment` | +| [`routes/order/_id/shipping.get.md`](routes/order/_id/shipping.get.md) | `GET /order/{id}/shipping` | +| [`routes/order/_id/shipping.post.md`](routes/order/_id/shipping.post.md) | `POST /order/{id}/shipping` | +| [`routes/package/_id.md`](routes/package/_id.md) | `DELETE /package/{id}` | +| [`routes/payment/_id.md`](routes/payment/_id.md) | `DELETE /payment/{id}` | | [`routes/posterminal/list.md`](routes/posterminal/list.md) | `GET /posterminal` | | [`routes/posterminal/upsert.md`](routes/posterminal/upsert.md) | `PUT /posterminal` | | [`routes/posterminal/delete.md`](routes/posterminal/delete.md) | `DELETE /posterminal/{id}` | @@ -194,6 +203,9 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/quote/update.md`](routes/quote/update.md) | `PATCH /quote/{id}` | | [`routes/serper/image.md`](routes/serper/image.md) | `POST /serper/image` | | [`routes/serper/images.md`](routes/serper/images.md) | `POST /serper/images` | +| [`routes/shipping/_id.md`](routes/shipping/_id.md) | `DELETE /shipping/{id}` | +| [`routes/shipping/_id/package.get.md`](routes/shipping/_id/package.get.md) | `GET /shipping/{id}/package` | +| [`routes/shipping/_id/package.post.md`](routes/shipping/_id/package.post.md) | `POST /shipping/{id}/package` | | [`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` | diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index 881968d..4649b3a 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -1,6 +1,6 @@ # Dash API Routes Overview (All) -Overview of all 192 API endpoints across all modules. +Overview of all 204 API endpoints across all modules. ### Notation @@ -57,6 +57,15 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). ### Routes +#### List accounts — `GET /account` +_none_ +- Returns: `200 { accounts: { id: int, companyId: int, type: str, name: str }[] }` +#### Create account — `POST /account` +- Params: `body: { type: str, name: str }` +- Returns: `201 { account: { id: int, companyId: int, type: str, name: str } }` +#### Delete account — `DELETE /account/{id}` +- Params: `id: int` +- Returns: `204 void` #### Create article in category — `POST /article` - Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }` - Returns: `201 T31` @@ -450,6 +459,24 @@ _none_ #### Update order — `PATCH /order/{id}` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T25[] }` - Returns: `200 T7` +#### List payments of an order — `GET /order/{id}/payment` +- Params: `id: int` +- Returns: `200 { payments: { id: int, accountId: int, manifestId: int, type: str, amount: num }[] }` +#### Add payment to an order — `POST /order/{id}/payment` +- Params: `id: int, body: { accountId: int, type: str, amount: num }` +- Returns: `201 { payment: { id: int, accountId: int, manifestId: int, type: str, amount: num } }` +#### List shippings of an order — `GET /order/{id}/shipping` +- Params: `id: int` +- Returns: `200 { shippings: { id: int, manifestId: int, number: str, type: str, state: str }[] }` +#### Create shipping for an order — `POST /order/{id}/shipping` +- Params: `id: int, body: { type: str, state: str }` +- Returns: `201 { shipping: { id: int, manifestId: int, number: str, type: str, state: str } }` +#### Delete package — `DELETE /package/{id}` +- Params: `id: int` +- Returns: `204 void` +#### Delete payment — `DELETE /payment/{id}` +- Params: `id: int` +- Returns: `204 void` #### List POS terminals — `GET /posterminal` _none_ - Returns: `200 { posterminals: T10[] }` @@ -543,6 +570,15 @@ _none_ #### Search images via Serper.dev and stream WebP thumbnails (SSE) — `POST /serper/images` - Params: `body: { q: str, gl?: str, hl?: str, num?: int, preferSource?: bool }` - Returns: `200 SSE` +#### Delete shipping — `DELETE /shipping/{id}` +- Params: `id: int` +- Returns: `204 void` +#### List packages of a shipping — `GET /shipping/{id}/package` +- Params: `id: int` +- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }` +#### Create package for a shipping — `POST /shipping/{id}/package` +- Params: `id: int, body: { entityaddressId: int, type: str, state: str, details?: str }` +- Returns: `201 { package: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null } }` #### Adjust stock manually — `POST /stock/adjust` - Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }` - Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T15[] }` diff --git a/docs/api/routes.md b/docs/api/routes.md index 336225d..975ce17 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -1,6 +1,6 @@ # Dash API Routes Overview -Overview of all 146 API endpoints across all modules. +Overview of all 158 API endpoints across all modules. ### Notation @@ -52,6 +52,15 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). ### Routes +#### List accounts — `GET /account` +_none_ +- Returns: `200 { accounts: { id: int, companyId: int, type: str, name: str }[] }` +#### Create account — `POST /account` +- Params: `body: { type: str, name: str }` +- Returns: `201 { account: { id: int, companyId: int, type: str, name: str } }` +#### Delete account — `DELETE /account/{id}` +- Params: `id: int` +- Returns: `204 void` #### Create article in category — `POST /article` - Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }` - Returns: `201 T29` @@ -388,6 +397,24 @@ _none_ #### Update order — `PATCH /order/{id}` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T24[] }` - Returns: `200 T7` +#### List payments of an order — `GET /order/{id}/payment` +- Params: `id: int` +- Returns: `200 { payments: { id: int, accountId: int, manifestId: int, type: str, amount: num }[] }` +#### Add payment to an order — `POST /order/{id}/payment` +- Params: `id: int, body: { accountId: int, type: str, amount: num }` +- Returns: `201 { payment: { id: int, accountId: int, manifestId: int, type: str, amount: num } }` +#### List shippings of an order — `GET /order/{id}/shipping` +- Params: `id: int` +- Returns: `200 { shippings: { id: int, manifestId: int, number: str, type: str, state: str }[] }` +#### Create shipping for an order — `POST /order/{id}/shipping` +- Params: `id: int, body: { type: str, state: str }` +- Returns: `201 { shipping: { id: int, manifestId: int, number: str, type: str, state: str } }` +#### Delete package — `DELETE /package/{id}` +- Params: `id: int` +- Returns: `204 void` +#### Delete payment — `DELETE /payment/{id}` +- Params: `id: int` +- Returns: `204 void` #### List printers — `GET /printer` _none_ - Returns: `200 { printers: T22[] }` @@ -439,6 +466,15 @@ _none_ #### Update quote — `PATCH /quote/{id}` - Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T24[] }` - Returns: `200 T8` +#### Delete shipping — `DELETE /shipping/{id}` +- Params: `id: int` +- Returns: `204 void` +#### List packages of a shipping — `GET /shipping/{id}/package` +- Params: `id: int` +- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }` +#### Create package for a shipping — `POST /shipping/{id}/package` +- Params: `id: int, body: { entityaddressId: int, type: str, state: str, details?: str }` +- Returns: `201 { package: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null } }` #### Adjust stock manually — `POST /stock/adjust` - Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }` - Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T14[] }` diff --git a/docs/api/routes/account.get.md b/docs/api/routes/account.get.md new file mode 100644 index 0000000..9cb584e --- /dev/null +++ b/docs/api/routes/account.get.md @@ -0,0 +1,35 @@ +# `GET /account` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List accounts + +Lists payment accounts for the selected company. + +## Response `200` + +List of accounts + +```json +{ + "accounts": [ + { + "id": 1, + "companyId": 1, + "type": "…", + "name": "…" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/account.post.md b/docs/api/routes/account.post.md new file mode 100644 index 0000000..3bf5905 --- /dev/null +++ b/docs/api/routes/account.post.md @@ -0,0 +1,48 @@ +# `POST /account` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create account + +Creates a payment account for the selected company. + +## Body (JSON) + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `type` | yes | string | | +| `name` | yes | string | | + +```json +{ + "type": "…", + "name": "…" +} +``` + +## Response `201` + +Created account + +```json +{ + "account": { + "id": 1, + "companyId": 1, + "type": "…", + "name": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Validation error 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/account/_id.md b/docs/api/routes/account/_id.md new file mode 100644 index 0000000..82c46aa --- /dev/null +++ b/docs/api/routes/account/_id.md @@ -0,0 +1,32 @@ +# `DELETE /account/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete account + +Deletes an account of the selected company. Its payments are removed as well. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Account ID | + +## Response `204` + +Account deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid ID or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Account not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/order/_id/payment.get.md b/docs/api/routes/order/_id/payment.get.md new file mode 100644 index 0000000..169628b --- /dev/null +++ b/docs/api/routes/order/_id/payment.get.md @@ -0,0 +1,43 @@ +# `GET /order/{id}/payment` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List payments of an order + +Lists payments of the order (manifest) for the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Order ID | + +## Response `200` + +List of payments + +```json +{ + "payments": [ + { + "id": 1, + "accountId": 1, + "manifestId": 1, + "type": "…", + "amount": 1 + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid ID or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 404 | Order not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/order/_id/payment.post.md b/docs/api/routes/order/_id/payment.post.md new file mode 100644 index 0000000..e696eec --- /dev/null +++ b/docs/api/routes/order/_id/payment.post.md @@ -0,0 +1,58 @@ +# `POST /order/{id}/payment` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Add payment to an order + +Adds a payment to the order using an account of the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Order ID | + +## Body (JSON) + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `accountId` | yes | integer | | +| `type` | yes | string | | +| `amount` | yes | number | | + +```json +{ + "accountId": 1, + "type": "…", + "amount": 1 +} +``` + +## Response `201` + +Created payment + +```json +{ + "payment": { + "id": 1, + "accountId": 1, + "manifestId": 1, + "type": "…", + "amount": 1 + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Validation error or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Order or account not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/order/_id/shipping.get.md b/docs/api/routes/order/_id/shipping.get.md new file mode 100644 index 0000000..dfa3533 --- /dev/null +++ b/docs/api/routes/order/_id/shipping.get.md @@ -0,0 +1,43 @@ +# `GET /order/{id}/shipping` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List shippings of an order + +Lists shippings of the order (manifest) for the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Order ID | + +## Response `200` + +List of shippings + +```json +{ + "shippings": [ + { + "id": 1, + "manifestId": 1, + "number": "…", + "type": "…", + "state": "…" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid ID or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 404 | Order not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/order/_id/shipping.post.md b/docs/api/routes/order/_id/shipping.post.md new file mode 100644 index 0000000..80f8b82 --- /dev/null +++ b/docs/api/routes/order/_id/shipping.post.md @@ -0,0 +1,58 @@ +# `POST /order/{id}/shipping` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create shipping for an order + +Creates a shipping for the order. The shipping number is allocated from the +`shipping` number sequence (prefix and counter from the numbers API). + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Order ID | + +## Body (JSON) + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `type` | yes | string | | +| `state` | yes | string | | + +```json +{ + "type": "…", + "state": "…" +} +``` + +## Response `201` + +Created shipping + +```json +{ + "shipping": { + "id": 1, + "manifestId": 1, + "number": "…", + "type": "…", + "state": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Validation error or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Order not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/package/_id.md b/docs/api/routes/package/_id.md new file mode 100644 index 0000000..1c53e09 --- /dev/null +++ b/docs/api/routes/package/_id.md @@ -0,0 +1,31 @@ +# `DELETE /package/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete package + +Deletes a package of the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Package ID | + +## Response `204` + +Package deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid ID 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/payment/_id.md b/docs/api/routes/payment/_id.md new file mode 100644 index 0000000..46a0117 --- /dev/null +++ b/docs/api/routes/payment/_id.md @@ -0,0 +1,31 @@ +# `DELETE /payment/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete payment + +Deletes a payment of an order in the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Payment ID | + +## Response `204` + +Payment deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid ID 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/shipping/_id.md b/docs/api/routes/shipping/_id.md new file mode 100644 index 0000000..b00bd2a --- /dev/null +++ b/docs/api/routes/shipping/_id.md @@ -0,0 +1,31 @@ +# `DELETE /shipping/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Delete shipping + +Deletes a shipping of the selected company. Its packages are removed as well. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Shipping ID | + +## Response `204` + +Shipping deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid ID 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/shipping/_id/package.get.md b/docs/api/routes/shipping/_id/package.get.md new file mode 100644 index 0000000..66c8621 --- /dev/null +++ b/docs/api/routes/shipping/_id/package.get.md @@ -0,0 +1,45 @@ +# `GET /shipping/{id}/package` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List packages of a shipping + +Lists packages of the shipping for the selected company. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Shipping ID | + +## Response `200` + +List of packages + +```json +{ + "packages": [ + { + "id": 1, + "shippingId": 1, + "entityaddressId": 1, + "number": "…", + "type": "…", + "state": "…", + "details": "…" + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid ID or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 404 | Shipping not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/shipping/_id/package.post.md b/docs/api/routes/shipping/_id/package.post.md new file mode 100644 index 0000000..a1b0520 --- /dev/null +++ b/docs/api/routes/shipping/_id/package.post.md @@ -0,0 +1,64 @@ +# `POST /shipping/{id}/package` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Create package for a shipping + +Creates a package for the shipping. The package number is allocated from the +`package` number sequence. `entityaddressId` is required (target address). + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `id` | yes | integer | Shipping ID | + +## Body (JSON) + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| `entityaddressId` | yes | integer | | +| `type` | yes | string | | +| `state` | yes | string | | +| `details` | no | string | | + +```json +{ + "entityaddressId": 1, + "type": "…", + "state": "…", + "details": "…" +} +``` + +## Response `201` + +Created package + +```json +{ + "package": { + "id": 1, + "shippingId": 1, + "entityaddressId": 1, + "number": "…", + "type": "…", + "state": "…", + "details": "…" + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Validation error or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Shipping or entity address not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/package-lock.json b/package-lock.json index 76b2687..a6f5add 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "vm2": "^3.12.2" }, "devDependencies": { + "acorn": "^8.19.0", "swagger-jsdoc": "^6.3.0" } }, diff --git a/package.json b/package.json index 8e55b9a..4589e7e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "manage-users": "node scripts/manageUsers.js", "wipe-company": "node scripts/wipeCompany.js", "docs:api": "node scripts/generateApiDocs.js", + "review:functions": "node scripts/extractFunctions.js", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { @@ -40,6 +41,7 @@ "vm2": "^3.12.2" }, "devDependencies": { + "acorn": "^8.19.0", "swagger-jsdoc": "^6.3.0" }, "allowScripts": { diff --git a/scripts/extractFunctions.js b/scripts/extractFunctions.js new file mode 100644 index 0000000..260555c --- /dev/null +++ b/scripts/extractFunctions.js @@ -0,0 +1,133 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse } from 'acorn'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(scriptDir, '..'); +const outputDir = path.join(rootDir, 'output'); +const excludedDirs = new Set(['.git', 'data', 'node_modules', 'output']); +const functions = []; + +function collectJavaScriptFiles(dir) { + const files = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory() && excludedDirs.has(entry.name)) continue; + + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...collectJavaScriptFiles(fullPath)); + } else if (entry.isFile() && entry.name.endsWith('.js')) { + files.push(fullPath); + } + } + return files; +} + +function inferredName(node, parent) { + if (node.id?.name) return node.id.name; + if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier') { + return parent.id.name; + } + if (parent?.type === 'AssignmentExpression') { + return parent.left.name ?? parent.left.property?.name ?? 'assigned'; + } + if (parent?.type === 'Property' || parent?.type === 'MethodDefinition') { + return parent.key.name ?? parent.key.value ?? 'method'; + } + if (parent?.type === 'ExportDefaultDeclaration') return 'default'; + return 'anonymous'; +} + +function visit(node, parent, source, relativePath) { + if (!node || typeof node !== 'object') return; + + if (node.type === 'FunctionDeclaration' + || node.type === 'FunctionExpression' + || node.type === 'ArrowFunctionExpression') { + const method = (parent?.type === 'MethodDefinition' + || (parent?.type === 'Property' && parent.method)) + ? parent + : node; + const start = method.start; + const end = method.end; + const startLine = source.slice(0, start).split('\n').length; + const endLine = source.slice(0, end).split('\n').length; + const name = inferredName(node, parent); + let signature = source.slice(start, node.body.start).replace(/\s+/g, ' ').trim(); + if (method === node + && name !== 'anonymous' + && node.type !== 'FunctionDeclaration' + && parent?.type !== 'ExportDefaultDeclaration') { + signature = `${name} = ${signature}`; + } + functions.push({ + source: relativePath, + start, + end, + startLine, + endLine, + name, + signature, + code: source.slice(start, end), + }); + } + + for (const [key, value] of Object.entries(node)) { + if (key === 'start' || key === 'end' || key === 'loc') continue; + if (Array.isArray(value)) { + for (const child of value) visit(child, node, source, relativePath); + } else if (value && typeof value === 'object') { + visit(value, node, source, relativePath); + } + } +} + +const sourceFiles = collectJavaScriptFiles(rootDir).sort(); +for (const filePath of sourceFiles) { + const source = fs.readFileSync(filePath, 'utf8'); + const relativePath = path.relative(rootDir, filePath).split(path.sep).join('/'); + let ast; + try { + ast = parse(source, { + ecmaVersion: 'latest', + sourceType: 'module', + allowHashBang: true, + }); + } catch (error) { + throw new Error(`Could not parse ${relativePath}: ${error.message}`, { cause: error }); + } + visit(ast, null, source, relativePath); +} + +fs.mkdirSync(outputDir, { recursive: true }); +for (const entry of fs.readdirSync(outputDir, { withFileTypes: true })) { + if (entry.isFile() && /^function-.*\.js$/.test(entry.name)) { + fs.unlinkSync(path.join(outputDir, entry.name)); + } +} + +functions.sort((a, b) => a.source.localeCompare(b.source) || a.start - b.start); +const nameCounts = new Map(); +for (const fn of functions) { + const sourceSlug = fn.source.replace(/\.js$/, '').replace(/[^a-zA-Z0-9_-]+/g, '-'); + const nameSlug = fn.name.replace(/[^a-zA-Z0-9_-]+/g, '-'); + const baseName = `function-${sourceSlug}-L${fn.startLine}-${nameSlug}`; + const sequence = (nameCounts.get(baseName) ?? 0) + 1; + nameCounts.set(baseName, sequence); + + const outputName = `${baseName}-${sequence}.js`; + const contents = [ + `// Source: ${fn.source} (lines ${fn.startLine}-${fn.endLine})`, + fn.code, + '', + ].join('\n'); + fs.writeFileSync(path.join(outputDir, outputName), contents); +} + +const index = functions + .map(fn => `${fn.source}:${fn.startLine}\t${fn.signature}`) + .join('\n'); +fs.writeFileSync(path.join(outputDir, 'functions.txt'), `${index}\n`); + +console.log(`Extracted ${functions.length} functions from ${sourceFiles.length} JavaScript files into ${path.relative(rootDir, outputDir)}/`); diff --git a/src/api/auth/meJson.js b/src/api/auth/meJson.js index 9d80109..b2118a2 100644 --- a/src/api/auth/meJson.js +++ b/src/api/auth/meJson.js @@ -1,5 +1,5 @@ import env from '../../config/env.js'; -export function meJson(res, status, body) { +export function respondWithAuthenticatedUser(res, status, body) { return res.status(status).json({ ...body, googleClientId: env.googleClientId }); } diff --git a/src/api/openapi.js b/src/api/openapi.js index c43f61a..4fd949e 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -1096,6 +1096,67 @@ * description: Tool response content payload * answer: * description: Shorthand answer for ask_user response + * Account: + * type: object + * required: [id, companyId, type, name] + * properties: + * id: + * type: integer + * companyId: + * type: integer + * type: + * type: string + * name: + * type: string + * Payment: + * type: object + * required: [id, accountId, manifestId, type, amount] + * properties: + * id: + * type: integer + * accountId: + * type: integer + * manifestId: + * type: integer + * type: + * type: string + * amount: + * type: number + * Shipping: + * type: object + * required: [id, manifestId, number, type, state] + * properties: + * id: + * type: integer + * manifestId: + * type: integer + * number: + * type: string + * description: Shipping number from the `shipping` number sequence + * type: + * type: string + * state: + * type: string + * Package: + * type: object + * required: [id, shippingId, entityaddressId, number, type, state] + * properties: + * id: + * type: integer + * shippingId: + * type: integer + * entityaddressId: + * type: integer + * number: + * type: string + * description: Package number from the `package` number sequence + * type: + * type: string + * state: + * type: string + * details: + * type: string + * nullable: true * Printer: * type: object * required: [id, company_id, address, type, sort] diff --git a/src/api/routes/account.js b/src/api/routes/account.js new file mode 100644 index 0000000..98db1d5 --- /dev/null +++ b/src/api/routes/account.js @@ -0,0 +1,213 @@ +import * as db from '../../db/account.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 accountErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message?.endsWith(' not found')) return 404; + if (error.message?.endsWith(' required')) return 400; + return 500; +} + +/** + * Registers account routes for the authenticated user's selected company. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /account: + * get: + * tags: [Account] + * summary: List accounts + * description: Lists payment accounts for the selected company. + * security: + * - cookieAuth: [] + * responses: + * 200: + * description: List of accounts + * content: + * application/json: + * schema: + * type: object + * properties: + * accounts: + * type: array + * items: + * $ref: '#/components/schemas/Account' + * 400: + * description: No company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/account`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const accounts = await db.list(req.user.id); + res.status(200).json({ accounts }); + } catch (error) { + const status = accountErrorStatus(error); + if (status === 500) console.error('List accounts failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /account: + * post: + * tags: [Account] + * summary: Create account + * description: Creates a payment account for the selected company. + * security: + * - cookieAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [type, name] + * properties: + * type: + * type: string + * name: + * type: string + * responses: + * 201: + * description: Created account + * content: + * application/json: + * schema: + * type: object + * properties: + * account: + * $ref: '#/components/schemas/Account' + * 400: + * description: Validation error 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.post(`${basePath}/account`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + try { + const account = await db.create(req.user.id, req.body ?? {}); + res.status(201).json({ account }); + } catch (error) { + const status = accountErrorStatus(error); + if (status === 500) console.error('Create account failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /account/{id}: + * delete: + * tags: [Account] + * summary: Delete account + * description: Deletes an account of the selected company. Its payments are removed as well. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Account ID + * responses: + * 204: + * description: Account 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: Account not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.delete(`${basePath}/account/: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); + res.status(204).end(); + } catch (error) { + const status = accountErrorStatus(error); + if (status === 500) console.error('Delete account failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/package.js b/src/api/routes/package.js new file mode 100644 index 0000000..885c09c --- /dev/null +++ b/src/api/routes/package.js @@ -0,0 +1,252 @@ +import * as db from '../../db/package.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 packageErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message?.endsWith(' not found')) return 404; + if (error.message?.endsWith(' required')) return 400; + return 500; +} + +/** + * Registers package routes (packages belong to a shipping and target an entity address). + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /shipping/{id}/package: + * get: + * tags: [Package] + * summary: List packages of a shipping + * description: Lists packages of the shipping for the selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Shipping ID + * responses: + * 200: + * description: List of packages + * content: + * application/json: + * schema: + * type: object + * properties: + * packages: + * type: array + * items: + * $ref: '#/components/schemas/Package' + * 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' + * 404: + * description: Shipping not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/shipping/:id/package`, 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 packages = await db.listByShippingId(req.user.id, parsedId.id); + res.status(200).json({ packages }); + } catch (error) { + const status = packageErrorStatus(error); + if (status === 500) console.error('List packages failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /shipping/{id}/package: + * post: + * tags: [Package] + * summary: Create package for a shipping + * description: | + * Creates a package for the shipping. The package number is allocated from the + * `package` number sequence. `entityaddressId` is required (target address). + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Shipping ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [entityaddressId, type, state] + * properties: + * entityaddressId: + * type: integer + * type: + * type: string + * state: + * type: string + * details: + * type: string + * responses: + * 201: + * description: Created package + * content: + * application/json: + * schema: + * type: object + * properties: + * package: + * $ref: '#/components/schemas/Package' + * 400: + * description: Validation error 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: Shipping or entity address not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.post(`${basePath}/shipping/:id/package`, 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 pkg = await db.create(req.user.id, { + ...(req.body ?? {}), + shippingId: parsedId.id, + }); + res.status(201).json({ package: pkg }); + } catch (error) { + const status = packageErrorStatus(error); + if (status === 500) console.error('Create package failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /package/{id}: + * delete: + * tags: [Package] + * summary: Delete package + * description: Deletes a package of the selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Package ID + * responses: + * 204: + * description: Package 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' + */ + app.delete(`${basePath}/package/: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); + res.status(204).end(); + } catch (error) { + const status = packageErrorStatus(error); + if (status === 500) console.error('Delete package failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/payment.js b/src/api/routes/payment.js new file mode 100644 index 0000000..829de72 --- /dev/null +++ b/src/api/routes/payment.js @@ -0,0 +1,249 @@ +import * as db from '../../db/payment.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 paymentErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message?.endsWith(' not found')) return 404; + if (error.message?.endsWith(' required')) return 400; + if (error.message === 'Invalid payment amount') return 400; + return 500; +} + +/** + * Registers payment routes (payments belong to an order / manifest). + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /order/{id}/payment: + * get: + * tags: [Payment] + * summary: List payments of an order + * description: Lists payments of the order (manifest) for the selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order ID + * responses: + * 200: + * description: List of payments + * content: + * application/json: + * schema: + * type: object + * properties: + * payments: + * type: array + * items: + * $ref: '#/components/schemas/Payment' + * 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' + * 404: + * description: Order not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/order/:id/payment`, 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 payments = await db.listByManifestId(req.user.id, parsedId.id); + res.status(200).json({ payments }); + } catch (error) { + const status = paymentErrorStatus(error); + if (status === 500) console.error('List payments failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /order/{id}/payment: + * post: + * tags: [Payment] + * summary: Add payment to an order + * description: Adds a payment to the order using an account of the selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [accountId, type, amount] + * properties: + * accountId: + * type: integer + * type: + * type: string + * amount: + * type: number + * responses: + * 201: + * description: Created payment + * content: + * application/json: + * schema: + * type: object + * properties: + * payment: + * $ref: '#/components/schemas/Payment' + * 400: + * description: Validation error 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: Order or account not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.post(`${basePath}/order/:id/payment`, 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 payment = await db.create(req.user.id, { + ...(req.body ?? {}), + manifestId: parsedId.id, + }); + res.status(201).json({ payment }); + } catch (error) { + const status = paymentErrorStatus(error); + if (status === 500) console.error('Create payment failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /payment/{id}: + * delete: + * tags: [Payment] + * summary: Delete payment + * description: Deletes a payment of an order in the selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Payment ID + * responses: + * 204: + * description: Payment 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' + */ + app.delete(`${basePath}/payment/: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); + res.status(204).end(); + } catch (error) { + const status = paymentErrorStatus(error); + if (status === 500) console.error('Delete payment failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/shipping.js b/src/api/routes/shipping.js new file mode 100644 index 0000000..42b6120 --- /dev/null +++ b/src/api/routes/shipping.js @@ -0,0 +1,248 @@ +import * as db from '../../db/shipping.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 shippingErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message?.endsWith(' not found')) return 404; + if (error.message?.endsWith(' required')) return 400; + return 500; +} + +/** + * Registers shipping routes (shippings belong to an order / manifest). + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /order/{id}/shipping: + * get: + * tags: [Shipping] + * summary: List shippings of an order + * description: Lists shippings of the order (manifest) for the selected company. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order ID + * responses: + * 200: + * description: List of shippings + * content: + * application/json: + * schema: + * type: object + * properties: + * shippings: + * type: array + * items: + * $ref: '#/components/schemas/Shipping' + * 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' + * 404: + * description: Order not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/order/:id/shipping`, 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 shippings = await db.listByManifestId(req.user.id, parsedId.id); + res.status(200).json({ shippings }); + } catch (error) { + const status = shippingErrorStatus(error); + if (status === 500) console.error('List shippings failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /order/{id}/shipping: + * post: + * tags: [Shipping] + * summary: Create shipping for an order + * description: | + * Creates a shipping for the order. The shipping number is allocated from the + * `shipping` number sequence (prefix and counter from the numbers API). + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Order ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [type, state] + * properties: + * type: + * type: string + * state: + * type: string + * responses: + * 201: + * description: Created shipping + * content: + * application/json: + * schema: + * type: object + * properties: + * shipping: + * $ref: '#/components/schemas/Shipping' + * 400: + * description: Validation error 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: Order not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.post(`${basePath}/order/:id/shipping`, 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 shipping = await db.create(req.user.id, { + ...(req.body ?? {}), + manifestId: parsedId.id, + }); + res.status(201).json({ shipping }); + } catch (error) { + const status = shippingErrorStatus(error); + if (status === 500) console.error('Create shipping failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /shipping/{id}: + * delete: + * tags: [Shipping] + * summary: Delete shipping + * description: Deletes a shipping of the selected company. Its packages are removed as well. + * security: + * - cookieAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: Shipping ID + * responses: + * 204: + * description: Shipping 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' + */ + app.delete(`${basePath}/shipping/: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); + res.status(204).end(); + } catch (error) { + const status = shippingErrorStatus(error); + if (status === 500) console.error('Delete shipping failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/db/account.js b/src/db/account.js new file mode 100644 index 0000000..0c71043 --- /dev/null +++ b/src/db/account.js @@ -0,0 +1,16 @@ +import { getStore } from './store.js'; + +export async function list(googleAuthId) { + const store = await getStore(); + return store.account.list(googleAuthId); +} + +export async function create(googleAuthId, data) { + const store = await getStore(); + return store.account.create(googleAuthId, data); +} + +export async function remove(googleAuthId, id) { + const store = await getStore(); + return store.account.delete(googleAuthId, id); +} diff --git a/src/db/package.js b/src/db/package.js new file mode 100644 index 0000000..96f32cd --- /dev/null +++ b/src/db/package.js @@ -0,0 +1,16 @@ +import { getStore } from './store.js'; + +export async function listByShippingId(googleAuthId, shippingId) { + const store = await getStore(); + return store.package.listByShippingId(googleAuthId, shippingId); +} + +export async function create(googleAuthId, data) { + const store = await getStore(); + return store.package.create(googleAuthId, data); +} + +export async function remove(googleAuthId, id) { + const store = await getStore(); + return store.package.delete(googleAuthId, id); +} diff --git a/src/db/payment.js b/src/db/payment.js new file mode 100644 index 0000000..9b08471 --- /dev/null +++ b/src/db/payment.js @@ -0,0 +1,16 @@ +import { getStore } from './store.js'; + +export async function listByManifestId(googleAuthId, manifestId) { + const store = await getStore(); + return store.payment.listByManifestId(googleAuthId, manifestId); +} + +export async function create(googleAuthId, data) { + const store = await getStore(); + return store.payment.create(googleAuthId, data); +} + +export async function remove(googleAuthId, id) { + const store = await getStore(); + return store.payment.delete(googleAuthId, id); +} diff --git a/src/db/shipping.js b/src/db/shipping.js new file mode 100644 index 0000000..bab6d8f --- /dev/null +++ b/src/db/shipping.js @@ -0,0 +1,16 @@ +import { getStore } from './store.js'; + +export async function listByManifestId(googleAuthId, manifestId) { + const store = await getStore(); + return store.shipping.listByManifestId(googleAuthId, manifestId); +} + +export async function create(googleAuthId, data) { + const store = await getStore(); + return store.shipping.create(googleAuthId, data); +} + +export async function remove(googleAuthId, id) { + const store = await getStore(); + return store.shipping.delete(googleAuthId, id); +} diff --git a/src/db/store.js b/src/db/store.js index e9da6b8..16dc2f6 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -48,6 +48,10 @@ const STORE_NAMESPACES = [ 'categoryattributevalue', 'modelconversation', 'modelmessage', + 'account', + 'payment', + 'shipping', + 'package', ]; /**