From 54700a145e0950c434af75d7fc91726c495c2043 Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 1 Aug 2026 05:16:50 +0200 Subject: [PATCH] Add article.setPricesBySku to replace POS prices for a SKU. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gives dashback a single store call that atomically replaces all price rows for the article’s poscustomergroup. --- README.md | 6 +- scripts/test-api.js | 34 +++++++++ src/index.js | 2 + src/sql/price.js | 11 +++ src/store/article/setPricesBySku.js | 104 ++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 src/sql/price.js create mode 100644 src/store/article/setPricesBySku.js diff --git a/README.md b/README.md index ce6a37d..28ae441 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ Backend mapping: | `createUnderParent` | `({ googleAuthId, parentId, name })` | `{ id, parentId, name, sku }` | | `getBySku` | `(googleAuthId, sku)` | detail object (see below) | | `updateBySku` | `(googleAuthId, sku, { name })` | same shape as `getBySku` | +| `setPricesBySku` | `(googleAuthId, sku, [{ price, tax, minmultiplier? }])` | same shape as `getBySku` | | `addImage` | `(googleAuthId, sku, imageBuffer)` | image result (see below) | | `deleteImage` | `(googleAuthId, sku, fullHash)` | `void` | | `orderImages` | `(googleAuthId, sku, { fullHashes })` | `{ fullHashes }` | @@ -164,7 +165,7 @@ Backend mapping: } ``` -`getBySku` / `updateBySku` return: +`getBySku` / `updateBySku` / `setPricesBySku` return: ```js { @@ -189,6 +190,8 @@ Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Err `updateBySku` updates `articletext` (company default language) in a transaction (`name` + slugified `slug`), then returns the same object as `getBySku`. Errors: `Article not found`, `Not authorized`, `Name required`. +`setPricesBySku` replaces all `price` rows for the article’s company `poscustomergroup_id` in one transaction. Each entry: `price` → `net`, `tax` → tax rate (ensured via `tax.ensure`), `minmultiplier` defaults to `1`. Empty array clears all POS prices. Returns the same object as `getBySku`. Errors: `Article not found`, `Not authorized`, `POS customer group required`, `Invalid prices`. + `deleteImage` removes `articlepic` and the `pic` row for this article (`fullHash`). Errors: `Article not found`, `Image not found`, `Not authorized`. `orderImages` sets `articlepic.sort` to `0..n-1` in the given order. `fullHashes` must list every image of the article exactly once. Errors: `Article not found`, `Invalid image order`, `Not authorized`. @@ -237,6 +240,7 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `POST /api/article` | `article.createUnderParent({ googleAuthId, parentId, name })` | | `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` | | `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name })` | +| `PUT /api/article/:sku/prices` | `article.setPricesBySku(googleAuthId, sku, [{ price, tax, minmultiplier? }])` | | `DELETE /api/article/:sku` | `article.delete(googleAuthId, sku)` | | `POST /api/article/:sku/image` | `article.addImage(googleAuthId, sku, imageBuffer)` | | `DELETE /api/article/:sku/image/:fullHash` | `article.deleteImage(googleAuthId, sku, fullHash)` | diff --git a/scripts/test-api.js b/scripts/test-api.js index 1f2b35c..72afbce 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -120,6 +120,11 @@ const MENU = { label: 'updateBySku', hint: 'googleAuthId, sku, name → row', }, + { + value: 'article.setPricesBySku', + label: 'setPricesBySku', + hint: 'googleAuthId, sku, JSON prices → row', + }, { value: 'article.addImage', label: 'addImage', @@ -456,6 +461,35 @@ async function runMethod(client, action) { await client.article.updateBySku(Number(googleAuthId), sku, { name }), ]; } + case 'article.setPricesBySku': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const sku = await askText('sku', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (sku == null) return; + const pricesJson = await askText('prices JSON', { + placeholder: '[{"price":9.99,"tax":19}]', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + const parsed = JSON.parse(v); + if (!Array.isArray(parsed)) return 'Must be a JSON array'; + } catch { + return 'Invalid JSON'; + } + }, + }); + if (pricesJson == null) return; + return [ + 'result', + await client.article.setPricesBySku( + Number(googleAuthId), + sku, + JSON.parse(pricesJson), + ), + ]; + } case 'article.addImage': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; diff --git a/src/index.js b/src/index.js index fc3a630..b966f98 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,7 @@ import articleAddImage from './store/article/addImage.js'; import articleCreateUnderParent from './store/article/createUnderParent.js'; import articleGetBySku from './store/article/getBySku.js'; import articleUpdateBySku from './store/article/updateBySku.js'; +import articleSetPricesBySku from './store/article/setPricesBySku.js'; import langCreate from './store/lang/create.js'; import langDelete from './store/lang/delete.js'; import langList from './store/lang/list.js'; @@ -63,6 +64,7 @@ export default class LibDashStore { createUnderParent: articleCreateUnderParent(pool), getBySku: articleGetBySku(pool), updateBySku: articleUpdateBySku(pool), + setPricesBySku: articleSetPricesBySku(pool), addImage: articleAddImage(pool), deleteImage: articleDeleteImage(pool), orderImages: articleOrderImages(pool), diff --git a/src/sql/price.js b/src/sql/price.js new file mode 100644 index 0000000..de94d4c --- /dev/null +++ b/src/sql/price.js @@ -0,0 +1,11 @@ +export const DELETE_BY_ARTICLE_AND_CUSTOMERGROUP = ` +DELETE FROM price +WHERE article_id = @articleId + AND customergroup_id = @customerGroupId +`; + +export const CREATE = ` +INSERT INTO price (tax_id, article_id, minmultiplier, customergroup_id, net) +OUTPUT INSERTED.id +VALUES (@taxId, @articleId, @minmultiplier, @customerGroupId, @net) +`; diff --git a/src/store/article/setPricesBySku.js b/src/store/article/setPricesBySku.js new file mode 100644 index 0000000..8a42919 --- /dev/null +++ b/src/store/article/setPricesBySku.js @@ -0,0 +1,104 @@ +import { + CREATE as CREATE_PRICE, + DELETE_BY_ARTICLE_AND_CUSTOMERGROUP, +} from '../../sql/price.js'; +import request from '../request.js'; +import taxEnsure from '../tax/ensure.js'; +import queryBySku from './queryBySku.js'; +import resolveArticleBySku from './resolveArticleBySku.js'; + +function normalizePrices(prices) { + if (!Array.isArray(prices)) { + throw new Error('Invalid prices'); + } + + const seen = new Set(); + const normalized = []; + + for (const entry of prices) { + const price = entry?.price; + const tax = entry?.tax; + const minmultiplier = + entry?.minmultiplier === undefined || entry?.minmultiplier === null + ? 1 + : entry.minmultiplier; + + if (!Number.isFinite(price) || !Number.isFinite(tax) || !Number.isFinite(minmultiplier)) { + throw new Error('Invalid prices'); + } + if (minmultiplier <= 0) { + throw new Error('Invalid prices'); + } + + const key = String(minmultiplier); + if (seen.has(key)) { + throw new Error('Invalid prices'); + } + seen.add(key); + + normalized.push({ price, tax, minmultiplier }); + } + + return normalized; +} + +export default function setPricesBySku(pool) { + const resolve = resolveArticleBySku(pool); + const ensureTax = taxEnsure(pool); + const load = queryBySku(pool); + + return async (googleAuthId, sku, prices) => { + const normalized = normalizePrices(prices); + + const { articleId, languageId, posCustomerGroupId } = await resolve( + googleAuthId, + sku, + ); + if (!posCustomerGroupId) { + throw new Error('POS customer group required'); + } + + const tx = pool.transaction(); + await tx.begin(); + + try { + const taxIds = new Map(); + for (const entry of normalized) { + if (!taxIds.has(entry.tax)) { + taxIds.set(entry.tax, await ensureTax(entry.tax, tx)); + } + } + + await request(pool, tx) + .input('articleId', articleId) + .input('customerGroupId', posCustomerGroupId) + .query(DELETE_BY_ARTICLE_AND_CUSTOMERGROUP); + + for (const entry of normalized) { + await request(pool, tx) + .input('taxId', taxIds.get(entry.tax)) + .input('articleId', articleId) + .input('minmultiplier', entry.minmultiplier) + .input('customerGroupId', posCustomerGroupId) + .input('net', entry.price) + .query(CREATE_PRICE); + } + + await tx.commit(); + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + + const article = await load(articleId, languageId); + if (!article) { + throw new Error('Article not found'); + } + + return article; + }; +}