From 09c7a8bc742164dbde8285962581bc3a1784e7eb Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 31 Jul 2026 05:34:45 +0200 Subject: [PATCH] u --- scripts/test-api.js | 111 +++++++++++++++++--- src/index.js | 23 ++++ src/sql/company.js | 40 ++++++- src/sql/google_auth.js | 6 ++ src/sql/language.js | 6 ++ src/sql/tax.js | 16 +++ src/store/company/create.js | 59 ++++++++--- src/store/company/getById.js | 13 +++ src/store/company/getByName.js | 13 +++ src/store/company/update.js | 54 ++++++++++ src/store/google_auth/setSelectedCompany.js | 12 +++ src/store/lang/create.js | 6 +- src/store/lang/ensure.js | 12 +++ src/store/lang/getByIso639.js | 13 +++ src/store/request.js | 5 + src/store/tax/create.js | 13 +++ src/store/tax/ensure.js | 12 +++ src/store/tax/getByRate.js | 13 +++ src/store/tax/list.js | 8 ++ 19 files changed, 405 insertions(+), 30 deletions(-) create mode 100644 src/sql/tax.js create mode 100644 src/store/company/getById.js create mode 100644 src/store/company/getByName.js create mode 100644 src/store/company/update.js create mode 100644 src/store/google_auth/setSelectedCompany.js create mode 100644 src/store/lang/ensure.js create mode 100644 src/store/lang/getByIso639.js create mode 100644 src/store/request.js create mode 100644 src/store/tax/create.js create mode 100644 src/store/tax/ensure.js create mode 100644 src/store/tax/getByRate.js create mode 100644 src/store/tax/list.js diff --git a/scripts/test-api.js b/scripts/test-api.js index cf6dfee..06d70fd 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -40,6 +40,16 @@ async function askInt(message, placeholder = '1') { }); } +async function askNumber(message, placeholder = '19') { + const value = await askText(message, { + placeholder, + validate: (v) => + v?.trim() === '' || Number.isNaN(Number(v)) ? 'Number required' : undefined, + }); + if (value == null) return null; + return Number(value); +} + async function shutdown(pool, code = 0) { try { await pool.close(); @@ -60,8 +70,10 @@ const MENU = { company: { label: 'company', items: [ - { value: 'company.create', label: 'create', hint: '{name} → id' }, + { value: 'company.create', label: 'create', hint: 'iso639,name,taxrate,reducedtaxrate → id' }, + { value: 'company.update', label: 'update', hint: 'id + same fields' }, { value: 'company.list', label: 'list', hint: '→ rows' }, + { value: 'company.getById', label: 'getById', hint: 'id → row' }, { value: 'company.setDefaultLang', label: 'setDefaultLang', hint: 'companyId, langId' }, { value: 'company.setRootCategory', label: 'setRootCategory', hint: 'companyId, categoryId' }, { value: 'company.delete', label: 'delete', hint: 'companyId' }, @@ -76,6 +88,14 @@ const MENU = { { value: 'lang.delete', label: 'delete', hint: 'id' }, ], }, + tax: { + label: 'tax', + items: [ + { value: 'tax.create', label: 'create', hint: 'rate → id' }, + { value: 'tax.list', label: 'list', hint: '→ rows' }, + { value: 'tax.ensure', label: 'ensure', hint: 'rate → id' }, + ], + }, category: { label: 'category', items: [ @@ -99,6 +119,7 @@ const MENU = { { value: 'google_auth.add', label: 'add', hint: 'email → authId' }, { value: 'google_auth.getById', label: 'getById', hint: 'id → row' }, { value: 'google_auth.getByEmail', label: 'getByEmail', hint: 'email → row' }, + { value: 'google_auth.setSelectedCompany', label: 'setSelectedCompany', hint: 'authId, companyId' }, { value: 'google_auth.list', label: 'list', hint: '→ rows' }, { value: 'google_auth.del', label: 'del', hint: 'id' }, ], @@ -157,15 +178,17 @@ async function runMethod(client, action) { validate: (v) => (!v?.trim() ? 'Required' : undefined), }); if (iso639 == null) return; + const taxrate = await askNumber('Tax rate', '19'); + if (taxrate == null) return; + const reducedtaxrate = await askNumber('Reduced tax rate', '7'); + if (reducedtaxrate == null) return; - const langs = await client.lang.list(); - const existingLang = langs.find((row) => row.iso639 === iso639); - const langId = existingLang - ? existingLang.id - : await client.lang.create(iso639); - - const companyId = await client.company.create({ name }); - await client.company.setDefaultLang(companyId, langId); + const companyId = await client.company.create({ + iso639, + name, + taxrate, + reducedtaxrate, + }); const auths = await client.google_auth.list(); const existingAuth = auths.find((row) => row.email === email); @@ -174,6 +197,7 @@ async function runMethod(client, action) { : await client.google_auth.add(email); const companyUserId = await client.companyuser.add(companyId, authId); + await client.google_auth.setSelectedCompany(authId, companyId); return [ 'created', @@ -181,11 +205,11 @@ async function runMethod(client, action) { companyId, companyUserId, authId, - langId, email, name, iso639, - langCreated: !existingLang, + taxrate, + reducedtaxrate, authCreated: !existingAuth, }, ]; @@ -196,11 +220,53 @@ async function runMethod(client, action) { validate: (v) => (!v?.trim() ? 'Required' : undefined), }); if (name == null) return; - return ['id', await client.company.create({ name })]; + const iso639 = await askText('iso639', { + placeholder: 'de', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (iso639 == null) return; + const taxrate = await askNumber('taxrate', '19'); + if (taxrate == null) return; + const reducedtaxrate = await askNumber('reducedtaxrate', '7'); + if (reducedtaxrate == null) return; + return [ + 'id', + await client.company.create({ iso639, name, taxrate, reducedtaxrate }), + ]; + } + case 'company.update': { + const id = await askInt('Company id'); + if (id == null) return; + const name = await askText('Name', { + placeholder: 'Acme', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (name == null) return; + const iso639 = await askText('iso639', { + placeholder: 'de', + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (iso639 == null) return; + const taxrate = await askNumber('taxrate', '19'); + if (taxrate == null) return; + const reducedtaxrate = await askNumber('reducedtaxrate', '7'); + if (reducedtaxrate == null) return; + await client.company.update(Number(id), { + iso639, + name, + taxrate, + reducedtaxrate, + }); + return ['ok', true]; } case 'company.list': { return ['rows', await client.company.list()]; } + case 'company.getById': { + const id = await askInt('Company id'); + if (id == null) return; + return ['row', await client.company.getById(Number(id))]; + } case 'company.setDefaultLang': { const companyId = await askInt('Company id'); if (companyId == null) return; @@ -231,6 +297,19 @@ async function runMethod(client, action) { await client.company.getByCompanyUserId(Number(id)), ]; } + case 'tax.create': { + const rate = await askNumber('rate', '19'); + if (rate == null) return; + return ['id', await client.tax.create(rate)]; + } + case 'tax.list': { + return ['rows', await client.tax.list()]; + } + case 'tax.ensure': { + const rate = await askNumber('rate', '19'); + if (rate == null) return; + return ['id', await client.tax.ensure(rate)]; + } case 'lang.create': { const iso639 = await askText('ISO 639', { placeholder: 'de', @@ -326,6 +405,14 @@ async function runMethod(client, action) { if (email == null) return; return ['row', await client.google_auth.getByEmail(email)]; } + case 'google_auth.setSelectedCompany': { + const id = await askInt('Auth id'); + if (id == null) return; + const companyId = await askInt('Company id'); + if (companyId == null) return; + await client.google_auth.setSelectedCompany(Number(id), Number(companyId)); + return ['ok', true]; + } case 'google_auth.list': { return ['rows', await client.google_auth.list()]; } diff --git a/src/index.js b/src/index.js index a82c29a..7424ede 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,10 @@ import companyuserGetByEmail from './store/companyuser/getByEmail.js'; import companyuserAdd from './store/companyuser/add.js'; import companyuserDelete from './store/companyuser/delete.js'; import companyGetByCompanyUserId from './store/company/getByCompanyUserId.js'; +import companyGetById from './store/company/getById.js'; +import companyGetByName from './store/company/getByName.js'; import companyCreate from './store/company/create.js'; +import companyUpdate from './store/company/update.js'; import companySetDefaultLang from './store/company/setDefaultLang.js'; import companySetRootCategory from './store/company/setRootCategory.js'; import companyDelete from './store/company/delete.js'; @@ -14,9 +17,16 @@ import categoryDelete from './store/category/delete.js'; import langCreate from './store/lang/create.js'; import langDelete from './store/lang/delete.js'; import langList from './store/lang/list.js'; +import langGetByIso639 from './store/lang/getByIso639.js'; +import langEnsure from './store/lang/ensure.js'; +import taxCreate from './store/tax/create.js'; +import taxGetByRate from './store/tax/getByRate.js'; +import taxEnsure from './store/tax/ensure.js'; +import taxList from './store/tax/list.js'; import googleAuthAdd from './store/google_auth/add.js'; import googleAuthGetById from './store/google_auth/getById.js'; import googleAuthGetByEmail from './store/google_auth/getByEmail.js'; +import googleAuthSetSelectedCompany from './store/google_auth/setSelectedCompany.js'; import googleAuthList from './store/google_auth/list.js'; import googleAuthDel from './store/google_auth/del.js'; import storePurge from './store/purge.js'; @@ -34,7 +44,10 @@ export default class LibDashStore { this.company = { getByCompanyUserId: companyGetByCompanyUserId(pool), + getById: companyGetById(pool), + getByName: companyGetByName(pool), create: companyCreate(pool), + update: companyUpdate(pool), setDefaultLang: companySetDefaultLang(pool), setRootCategory: companySetRootCategory(pool), delete: companyDelete(pool), @@ -50,14 +63,24 @@ export default class LibDashStore { this.lang = { create: langCreate(pool), + getByIso639: langGetByIso639(pool), + ensure: langEnsure(pool), delete: langDelete(pool), list: langList(pool), }; + this.tax = { + create: taxCreate(pool), + getByRate: taxGetByRate(pool), + ensure: taxEnsure(pool), + list: taxList(pool), + }; + this.google_auth = { add: googleAuthAdd(pool), getById: googleAuthGetById(pool), getByEmail: googleAuthGetByEmail(pool), + setSelectedCompany: googleAuthSetSelectedCompany(pool), list: googleAuthList(pool), del: googleAuthDel(pool), }; diff --git a/src/sql/company.js b/src/sql/company.js index dc5343c..d228180 100644 --- a/src/sql/company.js +++ b/src/sql/company.js @@ -9,10 +9,46 @@ INNER JOIN companyuser cu ON cu.company_id = c.id WHERE cu.id = @companyUserId `; +export const GET_BY_NAME = ` +SELECT id, name, root_category_id, default_language_id, taxrate, reducedtaxrate, taxfreerate +FROM company +WHERE name = @name +`; + +export const GET_BY_ID = ` +SELECT id, name, root_category_id, default_language_id, taxrate, reducedtaxrate, taxfreerate +FROM company +WHERE id = @id +`; + export const CREATE = ` -INSERT INTO company (name, root_category_id) +INSERT INTO company ( + name, + root_category_id, + default_language_id, + taxrate, + reducedtaxrate, + taxfreerate +) OUTPUT INSERTED.id -VALUES (@name, @rootCategoryId) +VALUES ( + @name, + @rootCategoryId, + @languageId, + @taxrate, + @reducedtaxrate, + @taxfreerate +) +`; + +export const UPDATE = ` +UPDATE company +SET + name = @name, + default_language_id = @languageId, + taxrate = @taxrate, + reducedtaxrate = @reducedtaxrate +WHERE id = @id `; export const LIST = ` diff --git a/src/sql/google_auth.js b/src/sql/google_auth.js index 6390396..b63e991 100644 --- a/src/sql/google_auth.js +++ b/src/sql/google_auth.js @@ -26,3 +26,9 @@ export const DEL = ` DELETE FROM google_auth WHERE id = @id `; + +export const SET_SELECTED_COMPANY = ` +UPDATE google_auth +SET selected_company = @companyId +WHERE id = @id +`; diff --git a/src/sql/language.js b/src/sql/language.js index 683ec2a..f06ceb3 100644 --- a/src/sql/language.js +++ b/src/sql/language.js @@ -4,6 +4,12 @@ OUTPUT INSERTED.id VALUES (@iso639) `; +export const GET_BY_ISO639 = ` +SELECT id, iso639 +FROM language +WHERE iso639 = @iso639 +`; + export const LIST = ` SELECT id, iso639 FROM language diff --git a/src/sql/tax.js b/src/sql/tax.js new file mode 100644 index 0000000..2f53587 --- /dev/null +++ b/src/sql/tax.js @@ -0,0 +1,16 @@ +export const GET_BY_RATE = ` +SELECT id, rate +FROM tax +WHERE rate = @rate +`; + +export const CREATE = ` +INSERT INTO tax (rate) +OUTPUT INSERTED.id +VALUES (@rate) +`; + +export const LIST = ` +SELECT id, rate +FROM tax +`; diff --git a/src/store/company/create.js b/src/store/company/create.js index eacd1bc..e0bc3ea 100644 --- a/src/store/company/create.js +++ b/src/store/company/create.js @@ -1,23 +1,56 @@ import sql from 'mssql'; import { CREATE } from '../../sql/company.js'; import { CREATE as CREATE_CATEGORY } from '../../sql/category.js'; +import getByName from './getByName.js'; +import langEnsure from '../lang/ensure.js'; +import taxEnsure from '../tax/ensure.js'; +import request from '../request.js'; export default function create(pool) { - return async ({ name }) => { - const category = await pool - .request() - .input('parentId', sql.Int, null) - .input('sort', sql.Int, 0) - .query(CREATE_CATEGORY); + const nameTaken = getByName(pool); + const ensureLang = langEnsure(pool); + const ensureTax = taxEnsure(pool); - const rootCategoryId = category.recordset[0].id; + return async ({ iso639, name, taxrate, reducedtaxrate }) => { + const tx = new sql.Transaction(pool); + await tx.begin(); - const result = await pool - .request() - .input('name', sql.NVarChar(255), name) - .input('rootCategoryId', sql.Int, rootCategoryId) - .query(CREATE); + try { + const existing = await nameTaken(name, tx); + if (existing) { + throw new Error('Company name not available'); + } - return result.recordset[0].id; + const languageId = await ensureLang(iso639, tx); + const taxrateId = await ensureTax(taxrate, tx); + const reducedtaxrateId = await ensureTax(reducedtaxrate, tx); + const taxfreerateId = await ensureTax(0, tx); + + const category = await request(pool, tx) + .input('parentId', sql.Int, null) + .input('sort', sql.Int, 0) + .query(CREATE_CATEGORY); + + const rootCategoryId = category.recordset[0].id; + + const result = await request(pool, tx) + .input('name', sql.NVarChar(255), name) + .input('rootCategoryId', sql.Int, rootCategoryId) + .input('languageId', sql.Int, languageId) + .input('taxrate', sql.Int, taxrateId) + .input('reducedtaxrate', sql.Int, reducedtaxrateId) + .input('taxfreerate', sql.Int, taxfreerateId) + .query(CREATE); + + await tx.commit(); + return result.recordset[0].id; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } }; } diff --git a/src/store/company/getById.js b/src/store/company/getById.js new file mode 100644 index 0000000..43ed5a1 --- /dev/null +++ b/src/store/company/getById.js @@ -0,0 +1,13 @@ +import sql from 'mssql'; +import { GET_BY_ID } from '../../sql/company.js'; +import request from '../request.js'; + +export default function getById(pool) { + return async (id, tx) => { + const result = await request(pool, tx) + .input('id', sql.Int, id) + .query(GET_BY_ID); + + return result.recordset[0]; + }; +} diff --git a/src/store/company/getByName.js b/src/store/company/getByName.js new file mode 100644 index 0000000..cd6d0bf --- /dev/null +++ b/src/store/company/getByName.js @@ -0,0 +1,13 @@ +import sql from 'mssql'; +import { GET_BY_NAME } from '../../sql/company.js'; +import request from '../request.js'; + +export default function getByName(pool) { + return async (name, tx) => { + const result = await request(pool, tx) + .input('name', sql.NVarChar(255), name) + .query(GET_BY_NAME); + + return result.recordset[0]; + }; +} diff --git a/src/store/company/update.js b/src/store/company/update.js new file mode 100644 index 0000000..09e2db3 --- /dev/null +++ b/src/store/company/update.js @@ -0,0 +1,54 @@ +import sql from 'mssql'; +import { UPDATE } from '../../sql/company.js'; +import getById from './getById.js'; +import getByName from './getByName.js'; +import langEnsure from '../lang/ensure.js'; +import taxEnsure from '../tax/ensure.js'; +import request from '../request.js'; + +export default function update(pool) { + const byId = getById(pool); + const byName = getByName(pool); + const ensureLang = langEnsure(pool); + const ensureTax = taxEnsure(pool); + + return async (id, { iso639, name, taxrate, reducedtaxrate }) => { + const tx = new sql.Transaction(pool); + await tx.begin(); + + try { + const company = await byId(id, tx); + if (!company) { + throw new Error('Company not found'); + } + + if (name !== company.name) { + const taken = await byName(name, tx); + if (taken) { + throw new Error('Company name not available'); + } + } + + const languageId = await ensureLang(iso639, tx); + const taxrateId = await ensureTax(taxrate, tx); + const reducedtaxrateId = await ensureTax(reducedtaxrate, tx); + + await request(pool, tx) + .input('id', sql.Int, id) + .input('name', sql.NVarChar(255), name) + .input('languageId', sql.Int, languageId) + .input('taxrate', sql.Int, taxrateId) + .input('reducedtaxrate', sql.Int, reducedtaxrateId) + .query(UPDATE); + + await tx.commit(); + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/google_auth/setSelectedCompany.js b/src/store/google_auth/setSelectedCompany.js new file mode 100644 index 0000000..5f7d66c --- /dev/null +++ b/src/store/google_auth/setSelectedCompany.js @@ -0,0 +1,12 @@ +import sql from 'mssql'; +import { SET_SELECTED_COMPANY } from '../../sql/google_auth.js'; + +export default function setSelectedCompany(pool) { + return async (id, companyId) => { + await pool + .request() + .input('id', sql.Int, id) + .input('companyId', sql.Int, companyId) + .query(SET_SELECTED_COMPANY); + }; +} diff --git a/src/store/lang/create.js b/src/store/lang/create.js index 1337305..7c73439 100644 --- a/src/store/lang/create.js +++ b/src/store/lang/create.js @@ -1,10 +1,10 @@ import sql from 'mssql'; import { CREATE } from '../../sql/language.js'; +import request from '../request.js'; export default function create(pool) { - return async (iso639) => { - const result = await pool - .request() + return async (iso639, tx) => { + const result = await request(pool, tx) .input('iso639', sql.NVarChar(10), iso639) .query(CREATE); diff --git a/src/store/lang/ensure.js b/src/store/lang/ensure.js new file mode 100644 index 0000000..f087ffc --- /dev/null +++ b/src/store/lang/ensure.js @@ -0,0 +1,12 @@ +import getByIso639 from './getByIso639.js'; +import create from './create.js'; + +export default function ensure(pool) { + const get = getByIso639(pool); + const add = create(pool); + return async (iso639, tx) => { + const row = await get(iso639, tx); + if (row) return row.id; + return add(iso639, tx); + }; +} diff --git a/src/store/lang/getByIso639.js b/src/store/lang/getByIso639.js new file mode 100644 index 0000000..44551fc --- /dev/null +++ b/src/store/lang/getByIso639.js @@ -0,0 +1,13 @@ +import sql from 'mssql'; +import { GET_BY_ISO639 } from '../../sql/language.js'; +import request from '../request.js'; + +export default function getByIso639(pool) { + return async (iso639, tx) => { + const result = await request(pool, tx) + .input('iso639', sql.NVarChar(10), iso639) + .query(GET_BY_ISO639); + + return result.recordset[0]; + }; +} diff --git a/src/store/request.js b/src/store/request.js new file mode 100644 index 0000000..1985667 --- /dev/null +++ b/src/store/request.js @@ -0,0 +1,5 @@ +import sql from 'mssql'; + +export default function request(pool, tx) { + return tx ? new sql.Request(tx) : pool.request(); +} diff --git a/src/store/tax/create.js b/src/store/tax/create.js new file mode 100644 index 0000000..701fb1c --- /dev/null +++ b/src/store/tax/create.js @@ -0,0 +1,13 @@ +import sql from 'mssql'; +import { CREATE } from '../../sql/tax.js'; +import request from '../request.js'; + +export default function create(pool) { + return async (rate, tx) => { + const result = await request(pool, tx) + .input('rate', sql.Decimal(18, 4), rate) + .query(CREATE); + + return result.recordset[0].id; + }; +} diff --git a/src/store/tax/ensure.js b/src/store/tax/ensure.js new file mode 100644 index 0000000..3e895fb --- /dev/null +++ b/src/store/tax/ensure.js @@ -0,0 +1,12 @@ +import getByRate from './getByRate.js'; +import create from './create.js'; + +export default function ensure(pool) { + const get = getByRate(pool); + const add = create(pool); + return async (rate, tx) => { + const row = await get(rate, tx); + if (row) return row.id; + return add(rate, tx); + }; +} diff --git a/src/store/tax/getByRate.js b/src/store/tax/getByRate.js new file mode 100644 index 0000000..4c3892d --- /dev/null +++ b/src/store/tax/getByRate.js @@ -0,0 +1,13 @@ +import sql from 'mssql'; +import { GET_BY_RATE } from '../../sql/tax.js'; +import request from '../request.js'; + +export default function getByRate(pool) { + return async (rate, tx) => { + const result = await request(pool, tx) + .input('rate', sql.Decimal(18, 4), rate) + .query(GET_BY_RATE); + + return result.recordset[0]; + }; +} diff --git a/src/store/tax/list.js b/src/store/tax/list.js new file mode 100644 index 0000000..52b9806 --- /dev/null +++ b/src/store/tax/list.js @@ -0,0 +1,8 @@ +import { LIST } from '../../sql/tax.js'; + +export default function list(pool) { + return async () => { + const result = await pool.request().query(LIST); + return result.recordset; + }; +}