Add unit API and article unitamount/unitSi patch.
Units use the selected company; article PATCH accepts name/unitamount/unitSi.
This commit is contained in:
@@ -53,6 +53,9 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
|||||||
| [`routes/health.md`](routes/health.md) | `GET /health` |
|
| [`routes/health.md`](routes/health.md) | `GET /health` |
|
||||||
| [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` |
|
| [`routes/image/get.md`](routes/image/get.md) | `GET /image/{fullHash}` |
|
||||||
| [`routes/thumb/get.md`](routes/thumb/get.md) | `GET /thumb/{fullHash}` |
|
| [`routes/thumb/get.md`](routes/thumb/get.md) | `GET /thumb/{fullHash}` |
|
||||||
|
| [`routes/unit/delete.md`](routes/unit/delete.md) | `DELETE /unit` |
|
||||||
|
| [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` |
|
||||||
|
| [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` |
|
||||||
|
|
||||||
Machine-readable spec: [`openapi.json`](openapi.json).
|
Machine-readable spec: [`openapi.json`](openapi.json).
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,280 @@
|
|||||||
"name": "Company",
|
"name": "Company",
|
||||||
"description": "Company CRUD"
|
"description": "Company CRUD"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Unit",
|
||||||
|
"description": "Company-scoped units of measure"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Image",
|
"name": "Image",
|
||||||
"description": "Content-addressed AVIF by fullHash"
|
"description": "Content-addressed AVIF by fullHash"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"/unit": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Unit"
|
||||||
|
],
|
||||||
|
"summary": "List units",
|
||||||
|
"description": "Lists units for the authenticated user's selected company (default-language symbol/name).",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-markdown": "unit/list.md",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Unit list",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"units": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Unit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "No company selected",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not authorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Company not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"Unit"
|
||||||
|
],
|
||||||
|
"summary": "Upsert unit",
|
||||||
|
"description": "Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language.\n`basesifactor` must be finite and `> 0`.\n",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-markdown": "unit/upsert.md",
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"si",
|
||||||
|
"basesi",
|
||||||
|
"basesifactor",
|
||||||
|
"symbol",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"si": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"basesi": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"basesifactor": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"si": "l",
|
||||||
|
"basesi": "l",
|
||||||
|
"basesifactor": 1,
|
||||||
|
"symbol": "L",
|
||||||
|
"name": "Liter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Upserted unit",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"unit": {
|
||||||
|
"$ref": "#/components/schemas/Unit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid unit or no company selected",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not authorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Company not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"Unit"
|
||||||
|
],
|
||||||
|
"summary": "Delete unit",
|
||||||
|
"description": "Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it.",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"cookieAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-markdown": "unit/delete.md",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "si",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "Deleted"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Missing si or no company selected",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Not authorized",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Company or unit not found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"description": "Unit in use",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/image/{fullHash}": {
|
"/image/{fullHash}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -1110,7 +1378,7 @@
|
|||||||
"Article"
|
"Article"
|
||||||
],
|
],
|
||||||
"summary": "Update article by SKU",
|
"summary": "Update article by SKU",
|
||||||
"description": "Updates an article name by SKU; returns the same detail shape as GET.",
|
"description": "Patches article fields. At least one of `name`, `unitamount`, `unitSi` required.\n`unitSi` must already exist for the company. Returns the same detail shape as GET.\n",
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"cookieAuth": []
|
"cookieAuth": []
|
||||||
@@ -1134,16 +1402,22 @@
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"unitamount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "Must be finite and `> 0`"
|
||||||
|
},
|
||||||
|
"unitSi": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Existing company unit `si`"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"example": {
|
"example": {
|
||||||
"name": "Updated name"
|
"unitamount": 0.24,
|
||||||
|
"unitSi": "l"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1166,7 +1440,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Validation error",
|
"description": "Validation error (Name required / Invalid unitamount)",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -1196,7 +1470,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"404": {
|
"404": {
|
||||||
"description": "Article not found",
|
"description": "Article or unit not found",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
@@ -1283,7 +1557,7 @@
|
|||||||
"Article"
|
"Article"
|
||||||
],
|
],
|
||||||
"summary": "Replace POS prices by SKU",
|
"summary": "Replace POS prices by SKU",
|
||||||
"description": "Replace-all prices for the company POS customer group.\nEach entry: `price` (gross), `tax` (rate %, ensured via tax.ensure), optional `minmultiplier` (default 1).\nStored as net = gross / (1 + tax/100). Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.\n",
|
"description": "Replace-all prices for the company POS customer group.\nEach entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1).\nEmpty `prices` clears all POS prices. Returns the same shape as GET by SKU.\n",
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"cookieAuth": []
|
"cookieAuth": []
|
||||||
@@ -1321,11 +1595,11 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"price": {
|
"price": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Gross price (Brutto)"
|
"description": "Net price"
|
||||||
},
|
},
|
||||||
"tax": {
|
"tax": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Tax rate percent (e.g. 19)"
|
"description": "Tax rate (e.g. 19)"
|
||||||
},
|
},
|
||||||
"minmultiplier": {
|
"minmultiplier": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@@ -1338,11 +1612,11 @@
|
|||||||
"example": {
|
"example": {
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"price": 11.9,
|
"price": 10.5,
|
||||||
"tax": 19
|
"tax": 19
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"price": 99,
|
"price": 9.9,
|
||||||
"tax": 19,
|
"tax": 19,
|
||||||
"minmultiplier": 10
|
"minmultiplier": 10
|
||||||
}
|
}
|
||||||
@@ -2042,6 +2316,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Unit": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"si",
|
||||||
|
"basesi",
|
||||||
|
"basesifactor"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"si": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"basesi": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"basesifactor": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"symbol": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ArticleSummary": {
|
"ArticleSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|||||||
Replace POS prices by SKU
|
Replace POS prices by SKU
|
||||||
|
|
||||||
Replace-all prices for the company POS customer group.
|
Replace-all prices for the company POS customer group.
|
||||||
Each entry: `price` (gross), `tax` (rate %, ensured via tax.ensure), optional `minmultiplier` (default 1).
|
Each entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1).
|
||||||
Stored as net = gross / (1 + tax/100). Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
|
Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
|
||||||
|
|
||||||
|
|
||||||
## Path parameters
|
## Path parameters
|
||||||
@@ -21,11 +21,11 @@ Stored as net = gross / (1 + tax/100). Empty `prices` clears all POS prices. Ret
|
|||||||
{
|
{
|
||||||
"prices": [
|
"prices": [
|
||||||
{
|
{
|
||||||
"price": 11.9,
|
"price": 10.5,
|
||||||
"tax": 19
|
"tax": 19
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"price": 99,
|
"price": 9.9,
|
||||||
"tax": 19,
|
"tax": 19,
|
||||||
"minmultiplier": 10
|
"minmultiplier": 10
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|||||||
|
|
||||||
Update article by SKU
|
Update article by SKU
|
||||||
|
|
||||||
Updates an article name by SKU; returns the same detail shape as GET.
|
Patches article fields. At least one of `name`, `unitamount`, `unitSi` required.
|
||||||
|
`unitSi` must already exist for the company. Returns the same detail shape as GET.
|
||||||
|
|
||||||
|
|
||||||
## Path parameters
|
## Path parameters
|
||||||
|
|
||||||
@@ -16,7 +18,8 @@ Updates an article name by SKU; returns the same detail shape as GET.
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Updated name"
|
"unitamount": 0.24,
|
||||||
|
"unitSi": "l"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -34,10 +37,10 @@ Updated article detail
|
|||||||
|
|
||||||
| Status | Description |
|
| Status | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| 400 | Validation error — `{"error":"…"}` |
|
| 400 | Validation error (Name required / Invalid unitamount) — `{"error":"…"}` |
|
||||||
| 401 | Unauthorized — `{"error":"…"}` |
|
| 401 | Unauthorized — `{"error":"…"}` |
|
||||||
| 403 | Not authorized — `{"error":"…"}` |
|
| 403 | Not authorized — `{"error":"…"}` |
|
||||||
| 404 | Article not found — `{"error":"…"}` |
|
| 404 | Article or unit not found — `{"error":"…"}` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
33
docs/api/routes/unit/delete.md
Normal file
33
docs/api/routes/unit/delete.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# `DELETE /unit`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Delete unit
|
||||||
|
|
||||||
|
Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it.
|
||||||
|
|
||||||
|
## Query parameters
|
||||||
|
|
||||||
|
| Param | Required | Type | Description |
|
||||||
|
|-------|----------|------|-------------|
|
||||||
|
| `si` | yes | string | |
|
||||||
|
|
||||||
|
## Response `204`
|
||||||
|
|
||||||
|
Deleted
|
||||||
|
|
||||||
|
_No content_
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Missing si or no company selected — `{"error":"…"}` |
|
||||||
|
| 401 | Unauthorized — `{"error":"…"}` |
|
||||||
|
| 403 | Not authorized — `{"error":"…"}` |
|
||||||
|
| 404 | Company or unit not found — `{"error":"…"}` |
|
||||||
|
| 409 | Unit in use — `{"error":"…"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
30
docs/api/routes/unit/list.md
Normal file
30
docs/api/routes/unit/list.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# `GET /unit`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
List units
|
||||||
|
|
||||||
|
Lists units for the authenticated user's selected company (default-language symbol/name).
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Unit list
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"units": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | No company selected — `{"error":"…"}` |
|
||||||
|
| 401 | Unauthorized — `{"error":"…"}` |
|
||||||
|
| 403 | Not authorized — `{"error":"…"}` |
|
||||||
|
| 404 | Company not found — `{"error":"…"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
44
docs/api/routes/unit/upsert.md
Normal file
44
docs/api/routes/unit/upsert.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# `PUT /unit`
|
||||||
|
|
||||||
|
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||||
|
|
||||||
|
Upsert unit
|
||||||
|
|
||||||
|
Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language.
|
||||||
|
`basesifactor` must be finite and `> 0`.
|
||||||
|
|
||||||
|
|
||||||
|
## Body (JSON)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"si": "l",
|
||||||
|
"basesi": "l",
|
||||||
|
"basesifactor": 1,
|
||||||
|
"symbol": "L",
|
||||||
|
"name": "Liter"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response `200`
|
||||||
|
|
||||||
|
Upserted unit
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"unit": { … }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 400 | Invalid unit or no company selected — `{"error":"…"}` |
|
||||||
|
| 401 | Unauthorized — `{"error":"…"}` |
|
||||||
|
| 403 | Not authorized — `{"error":"…"}` |
|
||||||
|
| 404 | Company not found — `{"error":"…"}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||||
@@ -33,6 +33,7 @@ const options = {
|
|||||||
{ name: 'Article', description: 'Articles under categories' },
|
{ name: 'Article', description: 'Articles under categories' },
|
||||||
{ name: 'Category', description: 'Category tree and membership' },
|
{ name: 'Category', description: 'Category tree and membership' },
|
||||||
{ name: 'Company', description: 'Company CRUD' },
|
{ name: 'Company', description: 'Company CRUD' },
|
||||||
|
{ name: 'Unit', description: 'Company-scoped units of measure' },
|
||||||
{ name: 'Image', description: 'Content-addressed AVIF by fullHash' },
|
{ name: 'Image', description: 'Content-addressed AVIF by fullHash' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,6 +57,22 @@
|
|||||||
* type: integer
|
* type: integer
|
||||||
* name:
|
* name:
|
||||||
* type: string
|
* type: string
|
||||||
|
* Unit:
|
||||||
|
* type: object
|
||||||
|
* required: [si, basesi, basesifactor]
|
||||||
|
* properties:
|
||||||
|
* si:
|
||||||
|
* type: string
|
||||||
|
* basesi:
|
||||||
|
* type: string
|
||||||
|
* basesifactor:
|
||||||
|
* type: number
|
||||||
|
* symbol:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
* ArticleSummary:
|
* ArticleSummary:
|
||||||
* type: object
|
* type: object
|
||||||
* properties:
|
* properties:
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ function articleErrorStatus(error) {
|
|||||||
if (error.message === 'Article has children') return 400;
|
if (error.message === 'Article has children') return 400;
|
||||||
if (error.message === 'Invalid prices') return 400;
|
if (error.message === 'Invalid prices') return 400;
|
||||||
if (error.message === 'POS customer group required') return 400;
|
if (error.message === 'POS customer group required') return 400;
|
||||||
|
if (error.message === 'Invalid unitamount') return 400;
|
||||||
|
if (error.message === 'Unit not found') return 404;
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +187,9 @@ export function register(app, basePath = '') {
|
|||||||
* patch:
|
* patch:
|
||||||
* tags: [Article]
|
* tags: [Article]
|
||||||
* summary: Update article by SKU
|
* summary: Update article by SKU
|
||||||
* description: Updates an article name by SKU; returns the same detail shape as GET.
|
* description: |
|
||||||
|
* Patches article fields. At least one of `name`, `unitamount`, `unitSi` required.
|
||||||
|
* `unitSi` must already exist for the company. Returns the same detail shape as GET.
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
* x-markdown: article/update.md
|
* x-markdown: article/update.md
|
||||||
@@ -202,12 +206,18 @@ export function register(app, basePath = '') {
|
|||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* type: object
|
||||||
* required: [name]
|
|
||||||
* properties:
|
* properties:
|
||||||
* name:
|
* name:
|
||||||
* type: string
|
* type: string
|
||||||
|
* unitamount:
|
||||||
|
* type: number
|
||||||
|
* description: Must be finite and `> 0`
|
||||||
|
* unitSi:
|
||||||
|
* type: string
|
||||||
|
* description: Existing company unit `si`
|
||||||
* example:
|
* example:
|
||||||
* name: Updated name
|
* unitamount: 0.24
|
||||||
|
* unitSi: l
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Updated article detail
|
* description: Updated article detail
|
||||||
@@ -219,7 +229,7 @@ export function register(app, basePath = '') {
|
|||||||
* article:
|
* article:
|
||||||
* $ref: '#/components/schemas/ArticleDetail'
|
* $ref: '#/components/schemas/ArticleDetail'
|
||||||
* 400:
|
* 400:
|
||||||
* description: Validation error
|
* description: Validation error (Name required / Invalid unitamount)
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
@@ -237,7 +247,7 @@ export function register(app, basePath = '') {
|
|||||||
* schema:
|
* schema:
|
||||||
* $ref: '#/components/schemas/Error'
|
* $ref: '#/components/schemas/Error'
|
||||||
* 404:
|
* 404:
|
||||||
* description: Article not found
|
* description: Article or unit not found
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
@@ -309,13 +319,24 @@ export function register(app, basePath = '') {
|
|||||||
return res.status(400).json({ error: 'sku is required' });
|
return res.status(400).json({ error: 'sku is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : '';
|
const body = req.body ?? {};
|
||||||
if (!name) {
|
const patch = {};
|
||||||
return res.status(400).json({ error: 'name is required' });
|
if (Object.prototype.hasOwnProperty.call(body, 'name')) {
|
||||||
|
patch.name = body.name;
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(body, 'unitamount')) {
|
||||||
|
patch.unitamount = body.unitamount;
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(body, 'unitSi')) {
|
||||||
|
patch.unitSi = body.unitSi;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('name' in patch) && !('unitamount' in patch) && !('unitSi' in patch)) {
|
||||||
|
return res.status(400).json({ error: 'name, unitamount, or unitSi is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const article = await db.updateBySku(req.user.id, decodeURIComponent(sku), { name });
|
const article = await db.updateBySku(req.user.id, decodeURIComponent(sku), patch);
|
||||||
res.status(200).json({ article });
|
res.status(200).json({ article });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const status = articleErrorStatus(error);
|
const status = articleErrorStatus(error);
|
||||||
|
|||||||
246
src/api/routes/unit.js
Normal file
246
src/api/routes/unit.js
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import * as db from '../../db/unit.js';
|
||||||
|
|
||||||
|
function requireSelectedCompany(req) {
|
||||||
|
const companyId = req.user.selectedCompanyId;
|
||||||
|
if (companyId == null) {
|
||||||
|
return { error: 'No company selected' };
|
||||||
|
}
|
||||||
|
return { companyId };
|
||||||
|
}
|
||||||
|
|
||||||
|
function unitErrorStatus(error) {
|
||||||
|
if (error.message === 'Not authorized') return 403;
|
||||||
|
if (error.message === 'Company not found') return 404;
|
||||||
|
if (error.message === 'Unit not found') return 404;
|
||||||
|
if (error.message === 'Invalid unit') return 400;
|
||||||
|
if (error.message === 'Unit in use') return 409;
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers unit routes for the authenticated user's selected company.
|
||||||
|
* @param {import('express').Application} app
|
||||||
|
* @param {string} [basePath='']
|
||||||
|
*/
|
||||||
|
export function register(app, basePath = '') {
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /unit:
|
||||||
|
* get:
|
||||||
|
* tags: [Unit]
|
||||||
|
* summary: List units
|
||||||
|
* description: Lists units for the authenticated user's selected company (default-language symbol/name).
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* x-markdown: unit/list.md
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Unit list
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* units:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/Unit'
|
||||||
|
* 400:
|
||||||
|
* description: No company selected
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 403:
|
||||||
|
* description: Not authorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 404:
|
||||||
|
* description: Company not found
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* put:
|
||||||
|
* tags: [Unit]
|
||||||
|
* summary: Upsert unit
|
||||||
|
* description: |
|
||||||
|
* Insert or update unit by `si` for the selected company. Upserts `unittext` for the company language.
|
||||||
|
* `basesifactor` must be finite and `> 0`.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* x-markdown: unit/upsert.md
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required: [si, basesi, basesifactor, symbol, name]
|
||||||
|
* properties:
|
||||||
|
* si:
|
||||||
|
* type: string
|
||||||
|
* basesi:
|
||||||
|
* type: string
|
||||||
|
* basesifactor:
|
||||||
|
* type: number
|
||||||
|
* symbol:
|
||||||
|
* type: string
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* example:
|
||||||
|
* si: l
|
||||||
|
* basesi: l
|
||||||
|
* basesifactor: 1
|
||||||
|
* symbol: L
|
||||||
|
* name: Liter
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Upserted unit
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* unit:
|
||||||
|
* $ref: '#/components/schemas/Unit'
|
||||||
|
* 400:
|
||||||
|
* description: Invalid unit or no company selected
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 403:
|
||||||
|
* description: Not authorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 404:
|
||||||
|
* description: Company not found
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* delete:
|
||||||
|
* tags: [Unit]
|
||||||
|
* summary: Delete unit
|
||||||
|
* description: Deletes `si` for the selected company. Fails with `Unit in use` if articles still reference it.
|
||||||
|
* security:
|
||||||
|
* - cookieAuth: []
|
||||||
|
* x-markdown: unit/delete.md
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: si
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* responses:
|
||||||
|
* 204:
|
||||||
|
* description: Deleted
|
||||||
|
* 400:
|
||||||
|
* description: Missing si or no company selected
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 401:
|
||||||
|
* description: Unauthorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 403:
|
||||||
|
* description: Not authorized
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 404:
|
||||||
|
* description: Company or unit not found
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
* 409:
|
||||||
|
* description: Unit in use
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/Error'
|
||||||
|
*/
|
||||||
|
app.get(`${basePath}/unit`, async (req, res) => {
|
||||||
|
const selected = requireSelectedCompany(req);
|
||||||
|
if (selected.error) {
|
||||||
|
return res.status(400).json({ error: selected.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const units = await db.list(req.user.id, selected.companyId);
|
||||||
|
res.status(200).json({ units });
|
||||||
|
} catch (error) {
|
||||||
|
const status = unitErrorStatus(error);
|
||||||
|
if (status === 500) console.error('List units failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put(`${basePath}/unit`, async (req, res) => {
|
||||||
|
const selected = requireSelectedCompany(req);
|
||||||
|
if (selected.error) {
|
||||||
|
return res.status(400).json({ error: selected.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { si, basesi, basesifactor, symbol, name } = req.body ?? {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const unit = await db.upsert(req.user.id, selected.companyId, {
|
||||||
|
si,
|
||||||
|
basesi,
|
||||||
|
basesifactor,
|
||||||
|
symbol,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
res.status(200).json({ unit });
|
||||||
|
} catch (error) {
|
||||||
|
const status = unitErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Upsert unit failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete(`${basePath}/unit`, async (req, res) => {
|
||||||
|
const selected = requireSelectedCompany(req);
|
||||||
|
if (selected.error) {
|
||||||
|
return res.status(400).json({ error: selected.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const si = typeof req.query.si === 'string' ? req.query.si.trim() : '';
|
||||||
|
if (!si) {
|
||||||
|
return res.status(400).json({ error: 'si is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.remove(req.user.id, selected.companyId, si);
|
||||||
|
res.sendStatus(204);
|
||||||
|
} catch (error) {
|
||||||
|
const status = unitErrorStatus(error);
|
||||||
|
if (status === 500) console.error('Delete unit failed:', error);
|
||||||
|
res.status(status).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ const STORE_NAMESPACES = [
|
|||||||
'category',
|
'category',
|
||||||
'article',
|
'article',
|
||||||
'pic',
|
'pic',
|
||||||
|
'unit',
|
||||||
'lang',
|
'lang',
|
||||||
'tax',
|
'tax',
|
||||||
'google_auth',
|
'google_auth',
|
||||||
|
|||||||
16
src/db/unit.js
Normal file
16
src/db/unit.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { getStore } from './store.js';
|
||||||
|
|
||||||
|
export async function list(googleAuthId, companyId) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.unit.list(googleAuthId, companyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function upsert(googleAuthId, companyId, data) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.unit.upsert(googleAuthId, companyId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(googleAuthId, companyId, si) {
|
||||||
|
const store = await getStore();
|
||||||
|
return store.unit.delete(googleAuthId, companyId, si);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user