diff --git a/README.md b/README.md index 28ae441..0a44535 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,38 @@ Backend mapping: --- +## `unit` + +| Method | Signature | Returns | +| --- | --- | --- | +| `list` | `(googleAuthId, companyId)` | `{ si, basesi, basesifactor, symbol, name }[]` | +| `upsert` | `(googleAuthId, companyId, { si, basesi, basesifactor, symbol, name })` | `{ si, basesi, basesifactor, symbol, name }` | +| `delete` | `(googleAuthId, companyId, si)` | `void` | + +Shape (überall): + +```js +{ si, basesi, basesifactor, symbol, name } +``` + +`symbol` / `name` come from `unittext` in the company default language (`LEFT JOIN`; missing → `null`). Sorted by `symbol`, `name`, `si`. Units are **company-scoped** (`unit.company_id`); Unique is `(company_id, si)`. + +### `list` + +Auth via `resolveCompanyAccess` (like `getTreeForCompany`). Returns only units of that company. Errors: `Company not found`, `Not authorized`. + +### `upsert` + +Match key `(companyId, si)`. Inserts `unit` + `unittext`, or updates `basesi` / `basesifactor` and upserts `unittext` for the company language. `basesifactor` must be finite and `> 0`. Errors: `Company not found`, `Not authorized`, `Invalid unit`. + +Default article unit `pc` is ensured **per company** on article create (with `unittext` symbol/name `pc` / `Piece` for the company language). + +### `delete` + +Deletes the unit for `(companyId, si)`. Blocked by FK if any article still references it (`Unit in use`). Missing → `Unit not found`. Errors: `Company not found`, `Not authorized`, `Unit not found`, `Unit in use`. + +--- + ## `category` | Method | Signature | Returns | @@ -143,7 +175,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` | +| `updateBySku` | `(googleAuthId, sku, { name?, unitamount?, unitSi? })` | 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` | @@ -171,7 +203,7 @@ Backend mapping: { id, sku, name, slug, unitamount, weight, - unit: { si }, + unit: { si, basesi, basesifactor, symbol, name }, brand: { name, thumbW, thumbH, thumbHash } | null, categoryIds: number[], photos: { sort, fullHash, thumbW, thumbH, thumbHash }[], @@ -188,7 +220,7 @@ Backend mapping: Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Errors: `Article not found`, `Not authorized`. -`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`. +`updateBySku` patches article fields in a transaction. At least one of `name`, `unitamount`, `unitSi` required. `name` → `articletext` (slugified `slug`); empty name → `Name required`. `unitSi` → existing `unit.si` lookup (no silent ensure); unknown → `Unit not found`. `unitamount` must be finite and `> 0` → `Invalid unitamount`. Returns the same object as `getBySku`. Errors: `Article not found`, `Not authorized`, `Name required`, `Unit not found`, `Invalid unitamount`. `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`. @@ -237,9 +269,12 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `GET /api/category/tree?companyId=` | `category.getTreeForCompany(googleAuthId, companyId)` | | `POST /api/category` | `category.createUnderParent({ googleAuthId, parentId, name })` | | `DELETE /api/category/:id` | `category.delete({ googleAuthId, id })` | +| `GET /api/unit?companyId=` | `unit.list(googleAuthId, companyId)` | +| `PUT /api/unit` | `unit.upsert(googleAuthId, companyId, { si, basesi, basesifactor, symbol, name })` | +| `DELETE /api/unit` | `unit.delete(googleAuthId, companyId, si)` | | `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 })` | +| `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name?, unitamount?, unitSi? })` | | `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)` | diff --git a/scripts/test-api.js b/scripts/test-api.js index 72afbce..bb58dc7 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -93,6 +93,26 @@ const MENU = { { value: 'tax.ensure', label: 'ensure', hint: 'rate → id' }, ], }, + unit: { + label: 'unit', + items: [ + { + value: 'unit.list', + label: 'list', + hint: 'googleAuthId, companyId → rows', + }, + { + value: 'unit.upsert', + label: 'upsert', + hint: 'googleAuthId, companyId, JSON unit → row', + }, + { + value: 'unit.delete', + label: 'delete', + hint: 'googleAuthId, companyId, si', + }, + ], + }, category: { label: 'category', items: [ @@ -118,7 +138,7 @@ const MENU = { { value: 'article.updateBySku', label: 'updateBySku', - hint: 'googleAuthId, sku, name → row', + hint: 'googleAuthId, sku, name/unitamount/unitSi → row', }, { value: 'article.setPricesBySku', @@ -346,6 +366,58 @@ async function runMethod(client, action) { if (rate == null) return; return ['id', await client.tax.ensure(rate)]; } + case 'unit.list': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + return [ + 'result[]', + await client.unit.list(Number(googleAuthId), Number(companyId)), + ]; + } + case 'unit.upsert': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const unitJson = await askText('unit JSON', { + placeholder: + '{"si":"ml","basesi":"l","basesifactor":0.001,"symbol":"ml","name":"Milliliter"}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + const parsed = JSON.parse(v); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return 'Must be a JSON object'; + } + } catch { + return 'Invalid JSON'; + } + }, + }); + if (unitJson == null) return; + return [ + 'result', + await client.unit.upsert( + Number(googleAuthId), + Number(companyId), + JSON.parse(unitJson), + ), + ]; + } + case 'unit.delete': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const companyId = await askInt('company id'); + if (companyId == null) return; + const si = await askText('si', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (si == null) return; + await client.unit.delete(Number(googleAuthId), Number(companyId), si); + return ['ok', true]; + } case 'lang.create': { const iso639 = await askText('ISO 639', { placeholder: 'de', @@ -452,13 +524,28 @@ async function runMethod(client, action) { validate: (v) => (!v?.trim() ? 'Required' : undefined), }); if (sku == null) return; - const name = await askText('name', { - validate: (v) => (!v?.trim() ? 'Required' : undefined), + const patchJson = await askText('patch JSON', { + placeholder: '{"name":"Tea","unitamount":0.24,"unitSi":"l"}', + validate: (v) => { + if (!v?.trim()) return 'Required'; + try { + const parsed = JSON.parse(v); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return 'Must be a JSON object'; + } + } catch { + return 'Invalid JSON'; + } + }, }); - if (name == null) return; + if (patchJson == null) return; return [ 'result', - await client.article.updateBySku(Number(googleAuthId), sku, { name }), + await client.article.updateBySku( + Number(googleAuthId), + sku, + JSON.parse(patchJson), + ), ]; } case 'article.setPricesBySku': { diff --git a/src/index.js b/src/index.js index b966f98..2aa2ce5 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,9 @@ 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 unitList from './store/unit/list.js'; +import unitUpsert from './store/unit/upsert.js'; +import unitDelete from './store/unit/delete.js'; import langCreate from './store/lang/create.js'; import langDelete from './store/lang/delete.js'; import langList from './store/lang/list.js'; @@ -86,6 +89,12 @@ export default class LibDashStore { list: taxList(pool), }; + this.unit = { + list: unitList(pool), + upsert: unitUpsert(pool), + delete: unitDelete(pool), + }; + this.google_auth = { add: googleAuthAdd(pool), getByEmail: googleAuthGetByEmail(pool), diff --git a/src/schema.js b/src/schema.js index ca67659..5f4c960 100644 --- a/src/schema.js +++ b/src/schema.js @@ -163,10 +163,12 @@ IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'unit') BEGIN CREATE TABLE unit ( id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, si NVARCHAR(50) NOT NULL, basesi NVARCHAR(50) NOT NULL, basesifactor FLOAT NOT NULL, - CONSTRAINT UQ_unit_si UNIQUE (si) + CONSTRAINT UQ_unit_company_si UNIQUE (company_id, si), + CONSTRAINT FK_unit_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION ); END `; @@ -233,7 +235,7 @@ BEGIN fathersort INT NOT NULL CONSTRAINT DF_article_fathersort DEFAULT 0, created_at DATETIME2 NOT NULL CONSTRAINT DF_article_created_at DEFAULT SYSUTCDATETIME(), updated_at DATETIME2 NOT NULL CONSTRAINT DF_article_updated_at DEFAULT SYSUTCDATETIME(), - CONSTRAINT FK_article_unit FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE CASCADE, + CONSTRAINT FK_article_unit FOREIGN KEY (unit_id) REFERENCES unit(id) ON DELETE NO ACTION, CONSTRAINT FK_article_brand FOREIGN KEY (brand_id) REFERENCES brand(id) ON DELETE SET NULL, CONSTRAINT FK_article_articlelist FOREIGN KEY (articlelist_id) REFERENCES articlelist(id) ON DELETE SET NULL, CONSTRAINT FK_article_optionslist FOREIGN KEY (optionslist_id) REFERENCES optionslist(id) ON DELETE SET NULL, diff --git a/src/sql/article.js b/src/sql/article.js index 0292418..3043fbe 100644 --- a/src/sql/article.js +++ b/src/sql/article.js @@ -98,6 +98,24 @@ LEFT JOIN companyuser cu ON cu.company_id = co.id AND cu.google_auth_id = @googleAuthId `; +export const UPDATE_UNIT = ` +UPDATE article +SET unit_id = @unitId +WHERE id = @articleId +`; + +export const UPDATE_UNITAMOUNT = ` +UPDATE article +SET unitamount = @unitamount +WHERE id = @articleId +`; + +export const UPDATE_UNIT_AND_AMOUNT = ` +UPDATE article +SET unit_id = @unitId, unitamount = @unitamount +WHERE id = @articleId +`; + export const CREATE = ` INSERT INTO article (sku, unit_id, unitamount, weight) OUTPUT INSERTED.id diff --git a/src/sql/articleDetail.js b/src/sql/articleDetail.js index 982a4e9..a4a3255 100644 --- a/src/sql/articleDetail.js +++ b/src/sql/articleDetail.js @@ -6,6 +6,10 @@ export const ARTICLE_DETAIL_COLUMNS = ` a.unitamount, a.weight, u.si AS unitSi, + u.basesi AS unitBasesi, + u.basesifactor AS unitBasesifactor, + ut.symbol AS unitSymbol, + ut.name AS unitName, b.name AS brandName, brand_pic.thumbw AS brandThumbW, brand_pic.thumbh AS brandThumbH, @@ -42,6 +46,8 @@ FROM article a LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId LEFT JOIN unit u ON u.id = a.unit_id +LEFT JOIN unittext ut + ON ut.unit_id = u.id AND ut.language_id = @languageId LEFT JOIN brand b ON b.id = a.brand_id OUTER APPLY ( SELECT TOP 1 bp.pic_id @@ -61,6 +67,8 @@ FROM article a LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId LEFT JOIN unit u ON u.id = a.unit_id +LEFT JOIN unittext ut + ON ut.unit_id = u.id AND ut.language_id = @languageId LEFT JOIN brand b ON b.id = a.brand_id OUTER APPLY ( SELECT TOP 1 bp.pic_id diff --git a/src/sql/unit.js b/src/sql/unit.js index 5ff8b46..2405550 100644 --- a/src/sql/unit.js +++ b/src/sql/unit.js @@ -1,11 +1,50 @@ +export const UNIT_COLUMNS = ` + u.si, + u.basesi, + u.basesifactor, + ut.symbol, + ut.name +`; + export const GET_BY_SI = ` SELECT id FROM unit -WHERE si = @si +WHERE company_id = @companyId AND si = @si +`; + +export const GET_FULL_BY_SI = ` +SELECT + u.id, +${UNIT_COLUMNS} +FROM unit u +LEFT JOIN unittext ut + ON ut.unit_id = u.id AND ut.language_id = @languageId +WHERE u.company_id = @companyId AND u.si = @si +`; + +export const LIST_BY_COMPANY = ` +SELECT +${UNIT_COLUMNS} +FROM unit u +LEFT JOIN unittext ut + ON ut.unit_id = u.id AND ut.language_id = @languageId +WHERE u.company_id = @companyId +ORDER BY ut.symbol, ut.name, u.si `; export const CREATE = ` -INSERT INTO unit (si, basesi, basesifactor) +INSERT INTO unit (company_id, si, basesi, basesifactor) OUTPUT INSERTED.id -VALUES (@si, @basesi, @basesifactor) +VALUES (@companyId, @si, @basesi, @basesifactor) +`; + +export const UPDATE_BY_SI = ` +UPDATE unit +SET basesi = @basesi, basesifactor = @basesifactor +WHERE company_id = @companyId AND si = @si +`; + +export const DELETE_BY_COMPANY_AND_SI = ` +DELETE FROM unit +WHERE company_id = @companyId AND si = @si `; diff --git a/src/sql/unittext.js b/src/sql/unittext.js new file mode 100644 index 0000000..72ae4de --- /dev/null +++ b/src/sql/unittext.js @@ -0,0 +1,16 @@ +export const CREATE = ` +INSERT INTO unittext (unit_id, language_id, symbol, name) +VALUES (@unitId, @languageId, @symbol, @name) +`; + +export const UPDATE_BY_UNIT_AND_LANGUAGE = ` +UPDATE unittext +SET symbol = @symbol, name = @name +WHERE unit_id = @unitId AND language_id = @languageId +`; + +export const GET_BY_UNIT_AND_LANGUAGE = ` +SELECT id +FROM unittext +WHERE unit_id = @unitId AND language_id = @languageId +`; diff --git a/src/store/article/createUnderParent.js b/src/store/article/createUnderParent.js index f6acf79..1587517 100644 --- a/src/store/article/createUnderParent.js +++ b/src/store/article/createUnderParent.js @@ -28,7 +28,7 @@ export default function createUnderParent(pool) { await tx.begin(); try { - const unitId = await ensureUnit(tx); + const unitId = await ensureUnit(tx, languageId, companyId); const sku = await allocateSku(companyId, tx); const slug = slugify(name); diff --git a/src/store/article/queryBySku.js b/src/store/article/queryBySku.js index 3bc29fe..9067612 100644 --- a/src/store/article/queryBySku.js +++ b/src/store/article/queryBySku.js @@ -14,7 +14,13 @@ function shapeArticleDetail(row, categoryIds, photos, eans, prices) { slug: row.slug, unitamount: row.unitamount, weight: row.weight, - unit: { si: row.unitSi }, + unit: { + si: row.unitSi, + basesi: row.unitBasesi, + basesifactor: row.unitBasesifactor, + symbol: row.unitSymbol ?? null, + name: row.unitName ?? null, + }, brand: row.brandName ? { name: row.brandName, diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js index 94f0bcf..48470de 100644 --- a/src/store/article/updateBySku.js +++ b/src/store/article/updateBySku.js @@ -1,4 +1,10 @@ +import { + UPDATE_UNIT, + UPDATE_UNITAMOUNT, + UPDATE_UNIT_AND_AMOUNT, +} from '../../sql/article.js'; import { UPDATE_BY_ARTICLE_AND_LANGUAGE } from '../../sql/articletext.js'; +import { GET_BY_SI } from '../../sql/unit.js'; import request from '../request.js'; import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; @@ -17,37 +23,78 @@ export default function updateBySku(pool) { const resolve = resolveArticleBySku(pool); const load = queryBySku(pool); - return async (googleAuthId, sku, { name }) => { - if (!name?.trim()) { + return async (googleAuthId, sku, patch = {}) => { + const hasName = Object.prototype.hasOwnProperty.call(patch, 'name'); + const hasUnitamount = Object.prototype.hasOwnProperty.call(patch, 'unitamount'); + const hasUnitSi = Object.prototype.hasOwnProperty.call(patch, 'unitSi'); + + if (!hasName && !hasUnitamount && !hasUnitSi) { throw new Error('Name required'); } - const { articleId, languageId } = await resolve(googleAuthId, sku); + if (hasName && !patch.name?.trim()) { + throw new Error('Name required'); + } + + if (hasUnitamount && (!Number.isFinite(patch.unitamount) || patch.unitamount <= 0)) { + throw new Error('Invalid unitamount'); + } + + const { articleId, languageId, companyId } = await resolve(googleAuthId, sku); + + let unitId; + if (hasUnitSi) { + if (!patch.unitSi?.trim()) { + throw new Error('Unit not found'); + } + const unit = await pool + .request() + .input('companyId', companyId) + .input('si', patch.unitSi.trim()) + .query(GET_BY_SI); + unitId = unit.recordset[0]?.id; + if (!unitId) { + throw new Error('Unit not found'); + } + } - const slug = slugify(name); const tx = pool.transaction(); await tx.begin(); try { - const update = await request(pool, tx) - .input('articleId', articleId) - .input('languageId', languageId) - .input('name', name.trim()) - .input('slug', slug) - .query(UPDATE_BY_ARTICLE_AND_LANGUAGE); + if (hasName) { + const slug = slugify(patch.name); + const update = await request(pool, tx) + .input('articleId', articleId) + .input('languageId', languageId) + .input('name', patch.name.trim()) + .input('slug', slug) + .query(UPDATE_BY_ARTICLE_AND_LANGUAGE); - if (!update.rowsAffected[0]) { - throw new Error('Article not found'); + if (!update.rowsAffected[0]) { + throw new Error('Article not found'); + } + } + + if (hasUnitSi && hasUnitamount) { + await request(pool, tx) + .input('articleId', articleId) + .input('unitId', unitId) + .input('unitamount', patch.unitamount) + .query(UPDATE_UNIT_AND_AMOUNT); + } else if (hasUnitSi) { + await request(pool, tx) + .input('articleId', articleId) + .input('unitId', unitId) + .query(UPDATE_UNIT); + } else if (hasUnitamount) { + await request(pool, tx) + .input('articleId', articleId) + .input('unitamount', patch.unitamount) + .query(UPDATE_UNITAMOUNT); } await tx.commit(); - - const article = await load(articleId, languageId); - if (!article) { - throw new Error('Article not found'); - } - - return article; } catch (err) { try { await tx.rollback(); @@ -56,5 +103,12 @@ export default function updateBySku(pool) { } throw err; } + + const article = await load(articleId, languageId); + if (!article) { + throw new Error('Article not found'); + } + + return article; }; } diff --git a/src/store/unit/delete.js b/src/store/unit/delete.js new file mode 100644 index 0000000..0489730 --- /dev/null +++ b/src/store/unit/delete.js @@ -0,0 +1,31 @@ +import { DELETE_BY_COMPANY_AND_SI } from '../../sql/unit.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; + +export default function del(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, si) => { + if (!si?.trim()) { + throw new Error('Unit not found'); + } + + await access(googleAuthId, companyId); + + try { + const result = await pool + .request() + .input('companyId', companyId) + .input('si', si.trim()) + .query(DELETE_BY_COMPANY_AND_SI); + + if (!result.rowsAffected[0]) { + throw new Error('Unit not found'); + } + } catch (err) { + if (err.number === 547) { + throw new Error('Unit in use'); + } + throw err; + } + }; +} diff --git a/src/store/unit/ensureDefault.js b/src/store/unit/ensureDefault.js index f981bf0..b7ed02c 100644 --- a/src/store/unit/ensureDefault.js +++ b/src/store/unit/ensureDefault.js @@ -1,24 +1,49 @@ import { CREATE, GET_BY_SI } from '../../sql/unit.js'; +import { + CREATE as CREATE_UNITTEXT, + GET_BY_UNIT_AND_LANGUAGE, +} from '../../sql/unittext.js'; import request from '../request.js'; const DEFAULT_SI = 'pc'; +const DEFAULT_SYMBOL = 'pc'; +const DEFAULT_NAME = 'Piece'; export default function ensureDefaultUnit(pool) { - return async (tx) => { + return async (tx, languageId, companyId) => { const existing = await request(pool, tx) + .input('companyId', companyId) .input('si', DEFAULT_SI) .query(GET_BY_SI); - if (existing.recordset[0]) { - return existing.recordset[0].id; + let unitId = existing.recordset[0]?.id; + + if (!unitId) { + const created = await request(pool, tx) + .input('companyId', companyId) + .input('si', DEFAULT_SI) + .input('basesi', DEFAULT_SI) + .input('basesifactor', 1) + .query(CREATE); + unitId = created.recordset[0].id; } - const created = await request(pool, tx) - .input('si', DEFAULT_SI) - .input('basesi', DEFAULT_SI) - .input('basesifactor', 1) - .query(CREATE); + if (languageId != null) { + const text = await request(pool, tx) + .input('unitId', unitId) + .input('languageId', languageId) + .query(GET_BY_UNIT_AND_LANGUAGE); - return created.recordset[0].id; + if (!text.recordset[0]) { + await request(pool, tx) + .input('unitId', unitId) + .input('languageId', languageId) + .input('symbol', DEFAULT_SYMBOL) + .input('name', DEFAULT_NAME) + .query(CREATE_UNITTEXT); + } + } + + return unitId; }; } diff --git a/src/store/unit/list.js b/src/store/unit/list.js new file mode 100644 index 0000000..4f0f8dc --- /dev/null +++ b/src/store/unit/list.js @@ -0,0 +1,19 @@ +import { LIST_BY_COMPANY } from '../../sql/unit.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import shapeUnit from './shapeUnit.js'; + +export default function list(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId) => { + const { languageId } = await access(googleAuthId, companyId); + + const result = await pool + .request() + .input('companyId', companyId) + .input('languageId', languageId) + .query(LIST_BY_COMPANY); + + return result.recordset.map(shapeUnit); + }; +} diff --git a/src/store/unit/shapeUnit.js b/src/store/unit/shapeUnit.js new file mode 100644 index 0000000..b70d7d1 --- /dev/null +++ b/src/store/unit/shapeUnit.js @@ -0,0 +1,11 @@ +function shapeUnit(row) { + return { + si: row.si, + basesi: row.basesi, + basesifactor: row.basesifactor, + symbol: row.symbol ?? null, + name: row.name ?? null, + }; +} + +export default shapeUnit; diff --git a/src/store/unit/upsert.js b/src/store/unit/upsert.js new file mode 100644 index 0000000..5ec0c6d --- /dev/null +++ b/src/store/unit/upsert.js @@ -0,0 +1,115 @@ +import { + CREATE as CREATE_UNIT, + GET_BY_SI, + GET_FULL_BY_SI, + UPDATE_BY_SI, +} from '../../sql/unit.js'; +import { + CREATE as CREATE_UNITTEXT, + GET_BY_UNIT_AND_LANGUAGE, + UPDATE_BY_UNIT_AND_LANGUAGE, +} from '../../sql/unittext.js'; +import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import request from '../request.js'; +import shapeUnit from './shapeUnit.js'; + +function validateUnit({ si, basesi, basesifactor, symbol, name }) { + if (!si?.trim() || !basesi?.trim() || !symbol?.trim() || !name?.trim()) { + throw new Error('Invalid unit'); + } + if (!Number.isFinite(basesifactor) || basesifactor <= 0) { + throw new Error('Invalid unit'); + } + + return { + si: si.trim(), + basesi: basesi.trim(), + basesifactor, + symbol: symbol.trim(), + name: name.trim(), + }; +} + +export default function upsert(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId, companyId, input) => { + const unit = validateUnit(input); + const { languageId } = await access(googleAuthId, companyId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const existing = await request(pool, tx) + .input('companyId', companyId) + .input('si', unit.si) + .query(GET_BY_SI); + + let unitId = existing.recordset[0]?.id; + + if (!unitId) { + const created = await request(pool, tx) + .input('companyId', companyId) + .input('si', unit.si) + .input('basesi', unit.basesi) + .input('basesifactor', unit.basesifactor) + .query(CREATE_UNIT); + unitId = created.recordset[0].id; + + await request(pool, tx) + .input('unitId', unitId) + .input('languageId', languageId) + .input('symbol', unit.symbol) + .input('name', unit.name) + .query(CREATE_UNITTEXT); + } else { + await request(pool, tx) + .input('companyId', companyId) + .input('si', unit.si) + .input('basesi', unit.basesi) + .input('basesifactor', unit.basesifactor) + .query(UPDATE_BY_SI); + + const text = await request(pool, tx) + .input('unitId', unitId) + .input('languageId', languageId) + .query(GET_BY_UNIT_AND_LANGUAGE); + + if (text.recordset[0]) { + await request(pool, tx) + .input('unitId', unitId) + .input('languageId', languageId) + .input('symbol', unit.symbol) + .input('name', unit.name) + .query(UPDATE_BY_UNIT_AND_LANGUAGE); + } else { + await request(pool, tx) + .input('unitId', unitId) + .input('languageId', languageId) + .input('symbol', unit.symbol) + .input('name', unit.name) + .query(CREATE_UNITTEXT); + } + } + + await tx.commit(); + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + + const result = await pool + .request() + .input('companyId', companyId) + .input('si', unit.si) + .input('languageId', languageId) + .query(GET_FULL_BY_SI); + + return shapeUnit(result.recordset[0]); + }; +}