From aefd97a17153920566c36fb6ddf6e43137442eb8 Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 7 Aug 2026 06:23:17 +0200 Subject: [PATCH] Rewrite store JSDoc, split quote/order into modules, and generate typed API docs - Rewrite JSDoc across all store methods to document the actual async methods (real params, returns, throws) instead of the internal wrappers - Split quote/order into individual module files backed by shared manifestDoc helpers; remove the createStore factory and thin wrappers - Deduplicate manifest line helpers into src/store/manifestLines.js - Extend generate-api-doc.js to render parameter/return/throws types and emit per-namespace API..md files with an API.md index - Regenerate all API docs --- .cursor/rules/sibling-repos.mdc | 6 +- API.article.md | 300 +++++++++++-- API.brand.md | 63 ++- API.category.md | 177 +++++++- API.company.md | 92 ++++ API.companysetting.md | 86 +++- API.companyuser.md | 37 ++ API.customergroup.md | 109 ++++- API.delivery.md | 112 ++++- API.entity.md | 159 ++++++- API.entityaddress.md | 89 +++- API.entitycontact.md | 83 +++- API.google_auth.md | 108 ++++- API.lang.md | 57 +++ API.md | 2 + API.order.md | 146 ++++++ API.pic.md | 40 +- API.posterminal.md | 149 +++++- API.quote.md | 146 ++++++ API.supplier.md | 84 +++- API.tax.md | 45 ++ API.unit.md | 66 ++- generate-api-doc.js | 77 +++- src/index.js | 29 +- src/store/article/addImage.js | 23 +- src/store/article/createUnderParent.js | 26 +- src/store/article/delete.js | 21 +- src/store/article/deleteImage.js | 22 +- src/store/article/getBySku.js | 25 +- src/store/article/getHistoryBySku.js | 22 +- src/store/article/listExport.js | 25 +- src/store/article/listSearchIndex.js | 21 +- src/store/article/orderImages.js | 25 +- src/store/article/setBrandIdBySku.js | 24 +- src/store/article/setCategoryIdsBySku.js | 24 +- src/store/article/setEansBySku.js | 24 +- src/store/article/setPricesBySku.js | 24 +- src/store/article/setWeightBySku.js | 24 +- src/store/article/updateBySku.js | 27 +- src/store/brand/delete.js | 21 +- src/store/brand/list.js | 21 +- src/store/brand/upsert.js | 25 +- src/store/category/addImage.js | 23 +- src/store/category/createUnderParent.js | 24 +- src/store/category/delete.js | 22 +- src/store/category/deleteImage.js | 23 +- src/store/category/getCategoryArticles.js | 22 +- src/store/category/getTreeForCompany.js | 23 +- src/store/category/move.js | 25 +- src/store/category/order.js | 25 +- src/store/category/rename.js | 25 +- src/store/companysetting/delete.js | 21 +- src/store/companysetting/get.js | 24 +- src/store/companysetting/list.js | 23 +- src/store/companysetting/set.js | 26 +- .../resolveSelectedCompanyAccess.js | 12 +- src/store/customergroup/create.js | 24 +- src/store/customergroup/delete.js | 21 +- src/store/customergroup/list.js | 21 +- src/store/customergroup/order.js | 24 +- src/store/customergroup/update.js | 25 +- src/store/delivery/create.js | 38 +- src/store/delivery/delete.js | 21 +- src/store/delivery/getById.js | 23 +- src/store/delivery/helpers.js | 61 +-- src/store/delivery/list.js | 21 +- src/store/delivery/update.js | 40 +- src/store/delivery/writeLines.js | 43 -- src/store/entity/create.js | 31 +- src/store/entity/delete.js | 21 +- src/store/entity/getById.js | 23 +- src/store/entity/isNumberFree.js | 31 +- src/store/entity/list.js | 27 +- src/store/entity/peekNextNumber.js | 23 +- src/store/entity/update.js | 29 +- src/store/entityaddress/create.js | 33 +- src/store/entityaddress/delete.js | 22 +- src/store/entityaddress/list.js | 22 +- src/store/entityaddress/update.js | 25 +- src/store/entitycontact/create.js | 27 +- src/store/entitycontact/delete.js | 22 +- src/store/entitycontact/list.js | 22 +- src/store/entitycontact/update.js | 25 +- src/store/google_auth/add.js | 21 +- src/store/google_auth/getByEmail.js | 21 +- src/store/google_auth/getById.js | 21 +- src/store/google_auth/listAllowedEmails.js | 20 +- src/store/google_auth/removeAllowedEmail.js | 19 +- src/store/google_auth/setProfile.js | 24 +- src/store/google_auth/setSelectedCompany.js | 20 +- src/store/initialize.js | 18 +- src/store/manifestDoc/createStore.js | 424 ------------------ src/store/manifestDoc/helpers.js | 64 +++ src/store/manifestLines.js | 133 ++++++ src/store/numbers/nextNumber.js | 12 + src/store/numbers/nextSku.js | 11 + src/store/numbers/peekNumber.js | 12 + src/store/order/create.js | 82 ++++ src/store/order/delete.js | 36 ++ src/store/order/getById.js | 33 ++ src/store/order/index.js | 18 - src/store/order/list.js | 29 ++ src/store/order/update.js | 132 ++++++ src/store/pic/getImageByHash.js | 23 +- src/store/pic/getThumbByHash.js | 23 +- src/store/posterminal/delete.js | 21 +- .../posterminal/getCategoryPosterminalIds.js | 23 +- src/store/posterminal/list.js | 21 +- src/store/posterminal/setOff.js | 24 +- src/store/posterminal/setOffAll.js | 23 +- src/store/posterminal/setOn.js | 24 +- src/store/posterminal/setOnAll.js | 23 +- src/store/posterminal/upsert.js | 27 +- src/store/purge.js | 18 +- src/store/quote/create.js | 82 ++++ src/store/quote/delete.js | 36 ++ src/store/quote/getById.js | 33 ++ src/store/quote/index.js | 18 - src/store/quote/list.js | 29 ++ src/store/quote/update.js | 132 ++++++ src/store/supplier/create.js | 24 +- src/store/supplier/delete.js | 21 +- src/store/supplier/list.js | 21 +- src/store/supplier/update.js | 25 +- src/store/unit/delete.js | 21 +- src/store/unit/list.js | 21 +- src/store/unit/upsert.js | 28 +- 127 files changed, 3956 insertions(+), 1684 deletions(-) create mode 100644 API.order.md create mode 100644 API.quote.md delete mode 100644 src/store/manifestDoc/createStore.js create mode 100644 src/store/manifestDoc/helpers.js create mode 100644 src/store/manifestLines.js create mode 100644 src/store/order/create.js create mode 100644 src/store/order/delete.js create mode 100644 src/store/order/getById.js delete mode 100644 src/store/order/index.js create mode 100644 src/store/order/list.js create mode 100644 src/store/order/update.js create mode 100644 src/store/quote/create.js create mode 100644 src/store/quote/delete.js create mode 100644 src/store/quote/getById.js delete mode 100644 src/store/quote/index.js create mode 100644 src/store/quote/list.js create mode 100644 src/store/quote/update.js diff --git a/.cursor/rules/sibling-repos.mdc b/.cursor/rules/sibling-repos.mdc index 27d7643..de7999f 100644 --- a/.cursor/rules/sibling-repos.mdc +++ b/.cursor/rules/sibling-repos.mdc @@ -29,7 +29,9 @@ Wenn ein Vertrag unklar oder fehlt: **nicht weiterwursteln**. Human fragen, Vors | Repo | Pfad | Inhalt | |------|------|--------| -| **dieses** | `README.md` | Store-Methoden, Signaturen, Returns — Vertrag für dashback | +| **dieses** | `README.md` | Store-Übersicht, Konventionen, HTTP-Mapping — Vertrag für dashback | +| **dieses** | `API.md` | Store-API-Index (TOC → `API..md`) | +| **dieses** | `API..md` | pro Namespace: Methoden, Signaturen, JSDoc-Beispiele (`npm run api-doc`) | | **dieses** | `src/schema.js`, `src/store/` | Schema + Implementierung | | dashback | `../dashback/docs/api/README.md` | HTTP-Routen-Index | | dashback | `../dashback/docs/api/routes/` | eine Markdown-Datei pro Endpoint | @@ -38,6 +40,6 @@ Wenn ein Vertrag unklar oder fehlt: **nicht weiterwursteln**. Human fragen, Vors ## Was der Human / dashback von dir erwarten -1. **README.md aktuell halten**, wenn Store-Methoden oder Returns sich ändern — dashback baut Services darauf. +1. **README.md** und **`API.md` / `API..md`** aktuell halten, wenn Store-Methoden oder Returns sich ändern (`npm run api-doc`) — dashback baut Services darauf. 2. Schema-Änderungen so, dass `npm run reset-db` den Zielzustand abbildet; Live-DB nur manuell per `sqlcmd` (Human). 3. Bei API-Bedarf aus dem Frontend: dashback-Docs/Logs lesen; fehlende HTTP-Schicht dem Human melden, nicht in dashback implementieren. diff --git a/API.article.md b/API.article.md index 1269958..69b46ca 100644 --- a/API.article.md +++ b/API.article.md @@ -8,9 +8,22 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Adds an image to an article by its SKU, processing it into AVIF variants. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `image` | `Buffer` | The source image buffer. | + +**Returns** + +`Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` — The added image info. + +**Example** + ```js -const fn = addImage(pool); -const result = await fn(42, 'SKU-1', imageBuffer); +const result = await store.article.addImage(42, 'SKU-1', imageBuffer); // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash } ``` @@ -20,9 +33,23 @@ const result = await fn(42, 'SKU-1', imageBuffer); Creates a new article under a given parent category, allocating a SKU and a default unit. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | The article creation input. | +| `input.googleAuthId` | `number` | The id of the Google auth user. | +| `input.parentId` | `number` | The id of the parent category. | +| `input.name` | `string` | The article name. | + +**Returns** + +`Promise<{id: number, parentId: number, name: string, sku: string}>` — The created article. + +**Example** + ```js -const fn = createUnderParent(pool); -const article = await fn({ googleAuthId: 42, parentId: 5, name: 'Cola' }); +const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' }); // => { id, parentId, name, sku } ``` @@ -32,9 +59,25 @@ const article = await fn({ googleAuthId: 42, parentId: 5, name: 'Cola' }); Deletes an article by its SKU, refusing when it still has children. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the article has children or is not found. + +**Example** + ```js -const fn = del(pool); -await fn(42, 'SKU-1'); +await store.article.delete(42, 'SKU-1'); ``` ## `article.deleteImage` @@ -43,9 +86,26 @@ await fn(42, 'SKU-1'); Deletes an image from an article by its SKU and full image hash. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `fullHash` | `string` | The full image hash to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the image is not found. + +**Example** + ```js -const fn = deleteImage(pool); -await fn(42, 'SKU-1', 'abc123'); +await store.article.deleteImage(42, 'SKU-1', 'abc123'); ``` ## `article.getBySku` @@ -54,9 +114,25 @@ await fn(42, 'SKU-1', 'abc123'); Returns the full detail of an article by its SKU, including unit, brand, categories, photos, EANs and prices. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | + +**Returns** + +`Promise<{article: object}>` — The full article detail. + +**Throws** + +`Error` — If the article is not found or the user is not authorized. + +**Example** + ```js -const fn = getBySku(pool); -const article = await fn(42, 'SKU-1'); +const article = await store.article.getBySku(42, 'SKU-1'); // => { article: { id, sku, name, unit, brand, categoryIds, photos, eans, prices, ... } } ``` @@ -66,9 +142,21 @@ const article = await fn(42, 'SKU-1'); Returns the change history of an article by its SKU. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | + +**Returns** + +`Promise>` — The article history entries. + +**Example** + ```js -const fn = getHistoryBySku(pool); -const history = await fn(42, 'SKU-1'); +const history = await store.article.getHistoryBySku(42, 'SKU-1'); // => [{ id, companyuserId, date, text }] ``` @@ -78,9 +166,25 @@ const history = await fn(42, 'SKU-1'); Returns articles for export, optionally scoped to a category, including category breadcrumbs and image hashes. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[categoryId]` | `number|null` | Optional category id to scope the export to; defaults to the company root. | + +**Returns** + +`Promise>` — The articles for export. + +**Throws** + +`Error` — If the category is not reachable from the selected company. + +**Example** + ```js -const fn = listExport(pool); -const articles = await fn(42, null); +const articles = await store.article.listExport(42, null); // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }] ``` @@ -88,11 +192,22 @@ const articles = await fn(42, null); *Source: `src/store/article/listSearchIndex.js`* -Returns a lightweight search index of all articles for a company. +Returns a lightweight search index of all articles for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise, categoryIds: Array}>>` — The article search index. + +**Example** ```js -const fn = listSearchIndex(pool); -const articles = await fn(42); +const articles = await store.article.listSearchIndex(42); // => [{ id, sku, name, eans, categoryIds }] ``` @@ -102,9 +217,27 @@ const articles = await fn(42); Reorders the images of an article by its SKU. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `input` | `object` | The reorder input. | +| `input.fullHashes` | `Array` | The full image hashes in their new order. | + +**Returns** + +`Promise<{fullHashes: Array}>` — The applied order. + +**Throws** + +`Error` — If the order is invalid. + +**Example** + ```js -const fn = orderImages(pool); -const result = await fn(42, 'SKU-1', { fullHashes: ['a', 'b'] }); +const result = await store.article.orderImages(42, 'SKU-1', { fullHashes: ['a', 'b'] }); // => { fullHashes: ['a', 'b'] } ``` @@ -114,9 +247,26 @@ const result = await fn(42, 'SKU-1', { fullHashes: ['a', 'b'] }); Sets (or clears) the brand of an article by its SKU. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `brandId` | `number|null` | The brand id, or `null` to clear the brand. | + +**Returns** + +`Promise` — The updated article. + +**Throws** + +`Error` — If the brand or article is not found. + +**Example** + ```js -const fn = setBrandIdBySku(pool); -const article = await fn(42, 'SKU-1', 3); +const article = await store.article.setBrandIdBySku(42, 'SKU-1', 3); // => { id, sku, name, ... } ``` @@ -126,9 +276,26 @@ const article = await fn(42, 'SKU-1', 3); Replaces the category membership of an article by its SKU. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `categoryIds` | `Array` | The category ids the article should belong to (at least one). | + +**Returns** + +`Promise` — The updated article. + +**Throws** + +`Error` — If the category ids are invalid or the article is not found. + +**Example** + ```js -const fn = setCategoryIdsBySku(pool); -const article = await fn(42, 'SKU-1', [5, 9]); +const article = await store.article.setCategoryIdsBySku(42, 'SKU-1', [5, 9]); // => { id, sku, name, ... } ``` @@ -138,9 +305,26 @@ const article = await fn(42, 'SKU-1', [5, 9]); Replaces the EAN list of an article by its SKU. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `eans` | `Array` | The EANs to set (must be unique, non-empty strings). | + +**Returns** + +`Promise` — The updated article. + +**Throws** + +`Error` — If the EANs are invalid or the article is not found. + +**Example** + ```js -const fn = setEansBySku(pool); -const article = await fn(42, 'SKU-1', ['4001234567890']); +const article = await store.article.setEansBySku(42, 'SKU-1', ['4001234567890']); // => { id, sku, name, ... } ``` @@ -150,9 +334,26 @@ const article = await fn(42, 'SKU-1', ['4001234567890']); Replaces the price list of an article by its SKU. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `prices` | `Array<{price: number, tax: number, minmultiplier?: number}>` | The price entries (minmultiplier defaults to 1). | + +**Returns** + +`Promise` — The updated article. + +**Throws** + +`Error` — If the prices are invalid or the article is not found. + +**Example** + ```js -const fn = setPricesBySku(pool); -const article = await fn(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]); +const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]); // => { id, sku, name, ... } ``` @@ -162,9 +363,26 @@ const article = await fn(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 } Sets the weight of an article by its SKU. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `weight` | `number` | The weight (must be >= 0). | + +**Returns** + +`Promise` — The updated article. + +**Throws** + +`Error` — If the weight is invalid or the article is not found. + +**Example** + ```js -const fn = setWeightBySku(pool); -const article = await fn(42, 'SKU-1', 1.5); +const article = await store.article.setWeightBySku(42, 'SKU-1', 1.5); // => { id, sku, name, ... } ``` @@ -174,8 +392,28 @@ const article = await fn(42, 'SKU-1', 1.5); Updates an article's name and/or unit (SI and amount), recording history. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `patch` | `object` | The update patch. | +| `[patch.name]` | `string` | The new article name. | +| `[patch.unitSi]` | `string` | The new unit SI code. | +| `[patch.unitamount]` | `number` | The new unit amount (must be > 0). | + +**Returns** + +`Promise` — The updated article. + +**Throws** + +`Error` — If no valid field is provided or the article is not found. + +**Example** + ```js -const fn = updateBySku(pool); -const article = await fn(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 }); +const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 }); // => { id, sku, name, ... } ``` diff --git a/API.brand.md b/API.brand.md index 814fd8a..4100f96 100644 --- a/API.brand.md +++ b/API.brand.md @@ -6,22 +6,49 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/brand/delete.js`* -Deletes a brand for a company by its id. +Deletes a brand for the user's selected company by its id. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the brand to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the brand is not found. + +**Example** ```js -const fn = del(pool); -await fn(42, 3); +await store.brand.delete(42, 3); ``` ## `brand.list` *Source: `src/store/brand/list.js`* -Lists the brands of a company. +Lists the brands of the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise>` — The brands. + +**Example** ```js -const fn = list(pool); -const brands = await fn(42); +const brands = await store.brand.list(42); // => [{ id, name, gsprpinfo }] ``` @@ -29,10 +56,28 @@ const brands = await fn(42); *Source: `src/store/brand/upsert.js`* -Creates or updates a brand for a company, keyed by name. +Creates or updates a brand for the user's selected company, keyed by name. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The brand input. | +| `input.name` | `string` | The brand name. | +| `[input.gsprpinfo]` | `string` | Optional GSPRP info. | + +**Returns** + +`Promise<{id: number, name: string, gsprpinfo: string|null}>` — The upserted brand. + +**Throws** + +`Error` — If the brand is invalid. + +**Example** ```js -const fn = upsert(pool); -const brand = await fn(42, { name: 'Coca-Cola', gsprpinfo: 'x' }); +const brand = await store.brand.upsert(42, { name: 'Coca-Cola', gsprpinfo: 'x' }); // => { id, name, gsprpinfo } ``` diff --git a/API.category.md b/API.category.md index b336e2e..adbec74 100644 --- a/API.category.md +++ b/API.category.md @@ -8,9 +8,22 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Adds an image to a category, processing it into AVIF full and thumb variants. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `categoryId` | `number` | The id of the category. | +| `image` | `Buffer` | The source image buffer. | + +**Returns** + +`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>` — The added image info. + +**Example** + ```js -const fn = addImage(pool); -const result = await fn(42, 5, imageBuffer); +const result = await store.category.addImage(42, 5, imageBuffer); // => { id, fullHash, thumbW, thumbH, thumbHash } ``` @@ -20,9 +33,23 @@ const result = await fn(42, 5, imageBuffer); Creates a new category under a given parent category. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | The category creation input. | +| `input.googleAuthId` | `number` | The id of the Google auth user. | +| `input.parentId` | `number` | The id of the parent category. | +| `input.name` | `string` | The category name. | + +**Returns** + +`Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>` — The created category. + +**Example** + ```js -const fn = createUnderParent(pool); -const category = await fn({ googleAuthId: 42, parentId: 5, name: 'Drinks' }); +const category = await store.category.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Drinks' }); // => { id, parentId, sort, name, slug } ``` @@ -32,9 +59,26 @@ const category = await fn({ googleAuthId: 42, parentId: 5, name: 'Drinks' }); Deletes a category, refusing to delete the company's root category. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | The delete input. | +| `input.googleAuthId` | `number` | The id of the Google auth user. | +| `input.id` | `number` | The id of the category to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the category is the root category. + +**Example** + ```js -const fn = del(pool); -await fn({ googleAuthId: 42, id: 5 }); +await store.category.delete({ googleAuthId: 42, id: 5 }); ``` ## `category.deleteImage` @@ -43,10 +87,26 @@ await fn({ googleAuthId: 42, id: 5 }); Deletes the image of a category. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `categoryId` | `number` | The id of the category. | + +**Returns** + +`Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>` — The cleared image info. + +**Throws** + +`Error` — If the image is not found. + +**Example** + ```js -const fn = deleteImage(pool); -const result = await fn(42, 5); -// => { id, thumbW: null, thumbH: null, thumbHash: null } +const result = await store.category.deleteImage(42, 5); +// => { id: 5, thumbW: null, thumbH: null, thumbHash: null } ``` ## `category.getCategoryArticles` @@ -55,9 +115,21 @@ const result = await fn(42, 5); Returns the articles belonging to a single category. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `categoryId` | `number` | The id of the category. | + +**Returns** + +`Promise>` — The articles in the category. + +**Example** + ```js -const fn = getCategoryArticles(pool); -const articles = await fn(42, 5); +const articles = await store.category.getCategoryArticles(42, 5); // => [{ id, sku, name, ... }] ``` @@ -65,11 +137,22 @@ const articles = await fn(42, 5); *Source: `src/store/category/getTreeForCompany.js`* -Returns the full category tree for a company together with its articles and the posterminal ids assigned to each category. +Returns the full category tree for the user's selected company together with its articles and the posterminal ids assigned to each category. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise<{languageId: number, tree: Array, articles: Array}>` — The category tree, articles and language. + +**Example** ```js -const fn = getTreeForCompany(pool); -const tree = await fn(42); +const tree = await store.category.getTreeForCompany(42); // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] } ``` @@ -79,9 +162,27 @@ const tree = await fn(42); Moves a category to a new parent, preventing cycles and cross-company moves. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | The move input. | +| `input.googleAuthId` | `number` | The id of the Google auth user. | +| `input.id` | `number` | The id of the category to move. | +| `input.parentId` | `number` | The id of the new parent category. | + +**Returns** + +`Promise<{id: number, parentId: number, sort: number}>` — The moved category. + +**Throws** + +`Error` — If the move would create a cycle or cross companies. + +**Example** + ```js -const fn = move(pool); -const result = await fn({ googleAuthId: 42, id: 5, parentId: 9 }); +const result = await store.category.move({ googleAuthId: 42, id: 5, parentId: 9 }); // => { id, parentId, sort } ``` @@ -91,9 +192,27 @@ const result = await fn({ googleAuthId: 42, id: 5, parentId: 9 }); Reorders the child categories of a given parent. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | The reorder input. | +| `input.googleAuthId` | `number` | The id of the Google auth user. | +| `input.parentId` | `number` | The id of the parent category. | +| `input.categoryIds` | `Array` | The child category ids in their new order. | + +**Returns** + +`Promise<{categoryIds: Array}>` — The applied order. + +**Throws** + +`Error` — If the order is invalid. + +**Example** + ```js -const fn = order(pool); -const result = await fn({ googleAuthId: 42, parentId: 5, categoryIds: [3, 1, 2] }); +const result = await store.category.order({ googleAuthId: 42, parentId: 5, categoryIds: [3, 1, 2] }); // => { categoryIds: [3, 1, 2] } ``` @@ -103,8 +222,26 @@ const result = await fn({ googleAuthId: 42, parentId: 5, categoryIds: [3, 1, 2] Renames a category and updates its slug. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | The rename input. | +| `input.googleAuthId` | `number` | The id of the Google auth user. | +| `input.id` | `number` | The id of the category to rename. | +| `input.name` | `string` | The new category name. | + +**Returns** + +`Promise<{id: number, name: string, slug: string}>` — The renamed category. + +**Throws** + +`Error` — If the name is empty or the category is not found. + +**Example** + ```js -const fn = rename(pool); -const result = await fn({ googleAuthId: 42, id: 5, name: 'Soft Drinks' }); +const result = await store.category.rename({ googleAuthId: 42, id: 5, name: 'Soft Drinks' }); // => { id, name, slug } ``` diff --git a/API.company.md b/API.company.md index 2d3296c..62f58c5 100644 --- a/API.company.md +++ b/API.company.md @@ -8,6 +8,27 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Creates a new company together with its root category, POS customer group, default language and tax rates, and links it to the given Google auth user. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | The company creation input. | +| `input.iso639` | `string` | The ISO 639 language code for the company. | +| `input.name` | `string` | The company name (must be unique). | +| `input.taxrate` | `number` | The standard tax rate in percent. | +| `input.reducedtaxrate` | `number` | The reduced tax rate in percent. | +| `input.googleAuthId` | `number` | The id of the Google auth user to link. | + +**Returns** + +`Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>` — The created company ids. + +**Throws** + +`Error` — If the company name is already taken. + +**Example** + ```js const company = await store.company.create({ iso639: 'de', @@ -25,6 +46,18 @@ const company = await store.company.create({ Deletes a company by its id. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `companyId` | `number` | The id of the company to delete. | + +**Returns** + +`Promise` + +**Example** + ```js await store.company.delete(1); ``` @@ -35,6 +68,19 @@ await store.company.delete(1); Returns a single company by its id. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `id` | `number` | The id of the company. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The company row, or `undefined` if not found. + +**Example** + ```js const company = await store.company.getById(1); // => { id, name, ... } @@ -46,6 +92,19 @@ const company = await store.company.getById(1); Returns a single company by its name. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `name` | `string` | The company name. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The company row, or `undefined` if not found. + +**Example** + ```js const company = await store.company.getByName('Acme'); // => { id, name, ... } @@ -57,6 +116,18 @@ const company = await store.company.getByName('Acme'); Lists the companies accessible to the given Google auth user. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise>` — The accessible company rows. + +**Example** + ```js const companies = await store.company.list(42); // => [{ id, name, ... }] @@ -68,6 +139,27 @@ const companies = await store.company.list(42); Updates an existing company's name, language and tax rates. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `id` | `number` | The id of the company to update. | +| `input` | `object` | The update input. | +| `input.iso639` | `string` | The ISO 639 language code. | +| `input.name` | `string` | The new company name (must be unique). | +| `input.taxrate` | `number` | The standard tax rate in percent. | +| `input.reducedtaxrate` | `number` | The reduced tax rate in percent. | + +**Returns** + +`Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number}>` — The updated company ids. + +**Throws** + +`Error` — If the company is not found or the name is taken. + +**Example** + ```js const result = await store.company.update(1, { iso639: 'de', name: 'Acme', taxrate: 19, reducedtaxrate: 7 }); // => { id, rootCategoryId, languageId, taxrateId, reducedtaxrateId, taxfreerateId } diff --git a/API.companysetting.md b/API.companysetting.md index 0e58d00..c2e2acd 100644 --- a/API.companysetting.md +++ b/API.companysetting.md @@ -6,22 +6,52 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/companysetting/delete.js`* -Deletes a company setting by key. +Deletes a company setting by key for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `key` | `string` | The setting key. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the setting is not found. + +**Example** ```js -const fn = del(pool); -await fn(42, 'theme'); +await store.companysetting.delete(42, 'theme'); ``` ## `companysetting.get` *Source: `src/store/companysetting/get.js`* -Returns a single company setting by key. +Returns a single company setting by key for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `key` | `string` | The setting key. | +| `[options]` | `object` | Optional options. | +| `[options.frontendOnly]` | `boolean` | Only return the setting if it is frontend-visible. | + +**Returns** + +`Promise<{key: string, value: string, frontend: boolean}|undefined>` — The setting, or `undefined` if not found. + +**Example** ```js -const fn = get(pool); -const setting = await fn(42, 'theme'); +const setting = await store.companysetting.get(42, 'theme'); // => { key, value, frontend } ``` @@ -29,11 +59,24 @@ const setting = await fn(42, 'theme'); *Source: `src/store/companysetting/list.js`* -Lists the settings of a company, optionally only frontend-visible ones. +Lists the settings of the user's selected company, optionally only frontend-visible ones. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[options]` | `object` | Optional options. | +| `[options.frontendOnly]` | `boolean` | Only return frontend-visible settings. | + +**Returns** + +`Promise>` — The settings. + +**Example** ```js -const fn = list(pool); -const settings = await fn(42, { frontendOnly: true }); +const settings = await store.companysetting.list(42, { frontendOnly: true }); // => [{ key, value, frontend }] ``` @@ -41,10 +84,29 @@ const settings = await fn(42, { frontendOnly: true }); *Source: `src/store/companysetting/set.js`* -Sets (upserts) a company setting by key. +Sets (upserts) a company setting by key for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The setting input. | +| `input.key` | `string` | The setting key. | +| `input.value` | `string` | The setting value. | +| `[input.frontend]` | `boolean` | Whether the setting is frontend-visible. | + +**Returns** + +`Promise<{key: string, value: string, frontend: boolean}>` — The upserted setting. + +**Throws** + +`Error` — If the input is invalid. + +**Example** ```js -const fn = set(pool); -const setting = await fn(42, { key: 'theme', value: 'dark', frontend: true }); +const setting = await store.companysetting.set(42, { key: 'theme', value: 'dark', frontend: true }); // => { key, value, frontend } ``` diff --git a/API.companyuser.md b/API.companyuser.md index b104706..4f50347 100644 --- a/API.companyuser.md +++ b/API.companyuser.md @@ -8,6 +8,19 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Adds a company user linking a company to a Google auth account. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `companyId` | `number` | The id of the company. | +| `googleAuthId` | `number` | The id of the Google auth account. | + +**Returns** + +`Promise` — The id of the created company user link. + +**Example** + ```js const id = await store.companyuser.add(1, 42); // => 7 @@ -19,6 +32,18 @@ const id = await store.companyuser.add(1, 42); Deletes a company user link by its id. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `id` | `number` | The id of the company user link to delete. | + +**Returns** + +`Promise` + +**Example** + ```js await store.companyuser.delete(7); ``` @@ -29,6 +54,18 @@ await store.companyuser.delete(7); Returns the company user ids associated with the given email address. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `email` | `string` | The email address. | + +**Returns** + +`Promise>` — The company user ids. + +**Example** + ```js const ids = await store.companyuser.getByEmail('user@example.com'); // => [1, 2, 3] diff --git a/API.customergroup.md b/API.customergroup.md index 967ff08..db9cfd9 100644 --- a/API.customergroup.md +++ b/API.customergroup.md @@ -6,11 +6,28 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/customergroup/create.js`* -Creates a new customer group for a company. +Creates a new customer group for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The customer group input. | +| `input.name` | `string` | The customer group name (must not be a reserved name). | + +**Returns** + +`Promise<{id: number, name: string, sort: number}>` — The created customer group. + +**Throws** + +`Error` — If the name is empty or reserved. + +**Example** ```js -const fn = create(pool); -const group = await fn(42, { name: 'Wholesale' }); +const group = await store.customergroup.create(42, { name: 'Wholesale' }); // => { id, name, sort } ``` @@ -18,22 +35,49 @@ const group = await fn(42, { name: 'Wholesale' }); *Source: `src/store/customergroup/delete.js`* -Deletes a customer group for a company by its id. +Deletes a customer group for the user's selected company by its id. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the customer group to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the customer group is not found. + +**Example** ```js -const fn = del(pool); -await fn(42, 5); +await store.customergroup.delete(42, 5); ``` ## `customergroup.list` *Source: `src/store/customergroup/list.js`* -Lists the customer groups of a company. +Lists the customer groups of the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise>` — The customer groups. + +**Example** ```js -const fn = list(pool); -const groups = await fn(42); +const groups = await store.customergroup.list(42); // => [{ id, name, sort }] ``` @@ -41,11 +85,28 @@ const groups = await fn(42); *Source: `src/store/customergroup/order.js`* -Reorders the customer groups of a company. +Reorders the customer groups of the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The reorder input. | +| `input.customergroupIds` | `Array` | The customer group ids in their new order. | + +**Returns** + +`Promise<{customergroupIds: Array}>` — The applied order. + +**Throws** + +`Error` — If the order is invalid. + +**Example** ```js -const fn = order(pool); -const result = await fn(42, { customergroupIds: [3, 1, 2] }); +const result = await store.customergroup.order(42, { customergroupIds: [3, 1, 2] }); // => { customergroupIds: [3, 1, 2] } ``` @@ -53,10 +114,28 @@ const result = await fn(42, { customergroupIds: [3, 1, 2] }); *Source: `src/store/customergroup/update.js`* -Updates the name of a customer group for a company. +Updates the name of a customer group for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the customer group to update. | +| `input` | `object` | The update input. | +| `input.name` | `string` | The new customer group name. | + +**Returns** + +`Promise<{id: number, name: string, sort: number}>` — The updated customer group. + +**Throws** + +`Error` — If the customer group is not found or the name is empty. + +**Example** ```js -const fn = update(pool); -const group = await fn(42, 5, { name: 'Retail' }); +const group = await store.customergroup.update(42, 5, { name: 'Retail' }); // => { id, name, sort } ``` diff --git a/API.delivery.md b/API.delivery.md index ceef999..20629c8 100644 --- a/API.delivery.md +++ b/API.delivery.md @@ -6,11 +6,31 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/delivery/create.js`* -Creates a new delivery for a company with its lines. +Creates a new delivery for the user's selected company with its lines. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `body` | `object` | The delivery input. | +| `body.number` | `string` | The delivery number. | +| `body.date` | `string` | The delivery date. | +| `body.supplierId` | `number` | The id of the supplier. | +| `[body.lines]` | `Array` | The delivery lines. | + +**Returns** + +`Promise<{id: number, number: string, type: string, entityId: number, date: string, lines: Array}>` — The created delivery. + +**Throws** + +`Error` — If the delivery is invalid. + +**Example** ```js -const fn = create(pool); -const delivery = await fn(42, { number: 'D-1', date: '2026-01-01', supplierId: 5, lines: [...] }); +const delivery = await store.delivery.create(42, { number: 'D-1', date: '2026-01-01', supplierId: 5, lines: [...] }); // => { id, number, type, entityId, date, lines: [...] } ``` @@ -18,22 +38,54 @@ const delivery = await fn(42, { number: 'D-1', date: '2026-01-01', supplierId: 5 *Source: `src/store/delivery/delete.js`* -Deletes a delivery by its id within a company. +Deletes a delivery by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the delivery to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the delivery is not found. + +**Example** ```js -const fn = del(pool); -await fn(42, 5); +await store.delivery.delete(42, 5); ``` ## `delivery.getById` *Source: `src/store/delivery/getById.js`* -Returns a single delivery by its id within a company. +Returns a single delivery by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the delivery. | + +**Returns** + +`Promise<{id: number, number: string, type: string, entityId: number, date: string, lines: Array}>` — The delivery. + +**Throws** + +`Error` — If the delivery is not found. + +**Example** ```js -const fn = getById(pool); -const delivery = await fn(42, 5); +const delivery = await store.delivery.getById(42, 5); // => { id, number, type, entityId, date, lines: [...] } ``` @@ -41,11 +93,22 @@ const delivery = await fn(42, 5); *Source: `src/store/delivery/list.js`* -Lists the deliveries of a company. +Lists the deliveries of the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise>` — The deliveries. + +**Example** ```js -const fn = list(pool); -const deliveries = await fn(42); +const deliveries = await store.delivery.list(42); // => [{ id, number, type, entityId, date }] ``` @@ -55,8 +118,29 @@ const deliveries = await fn(42); Updates a delivery's header and/or lines. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the delivery to update. | +| `patch` | `object` | The update patch. | +| `[patch.number]` | `string` | The new delivery number. | +| `[patch.date]` | `string` | The new delivery date. | +| `[patch.supplierId]` | `number` | The new supplier id. | +| `[patch.lines]` | `Array` | The new delivery lines. | + +**Returns** + +`Promise<{id: number, number: string, type: string, entityId: number, date: string, lines: Array}>` — The updated delivery. + +**Throws** + +`Error` — If the delivery is not found. + +**Example** + ```js -const fn = update(pool); -const delivery = await fn(42, 5, { number: 'D-2', lines: [...] }); +const delivery = await store.delivery.update(42, 5, { number: 'D-2', lines: [...] }); // => { id, number, type, entityId, date, lines: [...] } ``` diff --git a/API.entity.md b/API.entity.md index dae6c4f..51cf24f 100644 --- a/API.entity.md +++ b/API.entity.md @@ -8,9 +8,29 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Creates a new entity (customer) in a customer group, recording history. Optional `number`; if omitted, allocates via `getnextnumberwithprefixandpadding` (`type` `entity`). +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The entity creation input. | +| `input.customergroupId` | `number` | The id of the customer group. | +| `input.name` | `string` | The entity name. | +| `[input.firstName]` | `string` | The entity first name. | +| `[input.number]` | `string` | Optional explicit customer number. | + +**Returns** + +`Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>` — The created entity. + +**Throws** + +`Error` — If the name is empty. + +**Example** + ```js -const fn = create(pool); -const entity = await fn(42, { customergroupId: 5, name: 'Doe', firstName: 'John' }); +const entity = await store.entity.create(42, { customergroupId: 5, name: 'Doe', firstName: 'John' }); // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } ``` @@ -18,22 +38,54 @@ const entity = await fn(42, { customergroupId: 5, name: 'Doe', firstName: 'John' *Source: `src/store/entity/delete.js`* -Deletes an entity by its id within a company. +Deletes an entity by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the entity to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the entity is not found. + +**Example** ```js -const fn = del(pool); -await fn(42, 5); +await store.entity.delete(42, 5); ``` ## `entity.getById` *Source: `src/store/entity/getById.js`* -Returns a single entity by its id within a company. +Returns a single entity by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the entity. | + +**Returns** + +`Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>` — The entity. + +**Throws** + +`Error` — If the entity is not found. + +**Example** ```js -const fn = getById(pool); -const entity = await fn(42, 5); +const entity = await store.entity.getById(42, 5); // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } ``` @@ -41,13 +93,31 @@ const entity = await fn(42, 5); *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. +Returns whether a customer number is free within the user's selected company. Pass `excludeEntityId` when editing so the entity's own number counts as free. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `number` | `string` | The customer number to check. | +| `[options]` | `object` | Optional options. | +| `[options.excludeEntityId]` | `number` | An entity id to exclude from the check. | + +**Returns** + +`Promise` — Whether the number is free. + +**Throws** + +`Error` — If the number is empty. + +**Example** ```js -const fn = isNumberFree(pool); -const free = await fn(42, 'C-100'); +const free = await store.entity.isNumberFree(42, 'C-100'); // => true -const freeOnEdit = await fn(42, 'C-100', { excludeEntityId: 5 }); +const freeOnEdit = await store.entity.isNumberFree(42, 'C-100', { excludeEntityId: 5 }); // => true ``` @@ -55,11 +125,29 @@ const freeOnEdit = await fn(42, 'C-100', { excludeEntityId: 5 }); *Source: `src/store/entity/list.js`* -Lists the entities (customers) of a company, optionally filtered by customer group or restricted to the 'customer' role. +Lists the entities (customers) of the user's selected company, optionally filtered by customer group or restricted to the 'customer' role. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[options]` | `object` | Optional options. | +| `[options.customergroupId]` | `number` | Filter by customer group id. | +| `[options.role]` | `string` | Restrict to `'customer'` role if set. | + +**Returns** + +`Promise>` — The entities. + +**Throws** + +`Error` — If the role is invalid. + +**Example** ```js -const fn = list(pool); -const entities = await fn(42, { customergroupId: 5 }); +const entities = await store.entity.list(42, { customergroupId: 5 }); // => [{ id, customergroupId, firstName, name, number, createdAt, updatedAt }] ``` @@ -67,11 +155,22 @@ const entities = await fn(42, { customergroupId: 5 }); *Source: `src/store/entity/peekNextNumber.js`* -Returns the next customer number that would be allocated for the company (`numbers` type `entity`) without consuming it. +Returns the next customer number that would be allocated for the user's selected company (`numbers` type `entity`) without consuming it. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise` — The next entity number. + +**Example** ```js -const fn = peekNextNumber(pool); -const number = await fn(42); +const number = await store.entity.peekNextNumber(42); // => '1' ``` @@ -81,8 +180,28 @@ const number = await fn(42); Updates name, first name, and/or customer number of an entity, recording history. Optional `number` sets the customer number explicitly (does not auto-allocate). +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the entity to update. | +| `patch` | `object` | The update patch. | +| `[patch.name]` | `string` | The new entity name. | +| `[patch.firstName]` | `string` | The new first name. | +| `[patch.number]` | `string` | The new customer number. | + +**Returns** + +`Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>` — The updated entity. + +**Throws** + +`Error` — If no valid field is provided or the entity is not found. + +**Example** + ```js -const fn = update(pool); -const entity = await fn(42, 5, { name: 'Doe', firstName: 'Jane', number: 'C-42' }); +const entity = await store.entity.update(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 719d69c..2d42c29 100644 --- a/API.entityaddress.md +++ b/API.entityaddress.md @@ -8,9 +8,35 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Creates a new address for an entity. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `entityId` | `number` | The id of the entity. | +| `input` | `object` | The address input. | +| `input.addressType` | `string` | The address type (e.g. `'billing'`). | +| `[input.firstName]` | `string` | The first name. | +| `[input.name]` | `string` | The name. | +| `[input.street]` | `string` | The street. | +| `[input.streetNumber]` | `string` | The street number. | +| `[input.zip]` | `string` | The zip code. | +| `[input.city]` | `string` | The city. | +| `[input.county]` | `string` | The county. | +| `[input.sort]` | `number` | Optional sort order. | + +**Returns** + +`Promise` — The created address. + +**Throws** + +`Error` — If the address type is missing. + +**Example** + ```js -const fn = create(pool); -const address = await fn(42, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' }); +const address = await store.entityaddress.create(42, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' }); // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort } ``` @@ -20,9 +46,26 @@ const address = await fn(42, 5, { addressType: 'billing', street: 'Main St', cit Deletes an address of an entity. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `entityId` | `number` | The id of the entity. | +| `id` | `number` | The id of the address to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the address is not found. + +**Example** + ```js -const fn = del(pool); -await fn(42, 5, 9); +await store.entityaddress.delete(42, 5, 9); ``` ## `entityaddress.list` @@ -31,9 +74,21 @@ await fn(42, 5, 9); Lists the addresses of an entity. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `entityId` | `number` | The id of the entity. | + +**Returns** + +`Promise>` — The addresses. + +**Example** + ```js -const fn = list(pool); -const addresses = await fn(42, 5); +const addresses = await store.entityaddress.list(42, 5); // => [{ id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }] ``` @@ -43,8 +98,26 @@ const addresses = await fn(42, 5); Updates an address of an entity. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `entityId` | `number` | The id of the entity. | +| `id` | `number` | The id of the address to update. | +| `patch` | `object` | The update patch (any subset of address fields). | + +**Returns** + +`Promise` — The updated address. + +**Throws** + +`Error` — If the address is not found. + +**Example** + ```js -const fn = update(pool); -const address = await fn(42, 5, 9, { city: 'Hamburg' }); +const address = await store.entityaddress.update(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 d53b85a..85bb408 100644 --- a/API.entitycontact.md +++ b/API.entitycontact.md @@ -8,9 +8,29 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Creates a new contact for an entity. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `entityId` | `number` | The id of the entity. | +| `input` | `object` | The contact input. | +| `input.contactType` | `string` | The contact type (e.g. `'email'`). | +| `input.contact` | `string` | The contact value. | +| `[input.sort]` | `number` | Optional sort order. | + +**Returns** + +`Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>` — The created contact. + +**Throws** + +`Error` — If the contact type or value is missing. + +**Example** + ```js -const fn = create(pool); -const contact = await fn(42, 5, { contactType: 'email', contact: 'a@b.c' }); +const contact = await store.entitycontact.create(42, 5, { contactType: 'email', contact: 'a@b.c' }); // => { id, entityId, contactType, contact, sort } ``` @@ -20,9 +40,26 @@ const contact = await fn(42, 5, { contactType: 'email', contact: 'a@b.c' }); Deletes a contact of an entity. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `entityId` | `number` | The id of the entity. | +| `id` | `number` | The id of the contact to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the contact is not found. + +**Example** + ```js -const fn = del(pool); -await fn(42, 5, 9); +await store.entitycontact.delete(42, 5, 9); ``` ## `entitycontact.list` @@ -31,9 +68,21 @@ await fn(42, 5, 9); Lists the contacts of an entity. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `entityId` | `number` | The id of the entity. | + +**Returns** + +`Promise>` — The contacts. + +**Example** + ```js -const fn = list(pool); -const contacts = await fn(42, 5); +const contacts = await store.entitycontact.list(42, 5); // => [{ id, entityId, contactType, contact, sort }] ``` @@ -43,8 +92,26 @@ const contacts = await fn(42, 5); Updates a contact of an entity. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `entityId` | `number` | The id of the entity. | +| `id` | `number` | The id of the contact to update. | +| `patch` | `object` | The update patch (any subset of contact fields). | + +**Returns** + +`Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>` — The updated contact. + +**Throws** + +`Error` — If the contact is not found. + +**Example** + ```js -const fn = update(pool); -const contact = await fn(42, 5, 9, { contact: 'b@c.d' }); +const contact = await store.entitycontact.update(42, 5, 9, { contact: 'b@c.d' }); // => { id, entityId, contactType, contact, sort } ``` diff --git a/API.google_auth.md b/API.google_auth.md index bb2ddab..c140fa3 100644 --- a/API.google_auth.md +++ b/API.google_auth.md @@ -8,9 +8,20 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Adds a new allowed Google auth email. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `email` | `string` | The email address to allow. | + +**Returns** + +`Promise` — The id of the created Google auth row. + +**Example** + ```js -const fn = add(pool); -const id = await fn('user@example.com'); +const id = await store.google_auth.add('user@example.com'); // => 1 ``` @@ -20,9 +31,20 @@ const id = await fn('user@example.com'); Returns a Google auth row by email. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `email` | `string` | The email address. | + +**Returns** + +`Promise` — The Google auth row, or `undefined` if not found. + +**Example** + ```js -const fn = getByEmail(pool); -const auth = await fn('user@example.com'); +const auth = await store.google_auth.getByEmail('user@example.com'); // => { id, email, ... } ``` @@ -32,9 +54,20 @@ const auth = await fn('user@example.com'); Returns a Google auth row by its id. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth row. | + +**Returns** + +`Promise` — The Google auth row, or `undefined` if not found. + +**Example** + ```js -const fn = getById(pool); -const auth = await fn(42); +const auth = await store.google_auth.getById(42); // => { id, email, ... } ``` @@ -44,9 +77,14 @@ const auth = await fn(42); Lists all allowed Google auth emails. +**Returns** + +`Promise>` — The allowed emails. + +**Example** + ```js -const fn = listAllowedEmails(pool); -const emails = await fn(); +const emails = await store.google_auth.listAllowedEmails(); // => [{ id, email }] ``` @@ -56,9 +94,20 @@ const emails = await fn(); Removes an allowed Google auth email. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `email` | `string` | The email address to remove. | + +**Returns** + +`Promise` + +**Example** + ```js -const fn = removeAllowedEmail(pool); -await fn('user@example.com'); +await store.google_auth.removeAllowedEmail('user@example.com'); ``` ## `google_auth.setProfile` @@ -67,9 +116,28 @@ await fn('user@example.com'); Updates the profile (picture, picture binary, sub) of a Google auth user. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The profile input. | +| `[input.picture]` | `string` | The picture URL. | +| `[input.pictureBinary]` | `Buffer` | The picture binary. | +| `[input.sub]` | `string` | The Google sub identifier. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the user is not found or the picture binary is invalid. + +**Example** + ```js -const fn = setProfile(pool); -await fn(42, { picture: 'url', pictureBinary: buffer, sub: 'sub' }); +await store.google_auth.setProfile(42, { picture: 'url', pictureBinary: buffer, sub: 'sub' }); ``` ## `google_auth.setSelectedCompany` @@ -78,7 +146,19 @@ await fn(42, { picture: 'url', pictureBinary: buffer, sub: 'sub' }); Sets the currently selected company for a Google auth user. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `companyId` | `number` | The id of the company to select. | + +**Returns** + +`Promise` + +**Example** + ```js -const fn = setSelectedCompany(pool); -await fn(42, 1); +await store.google_auth.setSelectedCompany(42, 1); ``` diff --git a/API.lang.md b/API.lang.md index 31250f8..9cd1203 100644 --- a/API.lang.md +++ b/API.lang.md @@ -8,6 +8,19 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Creates a new language by its ISO 639 code. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `iso639` | `string` | The ISO 639 language code (e.g. `'de'`). | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The id of the created language. + +**Example** + ```js const id = await store.lang.create('de'); // => 1 @@ -19,6 +32,18 @@ const id = await store.lang.create('de'); Deletes a language by its id. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `id` | `number` | The id of the language to delete. | + +**Returns** + +`Promise` + +**Example** + ```js await store.lang.delete(1); ``` @@ -29,6 +54,19 @@ await store.lang.delete(1); Returns the id of a language by ISO code, creating it if it does not exist. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `iso639` | `string` | The ISO 639 language code (e.g. `'de'`). | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The id of the language. + +**Example** + ```js const id = await store.lang.ensure('de'); // => 1 @@ -40,6 +78,19 @@ const id = await store.lang.ensure('de'); Returns a language row by its ISO 639 code. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `iso639` | `string` | The ISO 639 language code (e.g. `'de'`). | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, iso639: string}>` — The language row, or `undefined` if not found. + +**Example** + ```js const lang = await store.lang.getByIso639('de'); // => { id: 1, iso639: 'de' } @@ -51,6 +102,12 @@ const lang = await store.lang.getByIso639('de'); Lists all languages. +**Returns** + +`Promise>` — All language rows. + +**Example** + ```js const languages = await store.lang.list(); // => [{ id: 1, iso639: 'de' }] diff --git a/API.md b/API.md index f82fa37..7fe96cf 100644 --- a/API.md +++ b/API.md @@ -21,6 +21,8 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - [entitycontact](API.entitycontact.md) - [supplier](API.supplier.md) - [delivery](API.delivery.md) +- [quote](API.quote.md) +- [order](API.order.md) - [posterminal](API.posterminal.md) - [google_auth](API.google_auth.md) - [pic](API.pic.md) diff --git a/API.order.md b/API.order.md new file mode 100644 index 0000000..2d04a2e --- /dev/null +++ b/API.order.md @@ -0,0 +1,146 @@ +# order + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `order.create` + +*Source: `src/store/order/create.js`* + +Creates a new order for the user's selected company with its lines. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `body` | `object` | The order input. | +| `body.number` | `string` | The order number. | +| `body.date` | `string` | The order date. | +| `[body.entityId]` | `number` | The id of the customer entity. | +| `[body.lines]` | `Array` | The order lines. | + +**Returns** + +`Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>` — The created order. + +**Throws** + +`Error` — If the order is invalid. + +**Example** + +```js +const order = await store.order.create(42, { number: 'B-2026-001', date: '2026-09-15', entityId: 12, lines: [...] }); +// => { id, number, type, entityId, date, lines: [...] } +``` + +## `order.delete` + +*Source: `src/store/order/delete.js`* + +Deletes an order by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the order to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the order is not found. + +**Example** + +```js +await store.order.delete(42, 5); +``` + +## `order.getById` + +*Source: `src/store/order/getById.js`* + +Returns a single order by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the order. | + +**Returns** + +`Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>` — The order with its lines. + +**Throws** + +`Error` — If the order is not found. + +**Example** + +```js +const order = await store.order.getById(42, 5); +// => { id, number, type, entityId, date, lines: [...] } +``` + +## `order.list` + +*Source: `src/store/order/list.js`* + +Lists the orders of the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise>` — The orders. + +**Example** + +```js +const orders = await store.order.list(42); +// => [{ id, number, type, entityId, date }] +``` + +## `order.update` + +*Source: `src/store/order/update.js`* + +Updates an order's header and/or lines. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the order to update. | +| `patch` | `object` | The update patch. | +| `[patch.number]` | `string` | The new order number. | +| `[patch.date]` | `string` | The new order date. | +| `[patch.entityId]` | `number` | The new customer entity id. | +| `[patch.lines]` | `Array` | The new order lines. | + +**Returns** + +`Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>` — The updated order. + +**Throws** + +`Error` — If the order is not found. + +**Example** + +```js +const order = await store.order.update(42, 5, { number: 'B-2026-002', lines: [...] }); +// => { id, number, type, entityId, date, lines: [...] } +``` diff --git a/API.pic.md b/API.pic.md index e792d04..9db5a2d 100644 --- a/API.pic.md +++ b/API.pic.md @@ -8,9 +8,25 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Returns the full-size image (AVIF) for a given hash. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `fullHash` | `string` | The full image hash. | + +**Returns** + +`Promise<{buffer: Buffer, w: number, h: number, size: number, contentType: string}>` — The full image. + +**Throws** + +`Error` — If the image is not found. + +**Example** + ```js -const fn = getImageByHash(pool); -const image = await fn(42, 'abc123'); +const image = await store.pic.getImageByHash(42, 'abc123'); // => { buffer, w, h, size, contentType: 'image/avif' } ``` @@ -20,8 +36,24 @@ const image = await fn(42, 'abc123'); Returns the thumbnail image (AVIF) for a given hash. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `fullHash` | `string` | The full image hash. | + +**Returns** + +`Promise<{buffer: Buffer, w: number, h: number, contentType: string}>` — The thumbnail image. + +**Throws** + +`Error` — If the image is not found. + +**Example** + ```js -const fn = getThumbByHash(pool); -const thumb = await fn(42, 'abc123'); +const thumb = await store.pic.getThumbByHash(42, 'abc123'); // => { buffer, w, h, contentType: 'image/avif' } ``` diff --git a/API.posterminal.md b/API.posterminal.md index 1890b0f..c067486 100644 --- a/API.posterminal.md +++ b/API.posterminal.md @@ -6,22 +6,49 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/posterminal/delete.js`* -Deletes a posterminal by its id within a company. +Deletes a posterminal by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the posterminal to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the posterminal is not found. + +**Example** ```js -const fn = del(pool); -await fn(42, 5); +await store.posterminal.delete(42, 5); ``` ## `posterminal.list` *Source: `src/store/posterminal/list.js`* -Lists the posterminals of a company. +Lists the posterminals of the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise>` — The posterminals. + +**Example** ```js -const fn = list(pool); -const terminals = await fn(42); +const terminals = await store.posterminal.list(42); // => [{ id, name, token, pairingpin }] ``` @@ -31,9 +58,26 @@ const terminals = await fn(42); Removes a category assignment from a posterminal (turns it off). +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `posterminalId` | `number` | The id of the posterminal. | +| `categoryId` | `number` | The id of the category to remove. | + +**Returns** + +`Promise<{id: number, categoryIds: Array}>` — The posterminal with its remaining category ids. + +**Throws** + +`Error` — If the posterminal is not found. + +**Example** + ```js -const fn = setOff(pool); -const result = await fn(42, 5, 9); +const result = await store.posterminal.setOff(42, 5, 9); // => { id, categoryIds: [...] } ``` @@ -43,9 +87,25 @@ const result = await fn(42, 5, 9); Removes all category assignments from a posterminal. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `posterminalId` | `number` | The id of the posterminal. | + +**Returns** + +`Promise<{id: number, categoryIds: Array}>` — The posterminal with an empty category list. + +**Throws** + +`Error` — If the posterminal is not found. + +**Example** + ```js -const fn = setOffAll(pool); -const result = await fn(42, 5); +const result = await store.posterminal.setOffAll(42, 5); // => { id, categoryIds: [] } ``` @@ -55,9 +115,26 @@ const result = await fn(42, 5); Assigns a category to a posterminal (turns it on for that category). +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `posterminalId` | `number` | The id of the posterminal. | +| `categoryId` | `number` | The id of the category to assign. | + +**Returns** + +`Promise<{id: number, categoryIds: Array}>` — The posterminal with its category ids. + +**Throws** + +`Error` — If the posterminal is not found. + +**Example** + ```js -const fn = setOn(pool); -const result = await fn(42, 5, 9); +const result = await store.posterminal.setOn(42, 5, 9); // => { id, categoryIds: [9, ...] } ``` @@ -65,11 +142,27 @@ const result = await fn(42, 5, 9); *Source: `src/store/posterminal/setOnAll.js`* -Assigns all categories of a company to a posterminal. +Assigns all categories of the user's selected company to a posterminal. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `posterminalId` | `number` | The id of the posterminal. | + +**Returns** + +`Promise<{id: number, categoryIds: Array}>` — The posterminal with all category ids. + +**Throws** + +`Error` — If the posterminal is not found. + +**Example** ```js -const fn = setOnAll(pool); -const result = await fn(42, 5); +const result = await store.posterminal.setOnAll(42, 5); // => { id, categoryIds: [...] } ``` @@ -77,10 +170,30 @@ const result = await fn(42, 5); *Source: `src/store/posterminal/upsert.js`* -Creates or updates a posterminal for a company. +Creates or updates a posterminal for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The posterminal input. | +| `input.name` | `string` | The posterminal name. | +| `input.pairingpin` | `string` | The pairing pin. | +| `[input.token]` | `string` | Optional token. | +| `[input.id]` | `number` | Optional id to update an existing posterminal. | + +**Returns** + +`Promise<{id: number, name: string, token: string, pairingpin: string}>` — The upserted posterminal. + +**Throws** + +`Error` — If the posterminal is invalid. + +**Example** ```js -const fn = upsert(pool); -const terminal = await fn(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' }); +const terminal = await store.posterminal.upsert(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' }); // => { id, name, token, pairingpin } ``` diff --git a/API.quote.md b/API.quote.md new file mode 100644 index 0000000..ac5eac6 --- /dev/null +++ b/API.quote.md @@ -0,0 +1,146 @@ +# quote + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `quote.create` + +*Source: `src/store/quote/create.js`* + +Creates a new quote for the user's selected company with its lines. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `body` | `object` | The quote input. | +| `body.number` | `string` | The quote number. | +| `body.date` | `string` | The quote date. | +| `[body.entityId]` | `number` | The id of the customer entity. | +| `[body.lines]` | `Array` | The quote lines. | + +**Returns** + +`Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>` — The created quote. + +**Throws** + +`Error` — If the quote is invalid. + +**Example** + +```js +const quote = await store.quote.create(42, { number: 'A-2026-001', date: '2026-09-01', entityId: 12, lines: [...] }); +// => { id, number, type, entityId, date, lines: [...] } +``` + +## `quote.delete` + +*Source: `src/store/quote/delete.js`* + +Deletes a quote by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the quote to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the quote is not found. + +**Example** + +```js +await store.quote.delete(42, 5); +``` + +## `quote.getById` + +*Source: `src/store/quote/getById.js`* + +Returns a single quote by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the quote. | + +**Returns** + +`Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>` — The quote with its lines. + +**Throws** + +`Error` — If the quote is not found. + +**Example** + +```js +const quote = await store.quote.getById(42, 5); +// => { id, number, type, entityId, date, lines: [...] } +``` + +## `quote.list` + +*Source: `src/store/quote/list.js`* + +Lists the quotes of the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise>` — The quotes. + +**Example** + +```js +const quotes = await store.quote.list(42); +// => [{ id, number, type, entityId, date }] +``` + +## `quote.update` + +*Source: `src/store/quote/update.js`* + +Updates a quote's header and/or lines. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the quote to update. | +| `patch` | `object` | The update patch. | +| `[patch.number]` | `string` | The new quote number. | +| `[patch.date]` | `string` | The new quote date. | +| `[patch.entityId]` | `number` | The new customer entity id. | +| `[patch.lines]` | `Array` | The new quote lines. | + +**Returns** + +`Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>` — The updated quote. + +**Throws** + +`Error` — If the quote is not found. + +**Example** + +```js +const quote = await store.quote.update(42, 5, { number: 'A-2026-002', lines: [...] }); +// => { id, number, type, entityId, date, lines: [...] } +``` diff --git a/API.supplier.md b/API.supplier.md index fbbe0e7..0a811f9 100644 --- a/API.supplier.md +++ b/API.supplier.md @@ -6,11 +6,28 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/supplier/create.js`* -Creates a new supplier for a company, recording history. +Creates a new supplier for the user's selected company, recording history. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The supplier input. | +| `input.name` | `string` | The supplier name. | + +**Returns** + +`Promise<{id: number, name: string, createdAt: string, updatedAt: string}>` — The created supplier. + +**Throws** + +`Error` — If the name is empty. + +**Example** ```js -const fn = create(pool); -const supplier = await fn(42, { name: 'Acme Supplies' }); +const supplier = await store.supplier.create(42, { name: 'Acme Supplies' }); // => { id, name, createdAt, updatedAt } ``` @@ -18,22 +35,49 @@ const supplier = await fn(42, { name: 'Acme Supplies' }); *Source: `src/store/supplier/delete.js`* -Deletes a supplier by its id within a company. +Deletes a supplier by its id within the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the supplier to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the supplier is not found. + +**Example** ```js -const fn = del(pool); -await fn(42, 5); +await store.supplier.delete(42, 5); ``` ## `supplier.list` *Source: `src/store/supplier/list.js`* -Lists the suppliers of a company. +Lists the suppliers of the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise>` — The suppliers. + +**Example** ```js -const fn = list(pool); -const suppliers = await fn(42); +const suppliers = await store.supplier.list(42); // => [{ id, name, createdAt, updatedAt }] ``` @@ -43,8 +87,26 @@ const suppliers = await fn(42); Updates the name of a supplier, recording history. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The id of the supplier to update. | +| `input` | `object` | The update input. | +| `input.name` | `string` | The new supplier name. | + +**Returns** + +`Promise<{id: number, name: string, createdAt: string, updatedAt: string}>` — The updated supplier. + +**Throws** + +`Error` — If the supplier is not found or the name is empty. + +**Example** + ```js -const fn = update(pool); -const supplier = await fn(42, 5, { name: 'Acme GmbH' }); +const supplier = await store.supplier.update(42, 5, { name: 'Acme GmbH' }); // => { id, name, createdAt, updatedAt } ``` diff --git a/API.tax.md b/API.tax.md index 40462b7..94399db 100644 --- a/API.tax.md +++ b/API.tax.md @@ -8,6 +8,19 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin Creates a new tax rate. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `rate` | `number` | The tax rate in percent (e.g. `19`). | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The id of the created tax rate. + +**Example** + ```js const id = await store.tax.create(19); // => 1 @@ -19,6 +32,19 @@ const id = await store.tax.create(19); Returns the id of a tax rate, creating it if it does not exist. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `rate` | `number` | The tax rate in percent (e.g. `19`). | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The id of the tax rate. + +**Example** + ```js const id = await store.tax.ensure(19); // => 1 @@ -30,6 +56,19 @@ const id = await store.tax.ensure(19); Returns a tax row by its rate. +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `rate` | `number` | The tax rate in percent (e.g. `19`). | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, rate: number}>` — The tax row, or `undefined` if not found. + +**Example** + ```js const tax = await store.tax.getByRate(19); // => { id: 1, rate: 19 } @@ -41,6 +80,12 @@ const tax = await store.tax.getByRate(19); Lists all tax rates. +**Returns** + +`Promise>` — All tax rate rows. + +**Example** + ```js const taxes = await store.tax.list(); // => [{ id: 1, rate: 19 }] diff --git a/API.unit.md b/API.unit.md index 517d441..bf48e1d 100644 --- a/API.unit.md +++ b/API.unit.md @@ -6,22 +6,49 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin *Source: `src/store/unit/delete.js`* -Deletes a unit for a company by its SI code. +Deletes a unit for the user's selected company by its SI code. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `si` | `string` | The SI code of the unit to delete. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If the unit is not found. + +**Example** ```js -const fn = del(pool); -await fn(42, 'l'); +await store.unit.delete(42, 'l'); ``` ## `unit.list` *Source: `src/store/unit/list.js`* -Lists the units available for a company. +Lists the units available for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | + +**Returns** + +`Promise>` — The units. + +**Example** ```js -const fn = list(pool); -const units = await fn(42); +const units = await store.unit.list(42); // => [{ id, si, basesi, basesifactor, symbol, name }] ``` @@ -29,10 +56,31 @@ const units = await fn(42); *Source: `src/store/unit/upsert.js`* -Creates or updates a unit for a company, keyed by its SI code. +Creates or updates a unit for the user's selected company, keyed by its SI code. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The unit input. | +| `input.si` | `string` | The SI code. | +| `input.basesi` | `string` | The base SI code. | +| `input.basesifactor` | `number` | The base SI factor (must be > 0). | +| `input.symbol` | `string` | The unit symbol. | +| `input.name` | `string` | The unit name. | + +**Returns** + +`Promise<{id: number, si: string, basesi: string, basesifactor: number, symbol: string, name: string}>` — The upserted unit. + +**Throws** + +`Error` — If the unit is invalid. + +**Example** ```js -const fn = upsert(pool); -const unit = await fn(42, { si: 'l', basesi: 'm3', basesifactor: 0.001, symbol: 'L', name: 'Liter' }); +const unit = await store.unit.upsert(42, { si: 'l', basesi: 'm3', basesifactor: 0.001, symbol: 'L', name: 'Liter' }); // => { id, si, basesi, basesifactor, symbol, name } ``` diff --git a/generate-api-doc.js b/generate-api-doc.js index e91a706..03d5ffe 100644 --- a/generate-api-doc.js +++ b/generate-api-doc.js @@ -58,13 +58,27 @@ const INTERNAL_FILES = new Set([ 'peekNumber.js', 'shape.js', 'createStore.js', + 'manifestLines.js', 'index.js', ]); +/** + * Strip the outer braces from a JSDoc type (e.g. `{number}` -> `number`), + * preserving nested braces inside generics. + * @param {string} type + * @returns {string} + */ +function stripBraces(type) { + if (type.startsWith('{') && type.endsWith('}')) { + return type.slice(1, -1); + } + return type; +} + /** * Extract the leading JSDoc block from a source string. * @param {string} source - * @returns {{ description: string, example: string } | null} + * @returns {{ description: string, example: string, params: Array, returns: string|null, throws: string|null } | null} */ function extractJsDoc(source) { const match = source.match(/\/\*\*([\s\S]*?)\*\//); @@ -77,27 +91,53 @@ function extractJsDoc(source) { const description = []; const example = []; + const params = []; + let returns = null; + let throws = null; let inExample = false; for (const line of lines) { - if (line.trim() === '@example') { + const trimmed = line.trim(); + if (trimmed === '@example') { inExample = true; continue; } - if (line.trim().startsWith('@')) { + if (trimmed.startsWith('@')) { inExample = false; + const paramMatch = trimmed.match(/^@param\s+(\{.*\})\s+(\S+)\s*[-–]?\s*(.*)$/); + if (paramMatch) { + params.push({ + type: stripBraces(paramMatch[1]), + name: paramMatch[2], + description: paramMatch[3].trim(), + }); + continue; + } + const returnsMatch = trimmed.match(/^@returns\s+(\{.*\})\s*[-–]?\s*(.*)$/); + if (returnsMatch) { + returns = { type: stripBraces(returnsMatch[1]), description: returnsMatch[2].trim() }; + continue; + } + const throwsMatch = trimmed.match(/^@throws\s+(\{.*\})\s*[-–]?\s*(.*)$/); + if (throwsMatch) { + throws = { type: stripBraces(throwsMatch[1]), description: throwsMatch[2].trim() }; + continue; + } continue; } if (inExample) { example.push(line); - } else if (line.trim()) { - description.push(line.trim()); + } else if (trimmed) { + description.push(trimmed); } } return { description: description.join(' ').trim(), example: example.join('\n').trim(), + params, + returns, + throws, }; } @@ -146,6 +186,9 @@ function buildModules() { file: rel.join('/'), description: doc.description, example: doc.example, + params: doc.params, + returns: doc.returns, + throws: doc.throws, }); } @@ -170,7 +213,31 @@ function renderSection(moduleName, entries) { lines.push(entry.description); lines.push(''); } + if (entry.params && entry.params.length) { + lines.push('**Parameters**'); + lines.push(''); + lines.push('| Name | Type | Description |'); + lines.push('| --- | --- | --- |'); + for (const p of entry.params) { + lines.push(`| \`${p.name}\` | \`${p.type}\` | ${p.description} |`); + } + lines.push(''); + } + if (entry.returns) { + lines.push('**Returns**'); + lines.push(''); + lines.push(`\`${entry.returns.type}\`${entry.returns.description ? ` — ${entry.returns.description}` : ''}`); + lines.push(''); + } + if (entry.throws) { + lines.push('**Throws**'); + lines.push(''); + lines.push(`\`${entry.throws.type}\`${entry.throws.description ? ` — ${entry.throws.description}` : ''}`); + lines.push(''); + } if (entry.example) { + lines.push('**Example**'); + lines.push(''); lines.push('```js'); lines.push(entry.example); lines.push('```'); diff --git a/src/index.js b/src/index.js index bf75f38..11b4dc1 100644 --- a/src/index.js +++ b/src/index.js @@ -68,8 +68,16 @@ import deliveryGetById from './store/delivery/getById.js'; import deliveryCreate from './store/delivery/create.js'; import deliveryUpdate from './store/delivery/update.js'; import deliveryDelete from './store/delivery/delete.js'; -import createQuoteStore from './store/quote/index.js'; -import createOrderStore from './store/order/index.js'; +import quoteList from './store/quote/list.js'; +import quoteGetById from './store/quote/getById.js'; +import quoteCreate from './store/quote/create.js'; +import quoteUpdate from './store/quote/update.js'; +import quoteDelete from './store/quote/delete.js'; +import orderList from './store/order/list.js'; +import orderGetById from './store/order/getById.js'; +import orderCreate from './store/order/create.js'; +import orderUpdate from './store/order/update.js'; +import orderDelete from './store/order/delete.js'; import posterminalList from './store/posterminal/list.js'; import posterminalUpsert from './store/posterminal/upsert.js'; import posterminalDelete from './store/posterminal/delete.js'; @@ -227,8 +235,21 @@ export default class LibDashStore { delete: deliveryDelete(pool), }; - this.quote = createQuoteStore(pool); - this.order = createOrderStore(pool); + this.quote = { + list: quoteList(pool), + getById: quoteGetById(pool), + create: quoteCreate(pool), + update: quoteUpdate(pool), + delete: quoteDelete(pool), + }; + + this.order = { + list: orderList(pool), + getById: orderGetById(pool), + create: orderCreate(pool), + update: orderUpdate(pool), + delete: orderDelete(pool), + }; this.posterminal = { list: posterminalList(pool), diff --git a/src/store/article/addImage.js b/src/store/article/addImage.js index 7a06d5c..5e92915 100644 --- a/src/store/article/addImage.js +++ b/src/store/article/addImage.js @@ -5,20 +5,21 @@ import newFullHash from '../../util/newFullHash.js'; import request from '../request.js'; import resolveArticleBySku from './resolveArticleBySku.js'; -/** - * Adds an image to an article by its SKU, processing it into AVIF variants. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that adds an article image. - * - * @example - * const fn = addImage(pool); - * const result = await fn(42, 'SKU-1', imageBuffer); - * // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash } - */ export default function addImage(pool) { const resolve = resolveArticleBySku(pool); + /** + * Adds an image to an article by its SKU, processing it into AVIF variants. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {Buffer} image - The source image buffer. + * @returns {Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>} The added image info. + * + * @example + * const result = await store.article.addImage(42, 'SKU-1', imageBuffer); + * // => { picId, sort, full: { w, h, size }, thumb: { w, h }, fullHash, thumbHash } + */ return async (googleAuthId, sku, image) => { const { articleId } = await resolve(googleAuthId, sku); diff --git a/src/store/article/createUnderParent.js b/src/store/article/createUnderParent.js index a47ab88..03850a9 100644 --- a/src/store/article/createUnderParent.js +++ b/src/store/article/createUnderParent.js @@ -7,23 +7,25 @@ import nextSku from '../numbers/nextSku.js'; import request from '../request.js'; import slugify from '../slugify.js'; -/** - * Creates a new article under a given parent category, allocating a SKU and - * a default unit. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that creates an article. - * - * @example - * const fn = createUnderParent(pool); - * const article = await fn({ googleAuthId: 42, parentId: 5, name: 'Cola' }); - * // => { id, parentId, name, sku } - */ export default function createUnderParent(pool) { const access = resolveCategoryAccess(pool); const ensureUnit = ensureDefaultUnit(pool); const allocateSku = nextSku(pool); + /** + * Creates a new article under a given parent category, allocating a SKU and + * a default unit. + * + * @param {object} input - The article creation input. + * @param {number} input.googleAuthId - The id of the Google auth user. + * @param {number} input.parentId - The id of the parent category. + * @param {string} input.name - The article name. + * @returns {Promise<{id: number, parentId: number, name: string, sku: string}>} The created article. + * + * @example + * const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' }); + * // => { id, parentId, name, sku } + */ return async ({ googleAuthId, parentId, name }) => { const { companyId, languageId } = await access(googleAuthId, parentId); diff --git a/src/store/article/delete.js b/src/store/article/delete.js index 0ed21bb..3bd631f 100644 --- a/src/store/article/delete.js +++ b/src/store/article/delete.js @@ -11,19 +11,20 @@ import { import resolveArticleBySku from './resolveArticleBySku.js'; import request from '../request.js'; -/** - * Deletes an article by its SKU, refusing when it still has children. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes an article. - * - * @example - * const fn = del(pool); - * await fn(42, 'SKU-1'); - */ export default function del(pool) { const resolve = resolveArticleBySku(pool); + /** + * Deletes an article by its SKU, refusing when it still has children. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @returns {Promise} + * @throws {Error} If the article has children or is not found. + * + * @example + * await store.article.delete(42, 'SKU-1'); + */ return async (googleAuthId, sku) => { const { articleId, languageId } = await resolve(googleAuthId, sku); diff --git a/src/store/article/deleteImage.js b/src/store/article/deleteImage.js index fc954a7..43b93ea 100644 --- a/src/store/article/deleteImage.js +++ b/src/store/article/deleteImage.js @@ -1,19 +1,21 @@ import { DELETE_BY_ARTICLE_AND_HASH } from '../../sql/pic.js'; import resolveArticleBySku from './resolveArticleBySku.js'; -/** - * Deletes an image from an article by its SKU and full image hash. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes an article image. - * - * @example - * const fn = deleteImage(pool); - * await fn(42, 'SKU-1', 'abc123'); - */ export default function deleteImage(pool) { const resolve = resolveArticleBySku(pool); + /** + * Deletes an image from an article by its SKU and full image hash. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {string} fullHash - The full image hash to delete. + * @returns {Promise} + * @throws {Error} If the image is not found. + * + * @example + * await store.article.deleteImage(42, 'SKU-1', 'abc123'); + */ return async (googleAuthId, sku, fullHash) => { const { articleId } = await resolve(googleAuthId, sku); diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index a206e1f..b4b2155 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -6,19 +6,20 @@ function parseJsonArray(raw) { return Array.isArray(arr) ? arr : []; } -/** - * Returns the full detail of an article by its SKU, including unit, brand, - * categories, photos, EANs and prices. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches an article by SKU. - * - * @example - * const fn = getBySku(pool); - * const article = await fn(42, 'SKU-1'); - * // => { article: { id, sku, name, unit, brand, categoryIds, photos, eans, prices, ... } } - */ export default function getBySku(pool) { + /** + * Returns the full detail of an article by its SKU, including unit, brand, + * categories, photos, EANs and prices. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @returns {Promise<{article: object}>} The full article detail. + * @throws {Error} If the article is not found or the user is not authorized. + * + * @example + * const article = await store.article.getBySku(42, 'SKU-1'); + * // => { article: { id, sku, name, unit, brand, categoryIds, photos, eans, prices, ... } } + */ return async (googleAuthId, sku) => { const t0 = performance.now(); const result = await pool diff --git a/src/store/article/getHistoryBySku.js b/src/store/article/getHistoryBySku.js index 221f718..5bede06 100644 --- a/src/store/article/getHistoryBySku.js +++ b/src/store/article/getHistoryBySku.js @@ -1,20 +1,20 @@ import { LIST_BY_ARTICLE_ID } from '../../sql/articlehistory.js'; import resolveArticleBySku from './resolveArticleBySku.js'; -/** - * Returns the change history of an article by its SKU. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches the article history. - * - * @example - * const fn = getHistoryBySku(pool); - * const history = await fn(42, 'SKU-1'); - * // => [{ id, companyuserId, date, text }] - */ export default function getHistoryBySku(pool) { const resolve = resolveArticleBySku(pool); + /** + * Returns the change history of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @returns {Promise>} The article history entries. + * + * @example + * const history = await store.article.getHistoryBySku(42, 'SKU-1'); + * // => [{ id, companyuserId, date, text }] + */ return async (googleAuthId, sku) => { const { articleId } = await resolve(googleAuthId, sku); diff --git a/src/store/article/listExport.js b/src/store/article/listExport.js index 5cee345..903fa47 100644 --- a/src/store/article/listExport.js +++ b/src/store/article/listExport.js @@ -10,22 +10,23 @@ function parseJsonArray(raw) { return Array.isArray(arr) ? arr : []; } -/** - * Returns articles for export, optionally scoped to a category, including - * category breadcrumbs and image hashes. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists articles for export. - * - * @example - * const fn = listExport(pool); - * const articles = await fn(42, null); - * // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }] - */ export default function listExport(pool) { const companyAccess = resolveSelectedCompanyAccess(pool); const categoryAccess = resolveCategoryAccess(pool); + /** + * Returns articles for export, optionally scoped to a category, including + * category breadcrumbs and image hashes. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number|null} [categoryId] - Optional category id to scope the export to; defaults to the company root. + * @returns {Promise>} The articles for export. + * @throws {Error} If the category is not reachable from the selected company. + * + * @example + * const articles = await store.article.listExport(42, null); + * // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }] + */ return async (googleAuthId, categoryId) => { const { languageId, posCustomerGroupId, rootCategoryId } = await companyAccess(googleAuthId); diff --git a/src/store/article/listSearchIndex.js b/src/store/article/listSearchIndex.js index 2697561..3405d68 100644 --- a/src/store/article/listSearchIndex.js +++ b/src/store/article/listSearchIndex.js @@ -1,20 +1,19 @@ import { SEARCH_ARTICLES } from '../../sql/article.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Returns a lightweight search index of all articles for a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists the article search index. - * - * @example - * const fn = listSearchIndex(pool); - * const articles = await fn(42); - * // => [{ id, sku, name, eans, categoryIds }] - */ export default function listSearchIndex(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Returns a lightweight search index of all articles for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise, categoryIds: Array}>>} The article search index. + * + * @example + * const articles = await store.article.listSearchIndex(42); + * // => [{ id, sku, name, eans, categoryIds }] + */ return async (googleAuthId) => { const { companyId, rootCategoryId, languageId } = await access(googleAuthId); diff --git a/src/store/article/orderImages.js b/src/store/article/orderImages.js index 8022e33..7f3ba64 100644 --- a/src/store/article/orderImages.js +++ b/src/store/article/orderImages.js @@ -6,20 +6,23 @@ import { import resolveArticleBySku from './resolveArticleBySku.js'; import request from '../request.js'; -/** - * Reorders the images of an article by its SKU. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that reorders article images. - * - * @example - * const fn = orderImages(pool); - * const result = await fn(42, 'SKU-1', { fullHashes: ['a', 'b'] }); - * // => { fullHashes: ['a', 'b'] } - */ export default function orderImages(pool) { const resolve = resolveArticleBySku(pool); + /** + * Reorders the images of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {object} input - The reorder input. + * @param {Array} input.fullHashes - The full image hashes in their new order. + * @returns {Promise<{fullHashes: Array}>} The applied order. + * @throws {Error} If the order is invalid. + * + * @example + * const result = await store.article.orderImages(42, 'SKU-1', { fullHashes: ['a', 'b'] }); + * // => { fullHashes: ['a', 'b'] } + */ return async (googleAuthId, sku, { fullHashes }) => { if (!Array.isArray(fullHashes)) { throw new Error('Invalid image order'); diff --git a/src/store/article/setBrandIdBySku.js b/src/store/article/setBrandIdBySku.js index d441802..37148da 100644 --- a/src/store/article/setBrandIdBySku.js +++ b/src/store/article/setBrandIdBySku.js @@ -5,21 +5,23 @@ import { import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; -/** - * Sets (or clears) the brand of an article by its SKU. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that sets the article brand. - * - * @example - * const fn = setBrandIdBySku(pool); - * const article = await fn(42, 'SKU-1', 3); - * // => { id, sku, name, ... } - */ export default function setBrandIdBySku(pool) { const resolve = resolveArticleBySku(pool); const load = queryBySku(pool); + /** + * Sets (or clears) the brand of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {number|null} brandId - The brand id, or `null` to clear the brand. + * @returns {Promise} The updated article. + * @throws {Error} If the brand or article is not found. + * + * @example + * const article = await store.article.setBrandIdBySku(42, 'SKU-1', 3); + * // => { id, sku, name, ... } + */ return async (googleAuthId, sku, brandId) => { const { articleId, languageId, companyId } = await resolve( googleAuthId, diff --git a/src/store/article/setCategoryIdsBySku.js b/src/store/article/setCategoryIdsBySku.js index 59ee97e..918e309 100644 --- a/src/store/article/setCategoryIdsBySku.js +++ b/src/store/article/setCategoryIdsBySku.js @@ -7,22 +7,24 @@ import resolveCategoryAccess from '../category/resolveCategoryAccess.js'; import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; -/** - * Replaces the category membership of an article by its SKU. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that sets the article categories. - * - * @example - * const fn = setCategoryIdsBySku(pool); - * const article = await fn(42, 'SKU-1', [5, 9]); - * // => { id, sku, name, ... } - */ export default function setCategoryIdsBySku(pool) { const resolve = resolveArticleBySku(pool); const categoryAccess = resolveCategoryAccess(pool); const load = queryBySku(pool); + /** + * Replaces the category membership of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {Array} categoryIds - The category ids the article should belong to (at least one). + * @returns {Promise} The updated article. + * @throws {Error} If the category ids are invalid or the article is not found. + * + * @example + * const article = await store.article.setCategoryIdsBySku(42, 'SKU-1', [5, 9]); + * // => { id, sku, name, ... } + */ return async (googleAuthId, sku, categoryIds) => { if (!Array.isArray(categoryIds)) { throw new Error('Invalid categoryIds'); diff --git a/src/store/article/setEansBySku.js b/src/store/article/setEansBySku.js index ef0c3d0..1171514 100644 --- a/src/store/article/setEansBySku.js +++ b/src/store/article/setEansBySku.js @@ -6,21 +6,23 @@ import request from '../request.js'; import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; -/** - * Replaces the EAN list of an article by its SKU. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that sets article EANs. - * - * @example - * const fn = setEansBySku(pool); - * const article = await fn(42, 'SKU-1', ['4001234567890']); - * // => { id, sku, name, ... } - */ export default function setEansBySku(pool) { const resolve = resolveArticleBySku(pool); const load = queryBySku(pool); + /** + * Replaces the EAN list of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {Array} eans - The EANs to set (must be unique, non-empty strings). + * @returns {Promise} The updated article. + * @throws {Error} If the EANs are invalid or the article is not found. + * + * @example + * const article = await store.article.setEansBySku(42, 'SKU-1', ['4001234567890']); + * // => { id, sku, name, ... } + */ return async (googleAuthId, sku, eans) => { if (!Array.isArray(eans)) { throw new Error('Invalid eans'); diff --git a/src/store/article/setPricesBySku.js b/src/store/article/setPricesBySku.js index 3084b14..5327f9c 100644 --- a/src/store/article/setPricesBySku.js +++ b/src/store/article/setPricesBySku.js @@ -42,22 +42,24 @@ function normalizePrices(prices) { return normalized; } -/** - * Replaces the price list of an article by its SKU. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that sets article prices. - * - * @example - * const fn = setPricesBySku(pool); - * const article = await fn(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]); - * // => { id, sku, name, ... } - */ export default function setPricesBySku(pool) { const resolve = resolveArticleBySku(pool); const ensureTax = taxEnsure(pool); const load = queryBySku(pool); + /** + * Replaces the price list of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {Array<{price: number, tax: number, minmultiplier?: number}>} prices - The price entries (minmultiplier defaults to 1). + * @returns {Promise} The updated article. + * @throws {Error} If the prices are invalid or the article is not found. + * + * @example + * const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, tax: 19, minmultiplier: 1 }]); + * // => { id, sku, name, ... } + */ return async (googleAuthId, sku, prices) => { const normalized = normalizePrices(prices); diff --git a/src/store/article/setWeightBySku.js b/src/store/article/setWeightBySku.js index 349c3fe..dc39602 100644 --- a/src/store/article/setWeightBySku.js +++ b/src/store/article/setWeightBySku.js @@ -2,21 +2,23 @@ import { UPDATE_WEIGHT } from '../../sql/article.js'; import queryBySku from './queryBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js'; -/** - * Sets the weight of an article by its SKU. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that sets the article weight. - * - * @example - * const fn = setWeightBySku(pool); - * const article = await fn(42, 'SKU-1', 1.5); - * // => { id, sku, name, ... } - */ export default function setWeightBySku(pool) { const resolve = resolveArticleBySku(pool); const load = queryBySku(pool); + /** + * Sets the weight of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {number} weight - The weight (must be >= 0). + * @returns {Promise} The updated article. + * @throws {Error} If the weight is invalid or the article is not found. + * + * @example + * const article = await store.article.setWeightBySku(42, 'SKU-1', 1.5); + * // => { id, sku, name, ... } + */ return async (googleAuthId, sku, weight) => { if (!Number.isFinite(weight) || weight < 0) { throw new Error('Invalid weight'); diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js index dd44e21..a881f3c 100644 --- a/src/store/article/updateBySku.js +++ b/src/store/article/updateBySku.js @@ -20,21 +20,26 @@ function formatUnitLabel(amount, symbol) { return `${Number(amount)}${sym}`; } -/** - * Updates an article's name and/or unit (SI and amount), recording history. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that updates an article by SKU. - * - * @example - * const fn = updateBySku(pool); - * const article = await fn(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 }); - * // => { id, sku, name, ... } - */ export default function updateBySku(pool) { const resolve = resolveArticleBySku(pool); const load = queryBySku(pool); + /** + * Updates an article's name and/or unit (SI and amount), recording history. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {object} patch - The update patch. + * @param {string} [patch.name] - The new article name. + * @param {string} [patch.unitSi] - The new unit SI code. + * @param {number} [patch.unitamount] - The new unit amount (must be > 0). + * @returns {Promise} The updated article. + * @throws {Error} If no valid field is provided or the article is not found. + * + * @example + * const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 }); + * // => { id, sku, name, ... } + */ return async (googleAuthId, sku, patch = {}) => { const hasName = Object.prototype.hasOwnProperty.call(patch, 'name'); const hasUnitamount = Object.prototype.hasOwnProperty.call(patch, 'unitamount'); diff --git a/src/store/brand/delete.js b/src/store/brand/delete.js index cd8de20..c8ef70d 100644 --- a/src/store/brand/delete.js +++ b/src/store/brand/delete.js @@ -1,19 +1,20 @@ import { DELETE_BY_COMPANY_AND_ID } from '../../sql/brand.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Deletes a brand for a company by its id. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes a brand. - * - * @example - * const fn = del(pool); - * await fn(42, 3); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Deletes a brand for the user's selected company by its id. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the brand to delete. + * @returns {Promise} + * @throws {Error} If the brand is not found. + * + * @example + * await store.brand.delete(42, 3); + */ return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Brand not found'); diff --git a/src/store/brand/list.js b/src/store/brand/list.js index 1879fb2..8171b93 100644 --- a/src/store/brand/list.js +++ b/src/store/brand/list.js @@ -1,20 +1,19 @@ import { LIST_BY_COMPANY } from '../../sql/brand.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Lists the brands of a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists brands. - * - * @example - * const fn = list(pool); - * const brands = await fn(42); - * // => [{ id, name, gsprpinfo }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Lists the brands of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise>} The brands. + * + * @example + * const brands = await store.brand.list(42); + * // => [{ id, name, gsprpinfo }] + */ return async (googleAuthId) => { const { companyId } = await access(googleAuthId); diff --git a/src/store/brand/upsert.js b/src/store/brand/upsert.js index 8d207d0..31b45e4 100644 --- a/src/store/brand/upsert.js +++ b/src/store/brand/upsert.js @@ -5,20 +5,23 @@ import { } from '../../sql/brand.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Creates or updates a brand for a company, keyed by name. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that upserts a brand. - * - * @example - * const fn = upsert(pool); - * const brand = await fn(42, { name: 'Coca-Cola', gsprpinfo: 'x' }); - * // => { id, name, gsprpinfo } - */ export default function upsert(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Creates or updates a brand for the user's selected company, keyed by name. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The brand input. + * @param {string} input.name - The brand name. + * @param {string} [input.gsprpinfo] - Optional GSPRP info. + * @returns {Promise<{id: number, name: string, gsprpinfo: string|null}>} The upserted brand. + * @throws {Error} If the brand is invalid. + * + * @example + * const brand = await store.brand.upsert(42, { name: 'Coca-Cola', gsprpinfo: 'x' }); + * // => { id, name, gsprpinfo } + */ return async (googleAuthId, input = {}) => { const name = input.name?.trim(); if (!name) { diff --git a/src/store/category/addImage.js b/src/store/category/addImage.js index b6d13ac..fe25d12 100644 --- a/src/store/category/addImage.js +++ b/src/store/category/addImage.js @@ -5,20 +5,21 @@ import newFullHash from '../../util/newFullHash.js'; import request from '../request.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; -/** - * Adds an image to a category, processing it into AVIF full and thumb variants. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that adds a category image. - * - * @example - * const fn = addImage(pool); - * const result = await fn(42, 5, imageBuffer); - * // => { id, fullHash, thumbW, thumbH, thumbHash } - */ export default function addImage(pool) { const access = resolveCategoryAccess(pool); + /** + * Adds an image to a category, processing it into AVIF full and thumb variants. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} categoryId - The id of the category. + * @param {Buffer} image - The source image buffer. + * @returns {Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>} The added image info. + * + * @example + * const result = await store.category.addImage(42, 5, imageBuffer); + * // => { id, fullHash, thumbW, thumbH, thumbHash } + */ return async (googleAuthId, categoryId, image) => { await access(googleAuthId, categoryId); diff --git a/src/store/category/createUnderParent.js b/src/store/category/createUnderParent.js index 5247439..4bd4382 100644 --- a/src/store/category/createUnderParent.js +++ b/src/store/category/createUnderParent.js @@ -7,20 +7,22 @@ import resolveCategoryAccess from './resolveCategoryAccess.js'; import request from '../request.js'; import slugify from '../slugify.js'; -/** - * Creates a new category under a given parent category. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that creates a category. - * - * @example - * const fn = createUnderParent(pool); - * const category = await fn({ googleAuthId: 42, parentId: 5, name: 'Drinks' }); - * // => { id, parentId, sort, name, slug } - */ export default function createUnderParent(pool) { const access = resolveCategoryAccess(pool); + /** + * Creates a new category under a given parent category. + * + * @param {object} input - The category creation input. + * @param {number} input.googleAuthId - The id of the Google auth user. + * @param {number} input.parentId - The id of the parent category. + * @param {string} input.name - The category name. + * @returns {Promise<{id: number, parentId: number, sort: number, name: string, slug: string}>} The created category. + * + * @example + * const category = await store.category.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Drinks' }); + * // => { id, parentId, sort, name, slug } + */ return async ({ googleAuthId, parentId, name }) => { const { languageId } = await access(googleAuthId, parentId); diff --git a/src/store/category/delete.js b/src/store/category/delete.js index 0514148..b4cbf8e 100644 --- a/src/store/category/delete.js +++ b/src/store/category/delete.js @@ -1,19 +1,21 @@ import { DELETE } from '../../sql/category.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; -/** - * Deletes a category, refusing to delete the company's root category. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes a category. - * - * @example - * const fn = del(pool); - * await fn({ googleAuthId: 42, id: 5 }); - */ export default function del(pool) { const access = resolveCategoryAccess(pool); + /** + * Deletes a category, refusing to delete the company's root category. + * + * @param {object} input - The delete input. + * @param {number} input.googleAuthId - The id of the Google auth user. + * @param {number} input.id - The id of the category to delete. + * @returns {Promise} + * @throws {Error} If the category is the root category. + * + * @example + * await store.category.delete({ googleAuthId: 42, id: 5 }); + */ return async ({ googleAuthId, id }) => { const { rootCategoryId } = await access(googleAuthId, id); diff --git a/src/store/category/deleteImage.js b/src/store/category/deleteImage.js index 90f346c..acafd2f 100644 --- a/src/store/category/deleteImage.js +++ b/src/store/category/deleteImage.js @@ -1,20 +1,21 @@ import { DELETE_BY_CATEGORY } from '../../sql/categorypic.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; -/** - * Deletes the image of a category. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes a category image. - * - * @example - * const fn = deleteImage(pool); - * const result = await fn(42, 5); - * // => { id, thumbW: null, thumbH: null, thumbHash: null } - */ export default function deleteImage(pool) { const access = resolveCategoryAccess(pool); + /** + * Deletes the image of a category. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} categoryId - The id of the category. + * @returns {Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>} The cleared image info. + * @throws {Error} If the image is not found. + * + * @example + * const result = await store.category.deleteImage(42, 5); + * // => { id: 5, thumbW: null, thumbH: null, thumbHash: null } + */ return async (googleAuthId, categoryId) => { await access(googleAuthId, categoryId); diff --git a/src/store/category/getCategoryArticles.js b/src/store/category/getCategoryArticles.js index 3009164..8645eb3 100644 --- a/src/store/category/getCategoryArticles.js +++ b/src/store/category/getCategoryArticles.js @@ -1,21 +1,21 @@ import resolveCategoryAccess from './resolveCategoryAccess.js'; import queryCategoryArticles from './queryCategoryArticles.js'; -/** - * Returns the articles belonging to a single category. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches the articles of a category. - * - * @example - * const fn = getCategoryArticles(pool); - * const articles = await fn(42, 5); - * // => [{ id, sku, name, ... }] - */ export default function getCategoryArticles(pool) { const access = resolveCategoryAccess(pool); const articles = queryCategoryArticles(pool); + /** + * Returns the articles belonging to a single category. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} categoryId - The id of the category. + * @returns {Promise>} The articles in the category. + * + * @example + * const articles = await store.category.getCategoryArticles(42, 5); + * // => [{ id, sku, name, ... }] + */ return async (googleAuthId, categoryId) => { const { languageId, posCustomerGroupId } = await access( googleAuthId, diff --git a/src/store/category/getTreeForCompany.js b/src/store/category/getTreeForCompany.js index 97e6150..309aa5a 100644 --- a/src/store/category/getTreeForCompany.js +++ b/src/store/category/getTreeForCompany.js @@ -3,23 +3,22 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA import queryCategoryArticles from './queryCategoryArticles.js'; import getCategoryPosterminalIds from '../posterminal/getCategoryPosterminalIds.js'; -/** - * Returns the full category tree for a company together with its articles and - * the posterminal ids assigned to each category. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches the category tree. - * - * @example - * const fn = getTreeForCompany(pool); - * const tree = await fn(42); - * // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] } - */ export default function getTreeForCompany(pool) { const access = resolveSelectedCompanyAccess(pool); const articles = queryCategoryArticles(pool); const posterminalIds = getCategoryPosterminalIds(pool); + /** + * Returns the full category tree for the user's selected company together + * with its articles and the posterminal ids assigned to each category. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise<{languageId: number, tree: Array, articles: Array}>} The category tree, articles and language. + * + * @example + * const tree = await store.category.getTreeForCompany(42); + * // => { languageId, tree: [{ id, posterminalIds, ... }], articles: [...] } + */ return async (googleAuthId) => { const { companyId, rootCategoryId, languageId, posCustomerGroupId } = await access(googleAuthId); diff --git a/src/store/category/move.js b/src/store/category/move.js index 88242ae..11fb3b2 100644 --- a/src/store/category/move.js +++ b/src/store/category/move.js @@ -6,20 +6,23 @@ import { import request from '../request.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; -/** - * Moves a category to a new parent, preventing cycles and cross-company moves. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that moves a category. - * - * @example - * const fn = move(pool); - * const result = await fn({ googleAuthId: 42, id: 5, parentId: 9 }); - * // => { id, parentId, sort } - */ export default function move(pool) { const access = resolveCategoryAccess(pool); + /** + * Moves a category to a new parent, preventing cycles and cross-company moves. + * + * @param {object} input - The move input. + * @param {number} input.googleAuthId - The id of the Google auth user. + * @param {number} input.id - The id of the category to move. + * @param {number} input.parentId - The id of the new parent category. + * @returns {Promise<{id: number, parentId: number, sort: number}>} The moved category. + * @throws {Error} If the move would create a cycle or cross companies. + * + * @example + * const result = await store.category.move({ googleAuthId: 42, id: 5, parentId: 9 }); + * // => { id, parentId, sort } + */ return async ({ googleAuthId, id, parentId }) => { // Source category auth (throws if missing / not authorized). const source = await access(googleAuthId, id); diff --git a/src/store/category/order.js b/src/store/category/order.js index 376067f..38a7473 100644 --- a/src/store/category/order.js +++ b/src/store/category/order.js @@ -5,20 +5,23 @@ import { import request from '../request.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; -/** - * Reorders the child categories of a given parent. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that reorders categories. - * - * @example - * const fn = order(pool); - * const result = await fn({ googleAuthId: 42, parentId: 5, categoryIds: [3, 1, 2] }); - * // => { categoryIds: [3, 1, 2] } - */ export default function order(pool) { const access = resolveCategoryAccess(pool); + /** + * Reorders the child categories of a given parent. + * + * @param {object} input - The reorder input. + * @param {number} input.googleAuthId - The id of the Google auth user. + * @param {number} input.parentId - The id of the parent category. + * @param {Array} input.categoryIds - The child category ids in their new order. + * @returns {Promise<{categoryIds: Array}>} The applied order. + * @throws {Error} If the order is invalid. + * + * @example + * const result = await store.category.order({ googleAuthId: 42, parentId: 5, categoryIds: [3, 1, 2] }); + * // => { categoryIds: [3, 1, 2] } + */ return async ({ googleAuthId, parentId, categoryIds }) => { if (!Array.isArray(categoryIds)) { throw new Error('Invalid category order'); diff --git a/src/store/category/rename.js b/src/store/category/rename.js index 554ea21..86d863b 100644 --- a/src/store/category/rename.js +++ b/src/store/category/rename.js @@ -2,20 +2,23 @@ import { UPDATE_BY_CATEGORY_AND_LANGUAGE } from '../../sql/categorytext.js'; import resolveCategoryAccess from './resolveCategoryAccess.js'; import slugify from '../slugify.js'; -/** - * Renames a category and updates its slug. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that renames a category. - * - * @example - * const fn = rename(pool); - * const result = await fn({ googleAuthId: 42, id: 5, name: 'Soft Drinks' }); - * // => { id, name, slug } - */ export default function rename(pool) { const access = resolveCategoryAccess(pool); + /** + * Renames a category and updates its slug. + * + * @param {object} input - The rename input. + * @param {number} input.googleAuthId - The id of the Google auth user. + * @param {number} input.id - The id of the category to rename. + * @param {string} input.name - The new category name. + * @returns {Promise<{id: number, name: string, slug: string}>} The renamed category. + * @throws {Error} If the name is empty or the category is not found. + * + * @example + * const result = await store.category.rename({ googleAuthId: 42, id: 5, name: 'Soft Drinks' }); + * // => { id, name, slug } + */ return async ({ googleAuthId, id, name }) => { if (!name?.trim()) { throw new Error('Name required'); diff --git a/src/store/companysetting/delete.js b/src/store/companysetting/delete.js index b0fde6d..3b9046d 100644 --- a/src/store/companysetting/delete.js +++ b/src/store/companysetting/delete.js @@ -1,19 +1,20 @@ import { DELETE_BY_COMPANY_AND_KEY } from '../../sql/companysetting.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Deletes a company setting by key. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes a company setting. - * - * @example - * const fn = del(pool); - * await fn(42, 'theme'); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Deletes a company setting by key for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} key - The setting key. + * @returns {Promise} + * @throws {Error} If the setting is not found. + * + * @example + * await store.companysetting.delete(42, 'theme'); + */ return async (googleAuthId, key) => { if (!key?.trim()) { throw new Error('Setting not found'); diff --git a/src/store/companysetting/get.js b/src/store/companysetting/get.js index ce4b7e2..470db46 100644 --- a/src/store/companysetting/get.js +++ b/src/store/companysetting/get.js @@ -2,20 +2,22 @@ import { GET_BY_COMPANY_AND_KEY } from '../../sql/companysetting.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shapeSetting from './shapeSetting.js'; -/** - * Returns a single company setting by key. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches a company setting. - * - * @example - * const fn = get(pool); - * const setting = await fn(42, 'theme'); - * // => { key, value, frontend } - */ export default function get(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Returns a single company setting by key for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} key - The setting key. + * @param {object} [options] - Optional options. + * @param {boolean} [options.frontendOnly] - Only return the setting if it is frontend-visible. + * @returns {Promise<{key: string, value: string, frontend: boolean}|undefined>} The setting, or `undefined` if not found. + * + * @example + * const setting = await store.companysetting.get(42, 'theme'); + * // => { key, value, frontend } + */ return async (googleAuthId, key, { frontendOnly = false } = {}) => { if (!key?.trim()) { return undefined; diff --git a/src/store/companysetting/list.js b/src/store/companysetting/list.js index c367e74..ec97126 100644 --- a/src/store/companysetting/list.js +++ b/src/store/companysetting/list.js @@ -5,20 +5,21 @@ import { import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shapeSetting from './shapeSetting.js'; -/** - * Lists the settings of a company, optionally only frontend-visible ones. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists company settings. - * - * @example - * const fn = list(pool); - * const settings = await fn(42, { frontendOnly: true }); - * // => [{ key, value, frontend }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Lists the settings of the user's selected company, optionally only frontend-visible ones. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [options] - Optional options. + * @param {boolean} [options.frontendOnly] - Only return frontend-visible settings. + * @returns {Promise>} The settings. + * + * @example + * const settings = await store.companysetting.list(42, { frontendOnly: true }); + * // => [{ key, value, frontend }] + */ return async (googleAuthId, { frontendOnly = false } = {}) => { const { companyId } = await access(googleAuthId); diff --git a/src/store/companysetting/set.js b/src/store/companysetting/set.js index ebb98f0..a2a5173 100644 --- a/src/store/companysetting/set.js +++ b/src/store/companysetting/set.js @@ -2,20 +2,24 @@ import { GET_BY_COMPANY_AND_KEY, UPSERT } from '../../sql/companysetting.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shapeSetting from './shapeSetting.js'; -/** - * Sets (upserts) a company setting by key. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that sets a company setting. - * - * @example - * const fn = set(pool); - * const setting = await fn(42, { key: 'theme', value: 'dark', frontend: true }); - * // => { key, value, frontend } - */ export default function set(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Sets (upserts) a company setting by key for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The setting input. + * @param {string} input.key - The setting key. + * @param {string} input.value - The setting value. + * @param {boolean} [input.frontend] - Whether the setting is frontend-visible. + * @returns {Promise<{key: string, value: string, frontend: boolean}>} The upserted setting. + * @throws {Error} If the input is invalid. + * + * @example + * const setting = await store.companysetting.set(42, { key: 'theme', value: 'dark', frontend: true }); + * // => { key, value, frontend } + */ return async (googleAuthId, input = {}) => { const key = input.key?.trim(); if (!key) { diff --git a/src/store/companyuser/resolveSelectedCompanyAccess.js b/src/store/companyuser/resolveSelectedCompanyAccess.js index 5bba71c..fb7c629 100644 --- a/src/store/companyuser/resolveSelectedCompanyAccess.js +++ b/src/store/companyuser/resolveSelectedCompanyAccess.js @@ -1,14 +1,16 @@ 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); + /** + * Resolves the user's selected company and checks company membership. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise<{companyId: number, rootCategoryId: number, languageId: number, posCustomerGroupId: number|null, companyuserId: number}>} The resolved company access context. + * @throws {Error} If the user is not authorized or has no company selected. + */ return async (googleAuthId) => { if (!Number.isFinite(googleAuthId)) { throw new Error('Not authorized'); diff --git a/src/store/customergroup/create.js b/src/store/customergroup/create.js index 7547a78..a7ac90e 100644 --- a/src/store/customergroup/create.js +++ b/src/store/customergroup/create.js @@ -8,20 +8,22 @@ function shape(row) { return { id: row.id, name: row.name, sort: row.sort }; } -/** - * Creates a new customer group for a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that creates a customer group. - * - * @example - * const fn = create(pool); - * const group = await fn(42, { name: 'Wholesale' }); - * // => { id, name, sort } - */ export default function create(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Creates a new customer group for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The customer group input. + * @param {string} input.name - The customer group name (must not be a reserved name). + * @returns {Promise<{id: number, name: string, sort: number}>} The created customer group. + * @throws {Error} If the name is empty or reserved. + * + * @example + * const group = await store.customergroup.create(42, { name: 'Wholesale' }); + * // => { id, name, sort } + */ return async (googleAuthId, { name } = {}) => { const cleaned = name?.trim(); if (!cleaned) { diff --git a/src/store/customergroup/delete.js b/src/store/customergroup/delete.js index 779754b..e534dc6 100644 --- a/src/store/customergroup/delete.js +++ b/src/store/customergroup/delete.js @@ -1,19 +1,20 @@ import { DELETE_BY_COMPANY_AND_ID } from '../../sql/customergroup.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Deletes a customer group for a company by its id. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes a customer group. - * - * @example - * const fn = del(pool); - * await fn(42, 5); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Deletes a customer group for the user's selected company by its id. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the customer group to delete. + * @returns {Promise} + * @throws {Error} If the customer group is not found. + * + * @example + * await store.customergroup.delete(42, 5); + */ return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Customer group not found'); diff --git a/src/store/customergroup/list.js b/src/store/customergroup/list.js index d70553e..b4074c5 100644 --- a/src/store/customergroup/list.js +++ b/src/store/customergroup/list.js @@ -1,20 +1,19 @@ import { LIST_BY_COMPANY } from '../../sql/customergroup.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Lists the customer groups of a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists customer groups. - * - * @example - * const fn = list(pool); - * const groups = await fn(42); - * // => [{ id, name, sort }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Lists the customer groups of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise>} The customer groups. + * + * @example + * const groups = await store.customergroup.list(42); + * // => [{ id, name, sort }] + */ return async (googleAuthId) => { const { companyId } = await access(googleAuthId); diff --git a/src/store/customergroup/order.js b/src/store/customergroup/order.js index a1ca8ba..272bf36 100644 --- a/src/store/customergroup/order.js +++ b/src/store/customergroup/order.js @@ -5,20 +5,22 @@ import { import request from '../request.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Reorders the customer groups of a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that reorders customer groups. - * - * @example - * const fn = order(pool); - * const result = await fn(42, { customergroupIds: [3, 1, 2] }); - * // => { customergroupIds: [3, 1, 2] } - */ export default function order(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Reorders the customer groups of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The reorder input. + * @param {Array} input.customergroupIds - The customer group ids in their new order. + * @returns {Promise<{customergroupIds: Array}>} The applied order. + * @throws {Error} If the order is invalid. + * + * @example + * const result = await store.customergroup.order(42, { customergroupIds: [3, 1, 2] }); + * // => { customergroupIds: [3, 1, 2] } + */ return async (googleAuthId, { customergroupIds } = {}) => { if (!Array.isArray(customergroupIds)) { throw new Error('Invalid customer group order'); diff --git a/src/store/customergroup/update.js b/src/store/customergroup/update.js index ce24516..0711dfa 100644 --- a/src/store/customergroup/update.js +++ b/src/store/customergroup/update.js @@ -8,20 +8,23 @@ function shape(row) { return { id: row.id, name: row.name, sort: row.sort }; } -/** - * Updates the name of a customer group for a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that updates a customer group. - * - * @example - * const fn = update(pool); - * const group = await fn(42, 5, { name: 'Retail' }); - * // => { id, name, sort } - */ export default function update(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Updates the name of a customer group for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the customer group to update. + * @param {object} input - The update input. + * @param {string} input.name - The new customer group name. + * @returns {Promise<{id: number, name: string, sort: number}>} The updated customer group. + * @throws {Error} If the customer group is not found or the name is empty. + * + * @example + * const group = await store.customergroup.update(42, 5, { name: 'Retail' }); + * // => { id, name, sort } + */ return async (googleAuthId, id, { name } = {}) => { if (!Number.isFinite(id)) { throw new Error('Customer group not found'); diff --git a/src/store/delivery/create.js b/src/store/delivery/create.js index a9b1441..3d3e4dc 100644 --- a/src/store/delivery/create.js +++ b/src/store/delivery/create.js @@ -2,31 +2,33 @@ import { CREATE_MANIFEST, DELIVERY_TYPE } from '../../sql/delivery.js'; import request from '../request.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import taxEnsure from '../tax/ensure.js'; -import { - loadDelivery, - normalizeLines, - parseDeliveryDate, -} from './helpers.js'; -import { assertSupplier, insertLines } from './writeLines.js'; +import { insertLines, normalizeLines, parseDate } from '../manifestLines.js'; +import { loadDelivery } from './helpers.js'; +import { assertSupplier } from './writeLines.js'; -/** - * Creates a new delivery for a company with its lines. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that creates a delivery. - * - * @example - * const fn = create(pool); - * 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 = resolveSelectedCompanyAccess(pool); const ensureTax = taxEnsure(pool); + /** + * Creates a new delivery for the user's selected company with its lines. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} body - The delivery input. + * @param {string} body.number - The delivery number. + * @param {string} body.date - The delivery date. + * @param {number} body.supplierId - The id of the supplier. + * @param {Array} [body.lines] - The delivery lines. + * @returns {Promise<{id: number, number: string, type: string, entityId: number, date: string, lines: Array}>} The created delivery. + * @throws {Error} If the delivery is invalid. + * + * @example + * const delivery = await store.delivery.create(42, { number: 'D-1', date: '2026-01-01', supplierId: 5, lines: [...] }); + * // => { id, number, type, entityId, date, lines: [...] } + */ return async (googleAuthId, body = {}) => { const number = body.number?.trim?.() ? body.number.trim() : null; - const date = parseDeliveryDate(body.date); + const date = parseDate(body.date); if (!number || !date) { throw new Error('Invalid delivery'); } diff --git a/src/store/delivery/delete.js b/src/store/delivery/delete.js index 145e522..2eb653e 100644 --- a/src/store/delivery/delete.js +++ b/src/store/delivery/delete.js @@ -1,19 +1,20 @@ import { DELETE_MANIFEST, DELIVERY_TYPE } from '../../sql/delivery.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Deletes a delivery by its id within a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes a delivery. - * - * @example - * const fn = del(pool); - * await fn(42, 5); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Deletes a delivery by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the delivery to delete. + * @returns {Promise} + * @throws {Error} If the delivery is not found. + * + * @example + * await store.delivery.delete(42, 5); + */ return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Delivery not found'); diff --git a/src/store/delivery/getById.js b/src/store/delivery/getById.js index cdbef6c..9885a0a 100644 --- a/src/store/delivery/getById.js +++ b/src/store/delivery/getById.js @@ -1,20 +1,21 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import { loadDelivery } from './helpers.js'; -/** - * Returns a single delivery by its id within a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches a delivery by id. - * - * @example - * const fn = getById(pool); - * const delivery = await fn(42, 5); - * // => { id, number, type, entityId, date, lines: [...] } - */ export default function getById(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Returns a single delivery by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the delivery. + * @returns {Promise<{id: number, number: string, type: string, entityId: number, date: string, lines: Array}>} The delivery. + * @throws {Error} If the delivery is not found. + * + * @example + * const delivery = await store.delivery.getById(42, 5); + * // => { id, number, type, entityId, date, lines: [...] } + */ return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Delivery not found'); diff --git a/src/store/delivery/helpers.js b/src/store/delivery/helpers.js index a46fc71..33517b0 100644 --- a/src/store/delivery/helpers.js +++ b/src/store/delivery/helpers.js @@ -4,6 +4,7 @@ import { GET_DELIVERY_LINES, } from '../../sql/delivery.js'; import request from '../request.js'; +import { shapeLine } from '../manifestLines.js'; export function shapeSummary(row) { return { @@ -15,19 +16,6 @@ export function shapeSummary(row) { }; } -export function shapeLine(row) { - return { - id: row.id, - articleId: row.articleId ?? null, - sku: row.sku ?? null, - amount: row.amount, - net: row.net, - rebate: row.rebate, - tax: row.tax, - comment: row.comment ?? null, - }; -} - export async function loadDelivery(pool, tx, companyId, id) { const header = await request(pool, tx) .input('id', id) @@ -49,50 +37,3 @@ export async function loadDelivery(pool, tx, companyId, id) { lines: lines.recordset.map(shapeLine), }; } - -export function parseDeliveryDate(value) { - if (value instanceof Date && !Number.isNaN(value.getTime())) { - return value; - } - if (typeof value === 'string' && value.trim()) { - const d = new Date(value); - if (!Number.isNaN(d.getTime())) { - return d; - } - } - return null; -} - -export function normalizeLines(lines) { - if (!Array.isArray(lines)) { - throw new Error('Invalid lines'); - } - - return lines.map((entry) => { - const sku = entry?.sku?.trim?.() ? entry.sku.trim() : null; - if (!sku) { - throw new Error('Invalid lines'); - } - - const amount = entry?.amount; - const net = entry?.net; - const rebate = - entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate; - const tax = entry?.tax; - const comment = - entry?.comment === undefined || entry?.comment === null - ? null - : String(entry.comment); - - if ( - !Number.isFinite(amount) || - !Number.isFinite(net) || - !Number.isFinite(rebate) || - !Number.isFinite(tax) - ) { - throw new Error('Invalid lines'); - } - - return { sku, amount, net, rebate, tax, comment }; - }); -} diff --git a/src/store/delivery/list.js b/src/store/delivery/list.js index b66ca06..60ca299 100644 --- a/src/store/delivery/list.js +++ b/src/store/delivery/list.js @@ -2,20 +2,19 @@ import { DELIVERY_TYPE, LIST_DELIVERIES } from '../../sql/delivery.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import { shapeSummary } from './helpers.js'; -/** - * Lists the deliveries of a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists deliveries. - * - * @example - * const fn = list(pool); - * const deliveries = await fn(42); - * // => [{ id, number, type, entityId, date }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Lists the deliveries of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise>} The deliveries. + * + * @example + * const deliveries = await store.delivery.list(42); + * // => [{ id, number, type, entityId, date }] + */ return async (googleAuthId) => { const { companyId } = await access(googleAuthId); diff --git a/src/store/delivery/update.js b/src/store/delivery/update.js index d7162c5..03dccfa 100644 --- a/src/store/delivery/update.js +++ b/src/store/delivery/update.js @@ -7,29 +7,31 @@ import { import request from '../request.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import taxEnsure from '../tax/ensure.js'; -import { - loadDelivery, - normalizeLines, - parseDeliveryDate, - shapeSummary, -} from './helpers.js'; -import { assertSupplier, insertLines } from './writeLines.js'; +import { insertLines, normalizeLines, parseDate } from '../manifestLines.js'; +import { loadDelivery, shapeSummary } from './helpers.js'; +import { assertSupplier } from './writeLines.js'; -/** - * Updates a delivery's header and/or lines. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that updates a delivery. - * - * @example - * const fn = update(pool); - * const delivery = await fn(42, 5, { number: 'D-2', lines: [...] }); - * // => { id, number, type, entityId, date, lines: [...] } - */ export default function update(pool) { const access = resolveSelectedCompanyAccess(pool); const ensureTax = taxEnsure(pool); + /** + * Updates a delivery's header and/or lines. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the delivery to update. + * @param {object} patch - The update patch. + * @param {string} [patch.number] - The new delivery number. + * @param {string} [patch.date] - The new delivery date. + * @param {number} [patch.supplierId] - The new supplier id. + * @param {Array} [patch.lines] - The new delivery lines. + * @returns {Promise<{id: number, number: string, type: string, entityId: number, date: string, lines: Array}>} The updated delivery. + * @throws {Error} If the delivery is not found. + * + * @example + * const delivery = await store.delivery.update(42, 5, { number: 'D-2', lines: [...] }); + * // => { id, number, type, entityId, date, lines: [...] } + */ return async (googleAuthId, id, patch = {}) => { if (!Number.isFinite(id)) { throw new Error('Delivery not found'); @@ -81,7 +83,7 @@ export default function update(pool) { let date = summary.date; if (hasDate) { - date = parseDeliveryDate(patch.date); + date = parseDate(patch.date); if (!date) { throw new Error('Invalid delivery'); } diff --git a/src/store/delivery/writeLines.js b/src/store/delivery/writeLines.js index a384df8..c1613f3 100644 --- a/src/store/delivery/writeLines.js +++ b/src/store/delivery/writeLines.js @@ -1,7 +1,3 @@ -import { - CREATE_LINE, - RESOLVE_ARTICLE_ID_BY_SKU, -} from '../../sql/delivery.js'; import { GET_SUPPLIER_BY_ID, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; import request from '../request.js'; @@ -24,42 +20,3 @@ export async function assertSupplier(pool, tx, companyId, supplierId) { } return supplierId; } - -async function resolveArticleId(pool, tx, rootCategoryId, sku) { - const result = await request(pool, tx) - .input('rootCategoryId', rootCategoryId) - .input('sku', sku) - .query(RESOLVE_ARTICLE_ID_BY_SKU); - - const articleId = result.recordset[0]?.id; - if (!articleId) { - throw new Error('Article not found'); - } - return articleId; -} - -export async function insertLines( - pool, - tx, - { manifestId, rootCategoryId, lines, ensureTax }, -) { - for (const line of lines) { - const articleId = await resolveArticleId( - pool, - tx, - rootCategoryId, - line.sku, - ); - const taxId = await ensureTax(line.tax, tx); - - await request(pool, tx) - .input('manifestId', manifestId) - .input('articleId', articleId) - .input('amount', line.amount) - .input('net', line.net) - .input('comment', line.comment) - .input('rebate', line.rebate) - .input('taxId', taxId) - .query(CREATE_LINE); - } -} diff --git a/src/store/entity/create.js b/src/store/entity/create.js index 3b64bce..66a474b 100644 --- a/src/store/entity/create.js +++ b/src/store/entity/create.js @@ -6,24 +6,29 @@ import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAcc 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, { customergroupId: 5, name: 'Doe', firstName: 'John' }); - * // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } - */ export default function create(pool) { const access = resolveSelectedCompanyAccess(pool); const groupAccess = resolveCustomerGroupAccess(pool); const allocateNumber = nextNumber(pool); + /** + * Creates a new entity (customer) in a customer group, recording history. + * Optional `number`; if omitted, allocates via `getnextnumberwithprefixandpadding` + * (`type` `entity`). + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The entity creation input. + * @param {number} input.customergroupId - The id of the customer group. + * @param {string} input.name - The entity name. + * @param {string} [input.firstName] - The entity first name. + * @param {string} [input.number] - Optional explicit customer number. + * @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The created entity. + * @throws {Error} If the name is empty. + * + * @example + * const entity = await store.entity.create(42, { customergroupId: 5, name: 'Doe', firstName: 'John' }); + * // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } + */ return async (googleAuthId, { customergroupId, name, firstName = null, number } = {}) => { const cleaned = name?.trim(); if (!cleaned) { diff --git a/src/store/entity/delete.js b/src/store/entity/delete.js index 6a6ee38..96d096e 100644 --- a/src/store/entity/delete.js +++ b/src/store/entity/delete.js @@ -1,19 +1,20 @@ import { DELETE_BY_COMPANY_AND_ID } from '../../sql/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Deletes an entity by its id within a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes an entity. - * - * @example - * const fn = del(pool); - * await fn(42, 5); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Deletes an entity by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the entity to delete. + * @returns {Promise} + * @throws {Error} If the entity is not found. + * + * @example + * await store.entity.delete(42, 5); + */ return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Entity not found'); diff --git a/src/store/entity/getById.js b/src/store/entity/getById.js index dcc3ffc..4190608 100644 --- a/src/store/entity/getById.js +++ b/src/store/entity/getById.js @@ -2,20 +2,21 @@ import { GET_BY_COMPANY_AND_ID } from '../../sql/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shape from './shape.js'; -/** - * Returns a single entity by its id within a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches an entity by id. - * - * @example - * const fn = getById(pool); - * const entity = await fn(42, 5); - * // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } - */ export default function getById(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Returns a single entity by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the entity. + * @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The entity. + * @throws {Error} If the entity is not found. + * + * @example + * const entity = await store.entity.getById(42, 5); + * // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } + */ return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Entity not found'); diff --git a/src/store/entity/isNumberFree.js b/src/store/entity/isNumberFree.js index 8188c37..94fd267 100644 --- a/src/store/entity/isNumberFree.js +++ b/src/store/entity/isNumberFree.js @@ -1,23 +1,26 @@ 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); + /** + * Returns whether a customer number is free within the user's selected company. + * Pass `excludeEntityId` when editing so the entity's own number counts as free. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} number - The customer number to check. + * @param {object} [options] - Optional options. + * @param {number} [options.excludeEntityId] - An entity id to exclude from the check. + * @returns {Promise} Whether the number is free. + * @throws {Error} If the number is empty. + * + * @example + * const free = await store.entity.isNumberFree(42, 'C-100'); + * // => true + * const freeOnEdit = await store.entity.isNumberFree(42, 'C-100', { excludeEntityId: 5 }); + * // => true + */ return async (googleAuthId, number, { excludeEntityId } = {}) => { const cleaned = number == null || number === '' ? null : String(number).trim() || null; diff --git a/src/store/entity/list.js b/src/store/entity/list.js index 69a9c06..ad3d9c1 100644 --- a/src/store/entity/list.js +++ b/src/store/entity/list.js @@ -9,22 +9,25 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA import resolveCustomerGroupAccess from '../customergroup/resolveCustomerGroupAccess.js'; import shape from './shape.js'; -/** - * Lists the entities (customers) of a company, optionally filtered by customer - * group or restricted to the 'customer' role. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists entities. - * - * @example - * const fn = list(pool); - * const entities = await fn(42, { customergroupId: 5 }); - * // => [{ id, customergroupId, firstName, name, number, createdAt, updatedAt }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); const groupAccess = resolveCustomerGroupAccess(pool); + /** + * Lists the entities (customers) of the user's selected company, optionally + * filtered by customer group or restricted to the 'customer' role. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [options] - Optional options. + * @param {number} [options.customergroupId] - Filter by customer group id. + * @param {string} [options.role] - Restrict to `'customer'` role if set. + * @returns {Promise>} The entities. + * @throws {Error} If the role is invalid. + * + * @example + * const entities = await store.entity.list(42, { customergroupId: 5 }); + * // => [{ id, customergroupId, firstName, name, number, createdAt, updatedAt }] + */ return async (googleAuthId, { customergroupId, role } = {}) => { const { companyId } = await access(googleAuthId); diff --git a/src/store/entity/peekNextNumber.js b/src/store/entity/peekNextNumber.js index 076659e..f2b941a 100644 --- a/src/store/entity/peekNextNumber.js +++ b/src/store/entity/peekNextNumber.js @@ -1,22 +1,21 @@ 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); + /** + * Returns the next customer number that would be allocated for the user's + * selected company (`numbers` type `entity`) without consuming it. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise} The next entity number. + * + * @example + * const number = await store.entity.peekNextNumber(42); + * // => '1' + */ return async (googleAuthId) => { const { companyId } = await access(googleAuthId); return peek(companyId, 'entity'); diff --git a/src/store/entity/update.js b/src/store/entity/update.js index 54138f8..c876667 100644 --- a/src/store/entity/update.js +++ b/src/store/entity/update.js @@ -4,21 +4,26 @@ import request from '../request.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shape from './shape.js'; -/** - * 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, 5, { name: 'Doe', firstName: 'Jane', number: 'C-42' }); - * // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } - */ export default function update(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * 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 {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the entity to update. + * @param {object} patch - The update patch. + * @param {string} [patch.name] - The new entity name. + * @param {string} [patch.firstName] - The new first name. + * @param {string} [patch.number] - The new customer number. + * @returns {Promise<{id: number, customergroupId: number, firstName: string|null, name: string, number: string, createdAt: string, updatedAt: string}>} The updated entity. + * @throws {Error} If no valid field is provided or the entity is not found. + * + * @example + * const entity = await store.entity.update(42, 5, { name: 'Doe', firstName: 'Jane', number: 'C-42' }); + * // => { id, customergroupId, firstName, name, number, createdAt, updatedAt } + */ return async (googleAuthId, id, patch = {}) => { if (!Number.isFinite(id)) { throw new Error('Entity not found'); diff --git a/src/store/entityaddress/create.js b/src/store/entityaddress/create.js index 752a4b8..f80b629 100644 --- a/src/store/entityaddress/create.js +++ b/src/store/entityaddress/create.js @@ -24,21 +24,32 @@ function optionalString(value) { return s || null; } -/** - * Creates a new address for an entity. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that creates an entity address. - * - * @example - * const fn = create(pool); - * 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 = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); + /** + * Creates a new address for an entity. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} entityId - The id of the entity. + * @param {object} input - The address input. + * @param {string} input.addressType - The address type (e.g. `'billing'`). + * @param {string} [input.firstName] - The first name. + * @param {string} [input.name] - The name. + * @param {string} [input.street] - The street. + * @param {string} [input.streetNumber] - The street number. + * @param {string} [input.zip] - The zip code. + * @param {string} [input.city] - The city. + * @param {string} [input.county] - The county. + * @param {number} [input.sort] - Optional sort order. + * @returns {Promise} The created address. + * @throws {Error} If the address type is missing. + * + * @example + * const address = await store.entityaddress.create(42, 5, { addressType: 'billing', street: 'Main St', city: 'Berlin' }); + * // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort } + */ return async (googleAuthId, entityId, input = {}) => { const addressType = input.addressType?.trim?.() ? input.addressType.trim() diff --git a/src/store/entityaddress/delete.js b/src/store/entityaddress/delete.js index 7e3613b..bf6f27d 100644 --- a/src/store/entityaddress/delete.js +++ b/src/store/entityaddress/delete.js @@ -2,20 +2,22 @@ import { DELETE_BY_ENTITY_AND_ID } from '../../sql/entityaddress.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; -/** - * Deletes an address of an entity. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes an entity address. - * - * @example - * const fn = del(pool); - * await fn(42, 5, 9); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); + /** + * Deletes an address of an entity. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} entityId - The id of the entity. + * @param {number} id - The id of the address to delete. + * @returns {Promise} + * @throws {Error} If the address is not found. + * + * @example + * await store.entityaddress.delete(42, 5, 9); + */ return async (googleAuthId, entityId, id) => { if (!Number.isFinite(id)) { throw new Error('Address not found'); diff --git a/src/store/entityaddress/list.js b/src/store/entityaddress/list.js index 61fde5d..7b54aa5 100644 --- a/src/store/entityaddress/list.js +++ b/src/store/entityaddress/list.js @@ -18,21 +18,21 @@ function shape(row) { }; } -/** - * Lists the addresses of an entity. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists entity addresses. - * - * @example - * const fn = list(pool); - * const addresses = await fn(42, 5); - * // => [{ id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); + /** + * Lists the addresses of an entity. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} entityId - The id of the entity. + * @returns {Promise>} The addresses. + * + * @example + * const addresses = await store.entityaddress.list(42, 5); + * // => [{ id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort }] + */ return async (googleAuthId, entityId) => { const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); diff --git a/src/store/entityaddress/update.js b/src/store/entityaddress/update.js index 81e76ff..1979a91 100644 --- a/src/store/entityaddress/update.js +++ b/src/store/entityaddress/update.js @@ -24,21 +24,24 @@ function optionalString(value) { return s || null; } -/** - * Updates an address of an entity. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that updates an entity address. - * - * @example - * const fn = update(pool); - * 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 = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); + /** + * Updates an address of an entity. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} entityId - The id of the entity. + * @param {number} id - The id of the address to update. + * @param {object} patch - The update patch (any subset of address fields). + * @returns {Promise} The updated address. + * @throws {Error} If the address is not found. + * + * @example + * const address = await store.entityaddress.update(42, 5, 9, { city: 'Hamburg' }); + * // => { id, entityId, firstName, name, street, streetNumber, zip, city, county, addressType, sort } + */ return async (googleAuthId, entityId, id, patch = {}) => { if (!Number.isFinite(id)) { throw new Error('Address not found'); diff --git a/src/store/entitycontact/create.js b/src/store/entitycontact/create.js index a080d0d..be2d543 100644 --- a/src/store/entitycontact/create.js +++ b/src/store/entitycontact/create.js @@ -12,21 +12,26 @@ function shape(row) { }; } -/** - * Creates a new contact for an entity. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that creates an entity contact. - * - * @example - * const fn = create(pool); - * const contact = await fn(42, 5, { contactType: 'email', contact: 'a@b.c' }); - * // => { id, entityId, contactType, contact, sort } - */ export default function create(pool) { const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); + /** + * Creates a new contact for an entity. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} entityId - The id of the entity. + * @param {object} input - The contact input. + * @param {string} input.contactType - The contact type (e.g. `'email'`). + * @param {string} input.contact - The contact value. + * @param {number} [input.sort] - Optional sort order. + * @returns {Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>} The created contact. + * @throws {Error} If the contact type or value is missing. + * + * @example + * const contact = await store.entitycontact.create(42, 5, { contactType: 'email', contact: 'a@b.c' }); + * // => { id, entityId, contactType, contact, sort } + */ return async (googleAuthId, entityId, input = {}) => { const contactType = input.contactType?.trim?.() ? input.contactType.trim() diff --git a/src/store/entitycontact/delete.js b/src/store/entitycontact/delete.js index b708d99..f9603c7 100644 --- a/src/store/entitycontact/delete.js +++ b/src/store/entitycontact/delete.js @@ -2,20 +2,22 @@ import { DELETE_BY_ENTITY_AND_ID } from '../../sql/entitycontact.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; -/** - * Deletes a contact of an entity. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes an entity contact. - * - * @example - * const fn = del(pool); - * await fn(42, 5, 9); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); + /** + * Deletes a contact of an entity. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} entityId - The id of the entity. + * @param {number} id - The id of the contact to delete. + * @returns {Promise} + * @throws {Error} If the contact is not found. + * + * @example + * await store.entitycontact.delete(42, 5, 9); + */ return async (googleAuthId, entityId, id) => { if (!Number.isFinite(id)) { throw new Error('Contact not found'); diff --git a/src/store/entitycontact/list.js b/src/store/entitycontact/list.js index 5cd772d..cdb3027 100644 --- a/src/store/entitycontact/list.js +++ b/src/store/entitycontact/list.js @@ -12,21 +12,21 @@ function shape(row) { }; } -/** - * Lists the contacts of an entity. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists entity contacts. - * - * @example - * const fn = list(pool); - * const contacts = await fn(42, 5); - * // => [{ id, entityId, contactType, contact, sort }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); + /** + * Lists the contacts of an entity. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} entityId - The id of the entity. + * @returns {Promise>} The contacts. + * + * @example + * const contacts = await store.entitycontact.list(42, 5); + * // => [{ id, entityId, contactType, contact, sort }] + */ return async (googleAuthId, entityId) => { const { companyId } = await access(googleAuthId); await entityAccess(companyId, entityId); diff --git a/src/store/entitycontact/update.js b/src/store/entitycontact/update.js index 155b000..9c3f241 100644 --- a/src/store/entitycontact/update.js +++ b/src/store/entitycontact/update.js @@ -12,21 +12,24 @@ function shape(row) { }; } -/** - * Updates a contact of an entity. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that updates an entity contact. - * - * @example - * const fn = update(pool); - * const contact = await fn(42, 5, 9, { contact: 'b@c.d' }); - * // => { id, entityId, contactType, contact, sort } - */ export default function update(pool) { const access = resolveSelectedCompanyAccess(pool); const entityAccess = resolveEntityAccess(pool); + /** + * Updates a contact of an entity. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} entityId - The id of the entity. + * @param {number} id - The id of the contact to update. + * @param {object} patch - The update patch (any subset of contact fields). + * @returns {Promise<{id: number, entityId: number, contactType: string, contact: string, sort: number}>} The updated contact. + * @throws {Error} If the contact is not found. + * + * @example + * const contact = await store.entitycontact.update(42, 5, 9, { contact: 'b@c.d' }); + * // => { id, entityId, contactType, contact, sort } + */ return async (googleAuthId, entityId, id, patch = {}) => { if (!Number.isFinite(id)) { throw new Error('Contact not found'); diff --git a/src/store/google_auth/add.js b/src/store/google_auth/add.js index 94e2c5f..d898a4a 100644 --- a/src/store/google_auth/add.js +++ b/src/store/google_auth/add.js @@ -1,17 +1,16 @@ import { ADD } from '../../sql/google_auth.js'; -/** - * Adds a new allowed Google auth email. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that adds an allowed email. - * - * @example - * const fn = add(pool); - * const id = await fn('user@example.com'); - * // => 1 - */ export default function add(pool) { + /** + * Adds a new allowed Google auth email. + * + * @param {string} email - The email address to allow. + * @returns {Promise} The id of the created Google auth row. + * + * @example + * const id = await store.google_auth.add('user@example.com'); + * // => 1 + */ return async (email) => { const result = await pool .request() diff --git a/src/store/google_auth/getByEmail.js b/src/store/google_auth/getByEmail.js index 40f2c24..1e7def7 100644 --- a/src/store/google_auth/getByEmail.js +++ b/src/store/google_auth/getByEmail.js @@ -1,17 +1,16 @@ import { GET_BY_EMAIL } from '../../sql/google_auth.js'; -/** - * Returns a Google auth row by email. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches a Google auth row by email. - * - * @example - * const fn = getByEmail(pool); - * const auth = await fn('user@example.com'); - * // => { id, email, ... } - */ export default function getByEmail(pool) { + /** + * Returns a Google auth row by email. + * + * @param {string} email - The email address. + * @returns {Promise} The Google auth row, or `undefined` if not found. + * + * @example + * const auth = await store.google_auth.getByEmail('user@example.com'); + * // => { id, email, ... } + */ return async (email) => { const result = await pool .request() diff --git a/src/store/google_auth/getById.js b/src/store/google_auth/getById.js index a079b79..49718ae 100644 --- a/src/store/google_auth/getById.js +++ b/src/store/google_auth/getById.js @@ -1,17 +1,16 @@ import { GET_BY_ID } from '../../sql/google_auth.js'; -/** - * Returns a Google auth row by its id. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches a Google auth row by id. - * - * @example - * const fn = getById(pool); - * const auth = await fn(42); - * // => { id, email, ... } - */ export default function getById(pool) { + /** + * Returns a Google auth row by its id. + * + * @param {number} googleAuthId - The id of the Google auth row. + * @returns {Promise} The Google auth row, or `undefined` if not found. + * + * @example + * const auth = await store.google_auth.getById(42); + * // => { id, email, ... } + */ return async (googleAuthId) => { const result = await pool .request() diff --git a/src/store/google_auth/listAllowedEmails.js b/src/store/google_auth/listAllowedEmails.js index 7cddf8d..5252714 100644 --- a/src/store/google_auth/listAllowedEmails.js +++ b/src/store/google_auth/listAllowedEmails.js @@ -1,17 +1,15 @@ import { LIST_ALLOWED_EMAILS } from '../../sql/google_auth.js'; -/** - * Lists all allowed Google auth emails. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists allowed emails. - * - * @example - * const fn = listAllowedEmails(pool); - * const emails = await fn(); - * // => [{ id, email }] - */ export default function listAllowedEmails(pool) { + /** + * Lists all allowed Google auth emails. + * + * @returns {Promise>} The allowed emails. + * + * @example + * const emails = await store.google_auth.listAllowedEmails(); + * // => [{ id, email }] + */ return async () => { const result = await pool.request().query(LIST_ALLOWED_EMAILS); return result.recordset; diff --git a/src/store/google_auth/removeAllowedEmail.js b/src/store/google_auth/removeAllowedEmail.js index 5fe377a..d67dfe6 100644 --- a/src/store/google_auth/removeAllowedEmail.js +++ b/src/store/google_auth/removeAllowedEmail.js @@ -1,16 +1,15 @@ import { REMOVE_ALLOWED_EMAIL } from '../../sql/google_auth.js'; -/** - * Removes an allowed Google auth email. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that removes an allowed email. - * - * @example - * const fn = removeAllowedEmail(pool); - * await fn('user@example.com'); - */ export default function removeAllowedEmail(pool) { + /** + * Removes an allowed Google auth email. + * + * @param {string} email - The email address to remove. + * @returns {Promise} + * + * @example + * await store.google_auth.removeAllowedEmail('user@example.com'); + */ return async (email) => { await pool .request() diff --git a/src/store/google_auth/setProfile.js b/src/store/google_auth/setProfile.js index cdc1259..8619aca 100644 --- a/src/store/google_auth/setProfile.js +++ b/src/store/google_auth/setProfile.js @@ -1,16 +1,20 @@ import { SET_PROFILE } from '../../sql/google_auth.js'; -/** - * Updates the profile (picture, picture binary, sub) of a Google auth user. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that sets the user profile. - * - * @example - * const fn = setProfile(pool); - * await fn(42, { picture: 'url', pictureBinary: buffer, sub: 'sub' }); - */ export default function setProfile(pool) { + /** + * Updates the profile (picture, picture binary, sub) of a Google auth user. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The profile input. + * @param {string} [input.picture] - The picture URL. + * @param {Buffer} [input.pictureBinary] - The picture binary. + * @param {string} [input.sub] - The Google sub identifier. + * @returns {Promise} + * @throws {Error} If the user is not found or the picture binary is invalid. + * + * @example + * await store.google_auth.setProfile(42, { picture: 'url', pictureBinary: buffer, sub: 'sub' }); + */ return async ( googleAuthId, { picture = null, pictureBinary = null, sub = null } = {}, diff --git a/src/store/google_auth/setSelectedCompany.js b/src/store/google_auth/setSelectedCompany.js index d571750..6a3554a 100644 --- a/src/store/google_auth/setSelectedCompany.js +++ b/src/store/google_auth/setSelectedCompany.js @@ -1,16 +1,16 @@ import { SET_SELECTED_COMPANY } from '../../sql/google_auth.js'; -/** - * Sets the currently selected company for a Google auth user. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that sets the selected company. - * - * @example - * const fn = setSelectedCompany(pool); - * await fn(42, 1); - */ export default function setSelectedCompany(pool) { + /** + * Sets the currently selected company for a Google auth user. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} companyId - The id of the company to select. + * @returns {Promise} + * + * @example + * await store.google_auth.setSelectedCompany(42, 1); + */ return async (googleAuthId, companyId) => { await pool .request() diff --git a/src/store/initialize.js b/src/store/initialize.js index 835be2e..34db917 100644 --- a/src/store/initialize.js +++ b/src/store/initialize.js @@ -1,16 +1,14 @@ import { SCHEMA_SCRIPTS } from '../schema.js'; -/** - * Initializes the database schema by running all schema scripts. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that initializes the schema. - * - * @example - * const fn = initialize(pool); - * await fn(); - */ export default function initialize(pool) { + /** + * Initializes the database schema by running all schema scripts. + * + * @returns {Promise} + * + * @example + * await store.initialize(); + */ return async () => { for (const script of SCHEMA_SCRIPTS) { await pool.request().query(script); diff --git a/src/store/manifestDoc/createStore.js b/src/store/manifestDoc/createStore.js deleted file mode 100644 index e51f762..0000000 --- a/src/store/manifestDoc/createStore.js +++ /dev/null @@ -1,424 +0,0 @@ -import { - CREATE_LINE, - CREATE_MANIFEST, - DELETE_LINES_BY_MANIFEST, - DELETE_MANIFEST, - GET_MANIFEST_HEADER, - GET_MANIFEST_LINES, - LIST_MANIFESTS, - RESOLVE_ARTICLE_ID_BY_SKU, - UPDATE_MANIFEST, -} from '../../sql/manifestDoc.js'; -import { - GET_CUSTOMER_BY_ID, - POS_GROUP_NAME, - SUPPLIER_GROUP_NAME, -} from '../../sql/entity.js'; -import request from '../request.js'; -import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -import taxEnsure from '../tax/ensure.js'; - -function shapeSummary(type, row) { - return { - id: row.id, - number: row.number, - type, - entityId: row.entityId ?? null, - date: row.date, - }; -} - -function shapeLine(row) { - return { - id: row.id, - articleId: row.articleId ?? null, - sku: row.sku ?? null, - amount: row.amount, - net: row.net, - rebate: row.rebate, - tax: row.tax, - comment: row.comment ?? null, - }; -} - -function parseDate(value) { - if (value instanceof Date && !Number.isNaN(value.getTime())) { - return value; - } - if (typeof value === 'string' && value.trim()) { - const d = new Date(value); - if (!Number.isNaN(d.getTime())) { - return d; - } - } - return null; -} - -function normalizeLines(lines) { - if (!Array.isArray(lines)) { - throw new Error('Invalid lines'); - } - - return lines.map((entry) => { - const sku = entry?.sku?.trim?.() ? entry.sku.trim() : null; - if (!sku) { - throw new Error('Invalid lines'); - } - - const amount = entry?.amount; - const net = entry?.net; - const rebate = - entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate; - const tax = entry?.tax; - const comment = - entry?.comment === undefined || entry?.comment === null - ? null - : String(entry.comment); - - if ( - !Number.isFinite(amount) || - !Number.isFinite(net) || - !Number.isFinite(rebate) || - !Number.isFinite(tax) - ) { - throw new Error('Invalid lines'); - } - - return { sku, amount, net, rebate, tax, comment }; - }); -} - -async function assertCustomerEntity(pool, tx, companyId, entityId) { - if (entityId == null) { - return null; - } - if (!Number.isFinite(entityId)) { - throw new Error('Entity not found'); - } - - const result = await request(pool, tx) - .input('id', entityId) - .input('companyId', companyId) - .input('supplierGroupName', SUPPLIER_GROUP_NAME) - .input('posGroupName', POS_GROUP_NAME) - .query(GET_CUSTOMER_BY_ID); - - if (!result.recordset[0]) { - throw new Error('Entity not found'); - } - return entityId; -} - -async function resolveArticleId(pool, tx, rootCategoryId, sku) { - const result = await request(pool, tx) - .input('rootCategoryId', rootCategoryId) - .input('sku', sku) - .query(RESOLVE_ARTICLE_ID_BY_SKU); - - const articleId = result.recordset[0]?.id; - if (!articleId) { - throw new Error('Article not found'); - } - return articleId; -} - -async function insertLines( - pool, - tx, - { manifestId, rootCategoryId, lines, ensureTax }, -) { - for (const line of lines) { - const articleId = await resolveArticleId( - pool, - tx, - rootCategoryId, - line.sku, - ); - const taxId = await ensureTax(line.tax, tx); - - await request(pool, tx) - .input('manifestId', manifestId) - .input('articleId', articleId) - .input('amount', line.amount) - .input('net', line.net) - .input('comment', line.comment) - .input('rebate', line.rebate) - .input('taxId', taxId) - .query(CREATE_LINE); - } -} - -async function loadDoc(pool, tx, { type, companyId, id }) { - const header = await request(pool, tx) - .input('id', id) - .input('companyId', companyId) - .input('type', type) - .query(GET_MANIFEST_HEADER); - - const row = header.recordset[0]; - if (!row) { - return null; - } - - const lines = await request(pool, tx) - .input('manifestId', id) - .query(GET_MANIFEST_LINES); - - return { - ...shapeSummary(type, row), - lines: lines.recordset.map(shapeLine), - }; -} - -/** - * Factory for quote / order (and similar) stores on manifest / manifestarticle. - * - * @param {object} pool - * @param {{ type: string, label: string }} options - * `type` → manifest.type (`quote` / `order`) - * `label` → error prefix (`Quote` / `Order` → "Quote not found") - */ -export default function createManifestDocStore(pool, { type, label }) { - const access = resolveSelectedCompanyAccess(pool); - const ensureTax = taxEnsure(pool); - const notFound = `${label} not found`; - const invalid = `Invalid ${label.toLowerCase()}`; - - /** - * Lists the quote/order documents of a company. - * - * @param {number} googleAuthId - The Google auth user id. - * @returns {Promise>} The document summaries. - */ - async function list(googleAuthId) { - const { companyId } = await access(googleAuthId); - - const result = await pool - .request() - .input('companyId', companyId) - .input('type', type) - .query(LIST_MANIFESTS); - - return result.recordset.map((row) => shapeSummary(type, row)); - } - - /** - * Returns a single quote/order document by its id within a company. - * - * @param {number} googleAuthId - The Google auth user id. - * @param {number} id - The document id. - * @returns {Promise} The document with its lines, or null if not found. - */ - async function getById(googleAuthId, id) { - if (!Number.isFinite(id)) { - throw new Error(notFound); - } - - const { companyId } = await access(googleAuthId); - - const doc = await loadDoc(pool, null, { type, companyId, id }); - if (!doc) { - throw new Error(notFound); - } - return doc; - } - - /** - * Creates a new quote/order document with its lines. - * - * @param {number} googleAuthId - The Google auth user id. - * @param {object} body - The document payload (`number`, `date`, `entityId`, `lines`). - * @returns {Promise} The created document with its lines. - */ - async function create(googleAuthId, body = {}) { - const number = body.number?.trim?.() ? body.number.trim() : null; - const date = parseDate(body.date); - if (!number || !date) { - throw new Error(invalid); - } - - const lines = normalizeLines(body.lines ?? []); - const { companyId, rootCategoryId } = await access(googleAuthId); - - const tx = pool.transaction(); - await tx.begin(); - - try { - const entityId = await assertCustomerEntity( - pool, - tx, - companyId, - body.entityId ?? null, - ); - - const created = await request(pool, tx) - .input('companyId', companyId) - .input('number', number) - .input('type', type) - .input('entityId', entityId) - .input('date', date) - .query(CREATE_MANIFEST); - - const manifestId = created.recordset[0].id; - - await insertLines(pool, tx, { - manifestId, - rootCategoryId, - lines, - ensureTax, - }); - - const doc = await loadDoc(pool, tx, { type, companyId, id: manifestId }); - await tx.commit(); - return doc; - } catch (err) { - try { - await tx.rollback(); - } catch { - // ignore - } - throw err; - } - } - - /** - * Updates a quote/order document's header and/or lines. - * - * @param {number} googleAuthId - The Google auth user 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, id, patch = {}) { - if (!Number.isFinite(id)) { - throw new Error(notFound); - } - - const hasNumber = Object.prototype.hasOwnProperty.call(patch, 'number'); - const hasDate = Object.prototype.hasOwnProperty.call(patch, 'date'); - const hasEntityId = Object.prototype.hasOwnProperty.call(patch, 'entityId'); - const hasLines = Object.prototype.hasOwnProperty.call(patch, 'lines'); - - if (!hasNumber && !hasDate && !hasEntityId && !hasLines) { - throw new Error(invalid); - } - - let normalizedLines; - if (hasLines) { - normalizedLines = normalizeLines(patch.lines); - } - - const { companyId, rootCategoryId } = await access(googleAuthId); - - const tx = pool.transaction(); - await tx.begin(); - - try { - const existing = await request(pool, tx) - .input('id', id) - .input('companyId', companyId) - .input('type', type) - .query(GET_MANIFEST_HEADER); - - const current = existing.recordset[0]; - if (!current) { - throw new Error(notFound); - } - - const summary = shapeSummary(type, current); - - let number = summary.number; - if (hasNumber) { - number = patch.number?.trim?.() ? patch.number.trim() : null; - if (!number) { - throw new Error(invalid); - } - } - - let date = summary.date; - if (hasDate) { - date = parseDate(patch.date); - if (!date) { - throw new Error(invalid); - } - } - - let entityId = summary.entityId; - if (hasEntityId) { - entityId = await assertCustomerEntity( - pool, - tx, - companyId, - patch.entityId ?? null, - ); - } - - await request(pool, tx) - .input('id', id) - .input('companyId', companyId) - .input('type', type) - .input('number', number) - .input('entityId', entityId) - .input('date', date) - .query(UPDATE_MANIFEST); - - if (hasLines) { - await request(pool, tx) - .input('manifestId', id) - .query(DELETE_LINES_BY_MANIFEST); - - await insertLines(pool, tx, { - manifestId: id, - rootCategoryId, - lines: normalizedLines, - ensureTax, - }); - } - - const doc = await loadDoc(pool, tx, { type, companyId, id }); - await tx.commit(); - return doc; - } catch (err) { - try { - await tx.rollback(); - } catch { - // ignore - } - throw err; - } - } - - /** - * Deletes a quote/order document by its id within a company. - * - * @param {number} googleAuthId - The Google auth user id. - * @param {number} id - The document id. - * @returns {Promise} - */ - async function del(googleAuthId, id) { - if (!Number.isFinite(id)) { - throw new Error(notFound); - } - - const { companyId } = await access(googleAuthId); - - const result = await pool - .request() - .input('id', id) - .input('companyId', companyId) - .input('type', type) - .query(DELETE_MANIFEST); - - if (!result.rowsAffected[0]) { - throw new Error(notFound); - } - } - - return { - list, - getById, - create, - update, - delete: del, - }; -} diff --git a/src/store/manifestDoc/helpers.js b/src/store/manifestDoc/helpers.js new file mode 100644 index 0000000..9eaa869 --- /dev/null +++ b/src/store/manifestDoc/helpers.js @@ -0,0 +1,64 @@ +import { + GET_MANIFEST_HEADER, + GET_MANIFEST_LINES, +} from '../../sql/manifestDoc.js'; +import { + GET_CUSTOMER_BY_ID, + POS_GROUP_NAME, + SUPPLIER_GROUP_NAME, +} from '../../sql/entity.js'; +import request from '../request.js'; +import { shapeLine } from '../manifestLines.js'; + +export function shapeSummary(type, row) { + return { + id: row.id, + number: row.number, + type, + entityId: row.entityId ?? null, + date: row.date, + }; +} + +export async function assertCustomerEntity(pool, tx, companyId, entityId) { + if (entityId == null) { + return null; + } + if (!Number.isFinite(entityId)) { + throw new Error('Entity not found'); + } + + const result = await request(pool, tx) + .input('id', entityId) + .input('companyId', companyId) + .input('supplierGroupName', SUPPLIER_GROUP_NAME) + .input('posGroupName', POS_GROUP_NAME) + .query(GET_CUSTOMER_BY_ID); + + if (!result.recordset[0]) { + throw new Error('Entity not found'); + } + return entityId; +} + +export async function loadDoc(pool, tx, { type, companyId, id }) { + const header = await request(pool, tx) + .input('id', id) + .input('companyId', companyId) + .input('type', type) + .query(GET_MANIFEST_HEADER); + + const row = header.recordset[0]; + if (!row) { + return null; + } + + const lines = await request(pool, tx) + .input('manifestId', id) + .query(GET_MANIFEST_LINES); + + return { + ...shapeSummary(type, row), + lines: lines.recordset.map(shapeLine), + }; +} diff --git a/src/store/manifestLines.js b/src/store/manifestLines.js new file mode 100644 index 0000000..b65bf36 --- /dev/null +++ b/src/store/manifestLines.js @@ -0,0 +1,133 @@ +import { CREATE_LINE, RESOLVE_ARTICLE_ID_BY_SKU } from '../sql/manifestDoc.js'; +import request from './request.js'; + +/** + * Normalizes and validates an array of manifest line inputs. + * + * @param {Array} lines - The raw line inputs. + * @returns {Array<{sku: string, amount: number, net: number, rebate: number, tax: number, comment: string|null}>} The normalized lines. + * @throws {Error} If the lines are invalid. + */ +export function normalizeLines(lines) { + if (!Array.isArray(lines)) { + throw new Error('Invalid lines'); + } + + return lines.map((entry) => { + const sku = entry?.sku?.trim?.() ? entry.sku.trim() : null; + if (!sku) { + throw new Error('Invalid lines'); + } + + const amount = entry?.amount; + const net = entry?.net; + const rebate = + entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate; + const tax = entry?.tax; + const comment = + entry?.comment === undefined || entry?.comment === null + ? null + : String(entry.comment); + + if ( + !Number.isFinite(amount) || + !Number.isFinite(net) || + !Number.isFinite(rebate) || + !Number.isFinite(tax) + ) { + throw new Error('Invalid lines'); + } + + return { sku, amount, net, rebate, tax, comment }; + }); +} + +/** + * Parses a date value (Date instance or ISO string) into a Date. + * + * @param {Date|string} value - The date value. + * @returns {Date|null} The parsed date, or null if invalid. + */ +export function parseDate(value) { + if (value instanceof Date && !Number.isNaN(value.getTime())) { + return value; + } + if (typeof value === 'string' && value.trim()) { + const d = new Date(value); + if (!Number.isNaN(d.getTime())) { + return d; + } + } + return null; +} + +/** + * Shapes a manifestarticle row into a line object. + * + * @param {object} row - The manifestarticle row. + * @returns {object} The shaped line. + */ +export function shapeLine(row) { + return { + id: row.id, + articleId: row.articleId ?? null, + sku: row.sku ?? null, + amount: row.amount, + net: row.net, + rebate: row.rebate, + tax: row.tax, + comment: row.comment ?? null, + }; +} + +async function resolveArticleId(pool, tx, rootCategoryId, sku) { + const result = await request(pool, tx) + .input('rootCategoryId', rootCategoryId) + .input('sku', sku) + .query(RESOLVE_ARTICLE_ID_BY_SKU); + + const articleId = result.recordset[0]?.id; + if (!articleId) { + throw new Error('Article not found'); + } + return articleId; +} + +/** + * Inserts manifest lines for a document, resolving each SKU to an article id + * and ensuring the tax rate. + * + * @param {object} pool - The mssql connection pool. + * @param {object} tx - The transaction. + * @param {object} params - The insert parameters. + * @param {number} params.manifestId - The manifest id. + * @param {number} params.rootCategoryId - The company root category id. + * @param {Array} params.lines - The normalized lines. + * @param {Function} params.ensureTax - The tax ensure function. + * @returns {Promise} + */ +export async function insertLines( + pool, + tx, + { manifestId, rootCategoryId, lines, ensureTax }, +) { + for (const line of lines) { + const articleId = await resolveArticleId( + pool, + tx, + rootCategoryId, + line.sku, + ); + const taxId = await ensureTax(line.tax, tx); + + await request(pool, tx) + .input('manifestId', manifestId) + .input('articleId', articleId) + .input('amount', line.amount) + .input('net', line.net) + .input('comment', line.comment) + .input('rebate', line.rebate) + .input('taxId', taxId) + .query(CREATE_LINE); + } +} diff --git a/src/store/numbers/nextNumber.js b/src/store/numbers/nextNumber.js index 4f9018a..83cb246 100644 --- a/src/store/numbers/nextNumber.js +++ b/src/store/numbers/nextNumber.js @@ -2,6 +2,18 @@ import { ENSURE_TYPE } from '../../sql/numbers.js'; import request from '../request.js'; export default function nextNumber(pool) { + /** + * Allocates and returns the next number for a company and type, consuming it. + * + * @param {number} companyId - The id of the company. + * @param {string} type - The number type (e.g. `'entity'`, `'sku'`). + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} The allocated number. + * + * @example + * const number = await store.numbers.next(1, 'entity'); + * // => '1' + */ return async (companyId, type, tx) => { await request(pool, tx) .input('companyId', companyId) diff --git a/src/store/numbers/nextSku.js b/src/store/numbers/nextSku.js index 3a23b09..98a3523 100644 --- a/src/store/numbers/nextSku.js +++ b/src/store/numbers/nextSku.js @@ -2,5 +2,16 @@ import nextNumber from './nextNumber.js'; export default function nextSku(pool) { const allocate = nextNumber(pool); + /** + * Allocates and returns the next SKU for a company, consuming it. + * + * @param {number} companyId - The id of the company. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} The allocated SKU. + * + * @example + * const sku = await store.numbers.nextSku(1); + * // => '1' + */ return (companyId, tx) => allocate(companyId, 'sku', tx); } diff --git a/src/store/numbers/peekNumber.js b/src/store/numbers/peekNumber.js index c559a1f..ff51293 100644 --- a/src/store/numbers/peekNumber.js +++ b/src/store/numbers/peekNumber.js @@ -2,6 +2,18 @@ import { ENSURE_TYPE } from '../../sql/numbers.js'; import request from '../request.js'; export default function peekNumber(pool) { + /** + * Returns the next number for a company and type without consuming it. + * + * @param {number} companyId - The id of the company. + * @param {string} type - The number type (e.g. `'entity'`, `'sku'`). + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} The next number. + * + * @example + * const number = await store.numbers.peek(1, 'entity'); + * // => '1' + */ return async (companyId, type, tx) => { await request(pool, tx) .input('companyId', companyId) diff --git a/src/store/order/create.js b/src/store/order/create.js new file mode 100644 index 0000000..8370074 --- /dev/null +++ b/src/store/order/create.js @@ -0,0 +1,82 @@ +import { CREATE_MANIFEST, ORDER_TYPE } from '../../sql/manifestDoc.js'; +import request from '../request.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import taxEnsure from '../tax/ensure.js'; +import { insertLines, normalizeLines, parseDate } from '../manifestLines.js'; +import { assertCustomerEntity, loadDoc } from '../manifestDoc/helpers.js'; + +export default function create(pool) { + const access = resolveSelectedCompanyAccess(pool); + const ensureTax = taxEnsure(pool); + + /** + * Creates a new order for the user's selected company with its lines. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} body - The order input. + * @param {string} body.number - The order number. + * @param {string} body.date - The order date. + * @param {number} [body.entityId] - The id of the customer entity. + * @param {Array} [body.lines] - The order lines. + * @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>} The created order. + * @throws {Error} If the order is invalid. + * + * @example + * const order = await store.order.create(42, { number: 'B-2026-001', date: '2026-09-15', entityId: 12, lines: [...] }); + * // => { id, number, type, entityId, date, lines: [...] } + */ + return async (googleAuthId, body = {}) => { + const number = body.number?.trim?.() ? body.number.trim() : null; + const date = parseDate(body.date); + if (!number || !date) { + throw new Error('Invalid order'); + } + + const lines = normalizeLines(body.lines ?? []); + const { companyId, rootCategoryId } = await access(googleAuthId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const entityId = await assertCustomerEntity( + pool, + tx, + companyId, + body.entityId ?? null, + ); + + const created = await request(pool, tx) + .input('companyId', companyId) + .input('number', number) + .input('type', ORDER_TYPE) + .input('entityId', entityId) + .input('date', date) + .query(CREATE_MANIFEST); + + const manifestId = created.recordset[0].id; + + await insertLines(pool, tx, { + manifestId, + rootCategoryId, + lines, + ensureTax, + }); + + const doc = await loadDoc(pool, tx, { + type: ORDER_TYPE, + companyId, + id: manifestId, + }); + await tx.commit(); + return doc; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/order/delete.js b/src/store/order/delete.js new file mode 100644 index 0000000..bda8c41 --- /dev/null +++ b/src/store/order/delete.js @@ -0,0 +1,36 @@ +import { DELETE_MANIFEST, ORDER_TYPE } from '../../sql/manifestDoc.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; + +export default function del(pool) { + const access = resolveSelectedCompanyAccess(pool); + + /** + * Deletes an order by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the order to delete. + * @returns {Promise} + * @throws {Error} If the order is not found. + * + * @example + * await store.order.delete(42, 5); + */ + return async (googleAuthId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Order not found'); + } + + const { companyId } = await access(googleAuthId); + + const result = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .input('type', ORDER_TYPE) + .query(DELETE_MANIFEST); + + if (!result.rowsAffected[0]) { + throw new Error('Order not found'); + } + }; +} diff --git a/src/store/order/getById.js b/src/store/order/getById.js new file mode 100644 index 0000000..5b2f8a3 --- /dev/null +++ b/src/store/order/getById.js @@ -0,0 +1,33 @@ +import { ORDER_TYPE } from '../../sql/manifestDoc.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import { loadDoc } from '../manifestDoc/helpers.js'; + +export default function getById(pool) { + const access = resolveSelectedCompanyAccess(pool); + + /** + * Returns a single order by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the order. + * @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>} The order with its lines. + * @throws {Error} If the order is not found. + * + * @example + * const order = await store.order.getById(42, 5); + * // => { id, number, type, entityId, date, lines: [...] } + */ + return async (googleAuthId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Order not found'); + } + + const { companyId } = await access(googleAuthId); + + const doc = await loadDoc(pool, null, { type: ORDER_TYPE, companyId, id }); + if (!doc) { + throw new Error('Order not found'); + } + return doc; + }; +} diff --git a/src/store/order/index.js b/src/store/order/index.js deleted file mode 100644 index 6bdb051..0000000 --- a/src/store/order/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import { ORDER_TYPE } from '../../sql/manifestDoc.js'; -import createManifestDocStore from '../manifestDoc/createStore.js'; - -/** - * Creates an order store (list, getById, create, update, delete) backed by the - * manifest / manifestarticle tables. - * - * @param {object} pool - The mssql connection pool. - * @returns {object} An object with `list`, `getById`, `create`, `update` and `delete` functions. - * - * @example - * const store = createOrderStore(pool); - * const orders = await store.list(42); - * // => [{ id, number, type, entityId, date }] - */ -export default function createOrderStore(pool) { - return createManifestDocStore(pool, { type: ORDER_TYPE, label: 'Order' }); -} diff --git a/src/store/order/list.js b/src/store/order/list.js new file mode 100644 index 0000000..529eb29 --- /dev/null +++ b/src/store/order/list.js @@ -0,0 +1,29 @@ +import { LIST_MANIFESTS, ORDER_TYPE } from '../../sql/manifestDoc.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import { shapeSummary } from '../manifestDoc/helpers.js'; + +export default function list(pool) { + const access = resolveSelectedCompanyAccess(pool); + + /** + * Lists the orders of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise>} The orders. + * + * @example + * const orders = await store.order.list(42); + * // => [{ id, number, type, entityId, date }] + */ + return async (googleAuthId) => { + const { companyId } = await access(googleAuthId); + + const result = await pool + .request() + .input('companyId', companyId) + .input('type', ORDER_TYPE) + .query(LIST_MANIFESTS); + + return result.recordset.map((row) => shapeSummary(ORDER_TYPE, row)); + }; +} diff --git a/src/store/order/update.js b/src/store/order/update.js new file mode 100644 index 0000000..20b93fb --- /dev/null +++ b/src/store/order/update.js @@ -0,0 +1,132 @@ +import { + DELETE_LINES_BY_MANIFEST, + GET_MANIFEST_HEADER, + ORDER_TYPE, + UPDATE_MANIFEST, +} from '../../sql/manifestDoc.js'; +import request from '../request.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import taxEnsure from '../tax/ensure.js'; +import { insertLines, normalizeLines, parseDate } from '../manifestLines.js'; +import { assertCustomerEntity, loadDoc, shapeSummary } from '../manifestDoc/helpers.js'; + +export default function update(pool) { + const access = resolveSelectedCompanyAccess(pool); + const ensureTax = taxEnsure(pool); + + /** + * Updates an order's header and/or lines. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the order to update. + * @param {object} patch - The update patch. + * @param {string} [patch.number] - The new order number. + * @param {string} [patch.date] - The new order date. + * @param {number} [patch.entityId] - The new customer entity id. + * @param {Array} [patch.lines] - The new order lines. + * @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>} The updated order. + * @throws {Error} If the order is not found. + * + * @example + * const order = await store.order.update(42, 5, { number: 'B-2026-002', lines: [...] }); + * // => { id, number, type, entityId, date, lines: [...] } + */ + return async (googleAuthId, id, patch = {}) => { + if (!Number.isFinite(id)) { + throw new Error('Order not found'); + } + + const hasNumber = Object.prototype.hasOwnProperty.call(patch, 'number'); + const hasDate = Object.prototype.hasOwnProperty.call(patch, 'date'); + const hasEntityId = Object.prototype.hasOwnProperty.call(patch, 'entityId'); + const hasLines = Object.prototype.hasOwnProperty.call(patch, 'lines'); + + if (!hasNumber && !hasDate && !hasEntityId && !hasLines) { + throw new Error('Invalid order'); + } + + let normalizedLines; + if (hasLines) { + normalizedLines = normalizeLines(patch.lines); + } + + const { companyId, rootCategoryId } = await access(googleAuthId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const existing = await request(pool, tx) + .input('id', id) + .input('companyId', companyId) + .input('type', ORDER_TYPE) + .query(GET_MANIFEST_HEADER); + + const current = existing.recordset[0]; + if (!current) { + throw new Error('Order not found'); + } + + const summary = shapeSummary(ORDER_TYPE, current); + + let number = summary.number; + if (hasNumber) { + number = patch.number?.trim?.() ? patch.number.trim() : null; + if (!number) { + throw new Error('Invalid order'); + } + } + + let date = summary.date; + if (hasDate) { + date = parseDate(patch.date); + if (!date) { + throw new Error('Invalid order'); + } + } + + let entityId = summary.entityId; + if (hasEntityId) { + entityId = await assertCustomerEntity( + pool, + tx, + companyId, + patch.entityId ?? null, + ); + } + + await request(pool, tx) + .input('id', id) + .input('companyId', companyId) + .input('type', ORDER_TYPE) + .input('number', number) + .input('entityId', entityId) + .input('date', date) + .query(UPDATE_MANIFEST); + + if (hasLines) { + await request(pool, tx) + .input('manifestId', id) + .query(DELETE_LINES_BY_MANIFEST); + + await insertLines(pool, tx, { + manifestId: id, + rootCategoryId, + lines: normalizedLines, + ensureTax, + }); + } + + const doc = await loadDoc(pool, tx, { type: ORDER_TYPE, companyId, id }); + await tx.commit(); + return doc; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/pic/getImageByHash.js b/src/store/pic/getImageByHash.js index 7ab2d12..15a3e90 100644 --- a/src/store/pic/getImageByHash.js +++ b/src/store/pic/getImageByHash.js @@ -1,20 +1,21 @@ import { GET_FULL_BY_HASH } from '../../sql/pic.js'; import resolvePicAccess from './resolvePicAccess.js'; -/** - * Returns the full-size image (AVIF) for a given hash. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches a full image by hash. - * - * @example - * const fn = getImageByHash(pool); - * const image = await fn(42, 'abc123'); - * // => { buffer, w, h, size, contentType: 'image/avif' } - */ export default function getImageByHash(pool) { const authorize = resolvePicAccess(pool); + /** + * Returns the full-size image (AVIF) for a given hash. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} fullHash - The full image hash. + * @returns {Promise<{buffer: Buffer, w: number, h: number, size: number, contentType: string}>} The full image. + * @throws {Error} If the image is not found. + * + * @example + * const image = await store.pic.getImageByHash(42, 'abc123'); + * // => { buffer, w, h, size, contentType: 'image/avif' } + */ return async (googleAuthId, fullHash) => { await authorize(googleAuthId, fullHash); diff --git a/src/store/pic/getThumbByHash.js b/src/store/pic/getThumbByHash.js index 7c12ac5..8b3dde7 100644 --- a/src/store/pic/getThumbByHash.js +++ b/src/store/pic/getThumbByHash.js @@ -1,20 +1,21 @@ import { GET_THUMB_BY_HASH } from '../../sql/pic.js'; import resolvePicAccess from './resolvePicAccess.js'; -/** - * Returns the thumbnail image (AVIF) for a given hash. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that fetches a thumbnail by hash. - * - * @example - * const fn = getThumbByHash(pool); - * const thumb = await fn(42, 'abc123'); - * // => { buffer, w, h, contentType: 'image/avif' } - */ export default function getThumbByHash(pool) { const authorize = resolvePicAccess(pool); + /** + * Returns the thumbnail image (AVIF) for a given hash. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} fullHash - The full image hash. + * @returns {Promise<{buffer: Buffer, w: number, h: number, contentType: string}>} The thumbnail image. + * @throws {Error} If the image is not found. + * + * @example + * const thumb = await store.pic.getThumbByHash(42, 'abc123'); + * // => { buffer, w, h, contentType: 'image/avif' } + */ return async (googleAuthId, fullHash) => { await authorize(googleAuthId, fullHash); diff --git a/src/store/posterminal/delete.js b/src/store/posterminal/delete.js index 0212dbc..99c2063 100644 --- a/src/store/posterminal/delete.js +++ b/src/store/posterminal/delete.js @@ -1,19 +1,20 @@ import { DELETE_BY_COMPANY_AND_ID } from '../../sql/posterminal.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Deletes a posterminal by its id within a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes a posterminal. - * - * @example - * const fn = del(pool); - * await fn(42, 5); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Deletes a posterminal by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the posterminal to delete. + * @returns {Promise} + * @throws {Error} If the posterminal is not found. + * + * @example + * await store.posterminal.delete(42, 5); + */ return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Posterminal not found'); diff --git a/src/store/posterminal/getCategoryPosterminalIds.js b/src/store/posterminal/getCategoryPosterminalIds.js index 8ce5d1f..cd7ae99 100644 --- a/src/store/posterminal/getCategoryPosterminalIds.js +++ b/src/store/posterminal/getCategoryPosterminalIds.js @@ -1,18 +1,17 @@ import { POSTERMINAL_IDS_BY_COMPANY } from '../../sql/posterminal.js'; -/** - * Returns a map of category id to the posterminal ids assigned to it for a - * company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that builds the category→posterminal map. - * - * @example - * const fn = getCategoryPosterminalIds(pool); - * const map = await fn(1); - * // => { 5: [1, 2], 9: [1] } - */ export default function getCategoryPosterminalIds(pool) { + /** + * Returns a map of category id to the posterminal ids assigned to it for a + * company. + * + * @param {number} companyId - The id of the company. + * @returns {Promise>>} A map of category id to posterminal ids. + * + * @example + * const map = await getCategoryPosterminalIds(pool)(1); + * // => { 5: [1, 2], 9: [1] } + */ return async (companyId) => { const result = await pool .request() diff --git a/src/store/posterminal/list.js b/src/store/posterminal/list.js index 05875f7..de72524 100644 --- a/src/store/posterminal/list.js +++ b/src/store/posterminal/list.js @@ -1,20 +1,19 @@ import { LIST_BY_COMPANY } from '../../sql/posterminal.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Lists the posterminals of a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists posterminals. - * - * @example - * const fn = list(pool); - * const terminals = await fn(42); - * // => [{ id, name, token, pairingpin }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Lists the posterminals of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise>} The posterminals. + * + * @example + * const terminals = await store.posterminal.list(42); + * // => [{ id, name, token, pairingpin }] + */ return async (googleAuthId) => { const { companyId } = await access(googleAuthId); diff --git a/src/store/posterminal/setOff.js b/src/store/posterminal/setOff.js index e878efe..04d50f2 100644 --- a/src/store/posterminal/setOff.js +++ b/src/store/posterminal/setOff.js @@ -1,20 +1,22 @@ import { GET_BY_ID, SET_OFF, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Removes a category assignment from a posterminal (turns it off). - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that removes a category from a posterminal. - * - * @example - * const fn = setOff(pool); - * const result = await fn(42, 5, 9); - * // => { id, categoryIds: [...] } - */ export default function setOff(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Removes a category assignment from a posterminal (turns it off). + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} posterminalId - The id of the posterminal. + * @param {number} categoryId - The id of the category to remove. + * @returns {Promise<{id: number, categoryIds: Array}>} The posterminal with its remaining category ids. + * @throws {Error} If the posterminal is not found. + * + * @example + * const result = await store.posterminal.setOff(42, 5, 9); + * // => { id, categoryIds: [...] } + */ return async (googleAuthId, posterminalId, categoryId) => { if (!Number.isFinite(posterminalId) || !Number.isFinite(categoryId)) { throw new Error('Invalid posterminal or category'); diff --git a/src/store/posterminal/setOffAll.js b/src/store/posterminal/setOffAll.js index 33d9641..a1bb6fd 100644 --- a/src/store/posterminal/setOffAll.js +++ b/src/store/posterminal/setOffAll.js @@ -1,20 +1,21 @@ import { GET_BY_ID, SET_OFF_ALL, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Removes all category assignments from a posterminal. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that clears all categories from a posterminal. - * - * @example - * const fn = setOffAll(pool); - * const result = await fn(42, 5); - * // => { id, categoryIds: [] } - */ export default function setOffAll(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Removes all category assignments from a posterminal. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} posterminalId - The id of the posterminal. + * @returns {Promise<{id: number, categoryIds: Array}>} The posterminal with an empty category list. + * @throws {Error} If the posterminal is not found. + * + * @example + * const result = await store.posterminal.setOffAll(42, 5); + * // => { id, categoryIds: [] } + */ return async (googleAuthId, posterminalId) => { if (!Number.isFinite(posterminalId)) { throw new Error('Invalid posterminal'); diff --git a/src/store/posterminal/setOn.js b/src/store/posterminal/setOn.js index 3798c5d..b25029d 100644 --- a/src/store/posterminal/setOn.js +++ b/src/store/posterminal/setOn.js @@ -1,20 +1,22 @@ import { GET_BY_ID, SET_ON, LIST_CATEGORY_IDS } from '../../sql/posterminal.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Assigns a category to a posterminal (turns it on for that category). - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that assigns a category to a posterminal. - * - * @example - * const fn = setOn(pool); - * const result = await fn(42, 5, 9); - * // => { id, categoryIds: [9, ...] } - */ export default function setOn(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Assigns a category to a posterminal (turns it on for that category). + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} posterminalId - The id of the posterminal. + * @param {number} categoryId - The id of the category to assign. + * @returns {Promise<{id: number, categoryIds: Array}>} The posterminal with its category ids. + * @throws {Error} If the posterminal is not found. + * + * @example + * const result = await store.posterminal.setOn(42, 5, 9); + * // => { id, categoryIds: [9, ...] } + */ return async (googleAuthId, posterminalId, categoryId) => { if (!Number.isFinite(posterminalId) || !Number.isFinite(categoryId)) { throw new Error('Invalid posterminal or category'); diff --git a/src/store/posterminal/setOnAll.js b/src/store/posterminal/setOnAll.js index 02081e3..aa7c0df 100644 --- a/src/store/posterminal/setOnAll.js +++ b/src/store/posterminal/setOnAll.js @@ -2,20 +2,21 @@ import { GET_BY_ID, SET_ON_ALL, LIST_CATEGORY_IDS } from '../../sql/posterminal. import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import { GET_COMPANY_ROOT_CATEGORY } from '../../sql/company.js'; -/** - * Assigns all categories of a company to a posterminal. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that assigns all categories to a posterminal. - * - * @example - * const fn = setOnAll(pool); - * const result = await fn(42, 5); - * // => { id, categoryIds: [...] } - */ export default function setOnAll(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Assigns all categories of the user's selected company to a posterminal. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} posterminalId - The id of the posterminal. + * @returns {Promise<{id: number, categoryIds: Array}>} The posterminal with all category ids. + * @throws {Error} If the posterminal is not found. + * + * @example + * const result = await store.posterminal.setOnAll(42, 5); + * // => { id, categoryIds: [...] } + */ return async (googleAuthId, posterminalId) => { if (!Number.isFinite(posterminalId)) { throw new Error('Invalid posterminal'); diff --git a/src/store/posterminal/upsert.js b/src/store/posterminal/upsert.js index 21baa7d..7510f82 100644 --- a/src/store/posterminal/upsert.js +++ b/src/store/posterminal/upsert.js @@ -1,20 +1,25 @@ import { GET_BY_ID, CREATE, UPDATE } from '../../sql/posterminal.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Creates or updates a posterminal for a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that upserts a posterminal. - * - * @example - * const fn = upsert(pool); - * const terminal = await fn(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' }); - * // => { id, name, token, pairingpin } - */ export default function upsert(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Creates or updates a posterminal for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The posterminal input. + * @param {string} input.name - The posterminal name. + * @param {string} input.pairingpin - The pairing pin. + * @param {string} [input.token] - Optional token. + * @param {number} [input.id] - Optional id to update an existing posterminal. + * @returns {Promise<{id: number, name: string, token: string, pairingpin: string}>} The upserted posterminal. + * @throws {Error} If the posterminal is invalid. + * + * @example + * const terminal = await store.posterminal.upsert(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' }); + * // => { id, name, token, pairingpin } + */ return async (googleAuthId, input = {}) => { const name = input.name?.trim(); if (!name) { diff --git a/src/store/purge.js b/src/store/purge.js index 907b728..3599f8c 100644 --- a/src/store/purge.js +++ b/src/store/purge.js @@ -1,16 +1,14 @@ import { buildPurgeSql } from '../sql/purge.js'; -/** - * Purges (wipes) all data from the database. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that purges the database. - * - * @example - * const fn = purge(pool); - * await fn(); - */ export default function purge(pool) { + /** + * Purges (wipes) all data from the database. + * + * @returns {Promise} + * + * @example + * await store.purge(); + */ return async () => { await pool.request().query(buildPurgeSql()); }; diff --git a/src/store/quote/create.js b/src/store/quote/create.js new file mode 100644 index 0000000..2389e68 --- /dev/null +++ b/src/store/quote/create.js @@ -0,0 +1,82 @@ +import { CREATE_MANIFEST, QUOTE_TYPE } from '../../sql/manifestDoc.js'; +import request from '../request.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import taxEnsure from '../tax/ensure.js'; +import { insertLines, normalizeLines, parseDate } from '../manifestLines.js'; +import { assertCustomerEntity, loadDoc } from '../manifestDoc/helpers.js'; + +export default function create(pool) { + const access = resolveSelectedCompanyAccess(pool); + const ensureTax = taxEnsure(pool); + + /** + * Creates a new quote for the user's selected company with its lines. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} body - The quote input. + * @param {string} body.number - The quote number. + * @param {string} body.date - The quote date. + * @param {number} [body.entityId] - The id of the customer entity. + * @param {Array} [body.lines] - The quote lines. + * @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>} The created quote. + * @throws {Error} If the quote is invalid. + * + * @example + * const quote = await store.quote.create(42, { number: 'A-2026-001', date: '2026-09-01', entityId: 12, lines: [...] }); + * // => { id, number, type, entityId, date, lines: [...] } + */ + return async (googleAuthId, body = {}) => { + const number = body.number?.trim?.() ? body.number.trim() : null; + const date = parseDate(body.date); + if (!number || !date) { + throw new Error('Invalid quote'); + } + + const lines = normalizeLines(body.lines ?? []); + const { companyId, rootCategoryId } = await access(googleAuthId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const entityId = await assertCustomerEntity( + pool, + tx, + companyId, + body.entityId ?? null, + ); + + const created = await request(pool, tx) + .input('companyId', companyId) + .input('number', number) + .input('type', QUOTE_TYPE) + .input('entityId', entityId) + .input('date', date) + .query(CREATE_MANIFEST); + + const manifestId = created.recordset[0].id; + + await insertLines(pool, tx, { + manifestId, + rootCategoryId, + lines, + ensureTax, + }); + + const doc = await loadDoc(pool, tx, { + type: QUOTE_TYPE, + companyId, + id: manifestId, + }); + await tx.commit(); + return doc; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/quote/delete.js b/src/store/quote/delete.js new file mode 100644 index 0000000..3e63681 --- /dev/null +++ b/src/store/quote/delete.js @@ -0,0 +1,36 @@ +import { DELETE_MANIFEST, QUOTE_TYPE } from '../../sql/manifestDoc.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; + +export default function del(pool) { + const access = resolveSelectedCompanyAccess(pool); + + /** + * Deletes a quote by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the quote to delete. + * @returns {Promise} + * @throws {Error} If the quote is not found. + * + * @example + * await store.quote.delete(42, 5); + */ + return async (googleAuthId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Quote not found'); + } + + const { companyId } = await access(googleAuthId); + + const result = await pool + .request() + .input('id', id) + .input('companyId', companyId) + .input('type', QUOTE_TYPE) + .query(DELETE_MANIFEST); + + if (!result.rowsAffected[0]) { + throw new Error('Quote not found'); + } + }; +} diff --git a/src/store/quote/getById.js b/src/store/quote/getById.js new file mode 100644 index 0000000..a5f9a47 --- /dev/null +++ b/src/store/quote/getById.js @@ -0,0 +1,33 @@ +import { QUOTE_TYPE } from '../../sql/manifestDoc.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import { loadDoc } from '../manifestDoc/helpers.js'; + +export default function getById(pool) { + const access = resolveSelectedCompanyAccess(pool); + + /** + * Returns a single quote by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the quote. + * @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>} The quote with its lines. + * @throws {Error} If the quote is not found. + * + * @example + * const quote = await store.quote.getById(42, 5); + * // => { id, number, type, entityId, date, lines: [...] } + */ + return async (googleAuthId, id) => { + if (!Number.isFinite(id)) { + throw new Error('Quote not found'); + } + + const { companyId } = await access(googleAuthId); + + const doc = await loadDoc(pool, null, { type: QUOTE_TYPE, companyId, id }); + if (!doc) { + throw new Error('Quote not found'); + } + return doc; + }; +} diff --git a/src/store/quote/index.js b/src/store/quote/index.js deleted file mode 100644 index 706a3de..0000000 --- a/src/store/quote/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import { QUOTE_TYPE } from '../../sql/manifestDoc.js'; -import createManifestDocStore from '../manifestDoc/createStore.js'; - -/** - * Creates a quote store (list, getById, create, update, delete) backed by the - * manifest / manifestarticle tables. - * - * @param {object} pool - The mssql connection pool. - * @returns {object} An object with `list`, `getById`, `create`, `update` and `delete` functions. - * - * @example - * const store = createQuoteStore(pool); - * const quotes = await store.list(42); - * // => [{ id, number, type, entityId, date }] - */ -export default function createQuoteStore(pool) { - return createManifestDocStore(pool, { type: QUOTE_TYPE, label: 'Quote' }); -} diff --git a/src/store/quote/list.js b/src/store/quote/list.js new file mode 100644 index 0000000..42dd836 --- /dev/null +++ b/src/store/quote/list.js @@ -0,0 +1,29 @@ +import { LIST_MANIFESTS, QUOTE_TYPE } from '../../sql/manifestDoc.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import { shapeSummary } from '../manifestDoc/helpers.js'; + +export default function list(pool) { + const access = resolveSelectedCompanyAccess(pool); + + /** + * Lists the quotes of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise>} The quotes. + * + * @example + * const quotes = await store.quote.list(42); + * // => [{ id, number, type, entityId, date }] + */ + return async (googleAuthId) => { + const { companyId } = await access(googleAuthId); + + const result = await pool + .request() + .input('companyId', companyId) + .input('type', QUOTE_TYPE) + .query(LIST_MANIFESTS); + + return result.recordset.map((row) => shapeSummary(QUOTE_TYPE, row)); + }; +} diff --git a/src/store/quote/update.js b/src/store/quote/update.js new file mode 100644 index 0000000..2298f8a --- /dev/null +++ b/src/store/quote/update.js @@ -0,0 +1,132 @@ +import { + DELETE_LINES_BY_MANIFEST, + GET_MANIFEST_HEADER, + QUOTE_TYPE, + UPDATE_MANIFEST, +} from '../../sql/manifestDoc.js'; +import request from '../request.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import taxEnsure from '../tax/ensure.js'; +import { insertLines, normalizeLines, parseDate } from '../manifestLines.js'; +import { assertCustomerEntity, loadDoc, shapeSummary } from '../manifestDoc/helpers.js'; + +export default function update(pool) { + const access = resolveSelectedCompanyAccess(pool); + const ensureTax = taxEnsure(pool); + + /** + * Updates a quote's header and/or lines. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the quote to update. + * @param {object} patch - The update patch. + * @param {string} [patch.number] - The new quote number. + * @param {string} [patch.date] - The new quote date. + * @param {number} [patch.entityId] - The new customer entity id. + * @param {Array} [patch.lines] - The new quote lines. + * @returns {Promise<{id: number, number: string, type: string, entityId: number|null, date: string, lines: Array}>} The updated quote. + * @throws {Error} If the quote is not found. + * + * @example + * const quote = await store.quote.update(42, 5, { number: 'A-2026-002', lines: [...] }); + * // => { id, number, type, entityId, date, lines: [...] } + */ + return async (googleAuthId, id, patch = {}) => { + if (!Number.isFinite(id)) { + throw new Error('Quote not found'); + } + + const hasNumber = Object.prototype.hasOwnProperty.call(patch, 'number'); + const hasDate = Object.prototype.hasOwnProperty.call(patch, 'date'); + const hasEntityId = Object.prototype.hasOwnProperty.call(patch, 'entityId'); + const hasLines = Object.prototype.hasOwnProperty.call(patch, 'lines'); + + if (!hasNumber && !hasDate && !hasEntityId && !hasLines) { + throw new Error('Invalid quote'); + } + + let normalizedLines; + if (hasLines) { + normalizedLines = normalizeLines(patch.lines); + } + + const { companyId, rootCategoryId } = await access(googleAuthId); + + const tx = pool.transaction(); + await tx.begin(); + + try { + const existing = await request(pool, tx) + .input('id', id) + .input('companyId', companyId) + .input('type', QUOTE_TYPE) + .query(GET_MANIFEST_HEADER); + + const current = existing.recordset[0]; + if (!current) { + throw new Error('Quote not found'); + } + + const summary = shapeSummary(QUOTE_TYPE, current); + + let number = summary.number; + if (hasNumber) { + number = patch.number?.trim?.() ? patch.number.trim() : null; + if (!number) { + throw new Error('Invalid quote'); + } + } + + let date = summary.date; + if (hasDate) { + date = parseDate(patch.date); + if (!date) { + throw new Error('Invalid quote'); + } + } + + let entityId = summary.entityId; + if (hasEntityId) { + entityId = await assertCustomerEntity( + pool, + tx, + companyId, + patch.entityId ?? null, + ); + } + + await request(pool, tx) + .input('id', id) + .input('companyId', companyId) + .input('type', QUOTE_TYPE) + .input('number', number) + .input('entityId', entityId) + .input('date', date) + .query(UPDATE_MANIFEST); + + if (hasLines) { + await request(pool, tx) + .input('manifestId', id) + .query(DELETE_LINES_BY_MANIFEST); + + await insertLines(pool, tx, { + manifestId: id, + rootCategoryId, + lines: normalizedLines, + ensureTax, + }); + } + + const doc = await loadDoc(pool, tx, { type: QUOTE_TYPE, companyId, id }); + await tx.commit(); + return doc; + } catch (err) { + try { + await tx.rollback(); + } catch { + // ignore + } + throw err; + } + }; +} diff --git a/src/store/supplier/create.js b/src/store/supplier/create.js index d57cff9..9a33614 100644 --- a/src/store/supplier/create.js +++ b/src/store/supplier/create.js @@ -4,20 +4,22 @@ import request from '../request.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import ensureLieferantenGroup from './ensureLieferantenGroup.js'; -/** - * Creates a new supplier for a company, recording history. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that creates a supplier. - * - * @example - * const fn = create(pool); - * const supplier = await fn(42, { name: 'Acme Supplies' }); - * // => { id, name, createdAt, updatedAt } - */ export default function create(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Creates a new supplier for the user's selected company, recording history. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The supplier input. + * @param {string} input.name - The supplier name. + * @returns {Promise<{id: number, name: string, createdAt: string, updatedAt: string}>} The created supplier. + * @throws {Error} If the name is empty. + * + * @example + * const supplier = await store.supplier.create(42, { name: 'Acme Supplies' }); + * // => { id, name, createdAt, updatedAt } + */ return async (googleAuthId, { name } = {}) => { const cleaned = name?.trim(); if (!cleaned) { diff --git a/src/store/supplier/delete.js b/src/store/supplier/delete.js index a3e8a6f..ad8544a 100644 --- a/src/store/supplier/delete.js +++ b/src/store/supplier/delete.js @@ -1,19 +1,20 @@ import { DELETE_BY_ID, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Deletes a supplier by its id within a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes a supplier. - * - * @example - * const fn = del(pool); - * await fn(42, 5); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Deletes a supplier by its id within the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the supplier to delete. + * @returns {Promise} + * @throws {Error} If the supplier is not found. + * + * @example + * await store.supplier.delete(42, 5); + */ return async (googleAuthId, id) => { if (!Number.isFinite(id)) { throw new Error('Supplier not found'); diff --git a/src/store/supplier/list.js b/src/store/supplier/list.js index 91abb90..c3b7896 100644 --- a/src/store/supplier/list.js +++ b/src/store/supplier/list.js @@ -1,20 +1,19 @@ import { LIST_SUPPLIERS_BY_COMPANY, SUPPLIER_GROUP_NAME } from '../../sql/entity.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Lists the suppliers of a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists suppliers. - * - * @example - * const fn = list(pool); - * const suppliers = await fn(42); - * // => [{ id, name, createdAt, updatedAt }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Lists the suppliers of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise>} The suppliers. + * + * @example + * const suppliers = await store.supplier.list(42); + * // => [{ id, name, createdAt, updatedAt }] + */ return async (googleAuthId) => { const { companyId } = await access(googleAuthId); diff --git a/src/store/supplier/update.js b/src/store/supplier/update.js index a1ef32a..833f978 100644 --- a/src/store/supplier/update.js +++ b/src/store/supplier/update.js @@ -17,20 +17,23 @@ function shape(row) { }; } -/** - * Updates the name of a supplier, recording history. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that updates a supplier. - * - * @example - * const fn = update(pool); - * const supplier = await fn(42, 5, { name: 'Acme GmbH' }); - * // => { id, name, createdAt, updatedAt } - */ export default function update(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Updates the name of a supplier, recording history. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The id of the supplier to update. + * @param {object} input - The update input. + * @param {string} input.name - The new supplier name. + * @returns {Promise<{id: number, name: string, createdAt: string, updatedAt: string}>} The updated supplier. + * @throws {Error} If the supplier is not found or the name is empty. + * + * @example + * const supplier = await store.supplier.update(42, 5, { name: 'Acme GmbH' }); + * // => { id, name, createdAt, updatedAt } + */ return async (googleAuthId, id, { name } = {}) => { if (!Number.isFinite(id)) { throw new Error('Supplier not found'); diff --git a/src/store/unit/delete.js b/src/store/unit/delete.js index 0b1c2da..69dae4d 100644 --- a/src/store/unit/delete.js +++ b/src/store/unit/delete.js @@ -1,19 +1,20 @@ import { DELETE_BY_COMPANY_AND_SI } from '../../sql/unit.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; -/** - * Deletes a unit for a company by its SI code. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that deletes a unit. - * - * @example - * const fn = del(pool); - * await fn(42, 'l'); - */ export default function del(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Deletes a unit for the user's selected company by its SI code. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} si - The SI code of the unit to delete. + * @returns {Promise} + * @throws {Error} If the unit is not found. + * + * @example + * await store.unit.delete(42, 'l'); + */ return async (googleAuthId, si) => { if (!si?.trim()) { throw new Error('Unit not found'); diff --git a/src/store/unit/list.js b/src/store/unit/list.js index 2b68374..c4c05cd 100644 --- a/src/store/unit/list.js +++ b/src/store/unit/list.js @@ -2,20 +2,19 @@ import { LIST_BY_COMPANY } from '../../sql/unit.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import shapeUnit from './shapeUnit.js'; -/** - * Lists the units available for a company. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that lists units. - * - * @example - * const fn = list(pool); - * const units = await fn(42); - * // => [{ id, si, basesi, basesifactor, symbol, name }] - */ export default function list(pool) { const access = resolveSelectedCompanyAccess(pool); + /** + * Lists the units available for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @returns {Promise>} The units. + * + * @example + * const units = await store.unit.list(42); + * // => [{ id, si, basesi, basesifactor, symbol, name }] + */ return async (googleAuthId) => { const { companyId, languageId } = await access(googleAuthId); diff --git a/src/store/unit/upsert.js b/src/store/unit/upsert.js index 899021a..ae4d551 100644 --- a/src/store/unit/upsert.js +++ b/src/store/unit/upsert.js @@ -30,20 +30,26 @@ function validateUnit({ si, basesi, basesifactor, symbol, name }) { }; } -/** - * Creates or updates a unit for a company, keyed by its SI code. - * - * @param {object} pool - The mssql connection pool. - * @returns {Function} An async function that upserts a unit. - * - * @example - * const fn = upsert(pool); - * 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 = resolveSelectedCompanyAccess(pool); + /** + * Creates or updates a unit for the user's selected company, keyed by its SI code. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The unit input. + * @param {string} input.si - The SI code. + * @param {string} input.basesi - The base SI code. + * @param {number} input.basesifactor - The base SI factor (must be > 0). + * @param {string} input.symbol - The unit symbol. + * @param {string} input.name - The unit name. + * @returns {Promise<{id: number, si: string, basesi: string, basesifactor: number, symbol: string, name: string}>} The upserted unit. + * @throws {Error} If the unit is invalid. + * + * @example + * const unit = await store.unit.upsert(42, { si: 'l', basesi: 'm3', basesifactor: 0.001, symbol: 'L', name: 'Liter' }); + * // => { id, si, basesi, basesifactor, symbol, name } + */ return async (googleAuthId, input) => { const unit = validateUnit(input); const { companyId, languageId } = await access(googleAuthId);