From 70e0a9ddc49c8982eade4ed566bca19744593fc2 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 7 Aug 2026 04:29:35 +0200 Subject: [PATCH] Add entity customer numbers and scope APIs via selected company. Entity gets an optional unique-per-company number with allocate/peek/isNumberFree helpers. Company-scoped store methods no longer take companyId and resolve google_auth.selected_company instead. --- API.article.md | 4 +- API.brand.md | 6 +- API.category.md | 2 +- API.companysetting.md | 8 +- API.customergroup.md | 10 +- API.delivery.md | 10 +- API.entity.md | 48 ++- API.entityaddress.md | 8 +- API.entitycontact.md | 8 +- API.posterminal.md | 14 +- API.supplier.md | 8 +- API.unit.md | 6 +- README.md | 293 ++++++++--------- generate-api-doc.js | 4 + schema-graph.dot | 2 +- schema-graph.svg | 90 +++--- scripts/test-api.js | 297 ++++++------------ src/index.js | 4 + src/schema.js | 69 ++++ src/sql/entity.js | 18 +- src/sql/purge.js | 11 +- src/store/article/listExport.js | 14 +- src/store/article/listSearchIndex.js | 10 +- src/store/brand/delete.js | 10 +- src/store/brand/list.js | 10 +- src/store/brand/upsert.js | 10 +- src/store/category/getTreeForCompany.js | 14 +- src/store/companysetting/delete.js | 10 +- src/store/companysetting/get.js | 10 +- src/store/companysetting/list.js | 10 +- src/store/companysetting/set.js | 10 +- .../resolveSelectedCompanyAccess.js | 35 +++ src/store/customergroup/create.js | 10 +- src/store/customergroup/delete.js | 10 +- src/store/customergroup/list.js | 10 +- src/store/customergroup/order.js | 10 +- src/store/customergroup/update.js | 10 +- src/store/delivery/create.js | 10 +- src/store/delivery/delete.js | 10 +- src/store/delivery/getById.js | 10 +- src/store/delivery/list.js | 10 +- src/store/delivery/update.js | 10 +- src/store/entity/create.js | 44 +-- src/store/entity/delete.js | 10 +- src/store/entity/getById.js | 24 +- src/store/entity/isNumberFree.js | 46 +++ src/store/entity/list.js | 28 +- src/store/entity/peekNextNumber.js | 24 ++ src/store/entity/shape.js | 11 + src/store/entity/update.js | 49 +-- src/store/entityaddress/create.js | 10 +- src/store/entityaddress/delete.js | 10 +- src/store/entityaddress/list.js | 10 +- src/store/entityaddress/update.js | 10 +- src/store/entitycontact/create.js | 10 +- src/store/entitycontact/delete.js | 10 +- src/store/entitycontact/list.js | 10 +- src/store/entitycontact/update.js | 10 +- src/store/manifestDoc/createStore.js | 29 +- src/store/numbers/nextNumber.js | 19 ++ src/store/numbers/nextSku.js | 21 +- src/store/numbers/peekNumber.js | 19 ++ src/store/order/index.js | 2 +- src/store/posterminal/delete.js | 10 +- src/store/posterminal/list.js | 10 +- src/store/posterminal/setOff.js | 10 +- src/store/posterminal/setOffAll.js | 10 +- src/store/posterminal/setOn.js | 10 +- src/store/posterminal/setOnAll.js | 10 +- src/store/posterminal/upsert.js | 10 +- src/store/quote/index.js | 2 +- src/store/supplier/create.js | 11 +- src/store/supplier/delete.js | 10 +- src/store/supplier/list.js | 10 +- src/store/supplier/update.js | 10 +- src/store/unit/delete.js | 10 +- src/store/unit/list.js | 10 +- src/store/unit/upsert.js | 10 +- 78 files changed, 940 insertions(+), 772 deletions(-) create mode 100644 src/store/companyuser/resolveSelectedCompanyAccess.js create mode 100644 src/store/entity/isNumberFree.js create mode 100644 src/store/entity/peekNextNumber.js create mode 100644 src/store/entity/shape.js create mode 100644 src/store/numbers/nextNumber.js create mode 100644 src/store/numbers/peekNumber.js diff --git a/API.article.md b/API.article.md index 283270e..1269958 100644 --- a/API.article.md +++ b/API.article.md @@ -80,7 +80,7 @@ Returns articles for export, optionally scoped to a category, including category ```js const fn = listExport(pool); -const articles = await fn(42, 1, null); +const articles = await fn(42, null); // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }] ``` @@ -92,7 +92,7 @@ Returns a lightweight search index of all articles for a company. ```js const fn = listSearchIndex(pool); -const articles = await fn(42, 1); +const articles = await fn(42); // => [{ id, sku, name, eans, categoryIds }] ``` diff --git a/API.brand.md b/API.brand.md index 5f83177..814fd8a 100644 --- a/API.brand.md +++ b/API.brand.md @@ -10,7 +10,7 @@ Deletes a brand for a company by its id. ```js const fn = del(pool); -await fn(42, 1, 3); +await fn(42, 3); ``` ## `brand.list` @@ -21,7 +21,7 @@ Lists the brands of a company. ```js const fn = list(pool); -const brands = await fn(42, 1); +const brands = await fn(42); // => [{ id, name, gsprpinfo }] ``` @@ -33,6 +33,6 @@ Creates or updates a brand for a company, keyed by name. ```js const fn = upsert(pool); -const brand = await fn(42, 1, { name: 'Coca-Cola', gsprpinfo: 'x' }); +const brand = await fn(42, { name: 'Coca-Cola', gsprpinfo: 'x' }); // => { id, name, gsprpinfo } ``` diff --git a/API.category.md b/API.category.md index 501534b..b336e2e 100644 --- a/API.category.md +++ b/API.category.md @@ -69,7 +69,7 @@ Returns the full category tree for a company together with its articles and the ```js const fn = getTreeForCompany(pool); -const tree = await fn(42, 1); +const tree = await fn(42); // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] } ``` diff --git a/API.companysetting.md b/API.companysetting.md index 31acc6f..0e58d00 100644 --- a/API.companysetting.md +++ b/API.companysetting.md @@ -10,7 +10,7 @@ Deletes a company setting by key. ```js const fn = del(pool); -await fn(42, 1, 'theme'); +await fn(42, 'theme'); ``` ## `companysetting.get` @@ -21,7 +21,7 @@ Returns a single company setting by key. ```js const fn = get(pool); -const setting = await fn(42, 1, 'theme'); +const setting = await fn(42, 'theme'); // => { key, value, frontend } ``` @@ -33,7 +33,7 @@ Lists the settings of a company, optionally only frontend-visible ones. ```js const fn = list(pool); -const settings = await fn(42, 1, { frontendOnly: true }); +const settings = await fn(42, { frontendOnly: true }); // => [{ key, value, frontend }] ``` @@ -45,6 +45,6 @@ Sets (upserts) a company setting by key. ```js const fn = set(pool); -const setting = await fn(42, 1, { key: 'theme', value: 'dark', frontend: true }); +const setting = await fn(42, { key: 'theme', value: 'dark', frontend: true }); // => { key, value, frontend } ``` diff --git a/API.customergroup.md b/API.customergroup.md index 5046957..967ff08 100644 --- a/API.customergroup.md +++ b/API.customergroup.md @@ -10,7 +10,7 @@ Creates a new customer group for a company. ```js const fn = create(pool); -const group = await fn(42, 1, { name: 'Wholesale' }); +const group = await fn(42, { name: 'Wholesale' }); // => { id, name, sort } ``` @@ -22,7 +22,7 @@ Deletes a customer group for a company by its id. ```js const fn = del(pool); -await fn(42, 1, 5); +await fn(42, 5); ``` ## `customergroup.list` @@ -33,7 +33,7 @@ Lists the customer groups of a company. ```js const fn = list(pool); -const groups = await fn(42, 1); +const groups = await fn(42); // => [{ id, name, sort }] ``` @@ -45,7 +45,7 @@ Reorders the customer groups of a company. ```js const fn = order(pool); -const result = await fn(42, 1, { customergroupIds: [3, 1, 2] }); +const result = await fn(42, { customergroupIds: [3, 1, 2] }); // => { customergroupIds: [3, 1, 2] } ``` @@ -57,6 +57,6 @@ Updates the name of a customer group for a company. ```js const fn = update(pool); -const group = await fn(42, 1, 5, { name: 'Retail' }); +const group = await fn(42, 5, { name: 'Retail' }); // => { id, name, sort } ``` diff --git a/API.delivery.md b/API.delivery.md index fcbe41a..ceef999 100644 --- a/API.delivery.md +++ b/API.delivery.md @@ -10,7 +10,7 @@ Creates a new delivery for a company with its lines. ```js const fn = create(pool); -const delivery = await fn(42, 1, { number: 'D-1', date: '2026-01-01', supplierId: 5, lines: [...] }); +const delivery = await fn(42, { number: 'D-1', date: '2026-01-01', supplierId: 5, lines: [...] }); // => { id, number, type, entityId, date, lines: [...] } ``` @@ -22,7 +22,7 @@ Deletes a delivery by its id within a company. ```js const fn = del(pool); -await fn(42, 1, 5); +await fn(42, 5); ``` ## `delivery.getById` @@ -33,7 +33,7 @@ Returns a single delivery by its id within a company. ```js const fn = getById(pool); -const delivery = await fn(42, 1, 5); +const delivery = await fn(42, 5); // => { id, number, type, entityId, date, lines: [...] } ``` @@ -45,7 +45,7 @@ Lists the deliveries of a company. ```js const fn = list(pool); -const deliveries = await fn(42, 1); +const deliveries = await fn(42); // => [{ id, number, type, entityId, date }] ``` @@ -57,6 +57,6 @@ Updates a delivery's header and/or lines. ```js const fn = update(pool); -const delivery = await fn(42, 1, 5, { number: 'D-2', lines: [...] }); +const delivery = await fn(42, 5, { number: 'D-2', lines: [...] }); // => { id, number, type, entityId, date, lines: [...] } ``` diff --git a/API.entity.md b/API.entity.md index d409b77..dae6c4f 100644 --- a/API.entity.md +++ b/API.entity.md @@ -6,12 +6,12 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/entity/create.js`* -Creates a new entity (customer) in a customer group, recording history. +Creates a new entity (customer) in a customer group, recording history. Optional `number`; if omitted, allocates via `getnextnumberwithprefixandpadding` (`type` `entity`). ```js const fn = create(pool); -const entity = await fn(42, 1, { customergroupId: 5, name: 'Doe', firstName: 'John' }); -// => { id, customergroupId, firstName, name, createdAt, updatedAt } +const entity = await fn(42, { customergroupId: 5, name: 'Doe', firstName: 'John' }); +// => { id, customergroupId, firstName, name, number, createdAt, updatedAt } ``` ## `entity.delete` @@ -22,7 +22,7 @@ Deletes an entity by its id within a company. ```js const fn = del(pool); -await fn(42, 1, 5); +await fn(42, 5); ``` ## `entity.getById` @@ -33,8 +33,22 @@ Returns a single entity by its id within a company. ```js const fn = getById(pool); -const entity = await fn(42, 1, 5); -// => { id, customergroupId, firstName, name, createdAt, updatedAt } +const entity = await fn(42, 5); +// => { id, customergroupId, firstName, name, number, createdAt, updatedAt } +``` + +## `entity.isNumberFree` + +*Source: `src/store/entity/isNumberFree.js`* + +Returns whether a customer number is free within the company. Pass `excludeEntityId` when editing so the entity's own number counts as free. + +```js +const fn = isNumberFree(pool); +const free = await fn(42, 'C-100'); +// => true +const freeOnEdit = await fn(42, 'C-100', { excludeEntityId: 5 }); +// => true ``` ## `entity.list` @@ -45,18 +59,30 @@ Lists the entities (customers) of a company, optionally filtered by customer gro ```js const fn = list(pool); -const entities = await fn(42, 1, { customergroupId: 5 }); -// => [{ id, customergroupId, firstName, name, createdAt, updatedAt }] +const entities = await fn(42, { customergroupId: 5 }); +// => [{ id, customergroupId, firstName, name, number, createdAt, updatedAt }] +``` + +## `entity.peekNextNumber` + +*Source: `src/store/entity/peekNextNumber.js`* + +Returns the next customer number that would be allocated for the company (`numbers` type `entity`) without consuming it. + +```js +const fn = peekNextNumber(pool); +const number = await fn(42); +// => '1' ``` ## `entity.update` *Source: `src/store/entity/update.js`* -Updates the name and/or first name of an entity, recording history. +Updates name, first name, and/or customer number of an entity, recording history. Optional `number` sets the customer number explicitly (does not auto-allocate). ```js const fn = update(pool); -const entity = await fn(42, 1, 5, { name: 'Doe', firstName: 'Jane' }); -// => { id, customergroupId, firstName, name, createdAt, updatedAt } +const entity = await fn(42, 5, { name: 'Doe', firstName: 'Jane', number: 'C-42' }); +// => { id, customergroupId, firstName, name, number, createdAt, updatedAt } ``` diff --git a/API.entityaddress.md b/API.entityaddress.md index b9d9a8a..719d69c 100644 --- a/API.entityaddress.md +++ b/API.entityaddress.md @@ -10,7 +10,7 @@ Creates a new address for an entity. ```js const fn = create(pool); -const address = await fn(42, 1, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' }); +const address = await fn(42, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' }); // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort } ``` @@ -22,7 +22,7 @@ Deletes an address of an entity. ```js const fn = del(pool); -await fn(42, 1, 5, 9); +await fn(42, 5, 9); ``` ## `entityaddress.list` @@ -33,7 +33,7 @@ Lists the addresses of an entity. ```js const fn = list(pool); -const addresses = await fn(42, 1, 5); +const addresses = await fn(42, 5); // => [{ id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }] ``` @@ -45,6 +45,6 @@ Updates an address of an entity. ```js const fn = update(pool); -const address = await fn(42, 1, 5, 9, { city: 'Hamburg' }); +const address = await fn(42, 5, 9, { city: 'Hamburg' }); // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort } ``` diff --git a/API.entitycontact.md b/API.entitycontact.md index 0add735..d53b85a 100644 --- a/API.entitycontact.md +++ b/API.entitycontact.md @@ -10,7 +10,7 @@ Creates a new contact for an entity. ```js const fn = create(pool); -const contact = await fn(42, 1, 5, { contactType: 'email', contact: 'a@b.c' }); +const contact = await fn(42, 5, { contactType: 'email', contact: 'a@b.c' }); // => { id, entityId, contactType, contact, sort } ``` @@ -22,7 +22,7 @@ Deletes a contact of an entity. ```js const fn = del(pool); -await fn(42, 1, 5, 9); +await fn(42, 5, 9); ``` ## `entitycontact.list` @@ -33,7 +33,7 @@ Lists the contacts of an entity. ```js const fn = list(pool); -const contacts = await fn(42, 1, 5); +const contacts = await fn(42, 5); // => [{ id, entityId, contactType, contact, sort }] ``` @@ -45,6 +45,6 @@ Updates a contact of an entity. ```js const fn = update(pool); -const contact = await fn(42, 1, 5, 9, { contact: 'b@c.d' }); +const contact = await fn(42, 5, 9, { contact: 'b@c.d' }); // => { id, entityId, contactType, contact, sort } ``` diff --git a/API.posterminal.md b/API.posterminal.md index 294ac18..1890b0f 100644 --- a/API.posterminal.md +++ b/API.posterminal.md @@ -10,7 +10,7 @@ Deletes a posterminal by its id within a company. ```js const fn = del(pool); -await fn(42, 1, 5); +await fn(42, 5); ``` ## `posterminal.list` @@ -21,7 +21,7 @@ Lists the posterminals of a company. ```js const fn = list(pool); -const terminals = await fn(42, 1); +const terminals = await fn(42); // => [{ id, name, token, pairingpin }] ``` @@ -33,7 +33,7 @@ Removes a category assignment from a posterminal (turns it off). ```js const fn = setOff(pool); -const result = await fn(42, 1, 5, 9); +const result = await fn(42, 5, 9); // => { id, categoryIds: [...] } ``` @@ -45,7 +45,7 @@ Removes all category assignments from a posterminal. ```js const fn = setOffAll(pool); -const result = await fn(42, 1, 5); +const result = await fn(42, 5); // => { id, categoryIds: [] } ``` @@ -57,7 +57,7 @@ Assigns a category to a posterminal (turns it on for that category). ```js const fn = setOn(pool); -const result = await fn(42, 1, 5, 9); +const result = await fn(42, 5, 9); // => { id, categoryIds: [9, ...] } ``` @@ -69,7 +69,7 @@ Assigns all categories of a company to a posterminal. ```js const fn = setOnAll(pool); -const result = await fn(42, 1, 5); +const result = await fn(42, 5); // => { id, categoryIds: [...] } ``` @@ -81,6 +81,6 @@ Creates or updates a posterminal for a company. ```js const fn = upsert(pool); -const terminal = await fn(42, 1, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' }); +const terminal = await fn(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' }); // => { id, name, token, pairingpin } ``` diff --git a/API.supplier.md b/API.supplier.md index 5abab79..fbbe0e7 100644 --- a/API.supplier.md +++ b/API.supplier.md @@ -10,7 +10,7 @@ Creates a new supplier for a company, recording history. ```js const fn = create(pool); -const supplier = await fn(42, 1, { name: 'Acme Supplies' }); +const supplier = await fn(42, { name: 'Acme Supplies' }); // => { id, name, createdAt, updatedAt } ``` @@ -22,7 +22,7 @@ Deletes a supplier by its id within a company. ```js const fn = del(pool); -await fn(42, 1, 5); +await fn(42, 5); ``` ## `supplier.list` @@ -33,7 +33,7 @@ Lists the suppliers of a company. ```js const fn = list(pool); -const suppliers = await fn(42, 1); +const suppliers = await fn(42); // => [{ id, name, createdAt, updatedAt }] ``` @@ -45,6 +45,6 @@ Updates the name of a supplier, recording history. ```js const fn = update(pool); -const supplier = await fn(42, 1, 5, { name: 'Acme GmbH' }); +const supplier = await fn(42, 5, { name: 'Acme GmbH' }); // => { id, name, createdAt, updatedAt } ``` diff --git a/API.unit.md b/API.unit.md index 7d10790..517d441 100644 --- a/API.unit.md +++ b/API.unit.md @@ -10,7 +10,7 @@ Deletes a unit for a company by its SI code. ```js const fn = del(pool); -await fn(42, 1, 'l'); +await fn(42, 'l'); ``` ## `unit.list` @@ -21,7 +21,7 @@ Lists the units available for a company. ```js const fn = list(pool); -const units = await fn(42, 1); +const units = await fn(42); // => [{ id, si, basesi, basesifactor, symbol, name }] ``` @@ -33,6 +33,6 @@ Creates or updates a unit for a company, keyed by its SI code. ```js const fn = upsert(pool); -const unit = await fn(42, 1, { si: 'l', basesi: 'm3', basesifactor: 0.001, symbol: 'L', name: 'Liter' }); +const unit = await fn(42, { si: 'l', basesi: 'm3', basesifactor: 0.001, symbol: 'L', name: 'Liter' }); // => { id, si, basesi, basesifactor, symbol, name } ``` diff --git a/README.md b/README.md index f459dcc..53113e7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ SQL lives in `src/sql/.js`. Schema DDL: `src/schema.js` / `import 'libdas ID fields in the JS API use camelCase `…Id` (DB columns stay snake_case). `googleAuthId` is always `google_auth.id` (the logged-in user), not `companyuser.id`. +Company-scoped methods take **no** `companyId` argument: they use `google_auth.selected_company` (set via `google_auth.setSelectedCompany`). Missing selection → `No company selected`. Exceptions that still take `companyId`: `setSelectedCompany`, `companyuser.add`, `company.delete`, and internal helpers. + --- ## Schema @@ -124,9 +126,9 @@ Backend mapping: | 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` | +| `list` | `(googleAuthId)` | `{ si, basesi, basesifactor, symbol, name }[]` | +| `upsert` | `(googleAuthId, { si, basesi, basesifactor, symbol, name })` | `{ si, basesi, basesifactor, symbol, name }` | +| `delete` | `(googleAuthId, si)` | `void` | Shape (überall): @@ -138,82 +140,84 @@ Shape (überall): ### `list` -Auth via `resolveCompanyAccess` (like `getTreeForCompany`). Returns only units of that company. Errors: `Company not found`, `Not authorized`. +Auth via selected company (like `getTreeForCompany`). Returns only units of that company. Errors: `No company selected`, `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`. +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: `No company selected`, `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`. +Deletes the unit for `(companyId, si)`. Blocked by FK if any article still references it (`Unit in use`). Missing → `Unit not found`. Errors: `No company selected`, `Company not found`, `Not authorized`, `Unit not found`, `Unit in use`. --- ## `companysetting` -Company-scoped key/value settings. Auth via `resolveCompanyAccess`. +Company-scoped key/value settings. Auth via selected company (`google_auth.selected_company` + membership). | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId, { frontendOnly? })` | `{ key, value, frontend }[]` | -| `get` | `(googleAuthId, companyId, key, { frontendOnly? })` | `{ key, value, frontend } \| undefined` | -| `set` | `(googleAuthId, companyId, { key, value, frontend? })` | `{ key, value, frontend }` | -| `delete` | `(googleAuthId, companyId, key)` | `void` | +| `list` | `(googleAuthId, { frontendOnly? })` | `{ key, value, frontend }[]` | +| `get` | `(googleAuthId, key, { frontendOnly? })` | `{ key, value, frontend } \| undefined` | +| `set` | `(googleAuthId, { key, value, frontend? })` | `{ key, value, frontend }` | +| `delete` | `(googleAuthId, key)` | `void` | - `value` is always a string (`NVARCHAR(MAX)`). - `frontend: true` → readable by frontend routes (`frontendOnly: true`). Default on `set` is `false` (backend-only). - With `frontendOnly: true`, backend-only keys are omitted / `get` returns `undefined` (no leak via error type beyond missing). -Errors: `Company not found`, `Not authorized`, `Invalid setting`, `Setting not found`. +Errors: `No company selected`, `Company not found`, `Not authorized`, `Invalid setting`, `Setting not found`. --- ## `brand` -Company-scoped brands. Auth via `resolveCompanyAccess`. +Company-scoped brands. Auth via selected company (`google_auth.selected_company` + membership). | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId)` | `{ id, name, gsprpinfo }[]` | -| `upsert` | `(googleAuthId, companyId, { name, gsprpinfo? })` | `{ id, name, gsprpinfo }` | -| `delete` | `(googleAuthId, companyId, id)` | `void` | +| `list` | `(googleAuthId)` | `{ id, name, gsprpinfo }[]` | +| `upsert` | `(googleAuthId, { name, gsprpinfo? })` | `{ id, name, gsprpinfo }` | +| `delete` | `(googleAuthId, id)` | `void` | -`brand.company_id` **NOT NULL**, unique `(company_id, name)`. `upsert` matches by `(companyId, name)`. `delete` blocked by FK if any article references it (`Brand in use`). Errors: `Company not found`, `Not authorized`, `Invalid brand`, `Brand not found`, `Brand in use`. +`brand.company_id` **NOT NULL**, unique `(company_id, name)`. `upsert` matches by `(companyId, name)`. `delete` blocked by FK if any article references it (`Brand in use`). Errors: `No company selected`, `Company not found`, `Not authorized`, `Invalid brand`, `Brand not found`, `Brand in use`. --- ## `customergroup` -Company-scoped customer groups. Auth via `resolveCompanyAccess`. +Company-scoped customer groups. Auth via selected company (`google_auth.selected_company` + membership). | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId)` | `{ id, name, sort }[]` | -| `create` | `(googleAuthId, companyId, { name })` | `{ id, name, sort }` | -| `update` | `(googleAuthId, companyId, id, { name })` | `{ id, name, sort }` | -| `order` | `(googleAuthId, companyId, { customergroupIds })` | `{ customergroupIds }` | -| `delete` | `(googleAuthId, companyId, id)` | `void` | +| `list` | `(googleAuthId)` | `{ id, name, sort }[]` | +| `create` | `(googleAuthId, { name })` | `{ id, name, sort }` | +| `update` | `(googleAuthId, id, { name })` | `{ id, name, sort }` | +| `order` | `(googleAuthId, { customergroupIds })` | `{ customergroupIds }` | +| `delete` | `(googleAuthId, id)` | `void` | List ordered by `sort`, `name`, `id`. `create` appends with next `sort` (`0…n`). `order` sets `sort` to each id’s index `0…n-1`; `customergroupIds` must be exactly the company’s groups (same set, no dups) else `Invalid customer group order`. Implicit groups `POS` (company create) and `Lieferanten` (first supplier) cannot be renamed (`Name cannot be changed`). Creating a group with those reserved names → `Name not allowed`. -Empty name → `Name required`. Deleting the POS group (or any group still referenced with `NO ACTION`) → `Customer group in use`. Cascades entities when the group is deleted. Errors: `Company not found`, `Not authorized`, `Name required`, `Name not allowed`, `Name cannot be changed`, `Invalid customer group order`, `Customer group not found`, `Customer group in use`. +Empty name → `Name required`. Deleting the POS group (or any group still referenced with `NO ACTION`) → `Customer group in use`. Cascades entities when the group is deleted. Errors: `No company selected`, `Company not found`, `Not authorized`, `Name required`, `Name not allowed`, `Name cannot be changed`, `Invalid customer group order`, `Customer group not found`, `Customer group in use`. --- ## `entity` -Entities belong to a `customergroup` of the company. Auth via `resolveCompanyAccess`. +Entities belong to a `customergroup` of the selected company. Company scope comes from `google_auth.selected_company` (not a `companyId` argument). | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId, { customergroupId?, role? })` | `Entity[]` | -| `getById` | `(googleAuthId, companyId, id)` | `Entity` | -| `create` | `(googleAuthId, companyId, { customergroupId, name, firstName? })` | `Entity` | -| `update` | `(googleAuthId, companyId, id, { name?, firstName? })` | `Entity` | -| `delete` | `(googleAuthId, companyId, id)` | `void` | +| `list` | `(googleAuthId, { customergroupId?, role? })` | `Entity[]` | +| `getById` | `(googleAuthId, id)` | `Entity` | +| `create` | `(googleAuthId, { customergroupId, name, firstName?, number? })` | `Entity` | +| `update` | `(googleAuthId, id, { name?, firstName?, number? })` | `Entity` | +| `delete` | `(googleAuthId, id)` | `void` | +| `peekNextNumber` | `(googleAuthId)` | `string` | +| `isNumberFree` | `(googleAuthId, number, { excludeEntityId? })` | `boolean` | ```ts type Entity = { @@ -221,6 +225,7 @@ type Entity = { customergroupId: number, firstName: string | null, name: string, + number: string | null, createdAt: Date, updatedAt: Date, } @@ -228,66 +233,68 @@ type Entity = { `list` optional filters: `customergroupId` (must belong to the company), or `role: 'customer'` (entities whose customergroup name is not `Lieferanten` and not `POS` — for quote/order pickers). `role` wins over `customergroupId` when both are set. Unknown `role` → `Invalid role`. -`create` / `update` write `entityhistory` (`Erstellt`, or name/firstName change lines). `customergroupId` must belong to the company. Errors: `Company not found`, `Not authorized`, `Name required`, `Customer group not found`, `Entity not found`, `Invalid role`. +`number` is optional on the row (e.g. suppliers leave it null). Non-null numbers are unique per company (via `customergroup`). `create` allocates via `getnextnumberwithprefixandpadding` (`type` `entity`) when `number` is omitted. `update` only changes `number` when the key is present (no auto-allocate). `peekNextNumber` returns what the next allocate would yield without consuming it (`peeknextnumberwithprefixandpadding`; ensures the `numbers` row exists). `isNumberFree` checks occupancy company-wide; use `excludeEntityId` on edit so the current entity’s number stays free. Empty `number` → `Number required`. + +`create` / `update` write `entityhistory` (`Erstellt`, or name/firstName/number change lines). `customergroupId` must belong to the company. Errors: `No company selected`, `Company not found`, `Not authorized`, `Name required`, `Customer group not found`, `Entity not found`, `Invalid role`, `Number already in use`, `Number required`. --- ## `entityaddress` -Addresses for an entity. Auth via company + entity ownership. +Addresses for an entity. Auth via selected company + entity ownership. | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId, entityId)` | `EntityAddress[]` | -| `create` | `(googleAuthId, companyId, entityId, { addressType, firstName?, name?, street?, streetNumber?, zip?, city?, county?, sort? })` | `EntityAddress` | -| `update` | `(googleAuthId, companyId, entityId, id, patch)` | `EntityAddress` | -| `delete` | `(googleAuthId, companyId, entityId, id)` | `void` | +| `list` | `(googleAuthId, entityId)` | `EntityAddress[]` | +| `create` | `(googleAuthId, entityId, { addressType, firstName?, name?, street?, streetNumber?, zip?, city?, county?, sort? })` | `EntityAddress` | +| `update` | `(googleAuthId, entityId, id, patch)` | `EntityAddress` | +| `delete` | `(googleAuthId, entityId, id)` | `void` | -`addressType` required. `sort` defaults to next index for the entity (`0…n`). List ordered by `sort`, `id`. Errors: `Company not found`, `Not authorized`, `Entity not found`, `Address type required`, `Address not found`, `Invalid sort`. +`addressType` required. `sort` defaults to next index for the entity (`0…n`). List ordered by `sort`, `id`. Errors: `No company selected`, `Company not found`, `Not authorized`, `Entity not found`, `Address type required`, `Address not found`, `Invalid sort`. --- ## `entitycontact` -Contacts for an entity (`contactType` e.g. `email`, `phone`). Auth via company + entity ownership. +Contacts for an entity (`contactType` e.g. `email`, `phone`). Auth via selected company + entity ownership. | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId, entityId)` | `{ id, entityId, contactType, contact, sort }[]` | -| `create` | `(googleAuthId, companyId, entityId, { contactType, contact, sort? })` | row | -| `update` | `(googleAuthId, companyId, entityId, id, { contactType?, contact?, sort? })` | row | -| `delete` | `(googleAuthId, companyId, entityId, id)` | `void` | +| `list` | `(googleAuthId, entityId)` | `{ id, entityId, contactType, contact, sort }[]` | +| `create` | `(googleAuthId, entityId, { contactType, contact, sort? })` | row | +| `update` | `(googleAuthId, entityId, id, { contactType?, contact?, sort? })` | row | +| `delete` | `(googleAuthId, entityId, id)` | `void` | -`sort` defaults to next index for the entity. List ordered by `sort`, `id`. Errors: `Company not found`, `Not authorized`, `Entity not found`, `Contact required`, `Contact not found`, `Invalid sort`. +`sort` defaults to next index for the entity. List ordered by `sort`, `id`. Errors: `No company selected`, `Company not found`, `Not authorized`, `Entity not found`, `Contact required`, `Contact not found`, `Invalid sort`. --- ## `supplier` -Suppliers are `entity` rows in the company `customergroup` named `Lieferanten` (created implicitly on first supplier). Auth via `resolveCompanyAccess`. +Suppliers are `entity` rows in the company `customergroup` named `Lieferanten` (created implicitly on first supplier). Auth via selected company (`google_auth.selected_company` + membership). | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId)` | `{ id, name, createdAt, updatedAt }[]` | -| `create` | `(googleAuthId, companyId, { name })` | `{ id, name, createdAt, updatedAt }` | -| `update` | `(googleAuthId, companyId, id, { name })` | `{ id, name, createdAt, updatedAt }` | -| `delete` | `(googleAuthId, companyId, id)` | `void` | +| `list` | `(googleAuthId)` | `{ id, name, createdAt, updatedAt }[]` | +| `create` | `(googleAuthId, { name })` | `{ id, name, createdAt, updatedAt }` | +| `update` | `(googleAuthId, id, { name })` | `{ id, name, createdAt, updatedAt }` | +| `delete` | `(googleAuthId, id)` | `void` | -`create` ensures `customergroup` `Lieferanten` for the company, inserts `entity` (`name` only; `first_name` null), writes `entityhistory` (`Erstellt`) with the auth `companyuser_id`. `update` renames and writes `Name geändert von "…" auf "…"` when the name actually changes; bumps `updated_at`. Empty name → `Name required`. `delete` removes the entity (`entityhistory` cascades; `manifest.entity_id` set null). Errors: `Company not found`, `Not authorized`, `Name required`, `Supplier not found`. +`create` ensures `customergroup` `Lieferanten` for the company, inserts `entity` (`name` only; `first_name` null), writes `entityhistory` (`Erstellt`) with the auth `companyuser_id`. `update` renames and writes `Name geändert von "…" auf "…"` when the name actually changes; bumps `updated_at`. Empty name → `Name required`. `delete` removes the entity (`entityhistory` cascades; `manifest.entity_id` set null). Errors: `No company selected`, `Company not found`, `Not authorized`, `Name required`, `Supplier not found`. --- ## `delivery` -Company-scoped goods receipts on `manifest` / `manifestarticle` with fixed `type = 'delivery'`. Auth via `resolveCompanyAccess`. `manifest.company_id` scopes the list (`ON DELETE NO ACTION` — avoid SQL Server multiple cascade paths with `entity_id`). +Company-scoped goods receipts on `manifest` / `manifestarticle` with fixed `type = 'delivery'`. Auth via selected company (`google_auth.selected_company` + membership). `manifest.company_id` scopes the list (`ON DELETE NO ACTION` — avoid SQL Server multiple cascade paths with `entity_id`). | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId)` | `DeliverySummary[]` | -| `getById` | `(googleAuthId, companyId, id)` | `Delivery` | -| `create` | `(googleAuthId, companyId, { number, date, supplierId?, lines })` | `Delivery` | -| `update` | `(googleAuthId, companyId, id, { number?, date?, supplierId?, lines? })` | `Delivery` | -| `delete` | `(googleAuthId, companyId, id)` | `void` | +| `list` | `(googleAuthId)` | `DeliverySummary[]` | +| `getById` | `(googleAuthId, id)` | `Delivery` | +| `create` | `(googleAuthId, { number, date, supplierId?, lines })` | `Delivery` | +| `update` | `(googleAuthId, id, { number?, date?, supplierId?, lines? })` | `Delivery` | +| `delete` | `(googleAuthId, id)` | `void` | ```ts type DeliverySummary = { @@ -316,21 +323,21 @@ type Delivery = DeliverySummary & { lines: DeliveryLine[] } `update` patches header fields that are present. If `lines` is present, replace-all lines; if omitted, leave lines unchanged. Returns full `Delivery`. -`delete` removes the manifest (`manifestarticle` cascades). Errors: `Company not found`, `Not authorized`, `Delivery not found`, `Supplier not found`, `Article not found`, `Invalid delivery`, `Invalid lines`. +`delete` removes the manifest (`manifestarticle` cascades). Errors: `No company selected`, `Company not found`, `Not authorized`, `Delivery not found`, `Supplier not found`, `Article not found`, `Invalid delivery`, `Invalid lines`. --- ## `quote` -Company-scoped quotes on `manifest` / `manifestarticle` with fixed `type = 'quote'`. Auth via `resolveCompanyAccess`. Same line shape as `delivery` (no `transactedAmount` — ledger owns movements). `manifest.date` is the document date; HTTP/OpenAPI may describe it as valid-until. +Company-scoped quotes on `manifest` / `manifestarticle` with fixed `type = 'quote'`. Auth via selected company (`google_auth.selected_company` + membership). Same line shape as `delivery` (no `transactedAmount` — ledger owns movements). `manifest.date` is the document date; HTTP/OpenAPI may describe it as valid-until. | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId)` | `QuoteSummary[]` | -| `getById` | `(googleAuthId, companyId, id)` | `Quote` | -| `create` | `(googleAuthId, companyId, { number, date, entityId?, lines })` | `Quote` | -| `update` | `(googleAuthId, companyId, id, { number?, date?, entityId?, lines? })` | `Quote` | -| `delete` | `(googleAuthId, companyId, id)` | `void` | +| `list` | `(googleAuthId)` | `QuoteSummary[]` | +| `getById` | `(googleAuthId, id)` | `Quote` | +| `create` | `(googleAuthId, { number, date, entityId?, lines })` | `Quote` | +| `update` | `(googleAuthId, id, { number?, date?, entityId?, lines? })` | `Quote` | +| `delete` | `(googleAuthId, id)` | `void` | ```ts type QuoteSummary = { @@ -357,7 +364,7 @@ type Quote = QuoteSummary & { lines: ManifestLine[] } `entityId` null or a company entity whose customergroup is not named `Lieferanten` or `POS` (else `Entity not found`). Lines: `sku` (category tree → `Article not found`), `amount`, `net`, `tax` (`tax.ensure`); `rebate` defaults `0`. Empty/invalid `number` or `date` → `Invalid quote`; bad lines → `Invalid lines`. -`update`: present fields only; `lines` present → replace-all. `delete` cascades lines. Errors: `Company not found`, `Not authorized`, `Quote not found`, `Entity not found`, `Article not found`, `Invalid quote`, `Invalid lines`. +`update`: present fields only; `lines` present → replace-all. `delete` cascades lines. Errors: `No company selected`, `Company not found`, `Not authorized`, `Quote not found`, `Entity not found`, `Article not found`, `Invalid quote`, `Invalid lines`. --- @@ -367,11 +374,11 @@ Company-scoped orders on `manifest` / `manifestarticle` with fixed `type = 'orde | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId)` | `OrderSummary[]` | -| `getById` | `(googleAuthId, companyId, id)` | `Order` | -| `create` | `(googleAuthId, companyId, { number, date, entityId?, lines })` | `Order` | -| `update` | `(googleAuthId, companyId, id, { number?, date?, entityId?, lines? })` | `Order` | -| `delete` | `(googleAuthId, companyId, id)` | `void` | +| `list` | `(googleAuthId)` | `OrderSummary[]` | +| `getById` | `(googleAuthId, id)` | `Order` | +| `create` | `(googleAuthId, { number, date, entityId?, lines })` | `Order` | +| `update` | `(googleAuthId, id, { number?, date?, entityId?, lines? })` | `Order` | +| `delete` | `(googleAuthId, id)` | `void` | ```ts type OrderSummary = { @@ -385,7 +392,7 @@ type OrderSummary = { type Order = OrderSummary & { lines: ManifestLine[] } // ManifestLine as in quote ``` -Errors: `Company not found`, `Not authorized`, `Order not found`, `Entity not found`, `Article not found`, `Invalid order`, `Invalid lines`. +Errors: `No company selected`, `Company not found`, `Not authorized`, `Order not found`, `Entity not found`, `Article not found`, `Invalid order`, `Invalid lines`. --- @@ -409,28 +416,28 @@ Engine also maintains `ledger_start_transaction_id` / `ledger_start_sequence_num ## `posterminal` -Company-scoped POS terminals. Auth via `resolveCompanyAccess`. +Company-scoped POS terminals. Auth via selected company (`google_auth.selected_company` + membership). | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId)` | `{ id, name, token, pairingpin }[]` | -| `upsert` | `(googleAuthId, companyId, { id?, name, token?, pairingpin })` | `{ id, name, token, pairingpin }` | -| `delete` | `(googleAuthId, companyId, id)` | `void` | -| `setOn` | `(googleAuthId, companyId, posterminalId, categoryId)` | `{ id, categoryIds[] }` | -| `setOff` | `(googleAuthId, companyId, posterminalId, categoryId)` | `{ id, categoryIds[] }` | -| `setOnAll` | `(googleAuthId, companyId, posterminalId)` | `{ id, categoryIds[] }` | -| `setOffAll` | `(googleAuthId, companyId, posterminalId)` | `{ id, categoryIds[] }` | +| `list` | `(googleAuthId)` | `{ id, name, token, pairingpin }[]` | +| `upsert` | `(googleAuthId, { id?, name, token?, pairingpin })` | `{ id, name, token, pairingpin }` | +| `delete` | `(googleAuthId, id)` | `void` | +| `setOn` | `(googleAuthId, posterminalId, categoryId)` | `{ id, categoryIds[] }` | +| `setOff` | `(googleAuthId, posterminalId, categoryId)` | `{ id, categoryIds[] }` | +| `setOnAll` | `(googleAuthId, posterminalId)` | `{ id, categoryIds[] }` | +| `setOffAll` | `(googleAuthId, posterminalId)` | `{ id, categoryIds[] }` | | `getCategoryPosterminalIds` | `(companyId)` | `{ [categoryId]: posterminalId[] }` (internal, used by category tree) | `posterminal.company_id` **NOT NULL**. `name` is required. `token` is nullable and unique (pairing code assigned during setup). `pairingpin` is required. -`upsert`: if `id` is provided, updates the existing posterminal (must belong to company); otherwise creates a new one. Errors: `Company not found`, `Not authorized`, `Invalid posterminal`, `Posterminal not found`. +`upsert`: if `id` is provided, updates the existing posterminal (must belong to company); otherwise creates a new one. Errors: `No company selected`, `Company not found`, `Not authorized`, `Invalid posterminal`, `Posterminal not found`. -`setOn` / `setOff`: toggle a single category's visibility on a single terminal. `setOn` inserts the row (idempotent — no error if already present); `setOff` deletes it (idempotent). Both return the updated `categoryIds` for that terminal. Errors: `Company not found`, `Not authorized`, `Posterminal not found`, `Invalid posterminal or category`. +`setOn` / `setOff`: toggle a single category's visibility on a single terminal. `setOn` inserts the row (idempotent — no error if already present); `setOff` deletes it (idempotent). Both return the updated `categoryIds` for that terminal. Errors: `No company selected`, `Company not found`, `Not authorized`, `Posterminal not found`, `Invalid posterminal or category`. -`setOnAll` / `setOffAll`: toggle **all** company categories on a single terminal. `setOnAll` walks the company's category tree and inserts any missing links; `setOffAll` removes all category links for the terminal. Both return the updated `categoryIds` for that terminal. Errors: `Company not found`, `Not authorized`, `Posterminal not found`, `Invalid posterminal`. +`setOnAll` / `setOffAll`: toggle **all** company categories on a single terminal. `setOnAll` walks the company's category tree and inserts any missing links; `setOffAll` removes all category links for the terminal. Both return the updated `categoryIds` for that terminal. Errors: `No company selected`, `Company not found`, `Not authorized`, `Posterminal not found`, `Invalid posterminal`. -`delete` cascades to `posterminalcategory`. Errors: `Company not found`, `Not authorized`, `Posterminal not found`. +`delete` cascades to `posterminalcategory`. Errors: `No company selected`, `Company not found`, `Not authorized`, `Posterminal not found`. ### Category tree integration @@ -447,7 +454,7 @@ Company-scoped POS terminals. Auth via `resolveCompanyAccess`. | `move` | `({ googleAuthId, id, parentId })` | `{ id, parentId, sort }` | | `order` | `({ googleAuthId, parentId, categoryIds })` | `{ categoryIds }` | | `delete` | `({ googleAuthId, id })` | `void` — `Category has children` if child categories remain; FK `categoryarticle` also blocks deletion | -| `getTreeForCompany` | `(googleAuthId, companyId)` | `{ languageId, tree, articles }` — tree rows carry `name` + `slug` | +| `getTreeForCompany` | `(googleAuthId)` | `{ languageId, tree, articles }` — tree rows carry `name` + `slug` | | `getCategoryArticles` | `(googleAuthId, categoryId)` | `{ id, sku, name, slug, net, taxRate, optionsCount, articleListCount, childCount, fatherArticleId, fatherSku, fullHash, thumbW, thumbH, thumbHash }[]` | | `addImage` | `(googleAuthId, categoryId, imageBuffer)` | `{ id, thumbW, thumbH, thumbHash }` | | `deleteImage` | `(googleAuthId, categoryId)` | `{ id, thumbW: null, thumbH: null, thumbHash: null }` | @@ -456,7 +463,7 @@ Company-scoped POS terminals. Auth via `resolveCompanyAccess`. 1. `parentId` exists, else `Parent not found` 2. Walk ancestors to root; find `company` with `root_category_id` = root -3. `companyuser` for `(googleAuthId, companyId)`, else `Not authorized` +3. `companyuser` for selected company via `googleAuthId`, else `Not authorized` 4. Insert `category` (next sibling `sort`) + `categorytext` (`name`, slugified `slug`, company default language); returns `{ id, parentId, sort, name, slug }` `getTreeForCompany` throws `Not authorized` if `googleAuthId` is not a `companyuser` of `companyId`, and `Company not found` if the company is missing, has no `root_category_id`, or no `default_language_id`. @@ -519,7 +526,7 @@ Company-scoped POS terminals. Auth via `resolveCompanyAccess`. } ``` -`listSearchIndex` returns every article in the company (reachable via the category tree). Designed for client-side search / autocomplete preload. Auth via `resolveCompanyAccess`. Errors: `Company not found`, `Not authorized`. +`listSearchIndex` returns every article in the company (reachable via the category tree). Designed for client-side search / autocomplete preload. Auth via selected company (`google_auth.selected_company` + membership). Errors: `No company selected`, `Company not found`, `Not authorized`. ```ts type SearchIndexEntry = { @@ -531,7 +538,7 @@ type SearchIndexEntry = { } ``` -`listExport` returns articles in a category subtree (root = company when `categoryId` omitted). `categoryId` must be reachable via the selected company's root category (else `Not authorized`). Auth via `resolveCompanyAccess` + `resolveCategoryAccess`. Errors: `Company not found`, `Parent not found`, `Not authorized`. +`listExport` returns articles in a category subtree (root = company when `categoryId` omitted). `categoryId` must be reachable via the selected company's root category (else `Not authorized`). Auth via selected company + `resolveCategoryAccess`. Errors: `No company selected`, `Company not found`, `Parent not found`, `Not authorized`. ```ts type ArticleExportEntry = { @@ -662,8 +669,8 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `PATCH /api/company/:id` | `company.update(id, { iso639, name, taxrate, reducedtaxrate })` | | `DELETE /api/company/:id` | `company.delete(id)` | | `PUT /api/auth/selected-company` | `google_auth.setSelectedCompany(googleAuthId, companyId)` | -| `GET /api/category/tree` | `category.getTreeForCompany(googleAuthId, companyId)` | -| `GET /api/category/export.zip` | `category.getTreeForCompany(googleAuthId, companyId)` + `pic.getImageByHash` | +| `GET /api/category/tree` | `category.getTreeForCompany(googleAuthId)` | +| `GET /api/category/export.zip` | `category.getTreeForCompany(googleAuthId)` + `pic.getImageByHash` | | `POST /api/category` | `category.createUnderParent({ googleAuthId, parentId, name })` | | `PATCH /api/category/:id` | `category.rename({ googleAuthId, id, name })` | | `PUT /api/category/order` | `category.order({ googleAuthId, parentId, categoryIds })` | @@ -671,64 +678,66 @@ res.set('Content-Type', thumb.contentType).send(thumb.buffer); | `DELETE /api/category/:id` | `category.delete({ googleAuthId, id })` | | `POST /api/category/:id/image` | `category.addImage(googleAuthId, categoryId, imageBuffer)` | | `DELETE /api/category/:id/image` | `category.deleteImage(googleAuthId, categoryId)` | -| `GET /api/unit` | `unit.list(googleAuthId, companyId)` | -| `PUT /api/unit` | `unit.upsert(googleAuthId, companyId, { si, basesi, basesifactor, symbol, name })` | -| `DELETE /api/unit` | `unit.delete(googleAuthId, companyId, si)` | -| `GET /api/company/:id/settings` | `companysetting.list(googleAuthId, companyId, { frontendOnly: true })` | -| `PUT /api/company/:id/settings` | `companysetting.set(googleAuthId, companyId, { key, value, frontend? })` | -| `DELETE /api/company/:id/settings/:key` | `companysetting.delete(googleAuthId, companyId, key)` | -| `GET /api/brand` | `brand.list(googleAuthId, companyId)` | -| `PUT /api/brand` | `brand.upsert(googleAuthId, companyId, { name, gsprpinfo? })` | -| `DELETE /api/brand/:id` | `brand.delete(googleAuthId, companyId, id)` | -| `GET /api/customergroup` | `customergroup.list(googleAuthId, companyId)` | -| `POST /api/customergroup` | `customergroup.create(googleAuthId, companyId, { name })` | -| `PATCH /api/customergroup/:id` | `customergroup.update(googleAuthId, companyId, id, { name })` | -| `PUT /api/customergroup/order` | `customergroup.order(googleAuthId, companyId, { customergroupIds })` | -| `DELETE /api/customergroup/:id` | `customergroup.delete(googleAuthId, companyId, id)` | -| `GET /api/entity` | `entity.list(googleAuthId, companyId, { customergroupId?, role? })` | -| `GET /api/entity/:id` | `entity.getById(googleAuthId, companyId, id)` | -| `POST /api/entity` | `entity.create(googleAuthId, companyId, { customergroupId, name, firstName? })` | -| `PATCH /api/entity/:id` | `entity.update(googleAuthId, companyId, id, { name?, firstName? })` | -| `DELETE /api/entity/:id` | `entity.delete(googleAuthId, companyId, id)` | -| `GET /api/entity/:entityId/address` | `entityaddress.list(googleAuthId, companyId, entityId)` | -| `POST /api/entity/:entityId/address` | `entityaddress.create(googleAuthId, companyId, entityId, { addressType, … })` | -| `PATCH /api/entity/:entityId/address/:id` | `entityaddress.update(googleAuthId, companyId, entityId, id, patch)` | -| `DELETE /api/entity/:entityId/address/:id` | `entityaddress.delete(googleAuthId, companyId, entityId, id)` | -| `GET /api/entity/:entityId/contact` | `entitycontact.list(googleAuthId, companyId, entityId)` | -| `POST /api/entity/:entityId/contact` | `entitycontact.create(googleAuthId, companyId, entityId, { contactType, contact })` | -| `PATCH /api/entity/:entityId/contact/:id` | `entitycontact.update(googleAuthId, companyId, entityId, id, patch)` | -| `DELETE /api/entity/:entityId/contact/:id` | `entitycontact.delete(googleAuthId, companyId, entityId, id)` | -| `GET /api/supplier` | `supplier.list(googleAuthId, companyId)` | -| `POST /api/supplier` | `supplier.create(googleAuthId, companyId, { name })` | -| `PATCH /api/supplier/:id` | `supplier.update(googleAuthId, companyId, id, { name })` | -| `DELETE /api/supplier/:id` | `supplier.delete(googleAuthId, companyId, id)` | -| `GET /api/delivery` | `delivery.list(googleAuthId, companyId)` | -| `GET /api/delivery/:id` | `delivery.getById(googleAuthId, companyId, id)` | -| `POST /api/delivery` | `delivery.create(googleAuthId, companyId, { number, date, supplierId?, lines })` | -| `PATCH /api/delivery/:id` | `delivery.update(googleAuthId, companyId, id, { number?, date?, supplierId?, lines? })` | -| `DELETE /api/delivery/:id` | `delivery.delete(googleAuthId, companyId, id)` | -| `GET /api/quote` | `quote.list(googleAuthId, companyId)` | -| `GET /api/quote/:id` | `quote.getById(googleAuthId, companyId, id)` | -| `POST /api/quote` | `quote.create(googleAuthId, companyId, { number, date, entityId?, lines })` | -| `PATCH /api/quote/:id` | `quote.update(googleAuthId, companyId, id, { number?, date?, entityId?, lines? })` | -| `DELETE /api/quote/:id` | `quote.delete(googleAuthId, companyId, id)` | -| `GET /api/order` | `order.list(googleAuthId, companyId)` | -| `GET /api/order/:id` | `order.getById(googleAuthId, companyId, id)` | -| `POST /api/order` | `order.create(googleAuthId, companyId, { number, date, entityId?, lines })` | -| `PATCH /api/order/:id` | `order.update(googleAuthId, companyId, id, { number?, date?, entityId?, lines? })` | -| `DELETE /api/order/:id` | `order.delete(googleAuthId, companyId, id)` | -| `GET /api/posterminal` | `posterminal.list(googleAuthId, companyId)` | -| `PUT /api/posterminal` | `posterminal.upsert(googleAuthId, companyId, { id?, name, token?, pairingpin })` | -| `DELETE /api/posterminal/:id` | `posterminal.delete(googleAuthId, companyId, id)` | -| `PUT /api/posterminal/:id/category/:categoryId` | `posterminal.setOn(googleAuthId, companyId, id, categoryId)` | -| `DELETE /api/posterminal/:id/category/:categoryId` | `posterminal.setOff(googleAuthId, companyId, id, categoryId)` | -| `PUT /api/posterminal/:id/categories` | `posterminal.setOnAll(googleAuthId, companyId, id)` | -| `DELETE /api/posterminal/:id/categories` | `posterminal.setOffAll(googleAuthId, companyId, id)` | +| `GET /api/unit` | `unit.list(googleAuthId)` | +| `PUT /api/unit` | `unit.upsert(googleAuthId, { si, basesi, basesifactor, symbol, name })` | +| `DELETE /api/unit` | `unit.delete(googleAuthId, si)` | +| `GET /api/company/:id/settings` | `companysetting.list(googleAuthId, { frontendOnly: true })` | +| `PUT /api/company/:id/settings` | `companysetting.set(googleAuthId, { key, value, frontend? })` | +| `DELETE /api/company/:id/settings/:key` | `companysetting.delete(googleAuthId, key)` | +| `GET /api/brand` | `brand.list(googleAuthId)` | +| `PUT /api/brand` | `brand.upsert(googleAuthId, { name, gsprpinfo? })` | +| `DELETE /api/brand/:id` | `brand.delete(googleAuthId, id)` | +| `GET /api/customergroup` | `customergroup.list(googleAuthId)` | +| `POST /api/customergroup` | `customergroup.create(googleAuthId, { name })` | +| `PATCH /api/customergroup/:id` | `customergroup.update(googleAuthId, id, { name })` | +| `PUT /api/customergroup/order` | `customergroup.order(googleAuthId, { customergroupIds })` | +| `DELETE /api/customergroup/:id` | `customergroup.delete(googleAuthId, id)` | +| `GET /api/entity` | `entity.list(googleAuthId, { customergroupId?, role? })` | +| `GET /api/entity/next-number` | `entity.peekNextNumber(googleAuthId)` | +| `GET /api/entity/number-free` | `entity.isNumberFree(googleAuthId, number, { excludeEntityId? })` | +| `GET /api/entity/:id` | `entity.getById(googleAuthId, id)` | +| `POST /api/entity` | `entity.create(googleAuthId, { customergroupId, name, firstName?, number? })` | +| `PATCH /api/entity/:id` | `entity.update(googleAuthId, id, { name?, firstName?, number? })` | +| `DELETE /api/entity/:id` | `entity.delete(googleAuthId, id)` | +| `GET /api/entity/:entityId/address` | `entityaddress.list(googleAuthId, entityId)` | +| `POST /api/entity/:entityId/address` | `entityaddress.create(googleAuthId, entityId, { addressType, … })` | +| `PATCH /api/entity/:entityId/address/:id` | `entityaddress.update(googleAuthId, entityId, id, patch)` | +| `DELETE /api/entity/:entityId/address/:id` | `entityaddress.delete(googleAuthId, entityId, id)` | +| `GET /api/entity/:entityId/contact` | `entitycontact.list(googleAuthId, entityId)` | +| `POST /api/entity/:entityId/contact` | `entitycontact.create(googleAuthId, entityId, { contactType, contact })` | +| `PATCH /api/entity/:entityId/contact/:id` | `entitycontact.update(googleAuthId, entityId, id, patch)` | +| `DELETE /api/entity/:entityId/contact/:id` | `entitycontact.delete(googleAuthId, entityId, id)` | +| `GET /api/supplier` | `supplier.list(googleAuthId)` | +| `POST /api/supplier` | `supplier.create(googleAuthId, { name })` | +| `PATCH /api/supplier/:id` | `supplier.update(googleAuthId, id, { name })` | +| `DELETE /api/supplier/:id` | `supplier.delete(googleAuthId, id)` | +| `GET /api/delivery` | `delivery.list(googleAuthId)` | +| `GET /api/delivery/:id` | `delivery.getById(googleAuthId, id)` | +| `POST /api/delivery` | `delivery.create(googleAuthId, { number, date, supplierId?, lines })` | +| `PATCH /api/delivery/:id` | `delivery.update(googleAuthId, id, { number?, date?, supplierId?, lines? })` | +| `DELETE /api/delivery/:id` | `delivery.delete(googleAuthId, id)` | +| `GET /api/quote` | `quote.list(googleAuthId)` | +| `GET /api/quote/:id` | `quote.getById(googleAuthId, id)` | +| `POST /api/quote` | `quote.create(googleAuthId, { number, date, entityId?, lines })` | +| `PATCH /api/quote/:id` | `quote.update(googleAuthId, id, { number?, date?, entityId?, lines? })` | +| `DELETE /api/quote/:id` | `quote.delete(googleAuthId, id)` | +| `GET /api/order` | `order.list(googleAuthId)` | +| `GET /api/order/:id` | `order.getById(googleAuthId, id)` | +| `POST /api/order` | `order.create(googleAuthId, { number, date, entityId?, lines })` | +| `PATCH /api/order/:id` | `order.update(googleAuthId, id, { number?, date?, entityId?, lines? })` | +| `DELETE /api/order/:id` | `order.delete(googleAuthId, id)` | +| `GET /api/posterminal` | `posterminal.list(googleAuthId)` | +| `PUT /api/posterminal` | `posterminal.upsert(googleAuthId, { id?, name, token?, pairingpin })` | +| `DELETE /api/posterminal/:id` | `posterminal.delete(googleAuthId, id)` | +| `PUT /api/posterminal/:id/category/:categoryId` | `posterminal.setOn(googleAuthId, id, categoryId)` | +| `DELETE /api/posterminal/:id/category/:categoryId` | `posterminal.setOff(googleAuthId, id, categoryId)` | +| `PUT /api/posterminal/:id/categories` | `posterminal.setOnAll(googleAuthId, id)` | +| `DELETE /api/posterminal/:id/categories` | `posterminal.setOffAll(googleAuthId, id)` | | `POST /api/article` | `article.createUnderParent({ googleAuthId, parentId, name })` | | `GET /api/article/:sku` | `article.getBySku(googleAuthId, sku)` | | `GET /api/article/:sku/history` | `article.getHistoryBySku(googleAuthId, sku)` | -| `GET /api/articleIndex` | `article.listSearchIndex(googleAuthId, companyId)` | -| `GET /api/article/export.zip` | `article.listExport(googleAuthId, companyId, categoryId?)` | +| `GET /api/articleIndex` | `article.listSearchIndex(googleAuthId)` | +| `GET /api/article/export.zip` | `article.listExport(googleAuthId, categoryId?)` | | `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name?, unitamount?, unitSi? })` | | `PUT /api/article/:sku/prices` | `article.setPricesBySku(googleAuthId, sku, [{ price, tax, minmultiplier? }])` | | `PUT /api/article/:sku/eans` | `article.setEansBySku(googleAuthId, sku, string[])` | diff --git a/generate-api-doc.js b/generate-api-doc.js index 1c4f0fa..e91a706 100644 --- a/generate-api-doc.js +++ b/generate-api-doc.js @@ -44,6 +44,7 @@ const INTERNAL_FILES = new Set([ 'ensureDefault.js', 'ensureLieferantenGroup.js', 'resolveCompanyAccess.js', + 'resolveSelectedCompanyAccess.js', 'resolveCategoryAccess.js', 'resolveCustomerGroupAccess.js', 'resolveEntityAccess.js', @@ -53,6 +54,9 @@ const INTERNAL_FILES = new Set([ 'queryCategoryArticles.js', 'getCategoryPosterminalIds.js', 'nextSku.js', + 'nextNumber.js', + 'peekNumber.js', + 'shape.js', 'createStore.js', 'index.js', ]); diff --git a/schema-graph.dot b/schema-graph.dot index 782f96f..7a68f3a 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -13,7 +13,7 @@ digraph schema { meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; entityaddress [label=<
entityaddress
idINT PK
entity_idINT FK
first_name?NVARCHAR(255)
name?NVARCHAR(255)
street?NVARCHAR(255)
streetnumber?NVARCHAR(50)
zip?NVARCHAR(50)
city?NVARCHAR(255)
county?NVARCHAR(255)
addresstypeNVARCHAR(50)
sortINT
>]; - entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
>]; + entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
number?NVARCHAR(100)
created_atDATETIME2
updated_atDATETIME2
>]; companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; entitycontact [label=<
entitycontact
idINT PK
entity_idINT FK
contacttypeNVARCHAR(50)
contactNVARCHAR(255)
sortINT
>]; company [label=<
company
idINT PK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
poscustomergroup_idINT FK
root_category_id?INT FK
default_language_id?INT FK
nameNVARCHAR(255)
>]; diff --git a/schema-graph.svg b/schema-graph.svg index e60985c..6718ae4 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -251,46 +251,50 @@ entity - - - - - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name -NVARCHAR(255) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name +NVARCHAR(255) + + +number? +NVARCHAR(100) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 entityaddress:sw->entity:n - - + + @@ -298,8 +302,8 @@ entity:nw->customergroup:n - - + + @@ -417,8 +421,8 @@ entitycontact:se->entity:n - - + + @@ -614,8 +618,8 @@ entityhistory:sw->entity:n - - + + @@ -1627,8 +1631,8 @@ manifest:e->entity:n - - + + diff --git a/scripts/test-api.js b/scripts/test-api.js index 3c7996f..f2f6024 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -99,17 +99,17 @@ const MENU = { { value: 'unit.list', label: 'list', - hint: 'googleAuthId, companyId → rows', + hint: 'googleAuthId → rows', }, { value: 'unit.upsert', label: 'upsert', - hint: 'googleAuthId, companyId, JSON unit → row', + hint: 'googleAuthId, JSON unit → row', }, { value: 'unit.delete', label: 'delete', - hint: 'googleAuthId, companyId, si', + hint: 'googleAuthId, si', }, ], }, @@ -119,22 +119,22 @@ const MENU = { { value: 'companysetting.list', label: 'list', - hint: 'googleAuthId, companyId, frontendOnly? → rows', + hint: 'googleAuthId, frontendOnly? → rows', }, { value: 'companysetting.get', label: 'get', - hint: 'googleAuthId, companyId, key → row', + hint: 'googleAuthId, key → row', }, { value: 'companysetting.set', label: 'set', - hint: 'googleAuthId, companyId, JSON setting → row', + hint: 'googleAuthId, JSON setting → row', }, { value: 'companysetting.delete', label: 'delete', - hint: 'googleAuthId, companyId, key', + hint: 'googleAuthId, key', }, ], }, @@ -144,56 +144,58 @@ const MENU = { { value: 'brand.list', label: 'list', - hint: 'googleAuthId, companyId → rows', + hint: 'googleAuthId → rows', }, { value: 'brand.upsert', label: 'upsert', - hint: 'googleAuthId, companyId, JSON brand → row', + hint: 'googleAuthId, JSON brand → row', }, { value: 'brand.delete', label: 'delete', - hint: 'googleAuthId, companyId, id', + hint: 'googleAuthId, id', }, ], }, customergroup: { label: 'customergroup', items: [ - { value: 'customergroup.list', label: 'list', hint: 'googleAuthId, companyId → rows' }, - { value: 'customergroup.create', label: 'create', hint: 'googleAuthId, companyId, name → row' }, - { value: 'customergroup.update', label: 'update', hint: 'googleAuthId, companyId, id, name → row' }, - { value: 'customergroup.order', label: 'order', hint: 'googleAuthId, companyId, JSON customergroupIds → { customergroupIds }' }, - { value: 'customergroup.delete', label: 'delete', hint: 'googleAuthId, companyId, id' }, + { value: 'customergroup.list', label: 'list', hint: 'googleAuthId → rows' }, + { value: 'customergroup.create', label: 'create', hint: 'googleAuthId, name → row' }, + { value: 'customergroup.update', label: 'update', hint: 'googleAuthId, id, name → row' }, + { value: 'customergroup.order', label: 'order', hint: 'googleAuthId, JSON customergroupIds → { customergroupIds }' }, + { value: 'customergroup.delete', label: 'delete', hint: 'googleAuthId, id' }, ], }, entity: { label: 'entity', items: [ - { value: 'entity.list', label: 'list', hint: 'googleAuthId, companyId, customergroupId?|role=customer → rows' }, - { value: 'entity.getById', label: 'getById', hint: 'googleAuthId, companyId, id → row' }, - { value: 'entity.create', label: 'create', hint: 'googleAuthId, companyId, JSON → row' }, - { value: 'entity.update', label: 'update', hint: 'googleAuthId, companyId, id, JSON → row' }, - { value: 'entity.delete', label: 'delete', hint: 'googleAuthId, companyId, id' }, + { value: 'entity.list', label: 'list', hint: 'googleAuthId, customergroupId?|role=customer → rows' }, + { value: 'entity.getById', label: 'getById', hint: 'googleAuthId, id → row' }, + { value: 'entity.create', label: 'create', hint: 'googleAuthId, JSON → row' }, + { value: 'entity.update', label: 'update', hint: 'googleAuthId, id, JSON → row' }, + { value: 'entity.delete', label: 'delete', hint: 'googleAuthId, id' }, + { value: 'entity.peekNextNumber', label: 'peekNextNumber', hint: 'googleAuthId → next number' }, + { value: 'entity.isNumberFree', label: 'isNumberFree', hint: 'googleAuthId, number, excludeEntityId? → bool' }, ], }, entityaddress: { label: 'entityaddress', items: [ - { value: 'entityaddress.list', label: 'list', hint: 'googleAuthId, companyId, entityId → rows' }, - { value: 'entityaddress.create', label: 'create', hint: 'googleAuthId, companyId, entityId, JSON → row' }, - { value: 'entityaddress.update', label: 'update', hint: 'googleAuthId, companyId, entityId, id, JSON → row' }, - { value: 'entityaddress.delete', label: 'delete', hint: 'googleAuthId, companyId, entityId, id' }, + { value: 'entityaddress.list', label: 'list', hint: 'googleAuthId, entityId → rows' }, + { value: 'entityaddress.create', label: 'create', hint: 'googleAuthId, entityId, JSON → row' }, + { value: 'entityaddress.update', label: 'update', hint: 'googleAuthId, entityId, id, JSON → row' }, + { value: 'entityaddress.delete', label: 'delete', hint: 'googleAuthId, entityId, id' }, ], }, entitycontact: { label: 'entitycontact', items: [ - { value: 'entitycontact.list', label: 'list', hint: 'googleAuthId, companyId, entityId → rows' }, - { value: 'entitycontact.create', label: 'create', hint: 'googleAuthId, companyId, entityId, JSON → row' }, - { value: 'entitycontact.update', label: 'update', hint: 'googleAuthId, companyId, entityId, id, JSON → row' }, - { value: 'entitycontact.delete', label: 'delete', hint: 'googleAuthId, companyId, entityId, id' }, + { value: 'entitycontact.list', label: 'list', hint: 'googleAuthId, entityId → rows' }, + { value: 'entitycontact.create', label: 'create', hint: 'googleAuthId, entityId, JSON → row' }, + { value: 'entitycontact.update', label: 'update', hint: 'googleAuthId, entityId, id, JSON → row' }, + { value: 'entitycontact.delete', label: 'delete', hint: 'googleAuthId, entityId, id' }, ], }, supplier: { @@ -202,22 +204,22 @@ const MENU = { { value: 'supplier.list', label: 'list', - hint: 'googleAuthId, companyId → rows', + hint: 'googleAuthId → rows', }, { value: 'supplier.create', label: 'create', - hint: 'googleAuthId, companyId, name → row', + hint: 'googleAuthId, name → row', }, { value: 'supplier.update', label: 'update', - hint: 'googleAuthId, companyId, id, name → row', + hint: 'googleAuthId, id, name → row', }, { value: 'supplier.delete', label: 'delete', - hint: 'googleAuthId, companyId, id', + hint: 'googleAuthId, id', }, ], }, @@ -227,27 +229,27 @@ const MENU = { { value: 'delivery.list', label: 'list', - hint: 'googleAuthId, companyId → summaries', + hint: 'googleAuthId → summaries', }, { value: 'delivery.getById', label: 'getById', - hint: 'googleAuthId, companyId, id → delivery', + hint: 'googleAuthId, id → delivery', }, { value: 'delivery.create', label: 'create', - hint: 'googleAuthId, companyId, JSON body → delivery', + hint: 'googleAuthId, JSON body → delivery', }, { value: 'delivery.update', label: 'update', - hint: 'googleAuthId, companyId, id, JSON patch → delivery', + hint: 'googleAuthId, id, JSON patch → delivery', }, { value: 'delivery.delete', label: 'delete', - hint: 'googleAuthId, companyId, id', + hint: 'googleAuthId, id', }, ], }, @@ -257,27 +259,27 @@ const MENU = { { value: 'quote.list', label: 'list', - hint: 'googleAuthId, companyId → summaries', + hint: 'googleAuthId → summaries', }, { value: 'quote.getById', label: 'getById', - hint: 'googleAuthId, companyId, id → quote', + hint: 'googleAuthId, id → quote', }, { value: 'quote.create', label: 'create', - hint: 'googleAuthId, companyId, JSON body → quote', + hint: 'googleAuthId, JSON body → quote', }, { value: 'quote.update', label: 'update', - hint: 'googleAuthId, companyId, id, JSON patch → quote', + hint: 'googleAuthId, id, JSON patch → quote', }, { value: 'quote.delete', label: 'delete', - hint: 'googleAuthId, companyId, id', + hint: 'googleAuthId, id', }, ], }, @@ -287,27 +289,27 @@ const MENU = { { value: 'order.list', label: 'list', - hint: 'googleAuthId, companyId → summaries', + hint: 'googleAuthId → summaries', }, { value: 'order.getById', label: 'getById', - hint: 'googleAuthId, companyId, id → order', + hint: 'googleAuthId, id → order', }, { value: 'order.create', label: 'create', - hint: 'googleAuthId, companyId, JSON body → order', + hint: 'googleAuthId, JSON body → order', }, { value: 'order.update', label: 'update', - hint: 'googleAuthId, companyId, id, JSON patch → order', + hint: 'googleAuthId, id, JSON patch → order', }, { value: 'order.delete', label: 'delete', - hint: 'googleAuthId, companyId, id', + hint: 'googleAuthId, id', }, ], }, @@ -319,7 +321,7 @@ const MENU = { { value: 'category.move', label: 'move', hint: 'googleAuthId, id, parentId → row' }, { value: 'category.order', label: 'order', hint: 'googleAuthId, parentId, JSON categoryIds' }, { value: 'category.delete', label: 'delete', hint: 'googleAuthId, id' }, - { value: 'category.getTreeForCompany', label: 'getTreeForCompany', hint: 'googleAuthId, companyId → rows' }, + { value: 'category.getTreeForCompany', label: 'getTreeForCompany', hint: 'googleAuthId → rows' }, { value: 'category.getCategoryArticles', label: 'getCategoryArticles', hint: 'googleAuthId, categoryId → rows' }, ], }, @@ -597,18 +599,14 @@ async function runMethod(client, action) { 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)), + await client.unit.list(Number(googleAuthId)), ]; } 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"}', @@ -629,7 +627,6 @@ async function runMethod(client, action) { 'result', await client.unit.upsert( Number(googleAuthId), - Number(companyId), JSON.parse(unitJson), ), ]; @@ -637,27 +634,23 @@ async function runMethod(client, action) { 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); + await client.unit.delete(Number(googleAuthId), si); return ['ok', true]; } case 'companysetting.list': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const frontendOnly = await askText('frontendOnly (true/false)', { placeholder: 'false', }); if (frontendOnly == null) return; return [ 'result[]', - await client.companysetting.list(Number(googleAuthId), Number(companyId), { + await client.companysetting.list(Number(googleAuthId), { frontendOnly: frontendOnly.trim().toLowerCase() === 'true', }), ]; @@ -665,8 +658,6 @@ async function runMethod(client, action) { case 'companysetting.get': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const key = await askText('key', { validate: (v) => (!v?.trim() ? 'Required' : undefined), }); @@ -679,7 +670,6 @@ async function runMethod(client, action) { 'result', await client.companysetting.get( Number(googleAuthId), - Number(companyId), key, { frontendOnly: frontendOnly.trim().toLowerCase() === 'true' }, ), @@ -688,8 +678,6 @@ async function runMethod(client, action) { case 'companysetting.set': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const settingJson = await askText('setting JSON', { placeholder: '{"key":"theme","value":"dark","frontend":true}', validate: (v) => { @@ -709,7 +697,6 @@ async function runMethod(client, action) { 'result', await client.companysetting.set( Number(googleAuthId), - Number(companyId), JSON.parse(settingJson), ), ]; @@ -717,15 +704,12 @@ async function runMethod(client, action) { case 'companysetting.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const key = await askText('key', { validate: (v) => (!v?.trim() ? 'Required' : undefined), }); if (key == null) return; await client.companysetting.delete( Number(googleAuthId), - Number(companyId), key, ); return ['ok', true]; @@ -733,18 +717,14 @@ async function runMethod(client, action) { case 'brand.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.brand.list(Number(googleAuthId), Number(companyId)), + await client.brand.list(Number(googleAuthId)), ]; } case 'brand.upsert': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const brandJson = await askText('brand JSON', { placeholder: '{"name":"Nike","gsprpinfo":"..."}', validate: (v) => { @@ -764,7 +744,6 @@ async function runMethod(client, action) { 'result', await client.brand.upsert( Number(googleAuthId), - Number(companyId), JSON.parse(brandJson), ), ]; @@ -772,28 +751,22 @@ async function runMethod(client, action) { case 'brand.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('brand id'); if (id == null) return; - await client.brand.delete(Number(googleAuthId), Number(companyId), id); + await client.brand.delete(Number(googleAuthId), id); return ['ok', true]; } case 'customergroup.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.customergroup.list(Number(googleAuthId), Number(companyId)), + await client.customergroup.list(Number(googleAuthId)), ]; } case 'customergroup.create': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const name = await askText('name', { validate: (v) => (!v?.trim() ? 'Required' : undefined), }); @@ -802,7 +775,6 @@ async function runMethod(client, action) { 'result', await client.customergroup.create( Number(googleAuthId), - Number(companyId), { name }, ), ]; @@ -810,8 +782,6 @@ async function runMethod(client, action) { case 'customergroup.update': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('customergroup id'); if (id == null) return; const name = await askText('name', { @@ -822,7 +792,6 @@ async function runMethod(client, action) { 'result', await client.customergroup.update( Number(googleAuthId), - Number(companyId), Number(id), { name }, ), @@ -831,8 +800,6 @@ async function runMethod(client, action) { case 'customergroup.order': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const idsJson = await askText('customergroupIds JSON', { placeholder: '[3,1,2]', validate: (v) => { @@ -850,7 +817,6 @@ async function runMethod(client, action) { 'result', await client.customergroup.order( Number(googleAuthId), - Number(companyId), { customergroupIds: JSON.parse(idsJson) }, ), ]; @@ -858,13 +824,10 @@ async function runMethod(client, action) { case 'customergroup.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('customergroup id'); if (id == null) return; await client.customergroup.delete( Number(googleAuthId), - Number(companyId), Number(id), ); return ['ok', true]; @@ -872,8 +835,6 @@ async function runMethod(client, action) { case 'entity.list': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const roleRaw = await askText('role (customer|empty)', { placeholder: '', validate: (v) => { @@ -900,7 +861,7 @@ async function runMethod(client, action) { } return [ 'result[]', - await client.entity.list(Number(googleAuthId), Number(companyId), { + await client.entity.list(Number(googleAuthId), { customergroupId, role, }), @@ -909,15 +870,12 @@ async function runMethod(client, action) { case 'entity.getById': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('entity id'); if (id == null) return; return [ 'result', await client.entity.getById( Number(googleAuthId), - Number(companyId), Number(id), ), ]; @@ -925,10 +883,8 @@ async function runMethod(client, action) { case 'entity.create': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const bodyJson = await askText('entity JSON', { - placeholder: '{"customergroupId":1,"name":"Acme","firstName":null}', + placeholder: '{"customergroupId":1,"name":"Acme","firstName":null,"number":null}', validate: (v) => { if (!v?.trim()) return 'Required'; try { @@ -943,7 +899,6 @@ async function runMethod(client, action) { 'result', await client.entity.create( Number(googleAuthId), - Number(companyId), JSON.parse(bodyJson), ), ]; @@ -951,12 +906,10 @@ async function runMethod(client, action) { case 'entity.update': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('entity id'); if (id == null) return; const patchJson = await askText('patch JSON', { - placeholder: '{"name":"Acme","firstName":"Ada"}', + placeholder: '{"name":"Acme","firstName":"Ada","number":"C-1"}', validate: (v) => { if (!v?.trim()) return 'Required'; try { @@ -971,7 +924,6 @@ async function runMethod(client, action) { 'result', await client.entity.update( Number(googleAuthId), - Number(companyId), Number(id), JSON.parse(patchJson), ), @@ -980,29 +932,61 @@ async function runMethod(client, action) { case 'entity.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('entity id'); if (id == null) return; await client.entity.delete( Number(googleAuthId), - Number(companyId), Number(id), ); return ['ok', true]; } + case 'entity.peekNextNumber': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + return [ + 'result', + await client.entity.peekNextNumber( + Number(googleAuthId), + ), + ]; + } + case 'entity.isNumberFree': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const number = await askText('number', { + validate: (v) => (v?.trim() ? undefined : 'Required'), + }); + if (number == null) return; + const excludeRaw = await askText('exclude entity id (optional)', { + placeholder: 'leave empty', + validate: (v) => { + const t = v?.trim() ?? ''; + if (!t) return undefined; + return /^\d+$/.test(t) ? undefined : 'Integer or empty'; + }, + }); + if (excludeRaw == null) return; + const excludeEntityId = excludeRaw.trim() + ? Number(excludeRaw.trim()) + : null; + return [ + 'result', + await client.entity.isNumberFree( + Number(googleAuthId), + number.trim(), + excludeEntityId == null ? {} : { excludeEntityId }, + ), + ]; + } case 'entityaddress.list': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const entityId = await askInt('entity id'); if (entityId == null) return; return [ 'result[]', await client.entityaddress.list( Number(googleAuthId), - Number(companyId), Number(entityId), ), ]; @@ -1010,8 +994,6 @@ async function runMethod(client, action) { case 'entityaddress.create': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const entityId = await askInt('entity id'); if (entityId == null) return; const bodyJson = await askText('address JSON', { @@ -1031,7 +1013,6 @@ async function runMethod(client, action) { 'result', await client.entityaddress.create( Number(googleAuthId), - Number(companyId), Number(entityId), JSON.parse(bodyJson), ), @@ -1040,8 +1021,6 @@ async function runMethod(client, action) { case 'entityaddress.update': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const entityId = await askInt('entity id'); if (entityId == null) return; const id = await askInt('address id'); @@ -1062,7 +1041,6 @@ async function runMethod(client, action) { 'result', await client.entityaddress.update( Number(googleAuthId), - Number(companyId), Number(entityId), Number(id), JSON.parse(patchJson), @@ -1072,15 +1050,12 @@ async function runMethod(client, action) { case 'entityaddress.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const entityId = await askInt('entity id'); if (entityId == null) return; const id = await askInt('address id'); if (id == null) return; await client.entityaddress.delete( Number(googleAuthId), - Number(companyId), Number(entityId), Number(id), ); @@ -1089,15 +1064,12 @@ async function runMethod(client, action) { case 'entitycontact.list': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const entityId = await askInt('entity id'); if (entityId == null) return; return [ 'result[]', await client.entitycontact.list( Number(googleAuthId), - Number(companyId), Number(entityId), ), ]; @@ -1105,8 +1077,6 @@ async function runMethod(client, action) { case 'entitycontact.create': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const entityId = await askInt('entity id'); if (entityId == null) return; const bodyJson = await askText('contact JSON', { @@ -1125,7 +1095,6 @@ async function runMethod(client, action) { 'result', await client.entitycontact.create( Number(googleAuthId), - Number(companyId), Number(entityId), JSON.parse(bodyJson), ), @@ -1134,8 +1103,6 @@ async function runMethod(client, action) { case 'entitycontact.update': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const entityId = await askInt('entity id'); if (entityId == null) return; const id = await askInt('contact id'); @@ -1156,7 +1123,6 @@ async function runMethod(client, action) { 'result', await client.entitycontact.update( Number(googleAuthId), - Number(companyId), Number(entityId), Number(id), JSON.parse(patchJson), @@ -1166,15 +1132,12 @@ async function runMethod(client, action) { case 'entitycontact.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const entityId = await askInt('entity id'); if (entityId == null) return; const id = await askInt('contact id'); if (id == null) return; await client.entitycontact.delete( Number(googleAuthId), - Number(companyId), Number(entityId), Number(id), ); @@ -1183,25 +1146,21 @@ async function runMethod(client, action) { case 'supplier.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.supplier.list(Number(googleAuthId), Number(companyId)), + await client.supplier.list(Number(googleAuthId)), ]; } case 'supplier.create': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const name = await askText('name', { validate: (v) => (!v?.trim() ? 'Required' : undefined), }); if (name == null) return; return [ 'result', - await client.supplier.create(Number(googleAuthId), Number(companyId), { + await client.supplier.create(Number(googleAuthId), { name, }), ]; @@ -1209,8 +1168,6 @@ async function runMethod(client, action) { case 'supplier.update': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('supplier id'); if (id == null) return; const name = await askText('name', { @@ -1221,7 +1178,6 @@ async function runMethod(client, action) { 'result', await client.supplier.update( Number(googleAuthId), - Number(companyId), Number(id), { name }, ), @@ -1230,13 +1186,10 @@ async function runMethod(client, action) { case 'supplier.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('supplier id'); if (id == null) return; await client.supplier.delete( Number(googleAuthId), - Number(companyId), Number(id), ); return ['ok', true]; @@ -1244,25 +1197,20 @@ async function runMethod(client, action) { case 'delivery.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.delivery.list(Number(googleAuthId), Number(companyId)), + await client.delivery.list(Number(googleAuthId)), ]; } case 'delivery.getById': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('delivery id'); if (id == null) return; return [ 'result', await client.delivery.getById( Number(googleAuthId), - Number(companyId), Number(id), ), ]; @@ -1270,8 +1218,6 @@ async function runMethod(client, action) { case 'delivery.create': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const bodyJson = await askText('delivery JSON', { placeholder: '{"number":"DHL-123","date":"2026-08-10","supplierId":1,"lines":[{"sku":"1","amount":10,"net":99.5,"tax":19}]}', @@ -1292,7 +1238,6 @@ async function runMethod(client, action) { 'result', await client.delivery.create( Number(googleAuthId), - Number(companyId), JSON.parse(bodyJson), ), ]; @@ -1300,8 +1245,6 @@ async function runMethod(client, action) { case 'delivery.update': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('delivery id'); if (id == null) return; const patchJson = await askText('patch JSON', { @@ -1323,7 +1266,6 @@ async function runMethod(client, action) { 'result', await client.delivery.update( Number(googleAuthId), - Number(companyId), Number(id), JSON.parse(patchJson), ), @@ -1332,13 +1274,10 @@ async function runMethod(client, action) { case 'delivery.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('delivery id'); if (id == null) return; await client.delivery.delete( Number(googleAuthId), - Number(companyId), Number(id), ); return ['ok', true]; @@ -1346,25 +1285,20 @@ async function runMethod(client, action) { case 'quote.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.quote.list(Number(googleAuthId), Number(companyId)), + await client.quote.list(Number(googleAuthId)), ]; } case 'quote.getById': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('quote id'); if (id == null) return; return [ 'result', await client.quote.getById( Number(googleAuthId), - Number(companyId), Number(id), ), ]; @@ -1372,8 +1306,6 @@ async function runMethod(client, action) { case 'quote.create': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const bodyJson = await askText('quote JSON', { placeholder: '{"number":"A-2026-001","date":"2026-09-01","entityId":12,"lines":[{"sku":"1","amount":10,"net":99.5,"tax":19}]}', @@ -1394,7 +1326,6 @@ async function runMethod(client, action) { 'result', await client.quote.create( Number(googleAuthId), - Number(companyId), JSON.parse(bodyJson), ), ]; @@ -1402,8 +1333,6 @@ async function runMethod(client, action) { case 'quote.update': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('quote id'); if (id == null) return; const patchJson = await askText('patch JSON', { @@ -1425,7 +1354,6 @@ async function runMethod(client, action) { 'result', await client.quote.update( Number(googleAuthId), - Number(companyId), Number(id), JSON.parse(patchJson), ), @@ -1434,13 +1362,10 @@ async function runMethod(client, action) { case 'quote.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('quote id'); if (id == null) return; await client.quote.delete( Number(googleAuthId), - Number(companyId), Number(id), ); return ['ok', true]; @@ -1448,25 +1373,20 @@ async function runMethod(client, action) { case 'order.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.order.list(Number(googleAuthId), Number(companyId)), + await client.order.list(Number(googleAuthId)), ]; } case 'order.getById': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('order id'); if (id == null) return; return [ 'result', await client.order.getById( Number(googleAuthId), - Number(companyId), Number(id), ), ]; @@ -1474,8 +1394,6 @@ async function runMethod(client, action) { case 'order.create': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const bodyJson = await askText('order JSON', { placeholder: '{"number":"B-2026-001","date":"2026-09-15","entityId":12,"lines":[{"sku":"1","amount":10,"net":99.5,"tax":19}]}', @@ -1496,7 +1414,6 @@ async function runMethod(client, action) { 'result', await client.order.create( Number(googleAuthId), - Number(companyId), JSON.parse(bodyJson), ), ]; @@ -1504,8 +1421,6 @@ async function runMethod(client, action) { case 'order.update': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('order id'); if (id == null) return; const patchJson = await askText('patch JSON', { @@ -1527,7 +1442,6 @@ async function runMethod(client, action) { 'result', await client.order.update( Number(googleAuthId), - Number(companyId), Number(id), JSON.parse(patchJson), ), @@ -1536,13 +1450,10 @@ async function runMethod(client, action) { case 'order.delete': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; const id = await askInt('order id'); if (id == null) return; await client.order.delete( Number(googleAuthId), - Number(companyId), Number(id), ); return ['ok', true]; @@ -1657,11 +1568,9 @@ async function runMethod(client, action) { case 'category.getTreeForCompany': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; - const companyId = await askInt('company id'); - if (companyId == null) return; return [ 'rows', - await client.category.getTreeForCompany(Number(googleAuthId), Number(companyId)), + await client.category.getTreeForCompany(Number(googleAuthId)), ]; } case 'category.getCategoryArticles': { diff --git a/src/index.js b/src/index.js index 22b3e37..bf75f38 100644 --- a/src/index.js +++ b/src/index.js @@ -49,6 +49,8 @@ import entityGetById from './store/entity/getById.js'; import entityCreate from './store/entity/create.js'; import entityUpdate from './store/entity/update.js'; import entityDelete from './store/entity/delete.js'; +import entityPeekNextNumber from './store/entity/peekNextNumber.js'; +import entityIsNumberFree from './store/entity/isNumberFree.js'; import entityaddressList from './store/entityaddress/list.js'; import entityaddressCreate from './store/entityaddress/create.js'; import entityaddressUpdate from './store/entityaddress/update.js'; @@ -192,6 +194,8 @@ export default class LibDashStore { create: entityCreate(pool), update: entityUpdate(pool), delete: entityDelete(pool), + peekNextNumber: entityPeekNextNumber(pool), + isNumberFree: entityIsNumberFree(pool), }; this.entityaddress = { diff --git a/src/schema.js b/src/schema.js index 78519ea..f7a9367 100644 --- a/src/schema.js +++ b/src/schema.js @@ -158,6 +158,7 @@ BEGIN customergroup_id INT NOT NULL, first_name NVARCHAR(255) NULL, name NVARCHAR(255) NOT NULL, + [number] NVARCHAR(100) NULL, created_at DATETIME2 NOT NULL CONSTRAINT DF_entity_created_at DEFAULT SYSUTCDATETIME(), updated_at DATETIME2 NOT NULL CONSTRAINT DF_entity_updated_at DEFAULT SYSUTCDATETIME(), CONSTRAINT FK_entity_customergroup FOREIGN KEY (customergroup_id) REFERENCES customergroup(id) ON DELETE CASCADE @@ -165,6 +166,35 @@ BEGIN END `; +/** Unique (company_id, number) for non-null entity numbers via customergroup. */ +export const INIT_ENTITY_COMPANY_NUMBER_UQ = ` +IF OBJECT_ID(N'dbo.v_entity_company_number', N'V') IS NULL +BEGIN + EXEC(' + CREATE VIEW dbo.v_entity_company_number + WITH SCHEMABINDING + AS + SELECT + e.id AS entity_id, + cg.company_id, + e.[number] + FROM dbo.entity AS e + INNER JOIN dbo.customergroup AS cg ON cg.id = e.customergroup_id + WHERE e.[number] IS NOT NULL + '); +END + +IF NOT EXISTS ( + SELECT 1 FROM sys.indexes + WHERE name = N'UQ_entity_company_number' + AND object_id = OBJECT_ID(N'dbo.v_entity_company_number') +) +BEGIN + CREATE UNIQUE CLUSTERED INDEX UQ_entity_company_number + ON dbo.v_entity_company_number (company_id, [number]); +END +`; + export const INIT_ENTITYADDRESS = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'entityaddress') BEGIN @@ -601,6 +631,43 @@ END '); `; +// EXEC dbo.peeknextnumberwithprefixandpadding +// @company_id = 1, @type = N'entity', @value = @n OUTPUT; +export const INIT_PEEK_NEXT_NUMBER_WITH_PREFIX_AND_PADDING = ` +IF OBJECT_ID(N'dbo.peeknextnumberwithprefixandpadding', N'P') IS NOT NULL + DROP PROCEDURE dbo.peeknextnumberwithprefixandpadding; + +EXEC(' +CREATE PROCEDURE dbo.peeknextnumberwithprefixandpadding + @company_id INT, + @type NVARCHAR(100), + @value NVARCHAR(100) OUTPUT +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @prefix NVARCHAR(50); + DECLARE @padding INT; + DECLARE @num INT; + + SELECT + @prefix = n.prefix, + @padding = n.padding, + @num = n.[next] + FROM numbers n WITH (UPDLOCK, ROWLOCK) + WHERE n.company_id = @company_id AND n.[type] = @type; + + IF @@ROWCOUNT = 0 + THROW 50000, ''Number type not found for company'', 1; + + SET @value = @prefix + CASE + WHEN @padding > 0 THEN RIGHT(REPLICATE(''0'', @padding) + CAST(@num AS NVARCHAR(20)), @padding) + ELSE CAST(@num AS NVARCHAR(20)) + END; +END +'); +`; + export const INIT_COMPANYUSER = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'companyuser') BEGIN @@ -686,6 +753,7 @@ export const SCHEMA_SCRIPTS = [ INIT_CUSTOMERGROUP, INIT_COMPANY_POS_CUSTOMERGROUP_FK, INIT_ENTITY, + INIT_ENTITY_COMPANY_NUMBER_UQ, INIT_ENTITYADDRESS, INIT_ENTITYCONTACT, INIT_UNIT, @@ -709,6 +777,7 @@ export const SCHEMA_SCRIPTS = [ INIT_LEDGER, INIT_NUMBERS, INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, + INIT_PEEK_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, INIT_COMPANYUSER, INIT_ARTICLEHISTORY, INIT_ENTITYHISTORY, diff --git a/src/sql/entity.js b/src/sql/entity.js index 631f303..bb48332 100644 --- a/src/sql/entity.js +++ b/src/sql/entity.js @@ -6,6 +6,7 @@ export const ENTITY_COLUMNS = ` e.customergroup_id AS customergroupId, e.first_name AS firstName, e.name, + e.[number] AS number, e.created_at AS createdAt, e.updated_at AS updatedAt `; @@ -49,6 +50,16 @@ WHERE e.id = @id AND cg.company_id = @companyId `; +/** Occupied when another entity in the company already has this number. */ +export const NUMBER_TAKEN_IN_COMPANY = ` +SELECT TOP 1 e.id +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +WHERE cg.company_id = @companyId + AND e.[number] = @number + AND (@excludeEntityId IS NULL OR e.id <> @excludeEntityId) +`; + /** Customer entity: company-scoped and not Lieferanten/POS. */ export const GET_CUSTOMER_BY_ID = ` SELECT @@ -87,15 +98,16 @@ WHERE e.id = @id `; export const CREATE = ` -INSERT INTO entity (customergroup_id, first_name, name) +INSERT INTO entity (customergroup_id, first_name, name, [number]) OUTPUT INSERTED.id, INSERTED.customergroup_id AS customergroupId, INSERTED.first_name AS firstName, INSERTED.name, + INSERTED.[number] AS number, INSERTED.created_at AS createdAt, INSERTED.updated_at AS updatedAt -VALUES (@customergroupId, @firstName, @name) +VALUES (@customergroupId, @firstName, @name, @number) `; export const UPDATE = ` @@ -103,12 +115,14 @@ UPDATE entity SET first_name = @firstName, name = @name, + [number] = @number, updated_at = SYSUTCDATETIME() OUTPUT INSERTED.id, INSERTED.customergroup_id AS customergroupId, INSERTED.first_name AS firstName, INSERTED.name, + INSERTED.[number] AS number, INSERTED.created_at AS createdAt, INSERTED.updated_at AS updatedAt WHERE id = @id diff --git a/src/sql/purge.js b/src/sql/purge.js index dcfe6c8..54445f3 100644 --- a/src/sql/purge.js +++ b/src/sql/purge.js @@ -40,7 +40,12 @@ export const TABLES = [ 'meta', ]; -export const PROCEDURES = ['getnextnumberwithprefixandpadding']; +export const PROCEDURES = [ + 'getnextnumberwithprefixandpadding', + 'peeknextnumberwithprefixandpadding', +]; + +export const VIEWS = ['v_entity_company_number']; /** Drop before tables — breaks company ↔ customergroup cycle for purge. */ export const CONSTRAINTS = [ @@ -54,6 +59,10 @@ export function buildPurgeSql() { (name) => `IF OBJECT_ID(N'dbo.${name}', N'P') IS NOT NULL DROP PROCEDURE dbo.[${name}];`, ), + ...VIEWS.map( + (name) => + `IF OBJECT_ID(N'dbo.${name}', N'V') IS NOT NULL DROP VIEW dbo.[${name}];`, + ), ...CONSTRAINTS, ...TABLES.map( (name) => diff --git a/src/store/article/listExport.js b/src/store/article/listExport.js index 51166de..5cee345 100644 --- a/src/store/article/listExport.js +++ b/src/store/article/listExport.js @@ -1,5 +1,5 @@ import { GET_ARTICLE_EXPORT, GET_CATEGORY_TREE_FLAT } from '../../sql/article.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveCategoryAccess from '../category/resolveCategoryAccess.js'; const ROOT_CATEGORY_NAME = 'Home'; @@ -19,18 +19,16 @@ function parseJsonArray(raw) { * * @example * const fn = listExport(pool); - * const articles = await fn(42, 1, null); + * const articles = await fn(42, null); * // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }] */ export default function listExport(pool) { - const companyAccess = resolveCompanyAccess(pool); + const companyAccess = resolveSelectedCompanyAccess(pool); const categoryAccess = resolveCategoryAccess(pool); - return async (googleAuthId, companyId, categoryId) => { - const { languageId, posCustomerGroupId, rootCategoryId } = await companyAccess( - googleAuthId, - companyId, - ); + return async (googleAuthId, categoryId) => { + const { languageId, posCustomerGroupId, rootCategoryId } = + await companyAccess(googleAuthId); // Default to the company's root category when no specific category is given. let exportCategoryId = rootCategoryId; diff --git a/src/store/article/listSearchIndex.js b/src/store/article/listSearchIndex.js index 1066965..2697561 100644 --- a/src/store/article/listSearchIndex.js +++ b/src/store/article/listSearchIndex.js @@ -1,5 +1,5 @@ import { SEARCH_ARTICLES } from '../../sql/article.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Returns a lightweight search index of all articles for a company. @@ -9,14 +9,14 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = listSearchIndex(pool); - * const articles = await fn(42, 1); + * const articles = await fn(42); * // => [{ id, sku, name, eans, categoryIds }] */ export default function listSearchIndex(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId) => { - const { rootCategoryId, languageId } = await access(googleAuthId, companyId); + return async (googleAuthId) => { + const { companyId, rootCategoryId, languageId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/brand/delete.js b/src/store/brand/delete.js index 0b5ea41..cd8de20 100644 --- a/src/store/brand/delete.js +++ b/src/store/brand/delete.js @@ -1,5 +1,5 @@ import { DELETE_BY_COMPANY_AND_ID } from '../../sql/brand.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Deletes a brand for a company by its id. @@ -9,17 +9,17 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 3); + * await fn(42, 3); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id) => { + return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Brand not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); try { const result = await pool diff --git a/src/store/brand/list.js b/src/store/brand/list.js index 18f225a..1879fb2 100644 --- a/src/store/brand/list.js +++ b/src/store/brand/list.js @@ -1,5 +1,5 @@ import { LIST_BY_COMPANY } from '../../sql/brand.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Lists the brands of a company. @@ -9,14 +9,14 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = list(pool); - * const brands = await fn(42, 1); + * const brands = await fn(42); * // => [{ id, name, gsprpinfo }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId) => { - await access(googleAuthId, companyId); + return async (googleAuthId) => { + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/brand/upsert.js b/src/store/brand/upsert.js index 1e9753b..8d207d0 100644 --- a/src/store/brand/upsert.js +++ b/src/store/brand/upsert.js @@ -3,7 +3,7 @@ import { GET_BY_COMPANY_AND_NAME, UPDATE_BY_ID, } from '../../sql/brand.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Creates or updates a brand for a company, keyed by name. @@ -13,20 +13,20 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = upsert(pool); - * const brand = await fn(42, 1, { name: 'Coca-Cola', gsprpinfo: 'x' }); + * const brand = await fn(42, { name: 'Coca-Cola', gsprpinfo: 'x' }); * // => { id, name, gsprpinfo } */ export default function upsert(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, input = {}) => { + return async (googleAuthId, input = {}) => { const name = input.name?.trim(); if (!name) { throw new Error('Invalid brand'); } const gsprpinfo = input.gsprpinfo ?? null; - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const existing = await pool .request() diff --git a/src/store/category/getTreeForCompany.js b/src/store/category/getTreeForCompany.js index 5d16ee3..97e6150 100644 --- a/src/store/category/getTreeForCompany.js +++ b/src/store/category/getTreeForCompany.js @@ -1,5 +1,5 @@ import { GET_TREE_FOR_COMPANY } from '../../sql/category.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import queryCategoryArticles from './queryCategoryArticles.js'; import getCategoryPosterminalIds from '../posterminal/getCategoryPosterminalIds.js'; @@ -12,19 +12,17 @@ import getCategoryPosterminalIds from '../posterminal/getCategoryPosterminalIds. * * @example * const fn = getTreeForCompany(pool); - * const tree = await fn(42, 1); + * const tree = await fn(42); * // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] } */ export default function getTreeForCompany(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const articles = queryCategoryArticles(pool); const posterminalIds = getCategoryPosterminalIds(pool); - return async (googleAuthId, companyId) => { - const { rootCategoryId, languageId, posCustomerGroupId } = await access( - googleAuthId, - companyId, - ); + return async (googleAuthId) => { + const { companyId, rootCategoryId, languageId, posCustomerGroupId } = + await access(googleAuthId); const [treeResult, articleRows, ptMap] = await Promise.all([ pool diff --git a/src/store/companysetting/delete.js b/src/store/companysetting/delete.js index 1d818ca..b0fde6d 100644 --- a/src/store/companysetting/delete.js +++ b/src/store/companysetting/delete.js @@ -1,5 +1,5 @@ import { DELETE_BY_COMPANY_AND_KEY } from '../../sql/companysetting.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Deletes a company setting by key. @@ -9,17 +9,17 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 'theme'); + * await fn(42, 'theme'); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, key) => { + return async (googleAuthId, key) => { if (!key?.trim()) { throw new Error('Setting not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/companysetting/get.js b/src/store/companysetting/get.js index 346c707..ce4b7e2 100644 --- a/src/store/companysetting/get.js +++ b/src/store/companysetting/get.js @@ -1,5 +1,5 @@ import { GET_BY_COMPANY_AND_KEY } from '../../sql/companysetting.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shapeSetting from './shapeSetting.js'; /** @@ -10,18 +10,18 @@ import shapeSetting from './shapeSetting.js'; * * @example * const fn = get(pool); - * const setting = await fn(42, 1, 'theme'); + * const setting = await fn(42, 'theme'); * // => { key, value, frontend } */ export default function get(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, key, { frontendOnly = false } = {}) => { + return async (googleAuthId, key, { frontendOnly = false } = {}) => { if (!key?.trim()) { return undefined; } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/companysetting/list.js b/src/store/companysetting/list.js index 5a1203c..c367e74 100644 --- a/src/store/companysetting/list.js +++ b/src/store/companysetting/list.js @@ -2,7 +2,7 @@ import { LIST_BY_COMPANY, LIST_BY_COMPANY_FRONTEND, } from '../../sql/companysetting.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shapeSetting from './shapeSetting.js'; /** @@ -13,14 +13,14 @@ import shapeSetting from './shapeSetting.js'; * * @example * const fn = list(pool); - * const settings = await fn(42, 1, { frontendOnly: true }); + * const settings = await fn(42, { frontendOnly: true }); * // => [{ key, value, frontend }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, { frontendOnly = false } = {}) => { - await access(googleAuthId, companyId); + return async (googleAuthId, { frontendOnly = false } = {}) => { + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/companysetting/set.js b/src/store/companysetting/set.js index 42798db..ebb98f0 100644 --- a/src/store/companysetting/set.js +++ b/src/store/companysetting/set.js @@ -1,5 +1,5 @@ import { GET_BY_COMPANY_AND_KEY, UPSERT } from '../../sql/companysetting.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shapeSetting from './shapeSetting.js'; /** @@ -10,13 +10,13 @@ import shapeSetting from './shapeSetting.js'; * * @example * const fn = set(pool); - * const setting = await fn(42, 1, { key: 'theme', value: 'dark', frontend: true }); + * const setting = await fn(42, { key: 'theme', value: 'dark', frontend: true }); * // => { key, value, frontend } */ export default function set(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, input = {}) => { + return async (googleAuthId, input = {}) => { const key = input.key?.trim(); if (!key) { throw new Error('Invalid setting'); @@ -26,7 +26,7 @@ export default function set(pool) { } const frontend = Boolean(input.frontend); - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); await pool .request() diff --git a/src/store/companyuser/resolveSelectedCompanyAccess.js b/src/store/companyuser/resolveSelectedCompanyAccess.js new file mode 100644 index 0000000..5bba71c --- /dev/null +++ b/src/store/companyuser/resolveSelectedCompanyAccess.js @@ -0,0 +1,35 @@ +import { GET_BY_ID } from '../../sql/google_auth.js'; +import resolveCompanyAccess from './resolveCompanyAccess.js'; + +/** + * Resolves the user's selected company and checks company membership. + * @param {object} pool + * @returns {(googleAuthId: number) => Promise} + */ +export default function resolveSelectedCompanyAccess(pool) { + const access = resolveCompanyAccess(pool); + + return async (googleAuthId) => { + if (!Number.isFinite(googleAuthId)) { + throw new Error('Not authorized'); + } + + const result = await pool + .request() + .input('googleAuthId', googleAuthId) + .query(GET_BY_ID); + + const row = result.recordset[0]; + if (!row) { + throw new Error('Not authorized'); + } + + const companyId = row.selectedCompanyId; + if (companyId == null) { + throw new Error('No company selected'); + } + + const ctx = await access(googleAuthId, companyId); + return { companyId, ...ctx }; + }; +} diff --git a/src/store/customergroup/create.js b/src/store/customergroup/create.js index 799f236..7547a78 100644 --- a/src/store/customergroup/create.js +++ b/src/store/customergroup/create.js @@ -1,6 +1,6 @@ import { CREATE, GET_NEXT_SORT } from '../../sql/customergroup.js'; import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; const RESERVED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]); @@ -16,13 +16,13 @@ function shape(row) { * * @example * const fn = create(pool); - * const group = await fn(42, 1, { name: 'Wholesale' }); + * const group = await fn(42, { name: 'Wholesale' }); * // => { id, name, sort } */ export default function create(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, { name } = {}) => { + return async (googleAuthId, { name } = {}) => { const cleaned = name?.trim(); if (!cleaned) { throw new Error('Name required'); @@ -31,7 +31,7 @@ export default function create(pool) { throw new Error('Name not allowed'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const next = await pool .request() diff --git a/src/store/customergroup/delete.js b/src/store/customergroup/delete.js index 83b9055..779754b 100644 --- a/src/store/customergroup/delete.js +++ b/src/store/customergroup/delete.js @@ -1,5 +1,5 @@ import { DELETE_BY_COMPANY_AND_ID } from '../../sql/customergroup.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Deletes a customer group for a company by its id. @@ -9,17 +9,17 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 5); + * await fn(42, 5); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id) => { + return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Customer group not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); try { const result = await pool diff --git a/src/store/customergroup/list.js b/src/store/customergroup/list.js index 5fa574d..d70553e 100644 --- a/src/store/customergroup/list.js +++ b/src/store/customergroup/list.js @@ -1,5 +1,5 @@ import { LIST_BY_COMPANY } from '../../sql/customergroup.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Lists the customer groups of a company. @@ -9,14 +9,14 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = list(pool); - * const groups = await fn(42, 1); + * const groups = await fn(42); * // => [{ id, name, sort }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId) => { - await access(googleAuthId, companyId); + return async (googleAuthId) => { + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/customergroup/order.js b/src/store/customergroup/order.js index 8482e30..a1ca8ba 100644 --- a/src/store/customergroup/order.js +++ b/src/store/customergroup/order.js @@ -3,7 +3,7 @@ import { UPDATE_SORT, } from '../../sql/customergroup.js'; import request from '../request.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Reorders the customer groups of a company. @@ -13,18 +13,18 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = order(pool); - * const result = await fn(42, 1, { customergroupIds: [3, 1, 2] }); + * const result = await fn(42, { customergroupIds: [3, 1, 2] }); * // => { customergroupIds: [3, 1, 2] } */ export default function order(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, { customergroupIds } = {}) => { + return async (googleAuthId, { customergroupIds } = {}) => { if (!Array.isArray(customergroupIds)) { throw new Error('Invalid customer group order'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const groups = await pool .request() diff --git a/src/store/customergroup/update.js b/src/store/customergroup/update.js index 6ed8076..ce24516 100644 --- a/src/store/customergroup/update.js +++ b/src/store/customergroup/update.js @@ -1,6 +1,6 @@ import { GET_BY_COMPANY_AND_ID, UPDATE } from '../../sql/customergroup.js'; import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; const LOCKED_NAMES = new Set([POS_GROUP_NAME, SUPPLIER_GROUP_NAME]); @@ -16,13 +16,13 @@ function shape(row) { * * @example * const fn = update(pool); - * const group = await fn(42, 1, 5, { name: 'Retail' }); + * const group = await fn(42, 5, { name: 'Retail' }); * // => { id, name, sort } */ export default function update(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id, { name } = {}) => { + return async (googleAuthId, id, { name } = {}) => { if (!Number.isFinite(id)) { throw new Error('Customer group not found'); } @@ -32,7 +32,7 @@ export default function update(pool) { throw new Error('Name required'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const existing = await pool .request() diff --git a/src/store/delivery/create.js b/src/store/delivery/create.js index 4e106c9..a9b1441 100644 --- a/src/store/delivery/create.js +++ b/src/store/delivery/create.js @@ -1,6 +1,6 @@ import { CREATE_MANIFEST, DELIVERY_TYPE } from '../../sql/delivery.js'; import request from '../request.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import taxEnsure from '../tax/ensure.js'; import { loadDelivery, @@ -17,14 +17,14 @@ import { assertSupplier, insertLines } from './writeLines.js'; * * @example * const fn = create(pool); - * const delivery = await fn(42, 1, { number: 'D-1', date: '2026-01-01', supplierId: 5, lines: [...] }); + * const delivery = await fn(42, { number: 'D-1', date: '2026-01-01', supplierId: 5, lines: [...] }); * // => { id, number, type, entityId, date, lines: [...] } */ export default function create(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const ensureTax = taxEnsure(pool); - return async (googleAuthId, companyId, body = {}) => { + return async (googleAuthId, body = {}) => { const number = body.number?.trim?.() ? body.number.trim() : null; const date = parseDeliveryDate(body.date); if (!number || !date) { @@ -33,7 +33,7 @@ export default function create(pool) { const lines = normalizeLines(body.lines ?? []); - const { rootCategoryId } = await access(googleAuthId, companyId); + const { companyId, rootCategoryId } = await access(googleAuthId); const tx = pool.transaction(); await tx.begin(); diff --git a/src/store/delivery/delete.js b/src/store/delivery/delete.js index 65a87be..145e522 100644 --- a/src/store/delivery/delete.js +++ b/src/store/delivery/delete.js @@ -1,5 +1,5 @@ import { DELETE_MANIFEST, DELIVERY_TYPE } from '../../sql/delivery.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Deletes a delivery by its id within a company. @@ -9,17 +9,17 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 5); + * await fn(42, 5); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id) => { + return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Delivery not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/delivery/getById.js b/src/store/delivery/getById.js index a3263c8..cdbef6c 100644 --- a/src/store/delivery/getById.js +++ b/src/store/delivery/getById.js @@ -1,4 +1,4 @@ -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import { loadDelivery } from './helpers.js'; /** @@ -9,18 +9,18 @@ import { loadDelivery } from './helpers.js'; * * @example * const fn = getById(pool); - * const delivery = await fn(42, 1, 5); + * const delivery = await fn(42, 5); * // => { id, number, type, entityId, date, lines: [...] } */ export default function getById(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id) => { + return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Delivery not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const delivery = await loadDelivery(pool, null, companyId, id); if (!delivery) { diff --git a/src/store/delivery/list.js b/src/store/delivery/list.js index 038060a..b66ca06 100644 --- a/src/store/delivery/list.js +++ b/src/store/delivery/list.js @@ -1,5 +1,5 @@ import { DELIVERY_TYPE, LIST_DELIVERIES } from '../../sql/delivery.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import { shapeSummary } from './helpers.js'; /** @@ -10,14 +10,14 @@ import { shapeSummary } from './helpers.js'; * * @example * const fn = list(pool); - * const deliveries = await fn(42, 1); + * const deliveries = await fn(42); * // => [{ id, number, type, entityId, date }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId) => { - await access(googleAuthId, companyId); + return async (googleAuthId) => { + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/delivery/update.js b/src/store/delivery/update.js index b8c3418..d7162c5 100644 --- a/src/store/delivery/update.js +++ b/src/store/delivery/update.js @@ -5,7 +5,7 @@ import { UPDATE_MANIFEST, } from '../../sql/delivery.js'; import request from '../request.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import taxEnsure from '../tax/ensure.js'; import { loadDelivery, @@ -23,14 +23,14 @@ import { assertSupplier, insertLines } from './writeLines.js'; * * @example * const fn = update(pool); - * const delivery = await fn(42, 1, 5, { number: 'D-2', lines: [...] }); + * const delivery = await fn(42, 5, { number: 'D-2', lines: [...] }); * // => { id, number, type, entityId, date, lines: [...] } */ export default function update(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const ensureTax = taxEnsure(pool); - return async (googleAuthId, companyId, id, patch = {}) => { + return async (googleAuthId, id, patch = {}) => { if (!Number.isFinite(id)) { throw new Error('Delivery not found'); } @@ -52,7 +52,7 @@ export default function update(pool) { normalizedLines = normalizeLines(patch.lines); } - const { rootCategoryId } = await access(googleAuthId, companyId); + const { companyId, rootCategoryId } = await access(googleAuthId); const tx = pool.transaction(); await tx.begin(); diff --git a/src/store/entity/create.js b/src/store/entity/create.js index e44954c..3b64bce 100644 --- a/src/store/entity/create.js +++ b/src/store/entity/create.js @@ -1,40 +1,30 @@ import { CREATE } from '../../sql/entity.js'; import { INSERT as INSERT_HISTORY } from '../../sql/entityhistory.js'; import request from '../request.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js'; - -function shape(row) { - return { - id: row.id, - customergroupId: row.customergroupId, - firstName: row.firstName ?? null, - name: row.name, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - }; -} +import nextNumber from '../numbers/nextNumber.js'; +import shape from './shape.js'; /** * Creates a new entity (customer) in a customer group, recording history. + * Optional `number`; if omitted, allocates via `getnextnumberwithprefixandpadding` + * (`type` `entity`). * * @param {object} pool - The mssql connection pool. * @returns {Function} An async function that creates an entity. * * @example * const fn = create(pool); - * const entity = await fn(42, 1, { customergroupId: 5, name: 'Doe', firstName: 'John' }); - * // => { id, customergroupId, firstName, name, createdAt, updatedAt } + * const entity = await fn(42, { customergroupId: 5, name: 'Doe', firstName: 'John' }); + * // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } */ export default function create(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const groupAccess = resolveCustomerGroupAccess(pool); + const allocateNumber = nextNumber(pool); - return async ( - googleAuthId, - companyId, - { customergroupId, name, firstName = null } = {}, - ) => { + return async (googleAuthId, { customergroupId, name, firstName = null, number } = {}) => { const cleaned = name?.trim(); if (!cleaned) { throw new Error('Name required'); @@ -45,17 +35,26 @@ export default function create(pool) { ? null : String(firstName).trim() || null; - const { companyuserId } = await access(googleAuthId, companyId); + const explicitNumber = + number == null || number === '' + ? null + : String(number).trim() || null; + + const { companyId, companyuserId } = await access(googleAuthId); await groupAccess(companyId, customergroupId); const tx = pool.transaction(); await tx.begin(); try { + const entityNumber = + explicitNumber ?? (await allocateNumber(companyId, 'entity', tx)); + const created = await request(pool, tx) .input('customergroupId', customergroupId) .input('firstName', first) .input('name', cleaned) + .input('number', entityNumber) .query(CREATE); const row = created.recordset[0]; @@ -74,6 +73,9 @@ export default function create(pool) { } catch { // ignore } + if (err.number === 2601 || err.number === 2627) { + throw new Error('Number already in use'); + } throw err; } }; diff --git a/src/store/entity/delete.js b/src/store/entity/delete.js index e32fd6d..6a6ee38 100644 --- a/src/store/entity/delete.js +++ b/src/store/entity/delete.js @@ -1,5 +1,5 @@ import { DELETE_BY_COMPANY_AND_ID } from '../../sql/entity.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Deletes an entity by its id within a company. @@ -9,17 +9,17 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 5); + * await fn(42, 5); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id) => { + return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Entity not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/entity/getById.js b/src/store/entity/getById.js index a4e37ab..dcc3ffc 100644 --- a/src/store/entity/getById.js +++ b/src/store/entity/getById.js @@ -1,16 +1,6 @@ import { GET_BY_COMPANY_AND_ID } from '../../sql/entity.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; - -function shape(row) { - return { - id: row.id, - customergroupId: row.customergroupId, - firstName: row.firstName ?? null, - name: row.name, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - }; -} +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import shape from './shape.js'; /** * Returns a single entity by its id within a company. @@ -20,18 +10,18 @@ function shape(row) { * * @example * const fn = getById(pool); - * const entity = await fn(42, 1, 5); - * // => { id, customergroupId, firstName, name, createdAt, updatedAt } + * const entity = await fn(42, 5); + * // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } */ export default function getById(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id) => { + return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Entity not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/entity/isNumberFree.js b/src/store/entity/isNumberFree.js new file mode 100644 index 0000000..8188c37 --- /dev/null +++ b/src/store/entity/isNumberFree.js @@ -0,0 +1,46 @@ +import { NUMBER_TAKEN_IN_COMPANY } from '../../sql/entity.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; + +/** + * Returns whether a customer number is free within the company. + * Pass `excludeEntityId` when editing so the entity's own number counts as free. + * + * @param {object} pool - The mssql connection pool. + * @returns {Function} An async function that checks number availability. + * + * @example + * const fn = isNumberFree(pool); + * const free = await fn(42, 'C-100'); + * // => true + * const freeOnEdit = await fn(42, 'C-100', { excludeEntityId: 5 }); + * // => true + */ +export default function isNumberFree(pool) { + const access = resolveSelectedCompanyAccess(pool); + + return async (googleAuthId, number, { excludeEntityId } = {}) => { + const cleaned = + number == null || number === '' ? null : String(number).trim() || null; + if (!cleaned) { + throw new Error('Number required'); + } + + if ( + excludeEntityId != null && + !Number.isFinite(excludeEntityId) + ) { + throw new Error('Entity not found'); + } + + const { companyId } = await access(googleAuthId); + + const result = await pool + .request() + .input('companyId', companyId) + .input('number', cleaned) + .input('excludeEntityId', excludeEntityId ?? null) + .query(NUMBER_TAKEN_IN_COMPANY); + + return !result.recordset[0]; + }; +} diff --git a/src/store/entity/list.js b/src/store/entity/list.js index 86de138..69a9c06 100644 --- a/src/store/entity/list.js +++ b/src/store/entity/list.js @@ -5,19 +5,9 @@ import { POS_GROUP_NAME, SUPPLIER_GROUP_NAME, } from '../../sql/entity.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js'; - -function shape(row) { - return { - id: row.id, - customergroupId: row.customergroupId, - firstName: row.firstName ?? null, - name: row.name, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - }; -} +import shape from './shape.js'; /** * Lists the entities (customers) of a company, optionally filtered by customer @@ -28,19 +18,15 @@ function shape(row) { * * @example * const fn = list(pool); - * const entities = await fn(42, 1, { customergroupId: 5 }); - * // => [{ id, customergroupId, firstName, name, createdAt, updatedAt }] + * const entities = await fn(42, { customergroupId: 5 }); + * // => [{ id, customergroupId, firstName, name, number, createdAt, updatedAt }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const groupAccess = resolveCustomerGroupAccess(pool); - return async ( - googleAuthId, - companyId, - { customergroupId, role } = {}, - ) => { - await access(googleAuthId, companyId); + return async (googleAuthId, { customergroupId, role } = {}) => { + const { companyId } = await access(googleAuthId); if (role != null && role !== 'customer') { throw new Error('Invalid role'); diff --git a/src/store/entity/peekNextNumber.js b/src/store/entity/peekNextNumber.js new file mode 100644 index 0000000..076659e --- /dev/null +++ b/src/store/entity/peekNextNumber.js @@ -0,0 +1,24 @@ +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import peekNumber from '../numbers/peekNumber.js'; + +/** + * Returns the next customer number that would be allocated for the company + * (`numbers` type `entity`) without consuming it. + * + * @param {object} pool - The mssql connection pool. + * @returns {Function} An async function that peeks the next entity number. + * + * @example + * const fn = peekNextNumber(pool); + * const number = await fn(42); + * // => '1' + */ +export default function peekNextNumber(pool) { + const access = resolveSelectedCompanyAccess(pool); + const peek = peekNumber(pool); + + return async (googleAuthId) => { + const { companyId } = await access(googleAuthId); + return peek(companyId, 'entity'); + }; +} diff --git a/src/store/entity/shape.js b/src/store/entity/shape.js new file mode 100644 index 0000000..8078be2 --- /dev/null +++ b/src/store/entity/shape.js @@ -0,0 +1,11 @@ +export default function shape(row) { + return { + id: row.id, + customergroupId: row.customergroupId, + firstName: row.firstName ?? null, + name: row.name, + number: row.number ?? null, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} diff --git a/src/store/entity/update.js b/src/store/entity/update.js index 5744f06..54138f8 100644 --- a/src/store/entity/update.js +++ b/src/store/entity/update.js @@ -1,45 +1,37 @@ import { GET_BY_COMPANY_AND_ID, UPDATE } from '../../sql/entity.js'; import { INSERT as INSERT_HISTORY } from '../../sql/entityhistory.js'; import request from '../request.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; - -function shape(row) { - return { - id: row.id, - customergroupId: row.customergroupId, - firstName: row.firstName ?? null, - name: row.name, - createdAt: row.createdAt, - updatedAt: row.updatedAt, - }; -} +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import shape from './shape.js'; /** - * Updates the name and/or first name of an entity, recording history. + * Updates name, first name, and/or customer number of an entity, recording history. + * Optional `number` sets the customer number explicitly (does not auto-allocate). * * @param {object} pool - The mssql connection pool. * @returns {Function} An async function that updates an entity. * * @example * const fn = update(pool); - * const entity = await fn(42, 1, 5, { name: 'Doe', firstName: 'Jane' }); - * // => { id, customergroupId, firstName, name, createdAt, updatedAt } + * const entity = await fn(42, 5, { name: 'Doe', firstName: 'Jane', number: 'C-42' }); + * // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } */ export default function update(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id, patch = {}) => { + return async (googleAuthId, id, patch = {}) => { if (!Number.isFinite(id)) { throw new Error('Entity not found'); } const hasName = Object.prototype.hasOwnProperty.call(patch, 'name'); const hasFirstName = Object.prototype.hasOwnProperty.call(patch, 'firstName'); - if (!hasName && !hasFirstName) { + const hasNumber = Object.prototype.hasOwnProperty.call(patch, 'number'); + if (!hasName && !hasFirstName && !hasNumber) { throw new Error('Name required'); } - const { companyuserId } = await access(googleAuthId, companyId); + const { companyId, companyuserId } = await access(googleAuthId); const existing = await pool .request() @@ -68,6 +60,14 @@ export default function update(pool) { : String(patch.firstName).trim() || null; } + let number = current.number ?? null; + if (hasNumber) { + number = + patch.number == null || patch.number === '' + ? null + : String(patch.number).trim() || null; + } + const historyLines = []; const oldFirst = current.firstName ?? ''; const newFirst = firstName ?? ''; @@ -81,6 +81,13 @@ export default function update(pool) { `Name geändert von "${current.name}" auf "${name}"`, ); } + const oldNumber = current.number ?? ''; + const newNumber = number ?? ''; + if (oldNumber !== newNumber) { + historyLines.push( + `Nummer geändert von "${oldNumber}" auf "${newNumber}"`, + ); + } if (!historyLines.length) { return shape(current); @@ -94,6 +101,7 @@ export default function update(pool) { .input('id', id) .input('firstName', firstName) .input('name', name) + .input('number', number) .query(UPDATE); const row = updated.recordset[0]; @@ -115,6 +123,9 @@ export default function update(pool) { } catch { // ignore } + if (err.number === 2601 || err.number === 2627) { + throw new Error('Number already in use'); + } throw err; } }; diff --git a/src/store/entityaddress/create.js b/src/store/entityaddress/create.js index 4f7d510..752a4b8 100644 --- a/src/store/entityaddress/create.js +++ b/src/store/entityaddress/create.js @@ -1,5 +1,5 @@ import { CREATE, GET_NEXT_SORT } from '../../sql/entityaddress.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; function shape(row) { @@ -32,14 +32,14 @@ function optionalString(value) { * * @example * const fn = create(pool); - * const address = await fn(42, 1, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' }); + * const address = await fn(42, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' }); * // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort } */ export default function create(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); - return async (googleAuthId, companyId, entityId, input = {}) => { + return async (googleAuthId, entityId, input = {}) => { const addressType = input.addressType?.trim?.() ? input.addressType.trim() : null; @@ -47,7 +47,7 @@ export default function create(pool) { throw new Error('Address type required'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); let sort = input.sort; diff --git a/src/store/entityaddress/delete.js b/src/store/entityaddress/delete.js index 0446de8..7e3613b 100644 --- a/src/store/entityaddress/delete.js +++ b/src/store/entityaddress/delete.js @@ -1,5 +1,5 @@ import { DELETE_BY_ENTITY_AND_ID } from '../../sql/entityaddress.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; /** @@ -10,18 +10,18 @@ import resolveEntityAccess from '../entity/resolveEntityAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 5, 9); + * await fn(42, 5, 9); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); - return async (googleAuthId, companyId, entityId, id) => { + return async (googleAuthId, entityId, id) => { if (!Number.isFinite(id)) { throw new Error('Address not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); const result = await pool diff --git a/src/store/entityaddress/list.js b/src/store/entityaddress/list.js index e6f433c..61fde5d 100644 --- a/src/store/entityaddress/list.js +++ b/src/store/entityaddress/list.js @@ -1,5 +1,5 @@ import { LIST_BY_ENTITY } from '../../sql/entityaddress.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; function shape(row) { @@ -26,15 +26,15 @@ function shape(row) { * * @example * const fn = list(pool); - * const addresses = await fn(42, 1, 5); + * const addresses = await fn(42, 5); * // => [{ id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); - return async (googleAuthId, companyId, entityId) => { - await access(googleAuthId, companyId); + return async (googleAuthId, entityId) => { + const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); const result = await pool diff --git a/src/store/entityaddress/update.js b/src/store/entityaddress/update.js index 68e22c7..81e76ff 100644 --- a/src/store/entityaddress/update.js +++ b/src/store/entityaddress/update.js @@ -1,5 +1,5 @@ import { GET_BY_ENTITY_AND_ID, UPDATE } from '../../sql/entityaddress.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; function shape(row) { @@ -32,19 +32,19 @@ function optionalString(value) { * * @example * const fn = update(pool); - * const address = await fn(42, 1, 5, 9, { city: 'Hamburg' }); + * const address = await fn(42, 5, 9, { city: 'Hamburg' }); * // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort } */ export default function update(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); - return async (googleAuthId, companyId, entityId, id, patch = {}) => { + return async (googleAuthId, entityId, id, patch = {}) => { if (!Number.isFinite(id)) { throw new Error('Address not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); const existing = await pool diff --git a/src/store/entitycontact/create.js b/src/store/entitycontact/create.js index dadcbe0..a080d0d 100644 --- a/src/store/entitycontact/create.js +++ b/src/store/entitycontact/create.js @@ -1,5 +1,5 @@ import { CREATE, GET_NEXT_SORT } from '../../sql/entitycontact.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; function shape(row) { @@ -20,14 +20,14 @@ function shape(row) { * * @example * const fn = create(pool); - * const contact = await fn(42, 1, 5, { contactType: 'email', contact: 'a@b.c' }); + * const contact = await fn(42, 5, { contactType: 'email', contact: 'a@b.c' }); * // => { id, entityId, contactType, contact, sort } */ export default function create(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); - return async (googleAuthId, companyId, entityId, input = {}) => { + return async (googleAuthId, entityId, input = {}) => { const contactType = input.contactType?.trim?.() ? input.contactType.trim() : null; @@ -36,7 +36,7 @@ export default function create(pool) { throw new Error('Contact required'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); let sort = input.sort; diff --git a/src/store/entitycontact/delete.js b/src/store/entitycontact/delete.js index 3d435c8..b708d99 100644 --- a/src/store/entitycontact/delete.js +++ b/src/store/entitycontact/delete.js @@ -1,5 +1,5 @@ import { DELETE_BY_ENTITY_AND_ID } from '../../sql/entitycontact.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; /** @@ -10,18 +10,18 @@ import resolveEntityAccess from '../entity/resolveEntityAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 5, 9); + * await fn(42, 5, 9); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); - return async (googleAuthId, companyId, entityId, id) => { + return async (googleAuthId, entityId, id) => { if (!Number.isFinite(id)) { throw new Error('Contact not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); const result = await pool diff --git a/src/store/entitycontact/list.js b/src/store/entitycontact/list.js index a43dfae..5cd772d 100644 --- a/src/store/entitycontact/list.js +++ b/src/store/entitycontact/list.js @@ -1,5 +1,5 @@ import { LIST_BY_ENTITY } from '../../sql/entitycontact.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; function shape(row) { @@ -20,15 +20,15 @@ function shape(row) { * * @example * const fn = list(pool); - * const contacts = await fn(42, 1, 5); + * const contacts = await fn(42, 5); * // => [{ id, entityId, contactType, contact, sort }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); - return async (googleAuthId, companyId, entityId) => { - await access(googleAuthId, companyId); + return async (googleAuthId, entityId) => { + const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); const result = await pool diff --git a/src/store/entitycontact/update.js b/src/store/entitycontact/update.js index cc64f36..155b000 100644 --- a/src/store/entitycontact/update.js +++ b/src/store/entitycontact/update.js @@ -1,5 +1,5 @@ import { GET_BY_ENTITY_AND_ID, UPDATE } from '../../sql/entitycontact.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; function shape(row) { @@ -20,19 +20,19 @@ function shape(row) { * * @example * const fn = update(pool); - * const contact = await fn(42, 1, 5, 9, { contact: 'b@c.d' }); + * const contact = await fn(42, 5, 9, { contact: 'b@c.d' }); * // => { id, entityId, contactType, contact, sort } */ export default function update(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); - return async (googleAuthId, companyId, entityId, id, patch = {}) => { + return async (googleAuthId, entityId, id, patch = {}) => { if (!Number.isFinite(id)) { throw new Error('Contact not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); const existing = await pool diff --git a/src/store/manifestDoc/createStore.js b/src/store/manifestDoc/createStore.js index 5698e7b..e51f762 100644 --- a/src/store/manifestDoc/createStore.js +++ b/src/store/manifestDoc/createStore.js @@ -15,7 +15,7 @@ import { SUPPLIER_GROUP_NAME, } from '../../sql/entity.js'; import request from '../request.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import taxEnsure from '../tax/ensure.js'; function shapeSummary(type, row) { @@ -179,7 +179,7 @@ async function loadDoc(pool, tx, { type, companyId, id }) { * `label` → error prefix (`Quote` / `Order` → "Quote not found") */ export default function createManifestDocStore(pool, { type, label }) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); const ensureTax = taxEnsure(pool); const notFound = `${label} not found`; const invalid = `Invalid ${label.toLowerCase()}`; @@ -188,11 +188,10 @@ export default function createManifestDocStore(pool, { type, label }) { * Lists the quote/order documents of a company. * * @param {number} googleAuthId - The Google auth user id. - * @param {number} companyId - The company id. * @returns {Promise>} The document summaries. */ - async function list(googleAuthId, companyId) { - await access(googleAuthId, companyId); + async function list(googleAuthId) { + const { companyId } = await access(googleAuthId); const result = await pool .request() @@ -207,16 +206,15 @@ export default function createManifestDocStore(pool, { type, label }) { * Returns a single quote/order document by its id within a company. * * @param {number} googleAuthId - The Google auth user id. - * @param {number} companyId - The company id. * @param {number} id - The document id. * @returns {Promise} The document with its lines, or null if not found. */ - async function getById(googleAuthId, companyId, id) { + async function getById(googleAuthId, id) { if (!Number.isFinite(id)) { throw new Error(notFound); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const doc = await loadDoc(pool, null, { type, companyId, id }); if (!doc) { @@ -229,11 +227,10 @@ export default function createManifestDocStore(pool, { type, label }) { * Creates a new quote/order document with its lines. * * @param {number} googleAuthId - The Google auth user id. - * @param {number} companyId - The company id. * @param {object} body - The document payload (`number`, `date`, `entityId`, `lines`). * @returns {Promise} The created document with its lines. */ - async function create(googleAuthId, companyId, body = {}) { + async function create(googleAuthId, body = {}) { const number = body.number?.trim?.() ? body.number.trim() : null; const date = parseDate(body.date); if (!number || !date) { @@ -241,7 +238,7 @@ export default function createManifestDocStore(pool, { type, label }) { } const lines = normalizeLines(body.lines ?? []); - const { rootCategoryId } = await access(googleAuthId, companyId); + const { companyId, rootCategoryId } = await access(googleAuthId); const tx = pool.transaction(); await tx.begin(); @@ -288,12 +285,11 @@ export default function createManifestDocStore(pool, { type, label }) { * Updates a quote/order document's header and/or lines. * * @param {number} googleAuthId - The Google auth user id. - * @param {number} companyId - The company id. * @param {number} id - The document id. * @param {object} patch - The fields to update (`number`, `date`, `entityId`, `lines`). * @returns {Promise} The updated document with its lines. */ - async function update(googleAuthId, companyId, id, patch = {}) { + async function update(googleAuthId, id, patch = {}) { if (!Number.isFinite(id)) { throw new Error(notFound); } @@ -312,7 +308,7 @@ export default function createManifestDocStore(pool, { type, label }) { normalizedLines = normalizeLines(patch.lines); } - const { rootCategoryId } = await access(googleAuthId, companyId); + const { companyId, rootCategoryId } = await access(googleAuthId); const tx = pool.transaction(); await tx.begin(); @@ -396,16 +392,15 @@ export default function createManifestDocStore(pool, { type, label }) { * Deletes a quote/order document by its id within a company. * * @param {number} googleAuthId - The Google auth user id. - * @param {number} companyId - The company id. * @param {number} id - The document id. * @returns {Promise} */ - async function del(googleAuthId, companyId, id) { + async function del(googleAuthId, id) { if (!Number.isFinite(id)) { throw new Error(notFound); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/numbers/nextNumber.js b/src/store/numbers/nextNumber.js new file mode 100644 index 0000000..4f9018a --- /dev/null +++ b/src/store/numbers/nextNumber.js @@ -0,0 +1,19 @@ +import { ENSURE_TYPE } from '../../sql/numbers.js'; +import request from '../request.js'; + +export default function nextNumber(pool) { + return async (companyId, type, tx) => { + await request(pool, tx) + .input('companyId', companyId) + .input('type', type) + .query(ENSURE_TYPE); + + const req = request(pool, tx); + req.input('company_id', companyId); + req.input('type', type); + req.output('value'); + + const result = await req.execute('getnextnumberwithprefixandpadding'); + return result.output.value; + }; +} diff --git a/src/store/numbers/nextSku.js b/src/store/numbers/nextSku.js index 128cf1c..3a23b09 100644 --- a/src/store/numbers/nextSku.js +++ b/src/store/numbers/nextSku.js @@ -1,21 +1,6 @@ -import { ENSURE_TYPE } from '../../sql/numbers.js'; -import request from '../request.js'; +import nextNumber from './nextNumber.js'; export default function nextSku(pool) { - return async (companyId, tx) => { - const type = 'sku'; - - await request(pool, tx) - .input('companyId', companyId) - .input('type', type) - .query(ENSURE_TYPE); - - const req = request(pool, tx); - req.input('company_id', companyId); - req.input('type', type); - req.output('value'); - - const result = await req.execute('getnextnumberwithprefixandpadding'); - return result.output.value; - }; + const allocate = nextNumber(pool); + return (companyId, tx) => allocate(companyId, 'sku', tx); } diff --git a/src/store/numbers/peekNumber.js b/src/store/numbers/peekNumber.js new file mode 100644 index 0000000..c559a1f --- /dev/null +++ b/src/store/numbers/peekNumber.js @@ -0,0 +1,19 @@ +import { ENSURE_TYPE } from '../../sql/numbers.js'; +import request from '../request.js'; + +export default function peekNumber(pool) { + return async (companyId, type, tx) => { + await request(pool, tx) + .input('companyId', companyId) + .input('type', type) + .query(ENSURE_TYPE); + + const req = request(pool, tx); + req.input('company_id', companyId); + req.input('type', type); + req.output('value'); + + const result = await req.execute('peeknextnumberwithprefixandpadding'); + return result.output.value; + }; +} diff --git a/src/store/order/index.js b/src/store/order/index.js index c98d075..6bdb051 100644 --- a/src/store/order/index.js +++ b/src/store/order/index.js @@ -10,7 +10,7 @@ import createManifestDocStore from '../manifestDoc/createStore.js'; * * @example * const store = createOrderStore(pool); - * const orders = await store.list(42, 1); + * const orders = await store.list(42); * // => [{ id, number, type, entityId, date }] */ export default function createOrderStore(pool) { diff --git a/src/store/posterminal/delete.js b/src/store/posterminal/delete.js index c610905..0212dbc 100644 --- a/src/store/posterminal/delete.js +++ b/src/store/posterminal/delete.js @@ -1,5 +1,5 @@ import { DELETE_BY_COMPANY_AND_ID } from '../../sql/posterminal.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Deletes a posterminal by its id within a company. @@ -9,17 +9,17 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 5); + * await fn(42, 5); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id) => { + return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Posterminal not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/posterminal/list.js b/src/store/posterminal/list.js index 4ea3fc4..05875f7 100644 --- a/src/store/posterminal/list.js +++ b/src/store/posterminal/list.js @@ -1,5 +1,5 @@ import { LIST_BY_COMPANY } from '../../sql/posterminal.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Lists the posterminals of a company. @@ -9,14 +9,14 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = list(pool); - * const terminals = await fn(42, 1); + * const terminals = await fn(42); * // => [{ id, name, token, pairingpin }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId) => { - await access(googleAuthId, companyId); + return async (googleAuthId) => { + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/posterminal/setOff.js b/src/store/posterminal/setOff.js index 57ca497..e878efe 100644 --- a/src/store/posterminal/setOff.js +++ b/src/store/posterminal/setOff.js @@ -1,5 +1,5 @@ import { GET_BY_ID, SET_OFF, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Removes a category assignment from a posterminal (turns it off). @@ -9,18 +9,18 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = setOff(pool); - * const result = await fn(42, 1, 5, 9); + * const result = await fn(42, 5, 9); * // => { id, categoryIds: [...] } */ export default function setOff(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, posterminalId, categoryId) => { + return async (googleAuthId, posterminalId, categoryId) => { if (!Number.isFinite(posterminalId) || !Number.isFinite(categoryId)) { throw new Error('Invalid posterminal or category'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const existing = await pool .request() diff --git a/src/store/posterminal/setOffAll.js b/src/store/posterminal/setOffAll.js index bc1cda2..33d9641 100644 --- a/src/store/posterminal/setOffAll.js +++ b/src/store/posterminal/setOffAll.js @@ -1,5 +1,5 @@ import { GET_BY_ID, SET_OFF_ALL, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Removes all category assignments from a posterminal. @@ -9,18 +9,18 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = setOffAll(pool); - * const result = await fn(42, 1, 5); + * const result = await fn(42, 5); * // => { id, categoryIds: [] } */ export default function setOffAll(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, posterminalId) => { + return async (googleAuthId, posterminalId) => { if (!Number.isFinite(posterminalId)) { throw new Error('Invalid posterminal'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); // Verify posterminal belongs to company const pt = await pool diff --git a/src/store/posterminal/setOn.js b/src/store/posterminal/setOn.js index dda0113..3798c5d 100644 --- a/src/store/posterminal/setOn.js +++ b/src/store/posterminal/setOn.js @@ -1,5 +1,5 @@ import { GET_BY_ID, SET_ON, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Assigns a category to a posterminal (turns it on for that category). @@ -9,18 +9,18 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = setOn(pool); - * const result = await fn(42, 1, 5, 9); + * const result = await fn(42, 5, 9); * // => { id, categoryIds: [9, ...] } */ export default function setOn(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, posterminalId, categoryId) => { + return async (googleAuthId, posterminalId, categoryId) => { if (!Number.isFinite(posterminalId) || !Number.isFinite(categoryId)) { throw new Error('Invalid posterminal or category'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const existing = await pool .request() diff --git a/src/store/posterminal/setOnAll.js b/src/store/posterminal/setOnAll.js index bc34152..02081e3 100644 --- a/src/store/posterminal/setOnAll.js +++ b/src/store/posterminal/setOnAll.js @@ -1,5 +1,5 @@ import { GET_BY_ID, SET_ON_ALL, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import { GET_COMPANY_ROOT_CATEGORY } from '../../sql/company.js'; /** @@ -10,18 +10,18 @@ import { GET_COMPANY_ROOT_CATEGORY } from '../../sql/company.js'; * * @example * const fn = setOnAll(pool); - * const result = await fn(42, 1, 5); + * const result = await fn(42, 5); * // => { id, categoryIds: [...] } */ export default function setOnAll(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, posterminalId) => { + return async (googleAuthId, posterminalId) => { if (!Number.isFinite(posterminalId)) { throw new Error('Invalid posterminal'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); // Verify posterminal belongs to company const pt = await pool diff --git a/src/store/posterminal/upsert.js b/src/store/posterminal/upsert.js index 7d7a617..21baa7d 100644 --- a/src/store/posterminal/upsert.js +++ b/src/store/posterminal/upsert.js @@ -1,5 +1,5 @@ import { GET_BY_ID, CREATE, UPDATE } from '../../sql/posterminal.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Creates or updates a posterminal for a company. @@ -9,13 +9,13 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = upsert(pool); - * const terminal = await fn(42, 1, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' }); + * const terminal = await fn(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' }); * // => { id, name, token, pairingpin } */ export default function upsert(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, input = {}) => { + return async (googleAuthId, input = {}) => { const name = input.name?.trim(); if (!name) { throw new Error('Invalid posterminal'); @@ -27,7 +27,7 @@ export default function upsert(pool) { const token = input.token ?? null; const id = input.id ?? null; - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); if (id) { const existing = await pool diff --git a/src/store/quote/index.js b/src/store/quote/index.js index 4a88521..706a3de 100644 --- a/src/store/quote/index.js +++ b/src/store/quote/index.js @@ -10,7 +10,7 @@ import createManifestDocStore from '../manifestDoc/createStore.js'; * * @example * const store = createQuoteStore(pool); - * const quotes = await store.list(42, 1); + * const quotes = await store.list(42); * // => [{ id, number, type, entityId, date }] */ export default function createQuoteStore(pool) { diff --git a/src/store/supplier/create.js b/src/store/supplier/create.js index 24d29ae..d57cff9 100644 --- a/src/store/supplier/create.js +++ b/src/store/supplier/create.js @@ -1,7 +1,7 @@ import { CREATE } from '../../sql/entity.js'; import { INSERT as INSERT_HISTORY } from '../../sql/entityhistory.js'; import request from '../request.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import ensureLieferantenGroup from './ensureLieferantenGroup.js'; /** @@ -12,19 +12,19 @@ import ensureLieferantenGroup from './ensureLieferantenGroup.js'; * * @example * const fn = create(pool); - * const supplier = await fn(42, 1, { name: 'Acme Supplies' }); + * const supplier = await fn(42, { name: 'Acme Supplies' }); * // => { id, name, createdAt, updatedAt } */ export default function create(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, { name } = {}) => { + return async (googleAuthId, { name } = {}) => { const cleaned = name?.trim(); if (!cleaned) { throw new Error('Name required'); } - const { companyuserId } = await access(googleAuthId, companyId); + const { companyId, companyuserId } = await access(googleAuthId); const tx = pool.transaction(); await tx.begin(); @@ -36,6 +36,7 @@ export default function create(pool) { .input('customergroupId', customergroupId) .input('firstName', null) .input('name', cleaned) + .input('number', null) .query(CREATE); const row = created.recordset[0]; diff --git a/src/store/supplier/delete.js b/src/store/supplier/delete.js index 0566bbd..a3e8a6f 100644 --- a/src/store/supplier/delete.js +++ b/src/store/supplier/delete.js @@ -1,5 +1,5 @@ import { DELETE_BY_ID, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Deletes a supplier by its id within a company. @@ -9,17 +9,17 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 5); + * await fn(42, 5); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id) => { + return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Supplier not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/supplier/list.js b/src/store/supplier/list.js index b686bbc..91abb90 100644 --- a/src/store/supplier/list.js +++ b/src/store/supplier/list.js @@ -1,5 +1,5 @@ import { LIST_SUPPLIERS_BY_COMPANY, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Lists the suppliers of a company. @@ -9,14 +9,14 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = list(pool); - * const suppliers = await fn(42, 1); + * const suppliers = await fn(42); * // => [{ id, name, createdAt, updatedAt }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId) => { - await access(googleAuthId, companyId); + return async (googleAuthId) => { + const { companyId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/supplier/update.js b/src/store/supplier/update.js index f2ed0cb..a1ef32a 100644 --- a/src/store/supplier/update.js +++ b/src/store/supplier/update.js @@ -6,7 +6,7 @@ import { } from '../../sql/entity.js'; import { INSERT as INSERT_HISTORY } from '../../sql/entityhistory.js'; import request from '../request.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; function shape(row) { return { @@ -25,13 +25,13 @@ function shape(row) { * * @example * const fn = update(pool); - * const supplier = await fn(42, 1, 5, { name: 'Acme GmbH' }); + * const supplier = await fn(42, 5, { name: 'Acme GmbH' }); * // => { id, name, createdAt, updatedAt } */ export default function update(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, id, { name } = {}) => { + return async (googleAuthId, id, { name } = {}) => { if (!Number.isFinite(id)) { throw new Error('Supplier not found'); } @@ -41,7 +41,7 @@ export default function update(pool) { throw new Error('Name required'); } - const { companyuserId } = await access(googleAuthId, companyId); + const { companyId, companyuserId } = await access(googleAuthId); const existing = await pool .request() diff --git a/src/store/unit/delete.js b/src/store/unit/delete.js index 89383dd..0b1c2da 100644 --- a/src/store/unit/delete.js +++ b/src/store/unit/delete.js @@ -1,5 +1,5 @@ import { DELETE_BY_COMPANY_AND_SI } from '../../sql/unit.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; /** * Deletes a unit for a company by its SI code. @@ -9,17 +9,17 @@ import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; * * @example * const fn = del(pool); - * await fn(42, 1, 'l'); + * await fn(42, 'l'); */ export default function del(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, si) => { + return async (googleAuthId, si) => { if (!si?.trim()) { throw new Error('Unit not found'); } - await access(googleAuthId, companyId); + const { companyId } = await access(googleAuthId); try { const result = await pool diff --git a/src/store/unit/list.js b/src/store/unit/list.js index 4613b04..2b68374 100644 --- a/src/store/unit/list.js +++ b/src/store/unit/list.js @@ -1,5 +1,5 @@ import { LIST_BY_COMPANY } from '../../sql/unit.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shapeUnit from './shapeUnit.js'; /** @@ -10,14 +10,14 @@ import shapeUnit from './shapeUnit.js'; * * @example * const fn = list(pool); - * const units = await fn(42, 1); + * const units = await fn(42); * // => [{ id, si, basesi, basesifactor, symbol, name }] */ export default function list(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId) => { - const { languageId } = await access(googleAuthId, companyId); + return async (googleAuthId) => { + const { companyId, languageId } = await access(googleAuthId); const result = await pool .request() diff --git a/src/store/unit/upsert.js b/src/store/unit/upsert.js index cf0a93a..899021a 100644 --- a/src/store/unit/upsert.js +++ b/src/store/unit/upsert.js @@ -9,7 +9,7 @@ import { GET_BY_UNIT_AND_LANGUAGE, UPDATE_BY_UNIT_AND_LANGUAGE, } from '../../sql/unittext.js'; -import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import request from '../request.js'; import shapeUnit from './shapeUnit.js'; @@ -38,15 +38,15 @@ function validateUnit({ si, basesi, basesifactor, symbol, name }) { * * @example * const fn = upsert(pool); - * const unit = await fn(42, 1, { si: 'l', basesi: 'm3', basesifactor: 0.001, symbol: 'L', name: 'Liter' }); + * const unit = await fn(42, { si: 'l', basesi: 'm3', basesifactor: 0.001, symbol: 'L', name: 'Liter' }); * // => { id, si, basesi, basesifactor, symbol, name } */ export default function upsert(pool) { - const access = resolveCompanyAccess(pool); + const access = resolveSelectedCompanyAccess(pool); - return async (googleAuthId, companyId, input) => { + return async (googleAuthId, input) => { const unit = validateUnit(input); - const { languageId } = await access(googleAuthId, companyId); + const { companyId, languageId } = await access(googleAuthId); const tx = pool.transaction(); await tx.begin();