diff --git a/docs/api/README.md b/docs/api/README.md index 8b10225..92bb451 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -37,6 +37,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/article/add-image.md`](routes/article/add-image.md) | `POST /article/{sku}/image` | | [`routes/article/delete-image.md`](routes/article/delete-image.md) | `DELETE /article/{sku}/image/{fullHash}` | | [`routes/article/order-images.md`](routes/article/order-images.md) | `PUT /article/{sku}/images/order` | +| [`routes/article/set-prices.md`](routes/article/set-prices.md) | `PUT /article/{sku}/prices` | | [`routes/auth/login.md`](routes/auth/login.md) | `POST /auth/login` | | [`routes/auth/logout.md`](routes/auth/logout.md) | `POST /auth/logout` | | [`routes/auth/me.md`](routes/auth/me.md) | `GET /auth/me` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 683e53d..9bacb6f 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -1277,6 +1277,140 @@ } } }, + "/article/{sku}/prices": { + "put": { + "tags": [ + "Article" + ], + "summary": "Replace POS prices by SKU", + "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": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "article/set-prices.md", + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "prices" + ], + "properties": { + "prices": { + "type": "array", + "items": { + "type": "object", + "required": [ + "price", + "tax" + ], + "properties": { + "price": { + "type": "number", + "description": "Net price" + }, + "tax": { + "type": "number", + "description": "Tax rate (e.g. 19)" + }, + "minmultiplier": { + "type": "number", + "description": "Defaults to 1; must be unique per request" + } + } + } + } + }, + "example": { + "prices": [ + { + "price": 10.5, + "tax": 19 + }, + { + "price": 9.9, + "tax": 19, + "minmultiplier": 10 + } + ] + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated article detail", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "article": { + "$ref": "#/components/schemas/ArticleDetail" + } + } + } + } + } + }, + "400": { + "description": "Invalid prices or POS customer group required", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Article not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/article/{sku}/image": { "post": { "tags": [ diff --git a/docs/api/routes/article/set-prices.md b/docs/api/routes/article/set-prices.md new file mode 100644 index 0000000..f10249b --- /dev/null +++ b/docs/api/routes/article/set-prices.md @@ -0,0 +1,57 @@ +# `PUT /article/{sku}/prices` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Replace POS prices by SKU + +Replace-all prices for the company POS customer group. +Each entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1). +Empty `prices` clears all POS prices. Returns the same shape as GET by SKU. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | | + +## Body (JSON) + +```json +{ + "prices": [ + { + "price": 10.5, + "tax": 19 + }, + { + "price": 9.9, + "tax": 19, + "minmultiplier": 10 + } + ] +} +``` + +## Response `200` + +Updated article detail + +```json +{ + "article": { … } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid prices or POS customer group required — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Article not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/src/api/routes/article.js b/src/api/routes/article.js index f5a27c3..5630d75 100644 --- a/src/api/routes/article.js +++ b/src/api/routes/article.js @@ -34,6 +34,8 @@ function articleErrorStatus(error) { if (error.message === 'Name required') return 400; if (error.message === 'Invalid image order') return 400; if (error.message === 'Article has children') return 400; + if (error.message === 'Invalid prices') return 400; + if (error.message === 'POS customer group required') return 400; return 500; } @@ -338,6 +340,110 @@ export function register(app, basePath = '') { } }); + /** + * @openapi + * /article/{sku}/prices: + * put: + * tags: [Article] + * summary: Replace POS prices by SKU + * description: | + * Replace-all prices for the company POS customer group. + * Each entry: `price` (net), `tax` (rate, ensured via tax.ensure), optional `minmultiplier` (default 1). + * Empty `prices` clears all POS prices. Returns the same shape as GET by SKU. + * security: + * - cookieAuth: [] + * x-markdown: article/set-prices.md + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [prices] + * properties: + * prices: + * type: array + * items: + * type: object + * required: [price, tax] + * properties: + * price: + * type: number + * description: Net price + * tax: + * type: number + * description: Tax rate (e.g. 19) + * minmultiplier: + * type: number + * description: Defaults to 1; must be unique per request + * example: + * prices: + * - price: 10.5 + * tax: 19 + * - price: 9.9 + * tax: 19 + * minmultiplier: 10 + * responses: + * 200: + * description: Updated article detail + * content: + * application/json: + * schema: + * type: object + * properties: + * article: + * $ref: '#/components/schemas/ArticleDetail' + * 400: + * description: Invalid prices or POS customer group required + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Article not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.put(`${basePath}/article/:sku/prices`, async (req, res) => { + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + if (!Array.isArray(req.body?.prices)) { + return res.status(400).json({ error: 'prices is required' }); + } + + try { + const article = await db.setPricesBySku(req.user.id, skuParsed.sku, req.body.prices); + res.status(200).json({ article }); + } catch (error) { + const status = articleErrorStatus(error); + if (status === 500) console.error('Set article prices failed:', error); + res.status(status).json({ error: error.message }); + } + }); + /** * @openapi * /article/{sku}/image: diff --git a/src/db/article.js b/src/db/article.js index 9a1193f..b82d003 100644 --- a/src/db/article.js +++ b/src/db/article.js @@ -34,3 +34,8 @@ export async function remove(googleAuthId, sku) { const store = await getStore(); return store.article.delete(googleAuthId, sku); } + +export async function setPricesBySku(googleAuthId, sku, prices) { + const store = await getStore(); + return store.article.setPricesBySku(googleAuthId, sku, prices); +}