Compare commits

..

83 Commits

Author SHA1 Message Date
seb
26f21cd7ac u 2026-11-02 00:51:18 +01:00
seb
d0a33b1f82 u 2026-11-02 00:19:21 +01:00
seb
55f04de8a8 u 2026-10-31 21:46:43 +01:00
seb
76ea6642f4 u 2026-10-31 21:02:50 +01:00
seb
f612c87b6e u 2026-10-31 20:28:39 +01:00
seb
c7d1b1a5d2 u 2026-10-30 18:09:33 +01:00
seb
745d0d0077 u 2026-10-30 18:06:03 +01:00
seb
2e00f4adb3 u 2026-10-30 17:28:22 +01:00
seb
60cf5873cd u 2026-10-30 16:56:28 +01:00
seb
211acd651d u 2026-10-30 16:48:36 +01:00
seb
a4718d81c0 u 2026-10-30 15:43:28 +01:00
seb
0fdffb864f u 2026-10-30 06:43:49 +01:00
seb
64050dffaf u 2026-10-30 05:50:41 +01:00
seb
3cb5ec5a7a u 2026-10-29 16:17:38 +01:00
seb
a5cef78d30 u 2026-10-29 03:39:07 +01:00
seb
c7db2262b0 u 2026-10-29 03:29:07 +01:00
seb
0fb5893a70 u 2026-10-29 03:28:39 +01:00
seb
75a83bdc28 u 2026-10-29 03:17:47 +01:00
seb
b0521a4ea6 u 2026-10-29 02:55:51 +01:00
seb
2ded87abb7 u 2026-10-28 18:33:32 +01:00
seb
2d4d894881 sharp by wasm 2026-10-28 01:46:48 +01:00
seb
ccb81cdb26 x 2026-10-27 06:48:35 +00:00
seb
bab5c151d0 u 2026-10-26 19:22:04 +01:00
seb
563945394b u 2026-10-26 01:01:08 +01:00
seb
358e3009b2 u 2026-10-25 22:38:04 +01:00
seb
5fd7f3228c u 2026-10-25 17:13:32 +01:00
seb
53f1b24ec0 u 2026-10-25 16:59:53 +01:00
seb
6b6c473f9d u 2026-10-25 15:46:58 +01:00
seb
c6171a1d90 u 2026-10-25 11:06:03 +00:00
seb
62d0643b99 u 2026-10-24 06:19:37 +02:00
seb
1da82e2eb9 u 2026-10-24 01:24:25 +02:00
seb
feffe4e1ab u 2026-10-23 23:18:31 +02:00
seb
c172f2f4b1 u 2026-10-23 22:22:32 +02:00
seb
82ab27ef2b u 2026-10-23 20:31:39 +02:00
seb
8002ca56e6 u 2026-10-23 04:59:04 +02:00
seb
8a085986e5 u 2026-10-23 02:43:35 +02:00
seb
919713fa53 u 2026-10-23 01:38:08 +02:00
seb
5fc316461d u 2026-10-23 00:47:38 +02:00
seb
e31566e6ee u 2026-10-22 23:33:00 +02:00
seb
d6e5da93cb u 2026-10-22 23:30:15 +02:00
seb
e9b4ac5d79 u 2026-10-22 14:39:45 +02:00
seb
9495ad51cc u 2026-10-22 12:30:06 +02:00
seb
bb880e3c84 u 2026-10-21 11:59:41 +02:00
seb
eb1e9e2eb9 u 2026-10-21 11:55:21 +02:00
seb
e085bcaeba u 2026-10-21 06:13:29 +02:00
seb
b0014d6629 u 2026-10-21 04:28:56 +02:00
seb
e9e98d041c u 2026-10-21 03:31:52 +02:00
seb
e3761bc48b u 2026-10-20 23:19:43 +02:00
seb
813e7c89b8 u 2026-10-20 23:16:47 +02:00
seb
bcefd81af7 u 2026-10-20 05:33:11 +02:00
seb
5cb38874ca u 2026-10-20 02:22:58 +02:00
seb
4b34902b92 u 2026-10-19 23:56:56 +02:00
seb
a5eee3f3d0 u 2026-10-19 13:24:59 +02:00
seb
5259e3bef9 u 2026-10-19 02:28:15 +02:00
seb
cce966d021 u 2026-10-19 01:32:42 +02:00
seb
c730244902 u 2026-10-18 20:54:00 +02:00
seb
9ed7e4ee3b u 2026-10-18 20:48:47 +02:00
seb
8cc7d0eb0b u 2026-10-18 20:29:51 +02:00
seb
f6b0ff7483 u 2026-10-18 14:51:33 +02:00
seb
d587f31953 u 2026-10-18 14:44:22 +02:00
seb
9e1edd5cad u 2026-10-18 14:39:03 +02:00
seb
da2cdd5b23 u 2026-10-18 14:20:35 +02:00
seb
bf4109d8ec u 2026-10-18 14:07:01 +02:00
seb
97c909d10b u 2026-10-18 03:57:53 +02:00
seb
c687553eea u 2026-10-18 03:15:20 +02:00
seb
fda895f75a u 2026-10-17 23:59:26 +02:00
seb
ce5da810a5 u 2026-10-17 23:34:29 +02:00
seb
3c2b933f93 u 2026-10-17 23:32:42 +02:00
seb
688fa4f3fd u 2026-10-17 23:31:37 +02:00
seb
ab42a3ffb4 u 2026-10-17 19:03:24 +02:00
seb
f8cc01baf0 u 2026-10-17 05:10:28 +02:00
seb
a194cf6b8b u 2026-10-17 02:59:37 +02:00
seb
59395f95d8 u 2026-10-17 02:47:20 +02:00
seb
8ee080b744 u 2026-10-17 00:52:45 +02:00
seb
e16c3b3f67 u 2026-10-16 22:41:03 +02:00
seb
c7d709de1f u 2026-10-16 22:34:48 +02:00
seb
7635821dad u 2026-10-16 22:09:12 +02:00
seb
796f6a3272 u 2026-10-16 12:35:07 +02:00
seb
70e3ae7a87 u 2026-10-16 02:31:54 +02:00
seb
fcbcb243e3 u 2026-10-16 01:59:11 +02:00
seb
0d726c76f8 u 2026-10-15 22:49:14 +02:00
seb
2179b2e241 u 2026-10-15 20:14:51 +02:00
seb
7c8c24eb4d u 2026-10-15 15:54:59 +02:00
359 changed files with 28013 additions and 6734 deletions

119
API.agentprinter.md Normal file
View File

@@ -0,0 +1,119 @@
# agentprinter
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `agentprinter.delete`
*Source: `src/store/agentprinter/delete.js`*
Deletes an agent printer by id for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The agent printer id. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number}>`
## `agentprinter.findOrCreate`
*Source: `src/store/agentprinter/findOrCreate.js`*
Finds or creates an agentprinter by (printeragentId, type, model, serial) combo.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `printeragentId` | `number` | The parent printer agent id. |
| `input` | `object` | |
| `input.type` | `string` | Printer type ("zpl", "esc/pos", etc.). |
| `[input.model]` | `string|null` | Discovered model name. |
| `[input.serial]` | `string|null` | Discovered serial number. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{created: boolean, agentprinter: {id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}}>`
## `agentprinter.getById`
*Source: `src/store/agentprinter/getById.js`*
Retrieves an agent printer by id, scoped to the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The agent printer id. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}|null>`
## `agentprinter.list`
*Source: `src/store/agentprinter/list.js`*
Lists agent printers of the user's selected company, optionally filtered by printer agent.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[printeragentId]` | `number` | Optional printer agent id filter. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<Array<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>>`
## `agentprinter.order`
*Source: `src/store/agentprinter/order.js`*
Reorders agent printers for a given printer agent or globally across the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | |
| `[input.printeragentId]` | `number` | Optional printer agent id. If omitted, reorders all printers in company. |
| `input.agentprinterIds` | `number[]` | Complete ordered list of agent printer IDs. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{agentprinterIds: number[]}>`
## `agentprinter.update`
*Source: `src/store/agentprinter/update.js`*
Updates enabled and/or sort of an agent printer for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | |
| `input.id` | `number` | Agent printer id. |
| `[input.enabled]` | `boolean` | Whether the printer is enabled. |
| `[input.sort]` | `number` | Display order integer. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>`

View File

@@ -97,7 +97,7 @@ Creates a new article under a given parent category, allocating a SKU and a defa
| `input.name` | `string` | The article name. | | `input.name` | `string` | The article name. |
| `[input.desc]` | `string` | The article description text/HTML. | | `[input.desc]` | `string` | The article description text/HTML. |
| `[input.seo]` | `string` | The article SEO text. | | `[input.seo]` | `string` | The article SEO text. |
| `[input.track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. | | `[input.track_stock=false]` | `boolean|number` | Whether inventory stock is tracked. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
@@ -181,12 +181,10 @@ Returns the full detail of an article by its SKU, including unit, brand, categor
| Name | Type | Description | | Name | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. | | `googleAuthId` | `number` | The id of the Google auth user. |
| `sku` | `string` | The article SKU. |
| `[tx]` | `object` | Optional transaction. |
**Returns** **Returns**
`Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` — The full article detail. `Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, warehouses: Array<{id: number, name: string, sort: number, stock: number}>, undeliveredOrders: Array<{orderId: number, orderNumber: string, date: string, customerName: string|null, amount: number}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` — The full article detail.
**Throws** **Throws**
@@ -211,22 +209,26 @@ Returns the change history of an article by its SKU.
| --- | --- | --- | | --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. | | `googleAuthId` | `number` | The id of the Google auth user. |
| `sku` | `string` | The article SKU. | | `sku` | `string` | The article SKU. |
| `[opts]` | `object` | Optional options. | | `[opts]` | `object` | Optional pagination and filter options. |
| `[opts.limit]` | `number` | Maximum number of entries to return (newest first). | | `[opts.offset=0]` | `number` | Number of entries to skip. |
| `[opts.limit=50]` | `number` | Number of entries to return. |
| `[opts.search]` | `string` | Optional search filter for history text. |
| `[opts.seekMonths]` | `number` | Negative integer cutoff months ago. |
| `[opts.paged]` | `boolean` | Explicitly return paged result object. |
| `[tx]` | `object` | Optional transaction. | | `[tx]` | `object` | Optional transaction. |
**Returns** **Returns**
`Promise<Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>` — The article history entries. `Promise<{history: Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>` — The article history entries.
**Example** **Example**
```js ```js
const result = await store.article.getHistoryBySku(42, 'SKU-1', { offset: 0, limit: 50, search: 'Tee' });
// => { history: [...], total, unfilteredTotal, offset, limit }
const history = await store.article.getHistoryBySku(42, 'SKU-1'); const history = await store.article.getHistoryBySku(42, 'SKU-1');
// => [{ id, companyuserId, date, text, meta }] // => [{ id, companyuserId, date, text, meta }]
const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
// => up to 5 most recent entries
``` ```
## `article.importRows` ## `article.importRows`
@@ -251,7 +253,7 @@ Imports article rows, creating articles by their SKU and returning per-row impor
| `[input.rows[].net]` | `number|string` | The POS net price (accepts German decimals). | | `[input.rows[].net]` | `number|string` | The POS net price (accepts German decimals). |
| `[input.rows[].tax]` | `number|string` | The tax rate in percent (e.g. `19`). | | `[input.rows[].tax]` | `number|string` | The tax rate in percent (e.g. `19`). |
| `[input.rows[].eans]` | `Array<string>|string` | The EANs to set. | | `[input.rows[].eans]` | `Array<string>|string` | The EANs to set. |
| `[input.rows[].track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. | | `[input.rows[].track_stock=false]` | `boolean|number` | Whether inventory stock is tracked. |
| `[input.rows[].parentSku]` | `string` | The parent article SKU for a V/K compound link. | | `[input.rows[].parentSku]` | `string` | The parent article SKU for a V/K compound link. |
| `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. | | `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. |
| `[tx]` | `object` | Optional transaction. | | `[tx]` | `object` | Optional transaction. |
@@ -320,6 +322,29 @@ const articles = await store.article.listExport(42, null);
// => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }] // => [{ id, sku, name, slug, unitSi, net, taxRate, eans, categoryBreadcrumbs, fullHashes, ... }]
``` ```
## `article.listHistory`
*Source: `src/store/article/listHistory.js`*
Lists article history entries for the user's selected company, supporting pagination, date cutoff via seekMonths, and search.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `import('../../providers/provider.js').Provider` | |
**Returns**
`(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{history: Array<{id: number, date: string, sku: string, name: string|null, historytext: string}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
**Example**
```js
const res = await store.article.listHistory(googleAuthId, { offset: 0, limit: 50, search: 'hjg', seekMonths: -13 });
// => { history: [...], total, unfilteredTotal, offset, limit }
```
## `article.listSearchIndex` ## `article.listSearchIndex`
*Source: `src/store/article/listSearchIndex.js`* *Source: `src/store/article/listSearchIndex.js`*
@@ -574,7 +599,7 @@ const article = await store.article.setPricesBySku(42, 'SKU-1', [{ gross: 11.9,
*Source: `src/store/article/setTrackStockBySku.js`* *Source: `src/store/article/setTrackStockBySku.js`*
Sets the track_stock flag of an article by its SKU. Sets the track_stock flag of an article by its SKU. If stock tracking is being disabled (1 -> 0), all non-zero warehouse stocks are adjusted to 0 on the ledger before disabling tracking.
**Parameters** **Parameters**
@@ -591,14 +616,7 @@ Sets the track_stock flag of an article by its SKU.
**Throws** **Throws**
`Error` — If trackStock is invalid or the article is not found. `Error` — If trackStock is invalid, article not found, or no warehouse exists when enabling.
**Example**
```js
const article = await store.article.setTrackStockBySku(42, 'SKU-1', true);
// => { id, sku, name, track_stock, ... }
```
## `article.setWeightBySku` ## `article.setWeightBySku`

View File

@@ -6,14 +6,4 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/articlehistory/record.js`* *Source: `src/store/articlehistory/record.js`*
Formats a numeric currency amount in German language style: "€ 1.000,99" ============================================================================ ARTICLE HISTORY LOGGER ============================================================================ Recorded History Event Types (`meta.type`): 1. Orders & Inventory Ledger - `order` : Customer order placement (`recordOrderHistory`) - `acceptance` : Incoming supplier delivery without warehouse (`recordAcceptanceHistory`) - `shipment` : Outgoing customer shipment (`recordShipmentHistory`) - `warehouse_in` : Inbound warehouse movement (`recordWarehouseHistory`) - `warehouse_out` : Outbound warehouse movement (`recordWarehouseHistory`) - `delivery` : Legacy incoming delivery with warehouse (`recordDeliveryHistory`) - `movement` : Legacy generic warehouse movement (`recordDeliveryHistory`) - `stock_adjustment` : Manual stock correction (`recordStockAdjustmentHistory`) 2. Core Article Lifecycle & Master Data - `article_created` : Initial article creation (`recordArticleCreatedHistory`) - `article_imported` : Article created via import (`recordArticleImportedHistory`) - `article_update` : General field updates (`recordArticleUpdateHistory`) - `price_change` : Price tier / net / tax modifications (`recordPriceChangeHistory`) - `category_change` : Category assignment changes (`recordCategoryChangeHistory`) - `brand_change` : Brand name updates (`recordBrandChangeHistory`) - `ean_change` : EAN list updates (`recordEanChangeHistory`) - `weight_change` : Article weight changes (`recordWeightChangeHistory`) - `track_stock_change` : Stock tracking toggle (`recordTrackStockChangeHistory`) 3. Variants & Parent/Child Relationships - `parent_converted` : Article converted to parent (`recordParentConvertedHistory`) - `parent_deactivated` : Parent mode disabled (`recordParentDeactivatedHistory`) - `variant_added_to_parent` : Variant linked on parent record (`recordVariantAddedOnParentHistory`) - `child_created_under_parent` : New variant created under parent (`recordChildAssignedToParentHistory`) - `child_assigned_to_parent` : Existing article assigned as variant (`recordChildAssignedToParentHistory`) - `child_unlinked` : Child unlinked from parent - child log (`recordChildUnlinkedFromParentHistory`) - `variant_separated` : Variant separated from parent - parent log (`recordChildUnlinkedFromParentHistory`) - `variant_deleted` : Variant deleted from parent (`recordChildDeletedFromParentHistory`) 4. Suppliers - `supplier_add` : Supplier linked to article (`recordSupplierAddHistory`) - `supplier_update` : Supplier details modified (`recordSupplierUpdateHistory`) - `supplier_delete` : Supplier unlinked from article (`recordSupplierDeleteHistory`) 5. Properties, Attributes & Bills of Materials (BOM) - `property_change` : Property key/value changes (`recordPropertyChangeHistory`) - `property_delete` : Single property removed (`recordPropertyDeleteHistory`) - `attribute_change` : Variant/article attribute changes (`recordAttributeChangeHistory`) - `articlelist_change` : Stückliste / component BOM set (`recordArticleListChangeHistory`) - `optionslist_change` : Optionsliste set (`recordOptionsListChangeHistory`) 6. Media & Images - `image_add` : Image uploaded/linked (`recordImageAddHistory`) - `image_delete` : Image removed (`recordImageDeleteHistory`) - `image_order` : Image gallery reordered (`recordImageOrderHistory`) ============================================================================
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `amount` | `number|string` | The currency amount. |
**Returns**
`string` — The formatted currency string.

View File

@@ -0,0 +1,9 @@
# articlewarehousesummary
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `articlewarehousesummary.syncSummary`
*Source: `src/store/articlewarehousesummary/syncSummary.js`*
Centralized sync and rebuild logic for the materialized `articlewarehousesummary` table. Keeps `articlewarehousesummary` in sync with ledger movements, track_stock status, and warehouses.

86
API.autoinvite.md Normal file
View File

@@ -0,0 +1,86 @@
# autoinvite
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `autoinvite.add`
*Source: `src/store/autoinvite/add.js`*
Registers an email for automatic company membership. When that email is registered (`google_auth.add`) the invitation is consumed: it is removed from the list and the new user is added to the company with the given role.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
| `input` | `{email: string, role?: 'admin'|'user'}` | The email to invite and the role (default 'user'). |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, email: string, role: 'admin'|'user'}>` — The created invitation.
**Throws**
`Error` — 'Not authorized', 'Invalid email', 'Invalid role', 'User already registered', 'Invitation already exists'
**Example**
```js
await store.autoinvite.add(42, { email: 'new@example.com', role: 'user' });
```
## `autoinvite.list`
*Source: `src/store/autoinvite/list.js`*
Lists the pending auto-invitations of the admin's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user (must be admin of the selected company). |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<Array<{id: number, email: string, role: 'admin'|'user', createdAt: string}>>` — The invitations ordered by email.
**Throws**
`Error` — 'Not authorized' / 'No company selected'
**Example**
```js
const invites = await store.autoinvite.list(42);
```
## `autoinvite.remove`
*Source: `src/store/autoinvite/remove.js`*
Deletes a pending auto-invitation of the admin's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
| `id` | `number` | The invitation id. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<void>`
**Throws**
`Error` — 'Not authorized', 'Invitation not found'
**Example**
```js
await store.autoinvite.remove(42, 3);
```

176
API.carrier.md Normal file
View File

@@ -0,0 +1,176 @@
# carrier
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `carrier.create`
*Source: `src/store/carrier/create.js`*
Creates a new carrier for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | Carrier input. |
| `input.name` | `string` | The carrier name. |
| `[input.sort]` | `number` | Optional sort order (auto-increments if omitted). |
| `[input.hasPackages=false]` | `boolean` | Whether this carrier uses packages. |
| `[input.isReverse=false]` | `boolean` | Whether this carrier handles reverse shipments. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>` — The created carrier.
**Throws**
`Error` — If name is missing, sort is invalid, or carrier already exists.
**Example**
```js
const c = await store.carrier.create(googleAuthId, { name: 'DHL', hasPackages: true });
// => { id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }
```
## `carrier.delete`
*Source: `src/store/carrier/delete.js`*
Deletes a carrier for the user's selected company, unless referenced in shipping.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The carrier ID to delete. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<void>`
**Throws**
`Error` — If carrier not found or in use in shipping.
**Example**
```js
await store.carrier.delete(googleAuthId, 1);
```
## `carrier.getById`
*Source: `src/store/carrier/getById.js`*
Retrieves a carrier by its ID for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The carrier ID. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}|undefined>` — The carrier or undefined.
**Example**
```js
const c = await store.carrier.getById(googleAuthId, 1);
// => { id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }
```
## `carrier.list`
*Source: `src/store/carrier/list.js`*
Lists all carriers for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<Array<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>>` — List of carriers.
**Example**
```js
const carriers = await store.carrier.list(googleAuthId);
// => [{ id: 1, name: 'DHL', sort: 0, hasPackages: true, isReverse: false }]
```
## `carrier.order`
*Source: `src/store/carrier/order.js`*
Reorders the carriers 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.carrierIds` | `Array<number>` | The carrier ids in their new order. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{carrierIds: Array<number>}>` — The applied order.
**Throws**
`Error` — If the order is invalid.
**Example**
```js
await store.carrier.order(googleAuthId, { carrierIds: [2, 1, 3] });
```
## `carrier.update`
*Source: `src/store/carrier/update.js`*
Updates a carrier name, sort order, hasPackages, and/or isReverse for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The carrier ID to update. |
| `input` | `object` | Update payload. |
| `[input.name]` | `string` | The new carrier name. |
| `[input.sort]` | `number` | The new sort order. |
| `[input.hasPackages]` | `boolean` | Whether carrier uses packages. |
| `[input.isReverse]` | `boolean` | Whether carrier handles reverse shipments. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>` — The updated carrier.
**Throws**
`Error` — If carrier not found, name is empty, sort is invalid, or carrier already exists.
**Example**
```js
const updated = await store.carrier.update(googleAuthId, 1, { name: 'DHL Express', hasPackages: true });
// => { id: 1, name: 'DHL Express', sort: 0, hasPackages: true, isReverse: false }
```

View File

@@ -142,6 +142,37 @@ const id = await store.category.ensureByPath({ googleAuthId: 42, parentId: 5, pa
// => 12 // => 12
``` ```
## `category.generateImage`
*Source: `src/store/category/generateImage.js`*
Generates a mosaic image directly from the category's and its subcategories' articles (grouped by subcategory) and sets it as the new main picture for the category.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `inputOrCategoryId` | `number|object` | The id of the category or an options object. |
| `[maybeOptsOrSize={}]` | `object|number` | Options object or numeric size. |
| `[maybeOptsOrSize.size]` | `number` | Grid size (2 for 2x2, 3 for 3x3, 4 for 4x4). Defaults to 3 (falls back to 2 if < 7 images). |
| `[maybeTx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, size: number, grid: string, articleCount: number}>` — The new main image info.
**Throws**
`Error` — If the category is not found, not authorized, size is invalid, or no images are found.
**Example**
```js
const result = await store.category.generateImage(42, 5, { size: 3 });
// => { id: 5, fullHash, thumbW, thumbH, thumbHash, smallThumbW, smallThumbH, size: 3, grid: '3x3', articleCount: 12 }
```
## `category.getCategoryArticles` ## `category.getCategoryArticles`
*Source: `src/store/category/getCategoryArticles.js`* *Source: `src/store/category/getCategoryArticles.js`*
@@ -158,7 +189,7 @@ Returns the articles belonging to a single category.
**Returns** **Returns**
`Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null}>>` — The articles in the category. `Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, smallThumbW: number|null, smallThumbH: number|null, smallThumbBinary: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, articlewarehousesummary: Array<{id: number, articleId: number, warehouseId: number, warehouseName: string, stock: number}>, warehouses: Array<{id: number, name: string, stock: number}>}>>` — The articles in the category.
**Example** **Example**

View File

@@ -44,7 +44,7 @@ Creates a new company together with its root category, POS customer group, defau
| --- | --- | --- | | --- | --- | --- |
| `input` | `object` | The company creation input. | | `input` | `object` | The company creation input. |
| `input.iso639` | `string` | The ISO 639 language code for the company. | | `input.iso639` | `string` | The ISO 639 language code for the company. |
| `input.name` | `string` | The company name (must be unique). | | `input.name` | `string` | The company name. |
| `input.taxrate` | `number` | The standard tax rate in percent. | | `input.taxrate` | `number` | The standard tax rate in percent. |
| `input.reducedtaxrate` | `number` | The reduced 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. | | `input.googleAuthId` | `number` | The id of the Google auth user to link. |
@@ -54,10 +54,6 @@ Creates a new company together with its root category, POS customer group, defau
`Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>` — The created company ids. `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** **Example**
```js ```js
@@ -208,7 +204,7 @@ Updates an existing company's name, language and tax rates.
| `id` | `number` | The id of the company to update. | | `id` | `number` | The id of the company to update. |
| `input` | `object` | The update input. | | `input` | `object` | The update input. |
| `input.iso639` | `string` | The ISO 639 language code. | | `input.iso639` | `string` | The ISO 639 language code. |
| `input.name` | `string` | The new company name (must be unique). | | `input.name` | `string` | The new company name. |
| `input.taxrate` | `number` | The standard tax rate in percent. | | `input.taxrate` | `number` | The standard tax rate in percent. |
| `input.reducedtaxrate` | `number` | The reduced tax rate in percent. | | `input.reducedtaxrate` | `number` | The reduced tax rate in percent. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
@@ -219,7 +215,7 @@ Updates an existing company's name, language and tax rates.
**Throws** **Throws**
`Error` — If the company is not found or the name is taken. `Error` — If the company is not found.
**Example** **Example**

View File

@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/companyuser/add.js`* *Source: `src/store/companyuser/add.js`*
Adds a company user linking a company to a Google auth account. Adds a company user linking a company to a Google auth account. No admin check: this is the low-level link primitive (company creation, invitations). Use `companyuser.addByEmail` for the admin-guarded flow.
**Parameters** **Parameters**
@@ -14,19 +14,52 @@ Adds a company user linking a company to a Google auth account.
| --- | --- | --- | | --- | --- | --- |
| `companyId` | `number` | The id of the company. | | `companyId` | `number` | The id of the company. |
| `googleAuthId` | `number` | The id of the Google auth account. | | `googleAuthId` | `number` | The id of the Google auth account. |
| `[role='user']` | `'admin'|'user'` | The role within the company. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<number>` — The id of the created company user link. `Promise<number>` — The id of the created company user link.
**Throws**
`Error` — 'Invalid role' if the role is not 'admin' or 'user'.
**Example** **Example**
```js ```js
const id = await store.companyuser.add(1, 42); const id = await store.companyuser.add(1, 42, 'admin');
// => 7 // => 7
``` ```
## `companyuser.addByEmail`
*Source: `src/store/companyuser/addByEmail.js`*
Adds an existing Google auth user to the admin's selected company. The added user can then select the company as their own selected company. For emails that are not registered yet use `autoinvite.add`.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
| `input` | `{email: string, role?: 'admin'|'user'}` | Email of the existing user and role (default 'user'). |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>` — The created company user.
**Throws**
`Error` — 'Not authorized', 'Invalid email', 'Invalid role', 'User not found', 'User already in company'
**Example**
```js
const member = await store.companyuser.addByEmail(42, { email: 'new@example.com', role: 'user' });
```
## `companyuser.delete` ## `companyuser.delete`
*Source: `src/store/companyuser/delete.js`* *Source: `src/store/companyuser/delete.js`*
@@ -73,3 +106,134 @@ Returns the company user ids associated with the given email address.
const ids = await store.companyuser.getByEmail('user@example.com'); const ids = await store.companyuser.getByEmail('user@example.com');
// => [1, 2, 3] // => [1, 2, 3]
``` ```
## `companyuser.list`
*Source: `src/store/companyuser/list.js`*
Lists the members of the admin's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user (must be admin of the selected company). |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<Array<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>>` — The company users ordered by email.
**Throws**
`Error` — 'Not authorized' / 'No company selected'
**Example**
```js
const members = await store.companyuser.list(42);
```
## `companyuser.remove`
*Source: `src/store/companyuser/remove.js`*
Removes a member from the admin's selected company. History entries keep existing but lose their author link; if the removed user had this company selected, their selection is cleared.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
| `id` | `number` | The company user id to remove. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, googleAuthId: number}>` — The removed company user.
**Throws**
`Error` — 'Not authorized', 'Company user not found', 'Cannot remove the last admin'
**Example**
```js
await store.companyuser.remove(42, 7);
```
## `companyuser.requireAdmin`
*Source: `src/store/companyuser/requireAdmin.js`*
Resolves the selected company and requires the user to be its admin.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | |
| `[tx]` | `object` | |
**Returns**
`Promise<{companyId: number, companyuserId: number, role: string}>`
**Throws**
`Error` — 'Not authorized' if the user is not an admin of the selected company.
## `companyuser.roles`
*Source: `src/store/companyuser/roles.js`*
Validates a company user role.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `role` | `unknown` | |
**Returns**
`'admin'|'user'`
**Throws**
`Error` — 'Invalid role'
## `companyuser.setRole`
*Source: `src/store/companyuser/setRole.js`*
Changes the role of a member of the admin's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the acting Google auth user (must be admin of the selected company). |
| `id` | `number` | The company user id. |
| `role` | `'admin'|'user'` | The new role. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>` — The updated company user.
**Throws**
`Error` — 'Not authorized', 'Invalid role', 'Company user not found', 'Cannot demote the last admin'
**Example**
```js
await store.companyuser.setRole(42, 7, 'admin');
```
## `companyuser.withTransaction`
*Source: `src/store/companyuser/withTransaction.js`*
Runs `fn(runner)` inside the given transaction, a new one, or (providers without transaction support) directly.

View File

@@ -7,3 +7,61 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/delivery/attachments.js`* *Source: `src/store/delivery/attachments.js`*
Loads attachment metadata (without binary content) for a manifest. Loads attachment metadata (without binary content) for a manifest.
## `delivery.book`
*Source: `src/store/delivery/book.js`*
Books all positions of an incoming delivery manifest to the ledger by delegating to store.stock.accept.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `object` | Storage provider instance. |
**Returns**
`Function` — `(googleAuthId, input, tx) => Promise<object>`
## `delivery.list`
*Source: `src/store/delivery/list.js`*
Lists the deliveries of the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[opts]` | `object` | Optional pagination and filter options. |
| `[opts.offset=0]` | `number` | Number of deliveries to skip. |
| `[opts.limit=50]` | `number` | Number of deliveries to return. |
| `[opts.search]` | `string` | Free-text search matching delivery number, external number, or supplier name. |
| `[opts.onlyUnbooked=false]` | `boolean` | When true, returns only deliveries with unbooked stock items. |
| `[opts.supplierId]` | `number` | Filter by supplier entity id. |
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<{deliveries: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
## `delivery.rebuildSummaries`
*Source: `src/store/delivery/rebuildSummaries.js`*
Rebuilds the `deliverysummary` table for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
## `delivery.syncSummary`
*Source: `src/store/delivery/syncSummary.js`*
Centralized sync and rebuild logic for the materialized `deliverysummary` table. Keeps `deliverysummary` in sync with manifest (delivery), manifestarticle, article (track_stock), and ledger (DELIVERY transactions).

View File

@@ -55,7 +55,7 @@ Deletes an entity by its id within the user's selected company.
**Throws** **Throws**
`Error` — If the entity is not found. `Error` — If the entity is not found or is in use.
**Example** **Example**

View File

@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/google_auth/add.js`* *Source: `src/store/google_auth/add.js`*
Adds a new allowed Google auth email. Adds a new allowed Google auth email (registration). Pending `autoinvite` rows for this email are consumed in the same transaction: each is deleted and turned into a `companyuser` with the invited role.
**Parameters** **Parameters**
@@ -144,7 +144,7 @@ await store.google_auth.setProfile(42, { picture: 'url', pictureBinary: buffer,
*Source: `src/store/google_auth/setSelectedCompany.js`* *Source: `src/store/google_auth/setSelectedCompany.js`*
Sets the currently selected company for a Google auth user. Sets the currently selected company for a Google auth user. The user must be a member (`companyuser`) of the company.
**Parameters** **Parameters**
@@ -157,6 +157,10 @@ Sets the currently selected company for a Google auth user.
`Promise<void>` `Promise<void>`
**Throws**
`Error` — 'Not authorized' if the user is not a member of the company.
**Example** **Example**
```js ```js

26
API.healthcheck.md Normal file
View File

@@ -0,0 +1,26 @@
# healthcheck
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `healthcheck.check`
*Source: `src/store/healthcheck/check.js`*
Executes a database health check: 1. Ensures the healthcheck table has one row (creates with sequence 0 if not). 2. Selects that row for update with minimal, reasonable locking within a transaction. 3. Updates the sequence by 1 in a separate statement within the transaction.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{ok: boolean, sequence: number}>` — The health check result.
**Example**
```js
const result = await store.healthcheck.check();
// => { ok: true, sequence: 1 }
```

View File

@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/ledger/add.js`* *Source: `src/store/ledger/add.js`*
Appends a new append-only entry to the ledger table. Appends new append-only entry/entries to the ledger table according to manifest rules: - quote / order: no bookings possible (throws error) - delivery: acceptance entry (warehouse_id=null) + warehouse entry (if tracked) in one transaction - shipment: shipment entry (warehouse_id=null, negative amount) + warehouse entry (if tracked) in one transaction - adjustment: single warehouse inventory movement
**Parameters** **Parameters**
@@ -23,23 +23,7 @@ Appends a new append-only entry to the ledger table.
**Returns** **Returns**
`Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>` — The created ledger row. `Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>` — The primary ledger row.
**Throws**
`Error` — If user not authorized, company not selected, manifest article not found, or input invalid.
**Example**
```js
const entry = await store.ledger.add(googleAuthId, {
manifestarticle_id: 1,
warehouse_id: 1,
transaction_type: 'DELIVERY',
amount: 100.5,
transaction_text: 'Invoice paid',
});
```
## `ledger.listByManifestarticleId` ## `ledger.listByManifestarticleId`
@@ -100,3 +84,26 @@ Lists ledger entries for a given article SKU, optionally filtered by transaction
const entries = await store.ledger.listBySku(googleAuthId, 'SKU-001', 'DELIVERY'); const entries = await store.ledger.listBySku(googleAuthId, 'SKU-001', 'DELIVERY');
// => [{ id, manifestarticle_id, warehouse_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }] // => [{ id, manifestarticle_id, warehouse_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }]
``` ```
## `ledger.listHistory`
*Source: `src/store/ledger/listHistory.js`*
Lists ledger entries for the user's selected company across all articles, supporting pagination, date cutoff via seekMonths, and search.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `import('../../providers/provider.js').Provider` | |
**Returns**
`(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{ledger: Array<{id: number, date: string, sku: string|null, name: string|null, transaction_type: string, transaction_text: string, amount: number}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
**Example**
```js
const res = await store.ledger.listHistory(googleAuthId, { offset: 0, limit: 50, search: 'kjh', seekMonths: -1 });
// => { ledger: [...], total, unfilteredTotal, offset, limit }
```

147
API.md
View File

@@ -8,6 +8,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
> **Convention**: `PK` primary key · `?` nullable · `->tbl` foreign key · `UQ` unique · `s(N)` string · `txt` text · `i` int · `bi` bigint · `f` float · `d` decimal · `b` bit · `dt` datetime · `bin` binary · `=val` default > **Convention**: `PK` primary key · `?` nullable · `->tbl` foreign key · `UQ` unique · `s(N)` string · `txt` text · `i` int · `bi` bigint · `f` float · `d` decimal · `b` bit · `dt` datetime · `bin` binary · `=val` default
- **account**: `id PK, company_id->company, type:s(100), name:s(255)`
- **article**: `id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)` - **article**: `id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)`
- **articleattributevalue**: `id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)` - **articleattributevalue**: `id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)`
- **articleean**: `id PK, article_id->article, ean:s(50) UQ` - **articleean**: `id PK, article_id->article, ean:s(50) UQ`
@@ -25,13 +26,17 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- **brandpic**: `id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)` - **brandpic**: `id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)`
- **category**: `id PK, parent_id?->category, sort:i=0` - **category**: `id PK, parent_id?->category, sort:i=0`
- **categoryarticle**: `id PK, category_id->category, article_id->article, UQ(category_id, article_id)` - **categoryarticle**: `id PK, category_id->category, article_id->article, UQ(category_id, article_id)`
- **categoryattributevalue**: `id PK, category_id->category, attributevalue_id->attributevalue, sort:i=0, UQ(category_id, attributevalue_id)`
- **categorypic**: `id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)` - **categorypic**: `id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)`
- **categoryproperty**: `id PK, category_id->category, property_id->propertyname, value:txt, sort:i=0`
- **categorytext**: `id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)` - **categorytext**: `id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)`
- **company**: `id PK, name:s(255) UQ, root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup` - **company**: `id PK, name:s(255), root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup`
- **companyhistory**: `id PK, company_id->company, companyuser_id?->companyuser, date:dt, text:txt` - **companyhistory**: `id PK, company_id->company, companyuser_id?->companyuser, date:dt, text:txt`
- **companysetting**: `id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)` - **companysetting**: `id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)`
- **companyuser**: `id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)` - **companyuser**: `id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)`
- **customergroup**: `id PK, name:s(255), sort:i=0, company_id?->company` - **customergroup**: `id PK, name:s(255), sort:i=0, company_id?->company`
- **articlewarehousesummary**: `id PK, article_id->article, warehouse_id->warehouse, stock:f=0, UQ(article_id, warehouse_id)`
- **deliverysummary**: `id PK, company_id->company, manifest_id->manifest UQ, delivery_number:s(100), external_number?:s(100), delivery_date:dt, created_at:dt, supplier_id?->entity, supplier_name?:s(255), positions:i=0, gross_total:f=0, net_total:f=0, total_quantity:f=0, stock_positions:i=0, booked_positions:i=0, unbooked_positions:i=0, stock_quantity:f=0, booked_quantity:f=0, unbooked_quantity:f=0, is_booked:b=0`
- **entity**: `id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)` - **entity**: `id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)`
- **entityaddress**: `id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0` - **entityaddress**: `id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0`
- **entitycontact**: `id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0` - **entitycontact**: `id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0`
@@ -44,16 +49,31 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- **manifest**: `id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt` - **manifest**: `id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt`
- **manifestarticle**: `id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0` - **manifestarticle**: `id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0`
- **manifestattachment**: `id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)` - **manifestattachment**: `id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)`
- **mcp_auth_code**: `code:s(128) PK, client_id:s(100), google_auth_id->google_auth, redirect_uri:s(2048), code_challenge:s(255), code_challenge_method:s(32), scope?:s(255), expires_at:dt, used:b=0, created_at:dt`
- **mcp_client**: `id PK, client_id:s(100) UQ, client_secret?:s(255), client_name:s(255), redirect_uris:txt, grant_types?:s(255), response_types?:s(255), scope?:s(255), created_at:dt`
- **mcp_token**: `id PK, refresh_token:s(128) UQ, client_id:s(100), google_auth_id->google_auth, scope?:s(255), expires_at:dt, revoked:b=0, created_at:dt`
- **meta**: `key:s(255) PK, value:txt` - **meta**: `key:s(255) PK, value:txt`
- **modelconversation**: `id PK, companyuser_id->companyuser, provider:s(100), model:s(255), created_at:dt, updated_at:dt, state:s(20)='idle', started_by?:i`
- **modelmessage**: `id PK, conversation_id->modelconversation, created_at:dt, role:s(20), message:txt`
- **modelvfs**: `id PK, company_id->company, name:s(255), mime:s(100), content:bin, meta?:txt, created_at:dt, updated_at:dt, UQ(company_id, name)`
- **numbers**: `id PK, company_id->company, type:s(100), prefix:s(50)='', padding:i=0, next:i=1, UQ(company_id, type)` - **numbers**: `id PK, company_id->company, type:s(100), prefix:s(50)='', padding:i=0, next:i=1, UQ(company_id, type)`
- **optionslist**: `id PK` - **optionslist**: `id PK`
- **optionslistarticle**: `id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)` - **optionslistarticle**: `id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)`
- **pic**: `id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), source:txt, fullbinary:bin, thumbbinary:bin` - **ordersummary**: `id PK, company_id->company, manifest_id->manifest UQ, order_number:s(100), order_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, paid_total:f=0, ordered_quantity:f=0, shipped_quantity:f=0, is_paid:b=0, is_shipped:b=0`
- **posterminal**: `id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt` - **package**: `id PK, shipping_id->shipping, entityaddress_id->entityaddress, number:s(100), type:s(100), state:s(100), details?:txt`
- **payment**: `id PK, account_id->account, manifest_id->manifest, type:s(100), amount:f`
- **pic**: `id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), smallthumbw:i, smallthumbh:i, smallthumbbinary:bin, source:txt`
- **posterminal**: `id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), maxknownobituaryid?:i, root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt`
- **posterminalcategory**: `id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)` - **posterminalcategory**: `id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)`
- **posterminalobituary**: `id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i, created_at:dt`
- **posterminalsyncqueue**: `id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i`
- **price**: `id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f` - **price**: `id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f`
- **printer**: `id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt` - **printer**: `id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt`
- **propertyname**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)` - **propertyname**: `id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)`
- **quotesummary**: `id PK, company_id->company, manifest_id->manifest UQ, quote_number:s(100), quote_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, total_quantity:f=0`
- **shipmentsummary**: `id PK, company_id->company, manifest_id->manifest UQ, shipment_number:s(100), shipment_date:dt, created_at:dt, order_id?:i, order_number?:s(100), entity_id?->entity, customer_name?:s(255), positions:i=0, total_quantity:f=0, package_count:i=0, carrier?:s(100), tracking_code?:s(255), state:s(50)='new'`
- **shipping**: `id PK, manifest_id->manifest, number:s(100), tracking_code?:s(255), carrier?:s(100), type:s(100), state:s(100)`
- **subagentschedule**: `id PK, company_id->company, desc?:txt, prompt:txt, skills?:txt, lastagentnote?:txt, max_itr:i=15, cron:s(100), created_at:dt, lastrun_at?:dt`
- **tax**: `id PK, rate:d(18,4) UQ` - **tax**: `id PK, rate:d(18,4) UQ`
- **unit**: `id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)` - **unit**: `id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)`
- **unittext**: `id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)` - **unittext**: `id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)`
@@ -62,9 +82,16 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
## Table of Contents ## Table of Contents
- [companyuser](API.companyuser.md) - [companyuser](API.companyuser.md)
- `add(companyId: number, googleAuthId: number, tx?: object): Promise<number>` - `add(companyId: number, googleAuthId: number, role?: 'admin'|'user', tx?: object): Promise<number>`
- `addByEmail(googleAuthId: number, input: {email: string, role?: 'admin'|'user'}, tx?: object): Promise<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>`
- `delete(id: number, tx?: object): Promise<void>` - `delete(id: number, tx?: object): Promise<void>`
- `getByEmail(email: string, tx?: object): Promise<Array<number>>` - `getByEmail(email: string, tx?: object): Promise<Array<number>>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>>`
- `remove(googleAuthId: number, id: number, tx?: object): Promise<{id: number, googleAuthId: number}>`
- `requireAdmin(googleAuthId: number, tx?: object): Promise<{companyId: number, companyuserId: number, role: string}>`
- `roles(role: unknown): 'admin'|'user'`
- `setRole(googleAuthId: number, id: number, role: 'admin'|'user', tx?: object): Promise<{id: number, googleAuthId: number, email: string, picture: string|null, role: 'admin'|'user'}>`
- `withTransaction(): void`
- [company](API.company.md) - [company](API.company.md)
- `addHistory(googleAuthId: number, input: {text?: string}, tx?: object): Promise<{id: number, companyuserId: number|null, date: string, text: string}>` - `addHistory(googleAuthId: number, input: {text?: string}, tx?: object): Promise<{id: number, companyuserId: number|null, date: string, text: string}>`
- `create(input: {iso639: string, name: string, taxrate: number, reducedtaxrate: number, googleAuthId: number}, tx?: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>` - `create(input: {iso639: string, name: string, taxrate: number, reducedtaxrate: number, googleAuthId: number}, tx?: object): Promise<{id: number, rootCategoryId: number, languageId: number, taxrateId: number, reducedtaxrateId: number, taxfreerateId: number, posCustomerGroupId: number}>`
@@ -80,7 +107,8 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `delete(input: {googleAuthId: number, id: number, recursive?: boolean}, tx?: object): Promise<void>` - `delete(input: {googleAuthId: number, id: number, recursive?: boolean}, tx?: object): Promise<void>`
- `deleteImage(googleAuthId: number, categoryId: number, tx?: object): Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>` - `deleteImage(googleAuthId: number, categoryId: number, tx?: object): Promise<{id: number, thumbW: null, thumbH: null, thumbHash: null}>`
- `ensureByPath(input: {googleAuthId: number, parentId: number, path: string}, tx?: object): Promise<number>` - `ensureByPath(input: {googleAuthId: number, parentId: number, path: string}, tx?: object): Promise<number>`
- `getCategoryArticles(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null}>>` - `generateImage(googleAuthId: number, inputOrCategoryId: number|object, maybeOptsOrSize?: {size?: number}, maybeTx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, smallThumbW: number, smallThumbH: number, size: number, grid: string, articleCount: number}>`
- `getCategoryArticles(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, slug: string, net: number|null, taxRate: number|null, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, smallThumbW: number|null, smallThumbH: number|null, smallThumbBinary: string|null, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, articlewarehousesummary: Array<{id: number, articleId: number, warehouseId: number, warehouseName: string, stock: number}>, warehouses: Array<{id: number, name: string, stock: number}>}>>`
- `getTreeForCompany(googleAuthId: number, tx?: object): Promise<{languageId: number, tree: Array<{id: number, parentId: number|null, sort: number, name: string|null, slug: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, articleCount: number, posterminalIds: number[]}>, articleCount: number}>` - `getTreeForCompany(googleAuthId: number, tx?: object): Promise<{languageId: number, tree: Array<{id: number, parentId: number|null, sort: number, name: string|null, slug: string|null, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null, articleCount: number, posterminalIds: number[]}>, articleCount: number}>`
- `move(input: {googleAuthId: number, id: number, parentId: number}, tx?: object): Promise<{id: number, parentId: number, sort: number}>` - `move(input: {googleAuthId: number, id: number, parentId: number}, tx?: object): Promise<{id: number, parentId: number, sort: number}>`
- `order(input: {googleAuthId: number, parentId: number, categoryIds: Array<number>}, tx?: object): Promise<{categoryIds: Array<number>}>` - `order(input: {googleAuthId: number, parentId: number, categoryIds: Array<number>}, tx?: object): Promise<{categoryIds: Array<number>}>`
@@ -92,11 +120,12 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, desc?: string, seo?: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>` - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, desc?: string, seo?: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>`
- `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise<void>` - `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise<void>`
- `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>` - `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>`
- `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` - `getBySku(googleAuthId: number): Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, warehouses: Array<{id: number, name: string, sort: number, stock: number}>, undeliveredOrders: Array<{orderId: number, orderNumber: string, date: string, customerName: string|null, amount: number}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>`
- `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>` - `getHistoryBySku(googleAuthId: number, sku: string, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number, paged?: boolean}, tx?: object): Promise<{history: Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>`
- `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>` - `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>`
- `listChildren(googleAuthId: number, parentSku: string, tx?: object): Promise<Array<{id: number, sku: string, parentsort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>` - `listChildren(googleAuthId: number, parentSku: string, tx?: object): Promise<Array<{id: number, sku: string, parentsort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>`
- `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>` - `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>`
- `listHistory(provider: import('../../providers/provider.js').Provider): (googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{history: Array<{id: number, date: string, sku: string, name: string|null, historytext: string}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
- `listSearchIndex(googleAuthId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>` - `listSearchIndex(googleAuthId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>`
- `orderImages(googleAuthId: number, sku: string, input: {fullHashes: Array<string>}, tx?: object): Promise<{fullHashes: Array<string>}>` - `orderImages(googleAuthId: number, sku: string, input: {fullHashes: Array<string>}, tx?: object): Promise<{fullHashes: Array<string>}>`
- `removeChild(googleAuthId: number, parentSku: string, childSku: string, options?: {deleteArticle?: boolean, cascade?: boolean}, tx?: object): Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>` - `removeChild(googleAuthId: number, parentSku: string, childSku: string, options?: {deleteArticle?: boolean, cascade?: boolean}, tx?: object): Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>`
@@ -174,6 +203,10 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `update(googleAuthId: number, sku: string, id: number, patch: {sku?: string|null, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>` - `update(googleAuthId: number, sku: string, id: number, patch: {sku?: string|null, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>`
- [delivery](API.delivery.md) - [delivery](API.delivery.md)
- `attachments(): void` - `attachments(): void`
- `book(provider: object): Function`
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnbooked?: boolean, supplierId?: number, seekMonths?: number}, tx?: object): Promise<{deliveries: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `syncSummary(): void`
- [quote](API.quote.md) - [quote](API.quote.md)
- `create(googleAuthId: number, body: {number: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `create(googleAuthId: number, body: {number: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
@@ -183,20 +216,26 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `syncSummary(): void` - `syncSummary(): void`
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- [order](API.order.md) - [order](API.order.md)
- `create(googleAuthId: number, body: {number: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `create(googleAuthId: number, body: {number: string, date: string, entityId: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- `createFromQuote(googleAuthId: number, quoteId: number, body?: {number?: string, date?: string, createdAt?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- `createShipment(googleAuthId: number, orderId: number, input: {lines: Array<{sku: string, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>, date?: string|Date, externalNumber?: string, shipping: object}, tx?: object): Promise<object>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>` - `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void` - `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `syncSummary(): void` - `syncSummary(): void`
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- [shipment](API.shipment.md) - [shipment](API.shipment.md)
- `create(provider: import('../../providers/provider.js').DatabaseProvider): void` - `create(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `delete(provider: import('../../providers/provider.js').DatabaseProvider): void` - `delete(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `getById(provider: import('../../providers/provider.js').DatabaseProvider): void` - `getById(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, state?: 'new'|'delivered'|'cancelled'|'all', includeCancelled?: boolean, seekMonths?: number, orderId?: number}, tx?: object): Promise<{shipments: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
- `listByOrderId(provider: import('../../providers/provider.js').DatabaseProvider): void` - `listByOrderId(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `rebuildSummaries(googleAuthId: number, tx?: object): Promise<void>`
- `syncSummary(): void`
- `update(provider: import('../../providers/provider.js').DatabaseProvider): void`
- [shipping](API.shipping.md) - [shipping](API.shipping.md)
- `create(googleAuthId: number, input: {manifestId: number, type: string, state: string, trackingCode?: string, carrier?: string, warehouseId?: number}, tx?: object): Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrier: string|null, type: string, state: string}>` - `create(googleAuthId: number, input: {manifestId: number, carrierId: number, type: string, state: string, trackingCode?: string, warehouseId?: number}, tx?: object): Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrierId: number, carrier: string|null, type: string, state: string}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, manifestId: number, number: string, type: string, state: string}>>` - `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, manifestId: number, number: string, type: string, state: string}>>`
- `update(provider: import('../../providers/provider.js').DatabaseProvider): void` - `update(provider: import('../../providers/provider.js').DatabaseProvider): void`
@@ -209,6 +248,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `add(googleAuthId: number, input: {manifestarticle_id: number, warehouse_id?: number|null, transaction_type: string, amount: number, transaction_text: string}, tx?: object): Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>` - `add(googleAuthId: number, input: {manifestarticle_id: number, warehouse_id?: number|null, transaction_type: string, amount: number, transaction_text: string}, tx?: object): Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>`
- `listByManifestarticleId(googleAuthId: number, manifestarticleId: number, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>` - `listByManifestarticleId(googleAuthId: number, manifestarticleId: number, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
- `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>` - `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
- `listHistory(provider: import('../../providers/provider.js').Provider): (googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, seekMonths?: number}, tx?: object) => Promise<{ledger: Array<{id: number, date: string, sku: string|null, name: string|null, transaction_type: string, transaction_text: string, amount: number}>, total: number, unfilteredTotal: number, offset: number, limit: number}>`
- [warehouse](API.warehouse.md) - [warehouse](API.warehouse.md)
- `create(googleAuthId: number, input: {name: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>` - `create(googleAuthId: number, input: {name: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
@@ -217,29 +257,38 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `order(googleAuthId: number, input: {warehouseIds: Array<number>}, tx?: object): Promise<{warehouseIds: Array<number>}>` - `order(googleAuthId: number, input: {warehouseIds: Array<number>}, tx?: object): Promise<{warehouseIds: Array<number>}>`
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>` - `update(googleAuthId: number, id: number, input: {name?: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>`
- [stock](API.stock.md) - [stock](API.stock.md)
- `accept(provider: object): Function`
- `adjust(googleAuthId: number, input: {sku?: string, warehouseId?: number, amount?: number, text?: string, externalNumber?: string, date?: string|Date, lines?: Array<object>}, tx?: object): Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>` - `adjust(googleAuthId: number, input: {sku?: string, warehouseId?: number, amount?: number, text?: string, externalNumber?: string, date?: string|Date, lines?: Array<object>}, tx?: object): Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>`
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `ship(provider: object): Function`
- [posterminal](API.posterminal.md) - [posterminal](API.posterminal.md)
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `findByPairingpin(pairingpin: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknowntombstoneid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}|null>` - `findByPairingpin(pairingpin: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>`
- `findByToken(token: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknowntombstoneid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}|null>` - `findByToken(token: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>`
- `getCategoryCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>` - `getCategoryCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
- `getCategoryList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>` - `getCategoryList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
- `getCompositeProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>` - `getCompositeProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
- `getCustomerCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>` - `getCustomerCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
- `getCustomerGroupCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>` - `getCustomerGroupCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
- `getDeletedEntityCount(input?: {posterminalId?: number, cursor?: number}): Promise<number>` - `getCustomerGroupList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<{customerGroupId: string, name: string, standard: string, discountPercent: string, lastChanged: string}>>`
- `getCustomerList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
- `getDeletedEntityCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
- `getDeletedEntityList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<{id: number, entityType: string, entityId: number}>>`
- `getMaxOrderIdCount(input: {posterminalId: number}, tx?: object): Promise<number>` - `getMaxOrderIdCount(input: {posterminalId: number}, tx?: object): Promise<number>`
- `getProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>` - `getProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise<number>`
- `getProductList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>` - `getProductList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknowntombstoneid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}>>` - `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}>>`
- `pair(id: number, input: {token: string, clientserial?: string, pairedAt?: string}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>` - `pair(id: number, input: {token: string, clientserial?: string, pairedAt?: string, clientversion?: string|null}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null, clientversion: string|null}>`
- `setMaxKnownTombstoneId(id: number, maxknowntombstoneid: number|null, tx?: object): Promise<{id: number, maxknowntombstoneid: number|null}>` - `recordObituary(input: {companyId: number, entityType: string, entityId: number}, tx?: object): Promise<void>`
- `recordSyncQueue(input: {companyId: number, entityType: string, entityId: number}, tx?: object): Promise<void>`
- `setClientversion(id: number, clientversion: string|null, tx?: object): Promise<{id: number, clientversion: string|null}>`
- `setMaxKnownObituaryId(id: number, maxknownobituaryid: number|null, tx?: object): Promise<{id: number, maxknownobituaryid: number|null}>`
- `setOff(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>` - `setOff(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
- `setOffAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>` - `setOffAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
- `setOn(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>` - `setOn(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
- `setOnAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>` - `setOnAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
- `touchLastseen(id: number, tx?: object): Promise<{id: number}>` - `touchLastseen(id: number, tx?: object): Promise<{id: number}>`
- `upsert(googleAuthId: number, input: {name: string, pairingpin: string, token?: string, id?: number}, tx?: object): Promise<{id: number, name: string, token: string, pairingpin: string}>` - `upsert(googleAuthId: number, input: {name: string, pairingpin: string, token?: string, id?: number, sort?: number, enabled?: boolean}, tx?: object): Promise<{id: number, name: string, token: string, pairingpin: string, sort: number, enabled: number}>`
- [printer](API.printer.md) - [printer](API.printer.md)
- `create(googleAuthId: number, input: {address: string, type: string, model?: string, serial?: string, sort?: number, desc?: string}, tx?: object): Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>` - `create(googleAuthId: number, input: {address: string, type: string, model?: string, serial?: string, sort?: number, desc?: string}, tx?: object): Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
@@ -260,6 +309,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `getSmallThumbByHash(googleAuthId: number, fullHash: string, tx?: object): Promise<{buffer: Buffer, w: number, h: number, contentType: string}>` - `getSmallThumbByHash(googleAuthId: number, fullHash: string, tx?: object): Promise<{buffer: Buffer, w: number, h: number, contentType: string}>`
- `getThumbByHash(googleAuthId: number, fullHash: string, tx?: object): Promise<{buffer: Buffer, w: number, h: number, contentType: string}>` - `getThumbByHash(googleAuthId: number, fullHash: string, tx?: object): Promise<{buffer: Buffer, w: number, h: number, contentType: string}>`
- [numbers](API.numbers.md) - [numbers](API.numbers.md)
- `ensureType(runner: object, companyId: number, type: string): Promise<{id: number, type: string, prefix: string, padding: number, next: number}>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, type: string, prefix: string, padding: number, next: number}>>` - `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, type: string, prefix: string, padding: number, next: number}>>`
- `update(googleAuthId: number, id: number, input: {prefix?: string, padding?: number, next?: number}, tx?: object): Promise<{id: number, type: string, prefix: string, padding: number, next: number}>` - `update(googleAuthId: number, id: number, input: {prefix?: string, padding?: number, next?: number}, tx?: object): Promise<{id: number, type: string, prefix: string, padding: number, next: number}>`
- [propertyname](API.propertyname.md) - [propertyname](API.propertyname.md)
@@ -295,15 +345,37 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>` - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}|undefined>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>>` - `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>>`
- `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>` - `update(googleAuthId: number, id: number, input: {name?: string, width?: number, height?: number, dpi?: number, type?: string, content?: object, thumb?: Buffer|null}, tx?: object): Promise<{id: number, company_id: number, name: string, width: number, height: number, dpi: number, type: string, content: object, thumb: Buffer|null}>`
- [healthcheck](API.healthcheck.md)
- `check(tx?: object): Promise<{ok: boolean, sequence: number}>`
- [account](API.account.md) - [account](API.account.md)
- `create(googleAuthId: number, input: {type: string, name: string}, tx?: object): Promise<{id: number, companyId: number, type: string, name: string}>` - `create(googleAuthId: number, input: {type: string, name: string}, tx?: object): Promise<{id: number, companyId: number, type: string, name: string}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyId: number, type: string, name: string}>>` - `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyId: number, type: string, name: string}>>`
- [agentprinter](API.agentprinter.md)
- `delete(googleAuthId: number, id: number, tx?: object): Promise<{id: number}>`
- `findOrCreate(printeragentId: number, input: {type: string, model?: string|null, serial?: string|null}, tx?: object): Promise<{created: boolean, agentprinter: {id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}}>`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}|null>`
- `list(googleAuthId: number, printeragentId?: number, tx?: object): Promise<Array<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>>`
- `order(googleAuthId: number, input: {printeragentId?: number, agentprinterIds: number[]}, tx?: object): Promise<{agentprinterIds: number[]}>`
- `update(googleAuthId: number, input: {id: number, enabled?: boolean, sort?: number}, tx?: object): Promise<{id: number, printeragentId: number, type: string, model: string|null, serial: string|null, enabled: boolean, sort: number}>`
- [articlehistory](API.articlehistory.md) - [articlehistory](API.articlehistory.md)
- `record(amount: number|string): string` - `record(): void`
- [articlelist](API.articlelist.md) - [articlelist](API.articlelist.md)
- `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>` - `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
- `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>` - `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
- [articlewarehousesummary](API.articlewarehousesummary.md)
- `syncSummary(): void`
- [autoinvite](API.autoinvite.md)
- `add(googleAuthId: number, input: {email: string, role?: 'admin'|'user'}, tx?: object): Promise<{id: number, email: string, role: 'admin'|'user'}>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, email: string, role: 'admin'|'user', createdAt: string}>>`
- `remove(googleAuthId: number, id: number, tx?: object): Promise<void>`
- [carrier](API.carrier.md)
- `create(googleAuthId: number, input: {name: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}|undefined>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>>`
- `order(googleAuthId: number, input: {carrierIds: Array<number>}, tx?: object): Promise<{carrierIds: Array<number>}>`
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number, hasPackages?: boolean, isReverse?: boolean}, tx?: object): Promise<{id: number, name: string, sort: number, hasPackages: boolean, isReverse: boolean}>`
- [categoryattributevalue](API.categoryattributevalue.md) - [categoryattributevalue](API.categoryattributevalue.md)
- `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>` - `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
- `setByCategoryId(googleAuthId: number, categoryId: number, values: Array<number|{attributeValueId: number, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>` - `setByCategoryId(googleAuthId: number, categoryId: number, values: Array<number|{attributeValueId: number, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
@@ -350,5 +422,40 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `create(googleAuthId: number, input: {accountId: number, manifestId: number, type: string, amount: number}, tx?: object): Promise<{id: number, accountId: number, manifestId: number, type: string, amount: number}>` - `create(googleAuthId: number, input: {accountId: number, manifestId: number, type: string, amount: number}, tx?: object): Promise<{id: number, accountId: number, manifestId: number, type: string, amount: number}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, accountId: number, manifestId: number, type: string, amount: number}>>` - `listByManifestId(googleAuthId: number, manifestId: number, tx?: object): Promise<Array<{id: number, accountId: number, manifestId: number, type: string, amount: number}>>`
- [runner.js](API.runner.js.md) - [printeragent](API.printeragent.md)
- `runner(provider: import('../providers/provider.js').DatabaseProvider, tx?: object): ((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }` - `create(googleAuthId: number, input: {name: string, pairingpin: string, sort?: number}, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `findByPairingpin(pairingpin: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}|null>`
- `findByToken(token: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}|null>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>>`
- `pair(id: number, input: {token: string, clientserial?: string, clientmodel?: string, agentversion?: string, pairedAt?: string}, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
- `touchLastseen(id: number, agentversion?: string|null, tx?: object): Promise<{id: number}>`
- `update(googleAuthId: number, input: {id: number, name?: string, sort?: number}, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
- `updateState(id: number, clientstate: string|null, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
- [printerjob](API.printerjob.md)
- `create(googleAuthId: number, input: {agentprinterId: number, data: string, type?: string, state?: string}, tx?: object): Promise<object>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<object>`
- `getById(auth: number|{agentId: number}, id: number, tx?: object): Promise<object|null>`
- `list(auth: number|{agentId: number}, filters?: {agentprinterId?: number, state?: string}, tx?: object): Promise<Array<object>>`
- `updateState(auth: number|{agentId: number}, id: number, state: string, tx?: object): Promise<object>`
- [retoure](API.retoure.md)
- `create(googleAuthId: number, body: {number?: string, date: string, entityId?: number, externalNumber?: string, lines?: Array<object>}, tx?: object): Promise<object>`
- `createShipment(googleAuthId: number, retoureId: number, input: object, tx?: object): Promise<object>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{retoures: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `syncSummary(): void`
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<object>}, tx?: object): Promise<object>`
- [return](API.return.md)
- `book(provider: object): Function`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<object>`
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnbooked?: boolean, supplierId?: number, seekMonths?: number}, tx?: object): Promise<{returns: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `syncSummary(): void`
- [subagentschedule](API.subagentschedule.md)
- `create(googleAuthId: number, input: {prompt: string, cron: string, desc?: string, skills?: string|Array<string>, lastagentnote?: string, max_itr?: number}, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}|null>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>>`
- `update(googleAuthId: number, id: number, input: {prompt?: string, cron?: string, desc?: string|null, skills?: string|Array<string>|null, lastagentnote?: string|null, max_itr?: number, lastrun_at?: string|null}, tx?: object): Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`

View File

@@ -2,11 +2,29 @@
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `numbers.ensureType`
*Source: `src/store/numbers/ensureType.js`*
Ensures a number sequence row exists for a company and type, inferring the company's prefix style, padding, and next counter if not yet created or unconfigured.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `runner` | `object` | The database runner. |
| `companyId` | `number` | The id of the company. |
| `type` | `string` | The number type (e.g. 'order', 'quote', 'sku'). |
**Returns**
`Promise<{id: number, type: string, prefix: string, padding: number, next: number}>` — The sequence row.
## `numbers.list` ## `numbers.list`
*Source: `src/store/numbers/list.js`* *Source: `src/store/numbers/list.js`*
Lists the number sequences of the user's selected company. Ensures that default sequences ('sku', 'entity', 'delivery', 'adjustment', 'shipment') exist. Lists the number sequences of the user's selected company. Ensures that default sequences ('sku', 'entity', 'delivery', 'adjustment', 'shipment', 'quote', 'order', 'return', 'retoure') exist.
**Parameters** **Parameters**

View File

@@ -16,18 +16,18 @@ Creates a new order for the user's selected company with its lines and records a
| `body` | `object` | The order input. | | `body` | `object` | The order input. |
| `body.number` | `string` | The order number. | | `body.number` | `string` | The order number. |
| `body.date` | `string` | The order date. | | `body.date` | `string` | The order date. |
| `[body.entityId]` | `number` | The id of the customer entity. | | `body.entityId` | `number` | The id of the customer entity. |
| `[body.externalNumber]` | `string` | An external (customer) reference number. | | `[body.externalNumber]` | `string` | An external (customer) reference number. |
| `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>` | The order lines. | | `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>` | The order lines. `shippable` (default true) false marks non-shippable lines (e.g. shipping cost, rounding) that are excluded from shipment and order completeness. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` — The created order. `Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` — The created order.
**Throws** **Throws**
`Error` — If the order is invalid. `Error` — If the order is invalid, no entity is selected, or entity not found.
**Example** **Example**
@@ -36,11 +36,76 @@ const order = await store.order.create(42, { number: 'B-2026-001', date: '2026-0
// => { id, number, type, entityId, externalNumber, date, lines: [...] } // => { id, number, type, entityId, externalNumber, date, lines: [...] }
``` ```
## `order.createFromQuote`
*Source: `src/store/order/createFromQuote.js`*
Creates a new sales order from an existing quote for the user's selected company. Copies lines and customer entity from the quote. Sets external reference to the quote number (quote.number) unless explicitly overridden. If order number is omitted, it is auto-allocated from the 'order' number sequence.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `quoteId` | `number` | The id of the source quote. |
| `[body]` | `object` | Optional overrides for the order. |
| `[body.number]` | `string` | The order number; auto-allocated from sequence if omitted. |
| `[body.date]` | `string` | The order date; defaults to current date if omitted. |
| `[body.createdAt]` | `string` | Creation timestamp; defaults to current time if omitted. |
| `[body.entityId]` | `number` | Customer entity id override; defaults to quote's entityId. |
| `[body.externalNumber]` | `string` | Customer reference override; defaults to quote's number. |
| `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>` | Lines override; defaults to quote's lines. `shippable` (default true) false excludes a line from shipment and order completeness. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` — The created order.
**Throws**
`Error` — If the quote is not found, no entity is selected, or the order is invalid.
**Example**
```js
const order = await store.order.createFromQuote(42, 10);
// => { id, number, type: 'order', entityId, externalNumber, date, lines: [...] }
```
## `order.createShipment`
*Source: `src/store/order/createShipment.js`*
Creates a new shipment manifest for an order. Can contain specified items from the parent order.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `orderId` | `number` | The parent order manifest id. |
| `input` | `object` | Shipment input. |
| `input.lines` | `Array<{sku: string, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>` | Positions to ship. |
| `[input.date]` | `string|Date` | Optional shipment date. |
| `[input.externalNumber]` | `string` | Optional external delivery reference. |
| `input.shipping` | `object` | Required shipping ({carrierId, type, state, trackingCode?, warehouseId?}); see shipment.create. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<object>` — The created shipment.
**Example**
```js
const shipment = await store.order.createShipment(googleAuthId, 10, { lines: [{ sku: 'ITEM-A', amount: 5 }], shipping: { carrierId: 1, type: 'parcel', state: 'draft' } });
```
## `order.delete` ## `order.delete`
*Source: `src/store/order/delete.js`* *Source: `src/store/order/delete.js`*
Deletes an order by its id within the user's selected company. Deletes an order by its id within the user's selected company. Refuses to delete the order if any shipment manifest references it.
**Parameters** **Parameters**
@@ -56,7 +121,7 @@ Deletes an order by its id within the user's selected company.
**Throws** **Throws**
`Error` — If the order is not found. `Error` — If the order is not found or has shipment manifests.
**Example** **Example**
@@ -162,7 +227,7 @@ Updates an order's header and/or lines.
| `[patch.date]` | `string` | The new order date. | | `[patch.date]` | `string` | The new order date. |
| `[patch.entityId]` | `number` | The new customer entity id. | | `[patch.entityId]` | `number` | The new customer entity id. |
| `[patch.externalNumber]` | `string` | The new external (customer) reference number. | | `[patch.externalNumber]` | `string` | The new external (customer) reference number. |
| `[patch.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>` | The new order lines. | | `[patch.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>` | The new order lines. `shippable` (default true) false marks non-shippable lines excluded from shipment and order completeness. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**

View File

@@ -45,7 +45,7 @@ Finds a posterminal by its pairingpin.
**Returns** **Returns**
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknowntombstoneid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}|null>` — The posterminal, or null if not found. `Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>` — The posterminal, or null if not found.
## `posterminal.findByToken` ## `posterminal.findByToken`
@@ -62,13 +62,13 @@ Finds a posterminal by its auth token.
**Returns** **Returns**
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknowntombstoneid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}|null>` — The posterminal, or null if not found. `Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>` — The posterminal, or null if not found.
## `posterminal.getCategoryCount` ## `posterminal.getCategoryCount`
*Source: `src/store/posterminal/getCategoryCount.js`* *Source: `src/store/posterminal/getCategoryCount.js`*
Counts categories assigned to the POS terminal updated after the given unix timestamp cursor. Counts categories assigned to the POS terminal queued after the given sync queue cursor ID.
**Parameters** **Parameters**
@@ -76,7 +76,7 @@ Counts categories assigned to the POS terminal updated after the given unix time
| --- | --- | --- | | --- | --- | --- |
| `input` | `object` | | | `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. | | `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | | `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
@@ -87,7 +87,7 @@ Counts categories assigned to the POS terminal updated after the given unix time
*Source: `src/store/posterminal/getCategoryList.js`* *Source: `src/store/posterminal/getCategoryList.js`*
Lists categories for a POS terminal modified after the cursor. Lists categories for a POS terminal queued after the sync queue cursor ID.
**Parameters** **Parameters**
@@ -95,7 +95,7 @@ Lists categories for a POS terminal modified after the cursor.
| --- | --- | --- | | --- | --- | --- |
| `input` | `object` | | | `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. | | `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | Unix timestamp cursor. | | `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
| `[input.limit=20]` | `number` | Max items to return. | | `[input.limit=20]` | `number` | Max items to return. |
| `[tx]` | `object` | Optional transaction. | | `[tx]` | `object` | Optional transaction. |
@@ -107,7 +107,7 @@ Lists categories for a POS terminal modified after the cursor.
*Source: `src/store/posterminal/getCompositeProductCount.js`* *Source: `src/store/posterminal/getCompositeProductCount.js`*
Counts composite products (Stücklisten) assigned to the POS terminal updated after the given unix timestamp cursor. Counts composite products (Stücklisten) assigned to the POS terminal queued after the given sync queue cursor ID.
**Parameters** **Parameters**
@@ -115,7 +115,7 @@ Counts composite products (Stücklisten) assigned to the POS terminal updated af
| --- | --- | --- | | --- | --- | --- |
| `input` | `object` | | | `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. | | `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | | `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
@@ -126,7 +126,7 @@ Counts composite products (Stücklisten) assigned to the POS terminal updated af
*Source: `src/store/posterminal/getCustomerCount.js`* *Source: `src/store/posterminal/getCustomerCount.js`*
Counts customers of the POS terminal company updated after the given unix timestamp cursor. Counts customers of the POS terminal company queued after the given sync queue cursor ID.
**Parameters** **Parameters**
@@ -134,7 +134,7 @@ Counts customers of the POS terminal company updated after the given unix timest
| --- | --- | --- | | --- | --- | --- |
| `input` | `object` | | | `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. | | `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | | `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
@@ -145,7 +145,7 @@ Counts customers of the POS terminal company updated after the given unix timest
*Source: `src/store/posterminal/getCustomerGroupCount.js`* *Source: `src/store/posterminal/getCustomerGroupCount.js`*
Counts customer groups of the POS terminal company updated after the given unix timestamp cursor. Counts customer groups of the POS terminal company queued after the given sync queue cursor ID.
**Parameters** **Parameters**
@@ -153,30 +153,91 @@ Counts customer groups of the POS terminal company updated after the given unix
| --- | --- | --- | | --- | --- | --- |
| `input` | `object` | | | `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. | | `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | | `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<number>` — The count of changed records. `Promise<number>` — The count of changed records.
## `posterminal.getDeletedEntityCount` ## `posterminal.getCustomerGroupList`
*Source: `src/store/posterminal/getDeletedEntityCount.js`* *Source: `src/store/posterminal/getCustomerGroupList.js`*
Counts deleted entities after the given cursor (currently always 0). Lists customer groups for a POS terminal queued after the sync queue cursor ID, excluding 'Lieferanten'.
**Parameters** **Parameters**
| Name | Type | Description | | Name | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| `[input]` | `object` | | | `input` | `object` | |
| `[input.posterminalId]` | `number` | The POS terminal id. | | `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | Unix timestamp cursor. | | `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
| `[input.limit=50]` | `number` | Max items to return. |
| `[tx]` | `object` | Optional transaction. |
**Returns** **Returns**
`Promise<number>` — Always 0. `Promise<Array<{customerGroupId: string, name: string, standard: string, discountPercent: string, lastChanged: string}>>`
## `posterminal.getCustomerList`
*Source: `src/store/posterminal/getCustomerList.js`*
Lists customers for a POS terminal queued after the sync queue cursor ID, excluding 'Lieferanten'.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
| `[input.limit=20]` | `number` | Max items to return. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<Array<object>>`
## `posterminal.getDeletedEntityCount`
*Source: `src/store/posterminal/getDeletedEntityCount.js`*
Counts deleted entities for a POS terminal after the given cursor ID.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | The highest synced obituary ID. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<number>` — Count of deleted entities.
## `posterminal.getDeletedEntityList`
*Source: `src/store/posterminal/getDeletedEntityList.js`*
Lists deleted entities for a POS terminal after the given cursor ID.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | The highest synced obituary ID. |
| `[input.limit=200]` | `number` | Max items to return. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<Array<{id: number, entityType: string, entityId: number}>>`
## `posterminal.getMaxOrderIdCount` ## `posterminal.getMaxOrderIdCount`
@@ -200,7 +261,7 @@ Returns the maximum order manifest id for the POS terminal company.
*Source: `src/store/posterminal/getProductCount.js`* *Source: `src/store/posterminal/getProductCount.js`*
Counts products assigned to the POS terminal updated after the given unix timestamp cursor. Counts products assigned to the POS terminal queued after the given sync queue cursor ID.
**Parameters** **Parameters**
@@ -208,7 +269,7 @@ Counts products assigned to the POS terminal updated after the given unix timest
| --- | --- | --- | | --- | --- | --- |
| `input` | `object` | | | `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. | | `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | | `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
@@ -219,7 +280,7 @@ Counts products assigned to the POS terminal updated after the given unix timest
*Source: `src/store/posterminal/getProductList.js`* *Source: `src/store/posterminal/getProductList.js`*
Lists products for a POS terminal modified after the cursor. Lists products for a POS terminal queued after the sync queue cursor ID.
**Parameters** **Parameters**
@@ -227,7 +288,7 @@ Lists products for a POS terminal modified after the cursor.
| --- | --- | --- | | --- | --- | --- |
| `input` | `object` | | | `input` | `object` | |
| `input.posterminalId` | `number` | The POS terminal id. | | `input.posterminalId` | `number` | The POS terminal id. |
| `[input.cursor=0]` | `number` | Unix timestamp cursor. | | `[input.cursor=0]` | `number` | Sequential ID cursor of last synced posterminalsyncqueue entry. |
| `[input.limit=20]` | `number` | Max items to return. | | `[input.limit=20]` | `number` | Max items to return. |
| `[tx]` | `object` | Optional transaction. | | `[tx]` | `object` | Optional transaction. |
@@ -250,13 +311,13 @@ Lists the posterminals of the user's selected company.
**Returns** **Returns**
`Promise<Array<{id: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknowntombstoneid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}>>` — The posterminals. `Promise<Array<{id: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}>>` — The posterminals.
**Example** **Example**
```js ```js
const terminals = await store.posterminal.list(42); const terminals = await store.posterminal.list(42);
// => [{ id, name, token, pairingpin, pairedAt, clientserial, maxknowntombstoneid, rootCategoryId, syncRootArticles, lastseenAt, updatedAt }] // => [{ id, name, token, pairingpin, pairedAt, clientserial, maxknownobituaryid, rootCategoryId, syncRootArticles, lastseenAt, updatedAt, sort, enabled, clientversion }]
``` ```
## `posterminal.pair` ## `posterminal.pair`
@@ -274,29 +335,95 @@ Completes POS device pairing for a terminal.
| `input.token` | `string` | The auth token for the paired device. | | `input.token` | `string` | The auth token for the paired device. |
| `[input.clientserial]` | `string` | The client name or serial identifier. | | `[input.clientserial]` | `string` | The client name or serial identifier. |
| `[input.pairedAt]` | `string` | Timestamp when paired. | | `[input.pairedAt]` | `string` | Timestamp when paired. |
| `[input.clientversion]` | `string|null` | Client software version reported by the POS device during pairing. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>` `Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null, clientversion: string|null}>`
## `posterminal.setMaxKnownTombstoneId` ## `posterminal.recordObituary`
*Source: `src/store/posterminal/setMaxKnownTombstoneId.js`* *Source: `src/store/posterminal/recordObituary.js`*
Updates the max known tombstone ID for a POS terminal. Records an obituary for all active paired POS terminals of a company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `input` | `object` | |
| `input.companyId` | `number` | The company id. |
| `input.entityType` | `string` | 'category' | 'article' | 'customer' | 'customergroup' |
| `input.entityId` | `number` | The deleted entity's id. |
| `[tx]` | `object` | Optional transaction/runner. |
**Returns**
`Promise<void>`
## `posterminal.recordSyncQueue`
*Source: `src/store/posterminal/recordSyncQueue.js`*
Enqueues a sync notification for all POS terminals of a company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `input` | `object` | |
| `input.companyId` | `number` | The company id. |
| `input.entityType` | `string` | 'category' | 'article' | 'customer' | 'customergroup' |
| `input.entityId` | `number` | The entity's id. |
| `[tx]` | `object` | Optional transaction/runner. |
**Returns**
`Promise<void>`
## `posterminal.setClientversion`
*Source: `src/store/posterminal/setClientversion.js`*
Stores the software version reported by a POS terminal.
**Parameters** **Parameters**
| Name | Type | Description | | Name | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| `id` | `number` | The posterminal id. | | `id` | `number` | The posterminal id. |
| `maxknowntombstoneid` | `number|null` | The highest synced tombstone ID. | | `clientversion` | `string|null` | Version string reported by the client (trimmed; empty → null). |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<{id: number, maxknowntombstoneid: number|null}>` `Promise<{id: number, clientversion: string|null}>`
**Example**
```js
await store.posterminal.setClientversion(7, '1.10.12.0');
// => { id: 7, clientversion: '1.10.12.0' }
```
## `posterminal.setMaxKnownObituaryId`
*Source: `src/store/posterminal/setMaxKnownObituaryId.js`*
Updates the max known obituary ID for a POS terminal.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `id` | `number` | The posterminal id. |
| `maxknownobituaryid` | `number|null` | The highest synced obituary ID. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, maxknownobituaryid: number|null}>`
## `posterminal.setOff` ## `posterminal.setOff`
@@ -449,11 +576,13 @@ Creates or updates a posterminal for the user's selected company.
| `input.pairingpin` | `string` | The pairing pin. | | `input.pairingpin` | `string` | The pairing pin. |
| `[input.token]` | `string` | Optional token. | | `[input.token]` | `string` | Optional token. |
| `[input.id]` | `number` | Optional id to update an existing posterminal. | | `[input.id]` | `number` | Optional id to update an existing posterminal. |
| `[input.sort]` | `number` | Display order (integer, ascending). Default 0 on create. |
| `[input.enabled]` | `boolean` | Whether the terminal may sync. Default true on create. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<{id: number, name: string, token: string, pairingpin: string}>` — The upserted posterminal. `Promise<{id: number, name: string, token: string, pairingpin: string, sort: number, enabled: number}>` — The upserted posterminal.
**Throws** **Throws**
@@ -463,5 +592,5 @@ Creates or updates a posterminal for the user's selected company.
```js ```js
const terminal = await store.posterminal.upsert(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 } // => { id, name, token, pairingpin, sort, enabled }
``` ```

222
API.printeragent.md Normal file
View File

@@ -0,0 +1,222 @@
# printeragent
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `printeragent.create`
*Source: `src/store/printeragent/create.js`*
Creates an unpaired printer agent for the user's selected company. The agent registers itself later using the pairing pin.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | The printer agent input. |
| `input.name` | `string` | The display name. |
| `input.pairingpin` | `string` | The pairing pin the agent uses to register. |
| `[input.sort]` | `number` | Display order (integer, ascending). Default 0. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>` — The created printer agent.
**Throws**
`Error` — 'Invalid printeragent' if name or pairingpin is missing/blank or sort is not an integer.
**Example**
```js
const agent = await store.printeragent.create(42, { name: 'Bar printer', pairingpin: '123456' });
```
## `printeragent.delete`
*Source: `src/store/printeragent/delete.js`*
Deletes a printer agent of the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The printer agent id. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<void>`
**Throws**
`Error` — 'Printeragent not found' if the id is unknown in the company.
**Example**
```js
await store.printeragent.delete(42, 5);
```
## `printeragent.findByPairingpin`
*Source: `src/store/printeragent/findByPairingpin.js`*
Finds a printer agent by its pairing pin.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `pairingpin` | `string` | The pairing pin to search for. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}|null>` — The printer agent, or null if not found.
**Example**
```js
const agent = await store.printeragent.findByPairingpin('123456');
```
## `printeragent.findByToken`
*Source: `src/store/printeragent/findByToken.js`*
Finds a printer agent by its authentication token.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `token` | `string` | The auth token to search for. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}|null>` — The printer agent, or null if not found.
**Example**
```js
const agent = await store.printeragent.findByToken('abc...');
```
## `printeragent.list`
*Source: `src/store/printeragent/list.js`*
Lists the printer agents of the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<Array<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>>` — The printer agents.
**Example**
```js
const agents = await store.printeragent.list(42);
// => [{ id, companyId, name, token, pairingpin, pairedAt, clientserial, clientmodel, clientstate, agentversion, lastseenAt, updatedAt, sort }]
```
## `printeragent.pair`
*Source: `src/store/printeragent/pair.js`*
Completes device pairing for a printer agent.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `id` | `number` | The printer agent id. |
| `input` | `object` | |
| `input.token` | `string` | Auth token for the paired device. |
| `[input.clientserial]` | `string` | Device serial identifier. |
| `[input.clientmodel]` | `string` | Device model name. |
| `[input.agentversion]` | `string` | Device agent software version. |
| `[input.pairedAt]` | `string` | Timestamp when paired. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`
## `printeragent.touchLastseen`
*Source: `src/store/printeragent/touchLastseen.js`*
Updates lastseen_at (and optionally agentversion) for a printer agent.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `id` | `number` | The printer agent id. |
| `[agentversion]` | `string|null` | Optional agent version to update. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number}>`
## `printeragent.update`
*Source: `src/store/printeragent/update.js`*
Updates name and/or sort of a printer agent of the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | The update input. |
| `input.id` | `number` | The printer agent id. |
| `[input.name]` | `string` | The new display name. |
| `[input.sort]` | `number` | The new display order (integer). |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>` — The updated printer agent.
**Throws**
`Error` — 'Invalid printeragent' if name is blank, sort is not an integer, or neither is given; 'Printeragent not found' if id is unknown in the company.
**Example**
```js
const agent = await store.printeragent.update(42, { id: 5, name: 'Kitchen printer', sort: 1 });
```
## `printeragent.updateState`
*Source: `src/store/printeragent/updateState.js`*
Updates the client state of a printer agent.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `id` | `number` | The printer agent id. |
| `clientstate` | `string|null` | The state payload/string. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, clientmodel: string|null, clientstate: string|null, agentversion: string|null, lastseenAt: string|null, updatedAt: string, sort: number}>`

100
API.printerjob.md Normal file
View File

@@ -0,0 +1,100 @@
# printerjob
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `printerjob.create`
*Source: `src/store/printerjob/create.js`*
Creates a new printer job for an agent printer belonging to the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | |
| `input.agentprinterId` | `number` | Target agent printer ID. |
| `input.data` | `string` | Job payload (ZPL code, etc.). |
| `[input.type]` | `string` | Job type ("zpl", "pdf", etc., defaults to agentprinter type or "zpl"). |
| `[input.state]` | `string` | Initial state (defaults to "pending"). |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<object>` — Created printer job with printeragentId attached.
## `printerjob.delete`
*Source: `src/store/printerjob/delete.js`*
Deletes a printer job belonging to the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | Printer job ID. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<object>` — The deleted job's snapshot.
## `printerjob.getById`
*Source: `src/store/printerjob/getById.js`*
Retrieves a printer job by ID. If caller is an agent ({ agentId: number }), checks that the job belongs to that printer agent. If caller is a user (googleAuthId: number), checks that the job belongs to the selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `auth` | `number|{agentId: number}` | User's googleAuthId or { agentId }. |
| `id` | `number` | Printer job ID. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<object|null>`
## `printerjob.list`
*Source: `src/store/printerjob/list.js`*
Lists printer jobs. If caller is an agent ({ agentId: number }), lists jobs for that agent. If caller is a user (googleAuthId: number), lists jobs for the selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `auth` | `number|{agentId: number}` | User's googleAuthId or { agentId }. |
| `[filters]` | `object` | |
| `[filters.agentprinterId]` | `number` | Filter by agentprinterId (company callers only). |
| `[filters.state]` | `string` | Filter by job state (e.g. "pending"). |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<Array<object>>`
## `printerjob.updateState`
*Source: `src/store/printerjob/updateState.js`*
Updates a printer job's state ("processing", "completed", "cancelled", etc.).
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `auth` | `number|{agentId: number}` | User's googleAuthId or { agentId }. |
| `id` | `number` | Printer job ID. |
| `state` | `string` | New state. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<object>` — Updated printer job.

155
API.retoure.md Normal file
View File

@@ -0,0 +1,155 @@
# retoure
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `retoure.create`
*Source: `src/store/retoure/create.js`*
Creates a new retoure 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 retoure input. |
| `[body.number]` | `string` | The retoure number. |
| `body.date` | `string` | The retoure date. |
| `[body.entityId]` | `number` | The id of the customer entity. |
| `[body.externalNumber]` | `string` | An external reference number. |
| `[body.lines]` | `Array<object>` | The retoure lines. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<object>` — The created retoure.
## `retoure.createShipment`
*Source: `src/store/retoure/createShipment.js`*
Creates a shipment manifest for a retoure.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `retoureId` | `number` | The parent retoure manifest id. |
| `input` | `object` | Shipment input. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<object>` — The created shipment.
## `retoure.delete`
*Source: `src/store/retoure/delete.js`*
Deletes a retoure 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 retoure to delete. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<void>`
**Throws**
`Error` — If the retoure is not found.
## `retoure.getById`
*Source: `src/store/retoure/getById.js`*
Returns a single retoure 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 retoure. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` — The retoure with its lines.
**Throws**
`Error` — If the retoure is not found.
## `retoure.list`
*Source: `src/store/retoure/list.js`*
Lists the retoures of the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[opts]` | `object` | Optional pagination and filter options. |
| `[opts.offset=0]` | `number` | Number of retoures to skip. |
| `[opts.limit=50]` | `number` | Number of retoures to return. |
| `[opts.search]` | `string` | Free-text search matching retoure number, external number, or customer name. |
| `[opts.onlyUnpaid=false]` | `boolean` | When true, returns only unpaid retoures. |
| `[opts.onlyUndelivered=false]` | `boolean` | When true, returns only undelivered retoures. |
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<{retoures: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
## `retoure.rebuildSummaries`
*Source: `src/store/retoure/rebuildSummaries.js`*
Rebuilds the `retouresummary` table for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
## `retoure.syncSummary`
*Source: `src/store/retoure/syncSummary.js`*
Centralized sync and rebuild logic for the materialized `retouresummary` table.
## `retoure.update`
*Source: `src/store/retoure/update.js`*
Updates a retoure's header and/or lines.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The id of the retoure to update. |
| `patch` | `object` | The update patch. |
| `[patch.number]` | `string` | The new retoure number. |
| `[patch.date]` | `string` | The new retoure date. |
| `[patch.entityId]` | `number` | The new customer entity id. |
| `[patch.externalNumber]` | `string` | The new external reference number. |
| `[patch.lines]` | `Array<object>` | The new retoure lines. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<object>` — The updated retoure.

83
API.return.md Normal file
View File

@@ -0,0 +1,83 @@
# return
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `return.book`
*Source: `src/store/return/book.js`*
Books all positions of a return manifest to the ledger.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `object` | Storage provider instance. |
**Returns**
`Function` — `(googleAuthId, input, tx) => Promise<object>`
## `return.getById`
*Source: `src/store/return/getById.js`*
Returns a return manifest 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 return manifest. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<object>` — The return manifest with lines and summary.
**Throws**
`Error` — If the return is not found.
## `return.list`
*Source: `src/store/return/list.js`*
Lists the returns of the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[opts]` | `object` | Optional pagination and filter options. |
| `[opts.offset=0]` | `number` | Number of returns to skip. |
| `[opts.limit=50]` | `number` | Number of returns to return. |
| `[opts.search]` | `string` | Free-text search matching return number, external number, or supplier name. |
| `[opts.onlyUnbooked=false]` | `boolean` | When true, returns only returns with unbooked stock items. |
| `[opts.supplierId]` | `number` | Filter by supplier entity id. |
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<{returns: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
## `return.rebuildSummaries`
*Source: `src/store/return/rebuildSummaries.js`*
Rebuilds the `returnsummary` table for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
## `return.syncSummary`
*Source: `src/store/return/syncSummary.js`*
Centralized sync and rebuild logic for the materialized `returnsummary` table. Keeps `returnsummary` in sync with manifest (return), manifestarticle, article (track_stock), and ledger (RETURN transactions).

View File

@@ -1,20 +0,0 @@
# runner.js
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `runner.js.runner`
*Source: `src/store/runner.js`*
Resolves the object a store method should query through. Converted store modules call `runner(sql, params)` or `runner.query(sql, params)`. During the incremental migration they may still be constructed with a raw `mssql` pool, or called with a raw `mssql` Transaction as `tx`. This adapter exposes the unified runner contract over both so mixed call sites keep working until every module is converted.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `import('../providers/provider.js').DatabaseProvider` | |
| `[tx]` | `object` | Optional provider runner or legacy mssql Transaction. |
**Returns**
`((sql: string, params?: object) => Promise<import('../providers/provider.js').QueryResult>) & { query: Function, dialect?: string }`

View File

@@ -18,7 +18,7 @@ Creates a new shipment manifest for an order. Validates that all article positio
*Source: `src/store/shipment/delete.js`* *Source: `src/store/shipment/delete.js`*
Deletes a shipment manifest of the selected company. Shipped shipments (with packages in 'shipped' state) cannot be deleted. Deletes or cancels a shipment manifest of the selected company. - If no ledger rows exist: physically deletes the shipment manifest. - If ledger rows exist: reverses the ledger entries (storno document entry + warehouse stock adjustment), marks the shipment as 'cancelled' in shipmentsummary, and updates the parent order's summary. - Shipped shipments (with packages in 'shipped' state) cannot be deleted.
**Parameters** **Parameters**
@@ -38,6 +38,31 @@ Retrieves a shipment manifest by ID for the selected company, including lines an
| --- | --- | --- | | --- | --- | --- |
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | | | `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
## `shipment.list`
*Source: `src/store/shipment/list.js`*
Lists the shipments of the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[opts]` | `object` | Optional pagination and filter options. |
| `[opts.offset=0]` | `number` | Number of shipments to skip. |
| `[opts.limit=50]` | `number` | Number of shipments to return. |
| `[opts.search]` | `string` | Free-text search matching shipment number, order number, external number, tracking code, or customer name. |
| `[opts.state]` | `'new'|'delivered'|'cancelled'|'all'` | Filter by delivery state. |
| `[opts.includeCancelled=false]` | `boolean` | Whether to include cancelled shipments when state is not set. |
| `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). |
| `[opts.orderId]` | `number` | Optional filter for parent order manifest id. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<{shipments: Array<object>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
## `shipment.listByOrderId` ## `shipment.listByOrderId`
*Source: `src/store/shipment/listByOrderId.js`* *Source: `src/store/shipment/listByOrderId.js`*
@@ -49,3 +74,38 @@ Lists all shipment manifests belonging to an order.
| Name | Type | Description | | Name | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | | | `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
## `shipment.rebuildSummaries`
*Source: `src/store/shipment/rebuildSummaries.js`*
Rebuilds the `shipmentsummary` table for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[tx]` | `object` | Optional transaction. |
**Returns**
`Promise<void>`
## `shipment.syncSummary`
*Source: `src/store/shipment/syncSummary.js`*
Centralized sync and rebuild logic for the materialized `shipmentsummary` table.
## `shipment.update`
*Source: `src/store/shipment/update.js`*
Updates a shipment's state or metadata.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |

View File

@@ -15,25 +15,25 @@ Creates a shipping (package) for a shipment manifest. The number is allocated fr
| `googleAuthId` | `number` | The id of the Google auth user. | | `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | Shipping input. | | `input` | `object` | Shipping input. |
| `input.manifestId` | `number` | The shipment manifest id. | | `input.manifestId` | `number` | The shipment manifest id. |
| `input.carrierId` | `number` | The carrier id. |
| `input.type` | `string` | Shipping type. | | `input.type` | `string` | Shipping type. |
| `input.state` | `string` | Shipping state ('shipped' or other). | | `input.state` | `string` | Shipping state ('shipped' or other). |
| `[input.trackingCode]` | `string` | Carrier tracking code. | | `[input.trackingCode]` | `string` | Carrier tracking code. |
| `[input.carrier]` | `string` | Carrier name. |
| `[input.warehouseId]` | `number` | Optional warehouse ID for stock deduction (defaults to company's first warehouse). | | `[input.warehouseId]` | `number` | Optional warehouse ID for stock deduction (defaults to company's first warehouse). |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrier: string|null, type: string, state: string}>` `Promise<{id: number, manifestId: number, number: string, trackingCode: string|null, carrierId: number, carrier: string|null, type: string, state: string}>`
**Throws** **Throws**
`Error` — If manifest is not found, manifest is not a shipment, or input is invalid. `Error` — If manifest is not found, carrier is not found, manifest is not a shipment, shipment is already shipped, or input is invalid.
**Example** **Example**
```js ```js
await store.shipping.create(googleAuthId, { manifestId: 42, type: 'parcel', state: 'shipped', trackingCode: '1Z999', carrier: 'UPS' }); await store.shipping.create(googleAuthId, { manifestId: 42, carrierId: 1, type: 'parcel', state: 'shipped', trackingCode: '1Z999' });
``` ```
## `shipping.delete` ## `shipping.delete`

View File

@@ -2,6 +2,22 @@
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `stock.accept`
*Source: `src/store/stock/accept.js`*
Accepts a delivery manifest item (or multiple delivery items). Requirements: - Every accepted delivery manifest item receives a document ledger entry: - If amount >= 0: transaction_type = 'ACCEPTANCE', amount = +amount, warehouse_id = null. - If amount < 0: transaction_type = 'RETURN', amount = amount (negative), warehouse_id = null. - Always records article history with delivery number, supplier, EK brutto, and quantity. - If the connected article has stock tracking (track_stock == 1): Calls adjust with the item's amount to adjust physical inventory in the target warehouse. - If the connected article does not have stock tracking: Only document acceptance is booked; no warehouse adjust is performed. - Double acceptance of an already accepted line is strictly prevented.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `object` | The database provider. |
**Returns**
`Function` — Async function accepting delivery items.
## `stock.adjust` ## `stock.adjust`
*Source: `src/store/stock/adjust.js`* *Source: `src/store/stock/adjust.js`*
@@ -26,3 +42,31 @@ Adjusts article stock manually by creating an implicit manifest of type "adjustm
**Returns** **Returns**
`Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>` `Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>`
## `stock.rebuildSummaries`
*Source: `src/store/stock/rebuildSummaries.js`*
Rebuilds the `articlewarehousesummary` table for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `import('../../providers/provider.js').DatabaseProvider` | |
## `stock.ship`
*Source: `src/store/stock/ship.js`*
Ships a shipment manifest item (or multiple shipment items). Requirements: - Every shipped shipment manifest item receives a document ledger entry: - transaction_type = 'SHIPMENT', amount = -amount, warehouse_id = null. - Line amount must be positive (> 0); negative or zero line amounts are rejected. - Always records article history with shipment number, customer, and quantity. - If the connected article has stock tracking (track_stock == 1): Calls adjust with: - -amount (outgoing stock from warehouse). - If the connected article does not have stock tracking: Only document shipment is booked; no warehouse adjust is performed. - Double shipping of an already shipped line is strictly prevented.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `provider` | `object` | The database provider. |
**Returns**
`Function` — Async function shipping shipment items.

130
API.subagentschedule.md Normal file
View File

@@ -0,0 +1,130 @@
# subagentschedule
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `subagentschedule.create`
*Source: `src/store/subagentschedule/create.js`*
Creates a new subagent schedule for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | Schedule input. |
| `input.prompt` | `string` | The task prompt for the subagent. |
| `input.cron` | `string` | Standard 5-field cron timetable string. |
| `[input.desc]` | `string` | Optional schedule description. |
| `[input.skills]` | `string|Array<string>` | Optional skill names or JSON string. |
| `[input.lastagentnote]` | `string` | Optional initial agent note. |
| `[input.max_itr=15]` | `number` | Optional max iterations per run. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`
**Throws**
`Error` — If prompt is missing, cron is invalid, or max_itr is invalid.
**Example**
```js
const schedule = await store.subagentschedule.create(googleAuthId, {
desc: 'Nightly inventory audit',
prompt: 'Check stock discrepancies across all warehouses and summarize.',
cron: '0 2 * * *',
skills: ['stock', 'reporting'],
max_itr: 20,
});
```
## `subagentschedule.delete`
*Source: `src/store/subagentschedule/delete.js`*
Deletes a subagent schedule by id for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The schedule ID to delete. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<void>`
**Throws**
`Error` — If schedule not found.
## `subagentschedule.getById`
*Source: `src/store/subagentschedule/getById.js`*
Retrieves a single subagent schedule by id for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The schedule ID. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}|null>`
## `subagentschedule.list`
*Source: `src/store/subagentschedule/list.js`*
Lists all subagent schedules for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<Array<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>>`
## `subagentschedule.update`
*Source: `src/store/subagentschedule/update.js`*
Updates an existing subagent schedule for the user's selected company.
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The schedule ID to update. |
| `input` | `object` | Update payload. |
| `[input.prompt]` | `string` | Optional updated task prompt. |
| `[input.cron]` | `string` | Optional updated cron expression. |
| `[input.desc]` | `string|null` | Optional updated description. |
| `[input.skills]` | `string|Array<string>|null` | Optional updated skills. |
| `[input.lastagentnote]` | `string|null` | Optional updated agent note. |
| `[input.max_itr]` | `number` | Optional updated max iterations. |
| `[input.lastrun_at]` | `string|null` | Optional updated last run timestamp. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{id: number, company_id: number, desc: string|null, prompt: string, skills: string|null, lastagentnote: string|null, max_itr: number, cron: string, created_at: string, lastrun_at: string|null}>`
**Throws**
`Error` — If schedule not found, prompt is empty, cron is invalid, or max_itr is invalid.

View File

@@ -52,7 +52,7 @@ Deletes a supplier by its id within the user's selected company.
**Throws** **Throws**
`Error` — If the supplier is not found. `Error` — If the supplier is not found or is in use.
**Example** **Example**

View File

@@ -37,7 +37,7 @@ const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
*Source: `src/store/warehouse/delete.js`* *Source: `src/store/warehouse/delete.js`*
Deletes a warehouse for the user's selected company, unless referenced in the ledger. Deletes a warehouse for the user's selected company, unless referenced in the ledger or it is the last warehouse while articles have stock tracking enabled.
**Parameters** **Parameters**
@@ -53,7 +53,7 @@ Deletes a warehouse for the user's selected company, unless referenced in the le
**Throws** **Throws**
`Error` — If warehouse not found or is in use in the ledger. `Error` — If warehouse not found, in use in the ledger, or is the last warehouse with track_stock articles.
**Example** **Example**

View File

@@ -42,12 +42,11 @@ Company-scoped methods take **no** `companyId` argument: they use `google_auth.s
For `POST /api/company`: For `POST /api/company`:
1. Fails if `name` is taken (`Company name not available`) 1. Ensures language by `iso639` (get or create)
2. Ensures language by `iso639` (get or create) 2. Ensures tax rows for `taxrate`, `reducedtaxrate`, and `0` → `taxfreerateId`
3. Ensures tax rows for `taxrate`, `reducedtaxrate`, and `0` → `taxfreerateId` 3. Creates root category (`parentId = null`)
4. Creates root category (`parentId = null`) 4. Creates POS `customergroup` (name `POS`), then inserts company with `poscustomergroup_id` (**NOT NULL**), then links the group via `customergroup.company_id`
5. Creates POS `customergroup` (name `POS`), then inserts company with `poscustomergroup_id` (**NOT NULL**), then links the group via `customergroup.company_id` 5. Links creator as `companyuser` (`googleAuthId` → `company_id`)
6. Links creator as `companyuser` (`googleAuthId` → `company_id`)
`company.poscustomergroup_id` is required (POS pricing in `getTreeForCompany` / category articles). Circular FK with `customergroup.company_id`: the group is inserted first with `company_id` null, then linked after the company row exists. `company.poscustomergroup_id` is required (POS pricing in `getTreeForCompany` / category articles). Circular FK with `customergroup.company_id`: the group is inserted first with `company_id` null, then linked after the company row exists.
@@ -59,7 +58,7 @@ Returns only companies where `googleAuthId` has a `companyuser` row.
For `PATCH /api/company/:id`: For `PATCH /api/company/:id`:
1. Fails if company missing / name taken by another company 1. Fails if company missing
2. Ensures lang + tax rates 2. Ensures lang + tax rates
3. Updates `name`, `default_language_id`, `taxrate`, `reducedtaxrate` 3. Updates `name`, `default_language_id`, `taxrate`, `reducedtaxrate`
(`taxfreerateId` / root category unchanged) (`taxfreerateId` / root category unchanged)
@@ -323,13 +322,31 @@ Company-scoped goods receipts on `manifest` / `manifestarticle` with fixed `type
| Method | Signature | Returns | | Method | Signature | Returns |
| --- | --- | --- | | --- | --- | --- |
| `list` | `(googleAuthId)` | `DeliverySummary[]` | | `list` | `(googleAuthId, opts?: DeliveryListOpts)` | `DeliverySummary[] | WindowedDeliveries` |
| `getById` | `(googleAuthId, id)` | `Delivery` | | `getById` | `(googleAuthId, id)` | `Delivery` |
| `create` | `(googleAuthId, { date, supplierId?, externalNumber?, lines })` | `Delivery` | | `create` | `(googleAuthId, { date, supplierId?, externalNumber?, lines })` | `Delivery` |
| `update` | `(googleAuthId, id, { number?, date?, supplierId?, externalNumber?, lines? })` | `Delivery` | | `update` | `(googleAuthId, id, { number?, date?, supplierId?, externalNumber?, lines? })` | `Delivery` |
| `delete` | `(googleAuthId, id)` | `void` | | `delete` | `(googleAuthId, id)` | `void` |
| `rebuildSummaries` | `(googleAuthId)` | `{ ok: boolean, companyId: number }` |
```ts ```ts
type DeliveryListOpts = {
offset?: number,
limit?: number,
search?: string,
onlyUnbooked?: boolean,
supplierId?: number,
seekMonths?: number,
}
type WindowedDeliveries = {
deliveries: DeliverySummary[],
total: number,
unfilteredTotal: number,
offset: number,
limit: number,
}
type DeliverySummary = { type DeliverySummary = {
id: number, id: number,
number: string, number: string,
@@ -337,6 +354,31 @@ type DeliverySummary = {
supplierId: number | null, // manifest.entity_id supplierId: number | null, // manifest.entity_id
externalNumber: string | null, // manifest.external_number externalNumber: string | null, // manifest.external_number
date: Date, date: Date,
createdAt?: string,
supplierName?: string | null,
summary?: {
positions: number,
grossTotal: number,
netTotal: number,
totalQuantity: number,
stockPositions: number,
bookedPositions: number,
unbookedPositions: number,
stockQuantity: number,
bookedQuantity: number,
unbookedQuantity: number,
isBooked: boolean,
},
ledger?: {
totalPositions: number,
completelyOnLedgerPositions: number,
bookedPositions: number,
unbookedPositions: number,
totalQuantity: number,
bookedQuantity: number,
unbookedQuantity: number,
isBooked: boolean,
},
} }
type DeliveryLine = { type DeliveryLine = {
@@ -521,7 +563,7 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c
`move` moves a category to a new parent. Both `id` (source) and `parentId` (target) are validated via `resolveCategoryAccess` — the target must be a real category of the same authorized company, else `Parent not found`. Moving into itself or into a descendant (cycle) → `Invalid parent`. The moved category gets the next sibling `sort` under the new parent. Returns `{ id, parentId, sort }`. For `POST /api/category/:id/move`. `move` moves a category to a new parent. Both `id` (source) and `parentId` (target) are validated via `resolveCategoryAccess` — the target must be a real category of the same authorized company, else `Parent not found`. Moving into itself or into a descendant (cycle) → `Invalid parent`. The moved category gets the next sibling `sort` under the new parent. Returns `{ id, parentId, sort }`. For `POST /api/category/:id/move`.
`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`pic.fullbinary`). Thumb always scaled to max 200×200 (`pic.thumbbinary`) via `sharp`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `categorypic`. Auth via `resolveCategoryAccess`. Errors: `Category not found`, `Not authorized`. `addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`storageDir/fullbinary`). Thumb always scaled to max 200×200 (`storageDir/thumbbinary`) via `wasm-image-optimization`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `categorypic`. Auth via `resolveCategoryAccess`. Errors: `Category not found`, `Not authorized`.
`addImage` returns: `addImage` returns:
@@ -559,7 +601,7 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c
| `orderImages` | `(googleAuthId: string, sku: string, { fullHashes: string[] })` | `{ fullHashes: string[] }` | | `orderImages` | `(googleAuthId: string, sku: string, { fullHashes: string[] })` | `{ fullHashes: string[] }` |
| `delete` | `(googleAuthId: string, sku: string)` | `void` | | `delete` | `(googleAuthId: string, sku: string)` | `void` |
`addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`pic.fullbinary`). Thumb always scaled to max 200×200 (`pic.thumbbinary`) via `sharp`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `articlepic`. Auth like `getBySku`. Errors: `Article not found`, `Not authorized`. `addImage` accepts a full-size AVIF `Buffer`. Full image: if already AVIF and ≤1000×1000, stored as-is; otherwise scaled to max 1000×1000 (`storageDir/fullbinary`). Thumb always scaled to max 200×200 (`storageDir/thumbbinary`) via `wasm-image-optimization`. Assigns random `fullHash` (`pic.hash`, unique), computes `thumbhash`, links `articlepic`. Auth like `getBySku`. Errors: `Article not found`, `Not authorized`.
`addImage` returns `ImageResult`: `addImage` returns `ImageResult`:
@@ -779,7 +821,8 @@ Company-scoped number sequences (`numbers` table). Auth via selected company (`g
| `PATCH /api/article/:sku/supplier/:id` | `articlesupplier.update(googleAuthId, sku, id, patch)` | | `PATCH /api/article/:sku/supplier/:id` | `articlesupplier.update(googleAuthId, sku, id, patch)` |
| `PUT /api/article/:sku/supplier/order` | `articlesupplier.order(googleAuthId, sku, { ids })` | | `PUT /api/article/:sku/supplier/order` | `articlesupplier.order(googleAuthId, sku, { ids })` |
| `DELETE /api/article/:sku/supplier/:id` | `articlesupplier.delete(googleAuthId, sku, id)` | | `DELETE /api/article/:sku/supplier/:id` | `articlesupplier.delete(googleAuthId, sku, id)` |
| `GET /api/delivery` | `delivery.list(googleAuthId)` | | `GET /api/delivery` | `delivery.list(googleAuthId, opts)` |
| `POST /api/delivery/rebuild-summaries` | `delivery.rebuildSummaries(googleAuthId)` |
| `GET /api/delivery/:id` | `delivery.getById(googleAuthId, id)` | | `GET /api/delivery/:id` | `delivery.getById(googleAuthId, id)` |
| `POST /api/delivery` | `delivery.create(googleAuthId, { number, date, supplierId?, lines })` | | `POST /api/delivery` | `delivery.create(googleAuthId, { number, date, supplierId?, lines })` |
| `PATCH /api/delivery/:id` | `delivery.update(googleAuthId, id, { number?, date?, supplierId?, lines? })` | | `PATCH /api/delivery/:id` | `delivery.update(googleAuthId, id, { number?, date?, supplierId?, lines? })` |

67
TODO.md
View File

@@ -133,3 +133,70 @@ Conversion order (bottom-up by dependency):
(queries/schema stay mssql-specific, so likely keep it) (queries/schema stay mssql-specific, so likely keep it)
- [ ] Decide export for `SCHEMA_SCRIPTS` (currently re-exported from `index.js` - [ ] Decide export for `SCHEMA_SCRIPTS` (currently re-exported from `index.js`
from the moved schema file) from the moved schema file)
I'll check how auth is applied across the routes.
Viewed index.js:1-305
Searched for "app\.(post|put|patch|delete|get|use|all)\("
Viewed auth.js:1-132
The middleware skips auth on several path prefixes. Next I'll check which of those can write.
Searched for "\.(post|put|patch|delete)\("
Searched for "'/(api/)?(public|mcp|oauth|\.well-known)"
Next: the telemetry public route, the `/oauth/register` handler, and the v1 router.
Viewed telemetry.js:1-118
Viewed mcp.js:360-460
Searched for "(router|app)\.(post|put|patch|delete|use|all)\(|auth|token"
Viewed index.js:1-27
Searched for "registerClient|export (async )?function"
Viewed mcp.js:455-540
Searched for "db\.\w+|verifyIdToken|allow|jwt\.sign"
**No. With no JWT, an attacker can still write to the database.** The checks I did are static code review only, and I haven't run anything.
`authMiddleware` ([auth.js](file:///home/seb/src/dashback/src/api/middleware/auth.js#L49-L68)) lets several path groups through without a token. Most of them can't write to the SQL database, but two can.
### 1. Open OAuth registration: `POST /oauth/register`
- This route is public, and it calls `mcpDb.registerClient` ([mcp.js:368](file:///home/seb/src/dashback/src/api/routes/mcp.js#L368)) to insert a client row.
- Anyone can create unlimited rows with no auth and no rate limit.
- `client_name`, `redirect_uris`, `grant_types` and `scope` all come straight from the request body.
### 2. Auth bypass: `POST /oauth/authorize`
- The middleware calls `next()` on this route even with no token ([auth.js:70-79](file:///home/seb/src/dashback/src/api/middleware/auth.js#L70-L79)).
- The handler ([mcp.js:455-503](file:///home/seb/src/dashback/src/api/routes/mcp.js#L455-L503)) takes an `email` from the body when there's no `req.user`.
- If that email is on the allowlist, it writes an auth code (`createAuthCode`) for that user and redirects with it. **No password, Google token or JWT is checked.**
- An attacker can chain this with the previous hole:
1. Register a client with `/oauth/register`.
2. Post to `/oauth/authorize` with the victim's allowlisted email and their own PKCE `code_challenge`.
3. Exchange the code at `/oauth/token`, which I haven't read.
4. Call `/mcp` as that user.
- That would be a full impersonation if the MCP tools can write. Please confirm this by testing against a dev instance.
- Also, `new URL(redirect_uri)` runs with no try/catch and no check that `redirect_uri` matches the registered URIs. A bad value throws, and a valid one is an open redirect that sends the code to the attacker.
### Not writing to the SQL database
- `/api/public/telemetry/crash` ([telemetry.js](file:///home/seb/src/dashback/src/api/routes/telemetry.js#L87-L104)) is public and unauthenticated. It writes to a file on disk rather than SQL, but there's no size or rate limit beyond `bodyLimit`, so it can fill the disk.
- `/v1` and `/api/v1` skip the middleware, but the routes I saw are all `GET`. The ones with data use `posAuthMiddleware`, and `/client`, `/pimage` and `/cimage` have no auth.
- `/api/auth/login` verifies a Google ID token and checks the allowlist. If `IMPLICIT_CREATE=true`, any valid Google account gets added to the allowlist ([login.js:107](file:///home/seb/src/dashback/src/api/routes/auth/login.js#L107)). Make sure that's off in production.
### Suggested fixes
- Make `/oauth/authorize` POST require `req.user`, and remove the `email` fallback.
- Validate `redirect_uri` against the registered URIs for the client.
- Rate-limit `/oauth/register`, or gate it.
Want me to implement these? I haven't changed any code.

View File

@@ -44,6 +44,7 @@ const MODULE_ORDER = [
'attributevalue', 'attributevalue',
'articleattributevalue', 'articleattributevalue',
'labeltemplate', 'labeltemplate',
'healthcheck',
]; ];
// Files that are internal helpers and should not appear as public API entries. // Files that are internal helpers and should not appear as public API entries.
@@ -75,6 +76,8 @@ const INTERNAL_FILES = new Set([
'shape.js', 'shape.js',
'createStore.js', 'createStore.js',
'manifestLines.js', 'manifestLines.js',
'runner.js',
'cron.js',
'index.js', 'index.js',
]); ]);
@@ -321,6 +324,7 @@ function render() {
} }
const SCHEMA_TABLES = [ const SCHEMA_TABLES = [
['account', 'id PK, company_id->company, type:s(100), name:s(255)'],
['article', 'id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)'], ['article', 'id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)'],
['articleattributevalue', 'id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)'], ['articleattributevalue', 'id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)'],
['articleean', 'id PK, article_id->article, ean:s(50) UQ'], ['articleean', 'id PK, article_id->article, ean:s(50) UQ'],
@@ -338,13 +342,17 @@ const SCHEMA_TABLES = [
['brandpic', 'id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)'], ['brandpic', 'id PK, brand_id->brand, pic_id->pic, sort:i=0, UQ(brand_id, pic_id), UQ(brand_id, sort)'],
['category', 'id PK, parent_id?->category, sort:i=0'], ['category', 'id PK, parent_id?->category, sort:i=0'],
['categoryarticle', 'id PK, category_id->category, article_id->article, UQ(category_id, article_id)'], ['categoryarticle', 'id PK, category_id->category, article_id->article, UQ(category_id, article_id)'],
['categoryattributevalue', 'id PK, category_id->category, attributevalue_id->attributevalue, sort:i=0, UQ(category_id, attributevalue_id)'],
['categorypic', 'id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)'], ['categorypic', 'id PK, category_id->category, pic_id->pic, sort:i=0, UQ(category_id, pic_id), UQ(category_id, sort)'],
['categoryproperty', 'id PK, category_id->category, property_id->propertyname, value:txt, sort:i=0'],
['categorytext', 'id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)'], ['categorytext', 'id PK, category_id->category, language_id->language, name:s(255), slug:s(255), seo?:txt, desc?:txt, UQ(category_id, language_id)'],
['company', 'id PK, name:s(255) UQ, root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup'], ['company', 'id PK, name:s(255), root_category_id?->category, default_language_id?->language, taxrate?->tax, reducedtaxrate?->tax, taxfreerate?->tax, poscustomergroup_id->customergroup'],
['companyhistory', 'id PK, company_id->company, companyuser_id?->companyuser, date:dt, text:txt'], ['companyhistory', 'id PK, company_id->company, companyuser_id?->companyuser, date:dt, text:txt'],
['companysetting', 'id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)'], ['companysetting', 'id PK, company_id->company, key:s(255), value:txt, frontend:b=0, writeonly:b=0, UQ(company_id, key)'],
['companyuser', 'id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)'], ['companyuser', 'id PK, google_auth_id->google_auth, company_id->company, UQ(google_auth_id, company_id)'],
['customergroup', 'id PK, name:s(255), sort:i=0, company_id?->company'], ['customergroup', 'id PK, name:s(255), sort:i=0, company_id?->company'],
['articlewarehousesummary', 'id PK, article_id->article, warehouse_id->warehouse, stock:f=0, UQ(article_id, warehouse_id)'],
['deliverysummary', 'id PK, company_id->company, manifest_id->manifest UQ, delivery_number:s(100), external_number?:s(100), delivery_date:dt, created_at:dt, supplier_id?->entity, supplier_name?:s(255), positions:i=0, gross_total:f=0, net_total:f=0, total_quantity:f=0, stock_positions:i=0, booked_positions:i=0, unbooked_positions:i=0, stock_quantity:f=0, booked_quantity:f=0, unbooked_quantity:f=0, is_booked:b=0'],
['entity', 'id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)'], ['entity', 'id PK, customergroup_id->customergroup, first_name?:s(255), name:s(255), number?:s(100), created_at:dt, updated_at:dt, UQ(company_id, number)'],
['entityaddress', 'id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0'], ['entityaddress', 'id PK, entity_id->entity, first_name?:s(255), name?:s(255), street?:s(255), streetnumber?:s(50), zip?:s(50), city?:s(255), county?:s(255), addresstype:s(50), sort:i=0'],
['entitycontact', 'id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0'], ['entitycontact', 'id PK, entity_id->entity, contacttype:s(50), contact:s(255), sort:i=0'],
@@ -357,16 +365,31 @@ const SCHEMA_TABLES = [
['manifest', 'id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt'], ['manifest', 'id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt'],
['manifestarticle', 'id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0'], ['manifestarticle', 'id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0'],
['manifestattachment', 'id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)'], ['manifestattachment', 'id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)'],
['mcp_auth_code', 'code:s(128) PK, client_id:s(100), google_auth_id->google_auth, redirect_uri:s(2048), code_challenge:s(255), code_challenge_method:s(32), scope?:s(255), expires_at:dt, used:b=0, created_at:dt'],
['mcp_client', 'id PK, client_id:s(100) UQ, client_secret?:s(255), client_name:s(255), redirect_uris:txt, grant_types?:s(255), response_types?:s(255), scope?:s(255), created_at:dt'],
['mcp_token', 'id PK, refresh_token:s(128) UQ, client_id:s(100), google_auth_id->google_auth, scope?:s(255), expires_at:dt, revoked:b=0, created_at:dt'],
['meta', 'key:s(255) PK, value:txt'], ['meta', 'key:s(255) PK, value:txt'],
['modelconversation', 'id PK, companyuser_id->companyuser, provider:s(100), model:s(255), created_at:dt, updated_at:dt, state:s(20)=\'idle\', started_by?:i'],
['modelmessage', 'id PK, conversation_id->modelconversation, created_at:dt, role:s(20), message:txt'],
['modelvfs', 'id PK, company_id->company, name:s(255), mime:s(100), content:bin, meta?:txt, created_at:dt, updated_at:dt, UQ(company_id, name)'],
['numbers', 'id PK, company_id->company, type:s(100), prefix:s(50)=\'\', padding:i=0, next:i=1, UQ(company_id, type)'], ['numbers', 'id PK, company_id->company, type:s(100), prefix:s(50)=\'\', padding:i=0, next:i=1, UQ(company_id, type)'],
['optionslist', 'id PK'], ['optionslist', 'id PK'],
['optionslistarticle', 'id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)'], ['optionslistarticle', 'id PK, optionslist_id->optionslist, article_id->article, count:i=1, sort:i=0, mandated:b=0, UQ(optionslist_id, article_id), UQ(optionslist_id, sort)'],
['pic', 'id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), source:txt, fullbinary:bin, thumbbinary:bin'], ['ordersummary', 'id PK, company_id->company, manifest_id->manifest UQ, order_number:s(100), order_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, paid_total:f=0, ordered_quantity:f=0, shipped_quantity:f=0, is_paid:b=0, is_shipped:b=0'],
['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt'], ['package', 'id PK, shipping_id->shipping, entityaddress_id->entityaddress, number:s(100), type:s(100), state:s(100), details?:txt'],
['payment', 'id PK, account_id->account, manifest_id->manifest, type:s(100), amount:f'],
['pic', 'id PK, w:i, h:i, size:bi, hash:s(64) UQ, thumbw:i, thumbh:i, thumbhash:s(64), smallthumbw:i, smallthumbh:i, smallthumbbinary:bin, source:txt'],
['posterminal', 'id PK, company_id->company, name:s(255), token?:s(255) UQ, pairingpin?:s(50), maxknownobituaryid?:i, root_category_id?->category, sync_root_articles:b=1, lastseen_at?:dt, updated_at:dt'],
['posterminalcategory', 'id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)'], ['posterminalcategory', 'id PK, posterminal_id->posterminal, category_id->category, UQ(posterminal_id, category_id)'],
['posterminalobituary', 'id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i, created_at:dt'],
['posterminalsyncqueue', 'id PK, posterminal_id->posterminal, entity_type:s(50), entity_id:i'],
['price', 'id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f'], ['price', 'id PK, tax_id->tax, article_id->article, minmultiplier:d(18,4)=1, customergroup_id->customergroup, net:f'],
['printer', 'id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt'], ['printer', 'id PK, company_id->company, address:s(255), type:s(50), model?:s(255), serial?:s(255), sort:i=0, desc?:txt'],
['propertyname', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'], ['propertyname', 'id PK, company_id->company, name:s(255), sort:i=0, exclusive:b=0, UQ(company_id, name)'],
['quotesummary', 'id PK, company_id->company, manifest_id->manifest UQ, quote_number:s(100), quote_date:dt, created_at:dt, entity_id?->entity, customer_name?:s(255), positions:i=0, gross_total:f=0, total_quantity:f=0'],
['shipmentsummary', 'id PK, company_id->company, manifest_id->manifest UQ, shipment_number:s(100), shipment_date:dt, created_at:dt, order_id?:i, order_number?:s(100), entity_id?->entity, customer_name?:s(255), positions:i=0, total_quantity:f=0, package_count:i=0, carrier?:s(100), tracking_code?:s(255), state:s(50)=\'new\''],
['shipping', 'id PK, manifest_id->manifest, number:s(100), tracking_code?:s(255), carrier?:s(100), type:s(100), state:s(100)'],
['subagentschedule', 'id PK, company_id->company, desc?:txt, prompt:txt, skills?:txt, lastagentnote?:txt, max_itr:i=15, cron:s(100), created_at:dt, lastrun_at?:dt'],
['tax', 'id PK, rate:d(18,4) UQ'], ['tax', 'id PK, rate:d(18,4) UQ'],
['unit', 'id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)'], ['unit', 'id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)'],
['unittext', 'id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)'], ['unittext', 'id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)'],

View File

@@ -170,8 +170,8 @@ function addInferredEdges(tables, edges) {
const match = column.name.match(/^(.+)_id$/i); const match = column.name.match(/^(.+)_id$/i);
if (!match) continue; if (!match) continue;
// Tombstone tables store references to deleted entities, not active foreign keys. // Obituary and sync queue tables store polymorphic entity_id references, not active foreign keys to entity.
if (tableName.includes('tombstone')) continue; if (tableName.includes('obituary') || tableName.includes('tombstone') || tableName.includes('syncqueue')) continue;
const candidate = match[1]; const candidate = match[1];
if (!tables.has(candidate) || candidate === tableName) continue; if (!tables.has(candidate) || candidate === tableName) continue;
@@ -354,7 +354,6 @@ const CLUSTERS = [
'article', 'article',
'articleean', 'articleean',
'articlehistory', 'articlehistory',
'articletombstone',
'articlelist', 'articlelist',
'articlelistarticle', 'articlelistarticle',
'optionslist', 'optionslist',
@@ -374,6 +373,11 @@ const CLUSTERS = [
'manifest', 'manifest',
'manifestarticle', 'manifestarticle',
'manifestattachment', 'manifestattachment',
'articlewarehousesummary',
'deliverysummary',
'ordersummary',
'quotesummary',
'shipmentsummary',
'ledger', 'ledger',
'warehouse', 'warehouse',
'shipping', 'shipping',
@@ -386,11 +390,11 @@ const CLUSTERS = [
tables: ['tax', 'price', 'account', 'payment'], tables: ['tax', 'price', 'account', 'payment'],
}, },
{ id: 'units', label: 'Units', tables: ['unit'] }, { id: 'units', label: 'Units', tables: ['unit'] },
{ id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'printer', 'labeltemplate'] }, { id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'posterminalobituary', 'posterminalsyncqueue', 'printer', 'labeltemplate'] },
{ {
id: 'ai', id: 'ai',
label: 'AI & Models', label: 'AI & Models',
tables: ['modelconversation', 'modelmessage', 'modelvfs'], tables: ['modelconversation', 'modelmessage', 'modelvfs', 'subagentschedule'],
}, },
{ {
id: 'attributes', id: 'attributes',

809
package-lock.json generated
View File

@@ -10,16 +10,17 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@clack/prompts": "^1.8.1", "@clack/prompts": "^1.8.1",
"@hpcc-js/wasm-graphviz": "^1.29.1", "@hpcc-js/wasm-graphviz": "^1.29.2",
"chalk": "^6.0.1", "chalk": "^6.0.1",
"dotenv": "^18.0.4", "dotenv": "^18.0.8",
"pdfconvert-wasm": "^0.3.0", "pdfconvert-wasm": "^0.3.0",
"sharp": "^0.35.5", "skelehash-wasm": "^0.1.1",
"thumbhash": "^0.1.1" "thumbhash": "^0.1.1",
"wasm-image-optimization": "^2.0.13"
}, },
"devDependencies": { "devDependencies": {
"better-sqlite3": "^13.0.3", "better-sqlite3": "^13.0.3",
"mssql": "^12.7.2" "mssql": "^12.7.9"
}, },
"peerDependencies": { "peerDependencies": {
"better-sqlite3": "^12.0.0", "better-sqlite3": "^12.0.0",
@@ -183,9 +184,9 @@
} }
}, },
"node_modules/@azure/identity": { "node_modules/@azure/identity": {
"version": "4.13.3", "version": "4.14.0",
"resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.3.tgz", "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.14.0.tgz",
"integrity": "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==", "integrity": "sha512-8utCLdTZn14CiAe0HW4JbtzUMsc0mdDwnjUvStFMrR4IxQP8S2eL9zVAdplpfORVJGYmidZElpeXRYjJQ4UyUw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -199,7 +200,7 @@
"@azure/logger": "^1.0.0", "@azure/logger": "^1.0.0",
"@azure/msal-browser": "^5.5.0", "@azure/msal-browser": "^5.5.0",
"@azure/msal-node": "^6.0.0", "@azure/msal-node": "^6.0.0",
"open": "^10.1.0", "open": "^11.0.0",
"tslib": "^2.2.0" "tslib": "^2.2.0"
}, },
"engines": { "engines": {
@@ -264,22 +265,22 @@
} }
}, },
"node_modules/@azure/msal-browser": { "node_modules/@azure/msal-browser": {
"version": "5.22.0", "version": "5.26.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.22.0.tgz", "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.26.0.tgz",
"integrity": "sha512-5kgu9xeEKgGc2JeidxAtU15NJTqiH/CMCRRQAJ4Rac56kB7KVg91vbNmn+z3RO1vNomPN69UvjKG9h1Pghx6dQ==", "integrity": "sha512-GYKRquzDZmZggCdStWy2bNJKnPui1upssSuznJgP+/h6Mb0ldkUkaGvaY6uSGGmZOm0CGiBQ3b5BiGy43BpXSg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@azure/msal-common": "16.14.1" "@azure/msal-common": "16.14.3"
}, },
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/@azure/msal-common": { "node_modules/@azure/msal-common": {
"version": "16.14.1", "version": "16.14.3",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.3.tgz",
"integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", "integrity": "sha512-tz3GvMrR9V/plb2lDdW8qaO8CPpfZevdExBKjltzzbAtyhK0eB2UIvji1aU+gkyHj3fxQ2ie+LNHyMXdmo8wOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -300,6 +301,16 @@
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/@azure/msal-node/node_modules/@azure/msal-common": {
"version": "16.14.1",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz",
"integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@clack/core": { "node_modules/@clack/core": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz",
@@ -328,574 +339,16 @@
"node": ">= 20.12.0" "node": ">= 20.12.0"
} }
}, },
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@hpcc-js/wasm-graphviz": { "node_modules/@hpcc-js/wasm-graphviz": {
"version": "1.29.1", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.29.1.tgz", "resolved": "https://registry.npmjs.org/@hpcc-js/wasm-graphviz/-/wasm-graphviz-1.30.0.tgz",
"integrity": "sha512-koAQL0wlryEMeBs4/cQC3GB5ziITv66gR9DU/SWYjjup+iQ+Nbjcqxeaef+sffN4F4cBYZ3P31B5z7+cbSABbw==", "integrity": "sha512-T20IKosOZuex2qV049fQokVJy2MWv8w9Md5V/4mRKCXDf49fBCR50VD1yBlPYHaQlHPsFGRdqXN//r59vZliWQ==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@img/colour": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz",
"integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.3.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz",
"integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.3.4"
}
},
"node_modules/@img/sharp-freebsd-wasm32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz",
"integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==",
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"dependencies": {
"@img/sharp-wasm32": "0.35.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz",
"integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz",
"integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz",
"integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz",
"integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz",
"integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz",
"integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz",
"integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz",
"integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz",
"integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz",
"integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz",
"integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.3.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz",
"integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.3.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz",
"integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.3.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz",
"integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-riscv64": "1.3.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz",
"integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.3.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz",
"integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.3.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz",
"integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz",
"integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.3.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz",
"integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==",
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.11.3"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-webcontainers-wasm32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz",
"integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"@img/sharp-wasm32": "0.35.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz",
"integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz",
"integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz",
"integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@js-joda/core": { "node_modules/@js-joda/core": {
"version": "6.1.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.1.0.tgz", "resolved": "https://registry.npmjs.org/@js-joda/core/-/core-6.3.0.tgz",
"integrity": "sha512-H8NTMRDJqad/leyv/D/A3kSOsf5/58Ydj4DJGDyaCWk9OU/zuZOLhndVffJgQjsgrn5GC0znHMHie7TfvPPG4w==", "integrity": "sha512-mdmjc6lJkTCORsHlMxBT9XM1yLqgtsNZmv/Hwme2v/HIb15D2XcmklZKTnynqBgsSizA9SkFvzpuksfIXhDV/w==",
"dev": true, "dev": true,
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
@@ -907,9 +360,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "26.6.2", "version": "26.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
"integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -917,9 +370,9 @@
} }
}, },
"node_modules/@types/readable-stream": { "node_modules/@types/readable-stream": {
"version": "4.0.24", "version": "4.0.25",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.25.tgz",
"integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==", "integrity": "sha512-7GsxL8Wd5X3jh73/qgW7WCnbBvGSdD8U3E4Jiuus3/8I+PN20DtlkJtwFhr/sImXfDDQ2Z03i/9wNiIF3TVSsw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1044,9 +497,9 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/bundle-name": { "node_modules/bundle-name": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz",
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1142,19 +595,10 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "18.0.4", "version": "18.0.9",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.4.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.9.tgz",
"integrity": "sha512-jGt9uI5BIxpVO15kvy+iEFqNAwxYClKWnkptLjSNPnFLC003Imd0ofr3aEVsbpHdSBdSsbcApNIlz4nbyGMwdA==", "integrity": "sha512-3fVOW4cB02R066PUSwn8O4TJyz9NckQl7F81blIbaq9jKBm2tSmOKky7ZHIXzGvkVPXCSn6jkpziBPZdGFs8Tw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"bin": { "bin": {
"dotenv": "dist/index.cjs" "dotenv": "dist/index.cjs"
@@ -1309,6 +753,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-in-ssh": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
"integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-inside-container": { "node_modules/is-inside-container": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
@@ -1454,9 +911,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/mssql": { "node_modules/mssql": {
"version": "12.7.2", "version": "12.7.11",
"resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.2.tgz", "resolved": "https://registry.npmjs.org/mssql/-/mssql-12.7.11.tgz",
"integrity": "sha512-zhpPw+WXBWLw7d7J6Y0aWgQy0EV8HKDGNSvmkAjP8bSM4H41X0fweAeKWI+bg9Ri+JHCbsf/QScjmACBFcEo8A==", "integrity": "sha512-O24UQA++JLvMUy5T6xL73kCHP4YjLMS6DBxKFeiWyc9kZW2mrVAZ6uqly2d3hGtBTXHRFRImfM+3EdCZVE5bWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1491,19 +948,21 @@
} }
}, },
"node_modules/open": { "node_modules/open": {
"version": "10.2.0", "version": "11.0.4",
"resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz",
"integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"default-browser": "^5.2.1", "default-browser": "^5.5.1",
"define-lazy-prop": "^3.0.0", "define-lazy-prop": "^3.0.0",
"is-in-ssh": "^1.0.0",
"is-inside-container": "^1.0.0", "is-inside-container": "^1.0.0",
"wsl-utils": "^0.1.0" "powershell-utils": "^0.2.1",
"wsl-utils": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=20"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -1515,6 +974,19 @@
"integrity": "sha512-FbEk+Grg0UST4kOPuN7OFfVsut1FvDOJNPS3pt4rzCS8VlFsFzD7ptg7YtFZjEOKgJ4MgqlfCgjK4RYaRBpT0Q==", "integrity": "sha512-FbEk+Grg0UST4kOPuN7OFfVsut1FvDOJNPS3pt4rzCS8VlFsFzD7ptg7YtFZjEOKgJ4MgqlfCgjK4RYaRBpT0Q==",
"license": "GPLv3" "license": "GPLv3"
}, },
"node_modules/powershell-utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz",
"integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/process": { "node_modules/process": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -1526,9 +998,9 @@
} }
}, },
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "4.7.0", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.8.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "integrity": "sha512-2jhTTU3Eilp/2DMT71gbcoqC35eQHasMJH0RHkYxwlxIv3+FR6Y5xAFZIwEGLxKkjZYEFlsiMNIdwUqjK2lLHQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1587,6 +1059,7 @@
"version": "7.8.5", "version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -1595,67 +1068,17 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/sharp": {
"version": "0.35.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
"integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
"license": "Apache-2.0",
"dependencies": {
"@img/colour": "^1.1.0",
"detect-libc": "^2.1.2",
"semver": "^7.8.5"
},
"engines": {
"node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.35.5",
"@img/sharp-darwin-x64": "0.35.5",
"@img/sharp-freebsd-wasm32": "0.35.5",
"@img/sharp-libvips-darwin-arm64": "1.3.4",
"@img/sharp-libvips-darwin-x64": "1.3.4",
"@img/sharp-libvips-linux-arm": "1.3.4",
"@img/sharp-libvips-linux-arm64": "1.3.4",
"@img/sharp-libvips-linux-ppc64": "1.3.4",
"@img/sharp-libvips-linux-riscv64": "1.3.4",
"@img/sharp-libvips-linux-s390x": "1.3.4",
"@img/sharp-libvips-linux-x64": "1.3.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.3.4",
"@img/sharp-libvips-linuxmusl-x64": "1.3.4",
"@img/sharp-linux-arm": "0.35.5",
"@img/sharp-linux-arm64": "0.35.5",
"@img/sharp-linux-ppc64": "0.35.5",
"@img/sharp-linux-riscv64": "0.35.5",
"@img/sharp-linux-s390x": "0.35.5",
"@img/sharp-linux-x64": "0.35.5",
"@img/sharp-linuxmusl-arm64": "0.35.5",
"@img/sharp-linuxmusl-x64": "0.35.5",
"@img/sharp-webcontainers-wasm32": "0.35.5",
"@img/sharp-win32-arm64": "0.35.5",
"@img/sharp-win32-ia32": "0.35.5",
"@img/sharp-win32-x64": "0.35.5"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/sisteransi": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/sprintf-js": { "node_modules/skelehash-wasm": {
"version": "1.1.3", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "resolved": "https://registry.npmjs.org/skelehash-wasm/-/skelehash-wasm-0.1.1.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "integrity": "sha512-5kC7dESMvQUdpLvRYfTc0oZ/Ff78bedUhqvYAGnVjs3agsS9PP8RJ/lGpol5sSKoYET/7KJbQ2UVSDTNpmDtTw==",
"dev": true, "license": "0BSD"
"license": "BSD-3-Clause"
}, },
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
@@ -1678,9 +1101,9 @@
} }
}, },
"node_modules/tedious": { "node_modules/tedious": {
"version": "20.0.0", "version": "20.3.8",
"resolved": "https://registry.npmjs.org/tedious/-/tedious-20.0.0.tgz", "resolved": "https://registry.npmjs.org/tedious/-/tedious-20.3.8.tgz",
"integrity": "sha512-bTR0aou0Ghucf0ytvZUJjnKHGKDV8tT57jPYtEkSpfTWFe++4uR1wxJLQ4mh5wlSvAYXzmgBxbI0vaE56qigXw==", "integrity": "sha512-ZrJiwyGn340Sa0V0g37Xqj5bvmJ1riYwvEtg7zdm/lpw70f2ZNIQ2fvHmntU5fEOorWDa48Jo1oKYAuUP+rh7Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1692,8 +1115,7 @@
"bl": "^6.1.4", "bl": "^6.1.4",
"iconv-lite": "^0.7.0", "iconv-lite": "^0.7.0",
"js-md4": "^0.3.2", "js-md4": "^0.3.2",
"native-duplexpair": "^1.0.0", "native-duplexpair": "^1.0.0"
"sprintf-js": "^1.1.3"
}, },
"engines": { "engines": {
"node": ">=22" "node": ">=22"
@@ -1709,7 +1131,7 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"devOptional": true, "dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
@@ -1719,17 +1141,46 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/wasm-image-optimization": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/wasm-image-optimization/-/wasm-image-optimization-2.0.13.tgz",
"integrity": "sha512-MGaFZtdxMHZyaL3VUUcNkN8qccJcy33lcXoUIaegav/y1TiK6c2aALbKAjoe4bXHFI7nDl+5+TG0e9Y8j1/h9Q==",
"license": "MIT",
"dependencies": {
"commander": "15.0.0",
"worker-lib": "2.2.1"
},
"bin": {
"wasm-image-optimization": "dist/cli.js"
}
},
"node_modules/wasm-image-optimization/node_modules/commander": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
"integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
"license": "MIT",
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/worker-lib": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/worker-lib/-/worker-lib-2.2.1.tgz",
"integrity": "sha512-CauU4vz3vZUBG3A2CY93GY94KLhbH8JtPKtuvKu2w5G07cHkZ73QkTAJ/lEHDL6arT5WDu1qpU++T0+kGDgSCg==",
"license": "MIT"
},
"node_modules/wsl-utils": { "node_modules/wsl-utils": {
"version": "0.1.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz",
"integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-wsl": "^3.1.0" "is-wsl": "^3.1.0",
"powershell-utils": "^0.2.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=20"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"

View File

@@ -20,6 +20,8 @@
"init-schema": "node scripts/init-schema.js", "init-schema": "node scripts/init-schema.js",
"purge-db": "node scripts/purge-db.js", "purge-db": "node scripts/purge-db.js",
"reset-db": "node scripts/reset-db.js", "reset-db": "node scripts/reset-db.js",
"downgrade": "node scripts/downgrade.js",
"test": "node --test \"test/*.test.js\"",
"test:api": "node scripts/test-api.js", "test:api": "node scripts/test-api.js",
"test:benchmark": "node scripts/benchmark.js" "test:benchmark": "node scripts/benchmark.js"
}, },
@@ -27,10 +29,11 @@
"@clack/prompts": "^1.8.1", "@clack/prompts": "^1.8.1",
"@hpcc-js/wasm-graphviz": "^1.29.2", "@hpcc-js/wasm-graphviz": "^1.29.2",
"chalk": "^6.0.1", "chalk": "^6.0.1",
"dotenv": "^18.0.5", "dotenv": "^18.0.8",
"pdfconvert-wasm": "^0.3.0", "pdfconvert-wasm": "^0.3.0",
"sharp": "^0.35.5", "skelehash-wasm": "^0.1.1",
"thumbhash": "^0.1.1" "thumbhash": "^0.1.1",
"wasm-image-optimization": "^2.0.13"
}, },
"peerDependencies": { "peerDependencies": {
"better-sqlite3": "^12.0.0", "better-sqlite3": "^12.0.0",
@@ -43,6 +46,6 @@
}, },
"devDependencies": { "devDependencies": {
"better-sqlite3": "^13.0.3", "better-sqlite3": "^13.0.3",
"mssql": "^12.7.2" "mssql": "^12.7.9"
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 459 KiB

View File

@@ -0,0 +1,171 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function test() {
console.log('--- Testing setOn and setOff with subcategories ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-posterminal-subcats-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('subcat-tester@example.com');
const company = await store.company.create({
name: 'SubCat Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS 1',
pairingpin: '1234',
});
await store.posterminal.pair(terminal.id, {
token: 'pos-token-subcat',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
});
// Create Category Tree:
// Root -> Cat 4 -> SubCat 41 -> SubCat 411
// Root -> Cat 5
const cat4 = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Getränke',
});
const subCat41 = await store.category.createUnderParent({
googleAuthId,
parentId: cat4.id,
name: 'Alkoholfrei',
});
const subCat411 = await store.category.createUnderParent({
googleAuthId,
parentId: subCat41.id,
name: 'Cola',
});
const cat5 = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Snacks',
});
// Create articles in various categories
const artCola = await store.article.createUnderParent({
googleAuthId,
parentId: subCat411.id,
name: 'Cola 0.5L',
});
const artSnack = await store.article.createUnderParent({
googleAuthId,
parentId: cat5.id,
name: 'Chips',
});
// Set old timestamps on categories and articles to test updated_at touch
provider.db.prepare("UPDATE category SET updated_at = '2020-01-01T00:00:00.000Z'").run();
provider.db.prepare("UPDATE article SET updated_at = '2020-01-01T00:00:00.000Z'").run();
console.log('1. Testing setOn on parent category Cat 4 (should enable Cat 4, SubCat 41, and SubCat 411)...');
const onResult = await store.posterminal.setOn(googleAuthId, terminal.id, cat4.id);
console.log('Category IDs after setOn(cat4):', onResult.categoryIds);
assert(onResult.categoryIds.includes(cat4.id), 'Should include cat4');
assert(onResult.categoryIds.includes(subCat41.id), 'Should include subCat41');
assert(onResult.categoryIds.includes(subCat411.id), 'Should include subCat411');
assert(!onResult.categoryIds.includes(cat5.id), 'Should NOT include cat5');
// Check that categories in the subtree were touched
const catsAfterOn = provider.db.prepare('SELECT id, updated_at FROM category WHERE id IN (?, ?, ?)').all(cat4.id, subCat41.id, subCat411.id);
for (const c of catsAfterOn) {
assert.notEqual(c.updated_at, '2020-01-01T00:00:00.000Z', `Category ${c.id} updated_at should be touched on setOn`);
}
// Check that articles in the subtree were touched
const artAfterOn = provider.db.prepare('SELECT id, updated_at FROM article WHERE id = ?').get(artCola.id);
assert.notEqual(artAfterOn.updated_at, '2020-01-01T00:00:00.000Z', 'Article updated_at should be touched on setOn');
// Cat 5 article should NOT be touched
const artSnackAfterOn = provider.db.prepare('SELECT id, updated_at FROM article WHERE id = ?').get(artSnack.id);
assert.equal(artSnackAfterOn.updated_at, '2020-01-01T00:00:00.000Z', 'Unrelated article updated_at should not be touched');
console.log('2. Also enabling Cat 5...');
await store.posterminal.setOn(googleAuthId, terminal.id, cat5.id);
// Reset timestamps to test setOff touch
provider.db.prepare("UPDATE category SET updated_at = '2020-01-01T00:00:00.000Z'").run();
provider.db.prepare("UPDATE article SET updated_at = '2020-01-01T00:00:00.000Z'").run();
console.log('3. Testing setOff on parent category Cat 4 (should disable Cat 4, SubCat 41, and SubCat 411)...');
const offResult = await store.posterminal.setOff(googleAuthId, terminal.id, cat4.id);
console.log('Category IDs after setOff(cat4):', offResult.categoryIds);
assert(!offResult.categoryIds.includes(cat4.id), 'Should NOT include cat4');
assert(!offResult.categoryIds.includes(subCat41.id), 'Should NOT include subCat41');
assert(!offResult.categoryIds.includes(subCat411.id), 'Should NOT include subCat411');
assert(offResult.categoryIds.includes(cat5.id), 'Should still include cat5');
// Check that categories in the subtree were touched on setOff
const catsAfterOff = provider.db.prepare('SELECT id, updated_at FROM category WHERE id IN (?, ?, ?)').all(cat4.id, subCat41.id, subCat411.id);
for (const c of catsAfterOff) {
assert.notEqual(c.updated_at, '2020-01-01T00:00:00.000Z', `Category ${c.id} updated_at should be touched on setOff`);
}
// Check that article in the subtree was touched on setOff
const artAfterOff = provider.db.prepare('SELECT id, updated_at FROM article WHERE id = ?').get(artCola.id);
assert.notEqual(artAfterOff.updated_at, '2020-01-01T00:00:00.000Z', 'Article updated_at should be touched on setOff');
// Verify obituaries generated
const obituariesAfter = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ? ORDER BY id ASC')
.all(terminal.id);
console.log('Obituaries after setOff:', obituariesAfter);
const catObituaryIds = obituariesAfter.filter(o => o.entity_type === 'category').map(o => o.entity_id);
assert(catObituaryIds.includes(cat4.id), 'Should have obituary for cat4');
assert(catObituaryIds.includes(subCat41.id), 'Should have obituary for subCat41');
assert(catObituaryIds.includes(subCat411.id), 'Should have obituary for subCat411');
const artObituaryIds = obituariesAfter.filter(o => o.entity_type === 'article').map(o => o.entity_id);
assert(artObituaryIds.includes(artCola.id), 'Should have obituary for artCola');
assert(!artObituaryIds.includes(artSnack.id), 'Should NOT have obituary for artSnack');
console.log('4. Testing setOn on an intermediate subcategory (SubCat 41)...');
const subOnResult = await store.posterminal.setOn(googleAuthId, terminal.id, subCat41.id);
console.log('Category IDs after setOn(subCat41):', subOnResult.categoryIds);
assert(!subOnResult.categoryIds.includes(cat4.id), 'Parent cat4 should not be enabled when enabling subCat41');
assert(subOnResult.categoryIds.includes(subCat41.id), 'subCat41 should be enabled');
assert(subOnResult.categoryIds.includes(subCat411.id), 'Child subCat411 should be enabled');
console.log('✅ ALL SUBCATEGORY SET_ON / SET_OFF TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
test()
.then(() => {
process.exit(0);
})
.catch(err => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,179 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing deposit via mandated optionslist ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-deposit-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('tester@example.com');
const company = await store.company.create({
name: 'Deposit Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// 1. Create a category
const cat = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Getränke',
});
// 2. Create terminal and enable category
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS 1',
pairingpin: '1234',
});
await store.posterminal.pair(terminal.id, {
token: 'pos-token-xyz',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
});
await store.posterminal.setOn(googleAuthId, terminal.id, cat.id);
// 3. Create beverage article
const clubMate = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
name: 'Club Mate 0.5l',
sku: 'MATE-05',
});
await store.article.setPricesBySku(googleAuthId, clubMate.sku, [
{ net: 1.5, tax: 19 },
]);
// 4. Create deposit article
const pfandArt = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
name: 'Mehrwegpfand 0,08 €',
sku: 'PFAND-0.08',
track_stock: 0,
});
await store.article.setPricesBySku(googleAuthId, pfandArt.sku, [
{ gross: 0.08, tax: 19 },
]);
// 5. Query products BEFORE linking mandated option
let prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
let mateProd = prods.find((p) => p.sku === 'MATE-05');
let pfandProd = prods.find((p) => p.sku === 'PFAND-0.08');
assert.ok(mateProd, 'Club Mate should be listed');
assert.equal(mateProd.deposit, '0');
assert.equal(mateProd.d_price, '0.0');
assert.equal(mateProd.deposit_name, '');
assert.ok(pfandProd, 'Pfand article should be listed');
assert.equal(pfandProd.deposit, '0');
// 6. Link Mehrwegpfand as mandated option to Club Mate
console.log('Linking Mehrwegpfand as mandated option...');
await store.optionslist.setOptionsByArticleSku(googleAuthId, clubMate.sku, [
{
articleId: pfandArt.id,
sku: pfandArt.sku,
count: 1,
sort: 0,
mandated: true,
},
]);
// 7. Query products AFTER linking mandated option
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
mateProd = prods.find((p) => p.sku === 'MATE-05');
console.log('Club Mate with deposit option:', {
sku: mateProd.sku,
deposit: mateProd.deposit,
d_price: mateProd.d_price,
deposit_name: mateProd.deposit_name,
});
assert.equal(mateProd.deposit, '1');
assert.equal(mateProd.d_price, '0.08');
assert.equal(mateProd.deposit_name, 'Mehrwegpfand 0,08 €');
// 8. Test option name change touching parent article
console.log('Testing option name change touching parent article...');
const mateBeforeRename = (await provider.query('SELECT updated_at FROM article WHERE id = @id', { id: clubMate.id })).rows[0].updated_at;
await new Promise((r) => setTimeout(r, 1100)); // wait >1s for timestamp delta
await store.article.updateBySku(googleAuthId, pfandArt.sku, {
name: 'Glas-Mehrwegpfand 0,08 €',
});
const mateAfterRename = (await provider.query('SELECT updated_at FROM article WHERE id = @id', { id: clubMate.id })).rows[0].updated_at;
assert.notEqual(
mateBeforeRename,
mateAfterRename,
'Club Mate updated_at must be bumped when its option article is renamed'
);
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
mateProd = prods.find((p) => p.sku === 'MATE-05');
assert.equal(mateProd.deposit_name, 'Glas-Mehrwegpfand 0,08 €');
console.log('Renamed deposit reflected on parent product:', mateProd.deposit_name);
// 9. Test option price change touching parent article
console.log('Testing option price change touching parent article...');
const mateBeforePrice = mateAfterRename;
await new Promise((r) => setTimeout(r, 1100));
await store.article.setPricesBySku(googleAuthId, pfandArt.sku, [
{ gross: 0.15, tax: 19 },
]);
const mateAfterPrice = (await provider.query('SELECT updated_at FROM article WHERE id = @id', { id: clubMate.id })).rows[0].updated_at;
assert.notEqual(
mateBeforePrice,
mateAfterPrice,
'Club Mate updated_at must be bumped when its option article price changes'
);
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
mateProd = prods.find((p) => p.sku === 'MATE-05');
assert.equal(mateProd.d_price, '0.15');
console.log('New deposit price reflected on parent product:', mateProd.d_price);
// 10. Test clearing options
console.log('Clearing options...');
await store.optionslist.setOptionsByArticleSku(googleAuthId, clubMate.sku, []);
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
mateProd = prods.find((p) => p.sku === 'MATE-05');
assert.equal(mateProd.deposit, '0');
assert.equal(mateProd.d_price, '0.0');
assert.equal(mateProd.deposit_name, '');
console.log('✅ ALL DEPOSIT OPTIONS TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,159 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function main() {
console.log('--- Testing updated_at touch on all inventory movements ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-touch-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('tester@example.com');
const company = await store.company.create({
name: 'Inventory Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// Create warehouse
const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
const cat = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Waren',
});
// Create article with past updated_at
const art = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
sku: 'TEST-SKU',
name: 'Test Article',
track_stock: 1,
});
const oldTime = '2020-01-01T00:00:00.000Z';
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
const getUpdatedAt = async (articleId) => {
const res = await provider.query(`SELECT updated_at FROM article WHERE id = @articleId`, { articleId });
return res.rows[0]?.updated_at;
};
const initialUpdatedAt = await getUpdatedAt(art.id);
assert.equal(initialUpdatedAt, oldTime, 'Should start with old updated_at');
// 1. Test store.stock.adjust
console.log('1. Testing store.stock.adjust touches article.updated_at...');
await store.stock.adjust(googleAuthId, {
sku: 'TEST-SKU',
warehouseId: wh.id,
amount: 2,
// reason omitted so it defaults to "Manual stock addition (+2)"
});
const afterAdjustUpdatedAt = await getUpdatedAt(art.id);
assert.notEqual(afterAdjustUpdatedAt, oldTime, 'stock.adjust should touch updated_at');
assert.ok(new Date(afterAdjustUpdatedAt).getTime() > new Date(oldTime).getTime());
// Check history was recorded with exact text
const historyRes = await provider.query(`SELECT text FROM articlehistory WHERE article_id = @id`, { id: art.id });
const adjustmentEntry = historyRes.rows.find((h) => h.text.includes('Bestandskorrektur +2 (Manual stock addition (+2))'));
assert.ok(adjustmentEntry, 'Should have exact article history entry');
// Reset updated_at to old time
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
// 2. Test Stückliste component touch
console.log('2. Testing Stückliste parent is touched when component has stock adjustment...');
const bundle = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
sku: 'BUNDLE-1',
name: 'Bundle 1',
track_stock: 0,
});
await store.articlelist.setItemsByArticleSku(googleAuthId, 'BUNDLE-1', [
{ sku: 'TEST-SKU', count: 1 },
]);
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: bundle.id });
await store.stock.adjust(googleAuthId, {
sku: 'TEST-SKU',
warehouseId: wh.id,
amount: 5,
text: 'Restock component',
});
const bundleUpdatedAt = await getUpdatedAt(bundle.id);
assert.notEqual(bundleUpdatedAt, oldTime, 'Stückliste bundle updated_at should be touched when component changes stock');
// 3. Test delivery acceptance touches article.updated_at
console.log('3. Testing delivery touches article.updated_at...');
const supplier = await store.supplier.create(googleAuthId, { name: 'Supplier Inc' });
const delivery = await store.delivery.create(googleAuthId, {
supplierId: supplier.id,
date: new Date().toISOString(),
lines: [{ sku: 'TEST-SKU', amount: 10, net: 5, taxrate: 19 }],
});
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
await store.delivery.book(googleAuthId, { id: delivery.id, warehouseId: wh.id });
const deliveryUpdatedAt = await getUpdatedAt(art.id);
assert.notEqual(deliveryUpdatedAt, oldTime, 'delivery.book should touch article.updated_at');
// 4. Test customer shipment touches article.updated_at
console.log('4. Testing shipment touches article.updated_at...');
const groups = await store.customergroup.list(googleAuthId);
const customer = await store.entity.create(googleAuthId, { name: 'Customer A', customergroupId: groups[0].id });
const order = await store.order.create(googleAuthId, {
number: 'AUF-000001',
date: new Date().toISOString(),
entityId: customer.id,
lines: [{ sku: 'TEST-SKU', amount: 3, net: 10, taxrate: 19 }],
});
const carrier = await store.carrier.create(googleAuthId, { name: 'DHL' });
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
await store.shipment.create(googleAuthId, {
orderId: order.id,
lines: [{ sku: 'TEST-SKU', amount: 3 }],
warehouseId: wh.id,
shipping: {
carrierId: carrier.id,
type: 'standard',
state: 'shipped',
warehouseId: wh.id,
},
});
const shipmentUpdatedAt = await getUpdatedAt(art.id);
assert.notEqual(shipmentUpdatedAt, oldTime, 'shipment.create should touch article.updated_at');
console.log('✅ ALL INVENTORY TOUCH TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
main().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

164
scratch/test-variants.js Normal file
View File

@@ -0,0 +1,164 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing POS father/child variants and options ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-variants-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('tester@example.com');
const company = await store.company.create({
name: 'Variant Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// 1. Create a category
const cat = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Kleidung',
});
// 2. Create terminal and enable category
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS 1',
pairingpin: '1234',
});
await store.posterminal.pair(terminal.id, {
token: 'pos-token-xyz',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
});
await store.posterminal.setOn(googleAuthId, terminal.id, cat.id);
// 3. Create attribute name "Farbe"
const attrName = await store.attributename.create(googleAuthId, {
name: 'Farbe',
sort: 1,
exclusive: true,
});
// 4. Create base article and convert to parent
const tShirt = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
name: 'T-Shirt Basic',
sku: 'TSHIRT',
});
await store.article.toggleParent(googleAuthId, tShirt.sku, {
isParent: true,
attributeNameId: attrName.id,
});
// 5. Add children "Rot" and "Blau"
console.log('Adding children "Rot" and "Blau"...');
const childRot = await store.article.addChild(googleAuthId, tShirt.sku, {
value: 'Rot',
name: 'T-Shirt Basic - Rot',
});
const childBlau = await store.article.addChild(googleAuthId, tShirt.sku, {
value: 'Blau',
name: 'T-Shirt Basic - Blau',
});
// 6. Query POS products
let prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
let parentProd = prods.find((p) => p.sku === 'TSHIRT');
let rotProd = prods.find((p) => p.sku === childRot.article.sku);
let blauProd = prods.find((p) => p.sku === childBlau.article.sku);
console.log('Parent product:', {
sku: parentProd.sku,
is_parent: parentProd.is_parent,
parent: parentProd.parent,
variants: parentProd.variants,
options: parentProd.options,
});
assert.equal(parentProd.is_parent, '1');
assert.equal(parentProd.parent, '0');
assert.equal(parentProd.variants, '');
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Blau' }]);
console.log('Child Rot:', {
sku: rotProd.sku,
is_parent: rotProd.is_parent,
parent: rotProd.parent,
variants: rotProd.variants,
sort: rotProd.sort,
options: rotProd.options,
});
assert.equal(rotProd.is_parent, '0');
assert.equal(rotProd.parent, String(parentProd._id));
assert.equal(rotProd.variants, 'Rot');
assert.equal(rotProd.sort, '0');
assert.equal(rotProd.options, null);
console.log('Child Blau:', {
sku: blauProd.sku,
is_parent: blauProd.is_parent,
parent: blauProd.parent,
variants: blauProd.variants,
sort: blauProd.sort,
options: blauProd.options,
});
assert.equal(blauProd.is_parent, '0');
assert.equal(blauProd.parent, String(parentProd._id));
assert.equal(blauProd.variants, 'Blau');
assert.equal(blauProd.sort, '1');
assert.equal(blauProd.options, null);
// 7. Add third child "Grün" and verify options list update
console.log('Adding third child "Grün"...');
const childGruen = await store.article.addChild(googleAuthId, tShirt.sku, {
value: 'Grün',
name: 'T-Shirt Basic - Grün',
});
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
parentProd = prods.find((p) => p.sku === 'TSHIRT');
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Blau,Grün' }]);
console.log('Updated parent options:', parentProd.options);
// 8. Remove child "Blau" and verify options list update
console.log('Removing child "Blau"...');
await store.article.removeChild(googleAuthId, tShirt.sku, childBlau.article.sku, { deleteArticle: true });
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
parentProd = prods.find((p) => p.sku === 'TSHIRT');
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Grün' }]);
console.log('Parent options after removal:', parentProd.options);
console.log('✅ ALL FATHER/CHILD VARIANT TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -1,35 +1,21 @@
import sharp from 'sharp'; import { optimizeImage } from 'wasm-image-optimization';
/** /**
* Creates sample PNG image buffer. * Creates sample PNG image buffer.
*/ */
async function makeSampleImage(width = 200, height = 200, color = { r: 50, g: 120, b: 220 }) { async function makeSampleImage(width = 200, height = 200, color = { r: 50, g: 120, b: 220 }) {
return sharp({ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="${width}" height="${height}" fill="rgb(${color.r},${color.g},${color.b})"/></svg>`;
create: { const res = await optimizeImage({ image: svg, format: 'png' });
width, return Buffer.from(res.data);
height,
channels: 4,
background: { ...color, alpha: 1 },
},
})
.png()
.toBuffer();
} }
/** /**
* Creates sample AVIF thumbnail buffer. * Creates sample AVIF thumbnail buffer.
*/ */
async function makeSampleThumb(width = 64, height = 64) { async function makeSampleThumb(width = 64, height = 64) {
return sharp({ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><rect width="${width}" height="${height}" fill="rgb(200,60,60)"/></svg>`;
create: { const res = await optimizeImage({ image: svg, format: 'avif' });
width, return Buffer.from(res.data);
height,
channels: 4,
background: { r: 200, g: 60, b: 60, alpha: 1 },
},
})
.avif()
.toBuffer();
} }
/** /**

54
scripts/downgrade.js Normal file
View File

@@ -0,0 +1,54 @@
/**
* Downgrades the database schema to a target version.
*
* npm run downgrade -- <targetVersion>
*
* Provider from env: DB_PROVIDER=sqlite (+ SQLITE_PATH) or mssql (MSSQL_*, default).
* Runs src/providers/<provider>/downgrade/<from>_to_<to>.js one step at a time,
* each in its own transaction. Not part of the server — the server only upgrades.
* Note: starting a server whose code knows a higher version upgrades again.
*/
import 'dotenv/config';
const [rawTarget] = process.argv.slice(2);
const target = Number(rawTarget);
if (!rawTarget || !Number.isInteger(target) || target < 1) {
console.error('Usage: npm run downgrade -- <targetVersion>');
process.exit(1);
}
const providerName = process.env.DB_PROVIDER || (process.env.SQLITE_PATH ? 'sqlite' : 'mssql');
let provider;
if (providerName === 'sqlite' || providerName === 'better-sqlite3') {
if (!process.env.SQLITE_PATH) {
console.error('SQLITE_PATH is required for DB_PROVIDER=sqlite');
process.exit(1);
}
const { BetterSqlite3Provider } = await import('../src/providers/sqlite3/index.js');
provider = new BetterSqlite3Provider({ filename: process.env.SQLITE_PATH, fileMustExist: true });
} else if (providerName === 'mssql') {
const { MssqlProvider } = await import('../src/providers/mssql/index.js');
provider = new MssqlProvider({
server: process.env.MSSQL_SERVER,
database: process.env.MSSQL_DATABASE,
user: process.env.MSSQL_USER,
password: process.env.MSSQL_PASSWORD,
options: { encrypt: false, trustServerCertificate: true },
});
} else {
console.error(`Unknown DB_PROVIDER: ${providerName}`);
process.exit(1);
}
try {
const before = await provider.getSchemaVersion();
const steps = await provider.downgradeSchema(target);
console.log(`Schema ${providerName}: ${before} -> ${target} (${steps} step(s))`);
} catch (err) {
console.error(`Downgrade failed: ${err.message}`);
process.exitCode = 1;
} finally {
await provider.close();
}

View File

@@ -1,112 +0,0 @@
import 'dotenv/config';
import Database from 'better-sqlite3';
import { MssqlProvider } from '../src/providers/mssql/index.js';
async function migrateSqlite(sqlitePath) {
console.log(`Migrating SQLite database at: ${sqlitePath}`);
const db = new Database(sqlitePath);
const columns = db.pragma('table_info("manifest")');
const hasCreatedAt = columns.some((c) => c.name === 'created_at');
if (hasCreatedAt) {
console.log('SQLite manifest already has created_at column.');
db.close();
return;
}
db.pragma('foreign_keys = OFF');
db.exec(`
BEGIN TRANSACTION;
CREATE TABLE manifest_new (
id INTEGER PRIMARY KEY,
company_id INTEGER NOT NULL,
"number" TEXT NOT NULL,
"type" TEXT NOT NULL,
entity_id INTEGER,
external_number TEXT,
"date" TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
);
INSERT INTO manifest_new (id, company_id, "number", "type", entity_id, external_number, "date", created_at)
SELECT id, company_id, "number", "type", entity_id, external_number, "date", "date"
FROM manifest;
DROP TABLE manifest;
ALTER TABLE manifest_new RENAME TO manifest;
COMMIT;
`);
db.pragma('foreign_keys = ON');
const fkErrors = db.pragma('foreign_key_check');
if (fkErrors.length > 0) {
console.error('Foreign key check failed after SQLite migration:', fkErrors);
} else {
console.log('SQLite migration completed successfully.');
}
console.log('Updated SQLite manifest columns:', db.pragma('table_info("manifest")'));
console.log('Updated SQLite manifest sample row:', db.prepare('SELECT * FROM manifest LIMIT 1').get());
db.close();
}
async function migrateMssql() {
console.log('Migrating MSSQL database...');
const config = {
server: process.env.MSSQL_SERVER,
database: process.env.MSSQL_DATABASE,
user: process.env.MSSQL_USER,
password: process.env.MSSQL_PASSWORD,
options: {
encrypt: false,
trustServerCertificate: true,
},
};
const provider = new MssqlProvider(config);
try {
const pool = await provider.ensurePool();
const checkRes = await pool.request().query(`
SELECT * FROM sys.columns
WHERE object_id = OBJECT_ID(N'manifest') AND name = 'created_at'
`);
if (checkRes.recordset.length > 0) {
console.log('MSSQL manifest already has created_at column.');
} else {
await pool.request().query(`
ALTER TABLE manifest ADD created_at DATETIME2 NOT NULL CONSTRAINT DF_manifest_created_at DEFAULT SYSUTCDATETIME();
`);
await pool.request().query(`
UPDATE manifest SET created_at = [date];
`);
console.log('MSSQL migration completed successfully.');
}
const colsRes = await pool.request().query(`
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'manifest'
`);
console.log('Updated MSSQL manifest columns:', colsRes.recordset);
const sampleRes = await pool.request().query(`
SELECT TOP 1 * FROM manifest
`);
console.log('Updated MSSQL manifest sample row:', sampleRes.recordset[0]);
await provider.close();
} catch (err) {
console.error('MSSQL migration failed:', err);
await provider.close();
throw err;
}
}
async function run() {
await migrateSqlite('/home/seb/src/dashback/data/dash.db');
await migrateMssql();
}
run().catch((err) => {
console.error('Migration failed:', err);
process.exit(1);
});

View File

@@ -1,105 +0,0 @@
import 'dotenv/config';
import Database from 'better-sqlite3';
import { MssqlProvider } from '../src/providers/mssql/index.js';
async function migrateSqlite(sqlitePath) {
console.log(`Migrating SQLite database at: ${sqlitePath}`);
const db = new Database(sqlitePath);
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'printer'").get();
if (!row) {
console.log('Printer table does not exist in SQLite.');
db.close();
return;
}
if (row.sql.includes("'ipp'")) {
console.log("SQLite printer table already includes 'ipp' in CHECK constraint.");
db.close();
return;
}
db.pragma('foreign_keys = OFF');
db.exec(`
BEGIN TRANSACTION;
CREATE TABLE printer_new (
id INTEGER PRIMARY KEY,
company_id INTEGER NOT NULL,
address TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('esc/pos', 'zpl', 'ipp')),
model TEXT,
serial TEXT,
sort INTEGER NOT NULL DEFAULT 0,
"desc" TEXT,
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
INSERT INTO printer_new (id, company_id, address, type, model, serial, sort, "desc")
SELECT id, company_id, address, type, model, serial, sort, "desc"
FROM printer;
DROP TABLE printer;
ALTER TABLE printer_new RENAME TO printer;
COMMIT;
`);
db.pragma('foreign_keys = ON');
const fkErrors = db.pragma('foreign_key_check');
if (fkErrors.length > 0) {
console.error('Foreign key check failed after SQLite migration:', fkErrors);
} else {
console.log('SQLite migration completed successfully.');
}
const updatedRow = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'printer'").get();
console.log('Updated SQLite printer table SQL:', updatedRow?.sql);
db.close();
}
async function migrateMssql() {
console.log('Migrating MSSQL database...');
const config = {
server: process.env.MSSQL_SERVER,
database: process.env.MSSQL_DATABASE,
user: process.env.MSSQL_USER,
password: process.env.MSSQL_PASSWORD,
options: {
encrypt: false,
trustServerCertificate: true,
},
};
try {
const provider = new MssqlProvider(config);
const pool = await provider.ensurePool();
const tableRes = await pool.request().query(`
SELECT * FROM sys.tables WHERE name = 'printer'
`);
if (tableRes.recordset.length === 0) {
console.log('Printer table does not exist in MSSQL.');
await provider.close();
return;
}
// Drop old CK_printer_type constraint if exists and add new one
await pool.request().query(`
IF EXISTS (SELECT * FROM sys.check_constraints WHERE name = 'CK_printer_type')
BEGIN
ALTER TABLE printer DROP CONSTRAINT CK_printer_type;
END
ALTER TABLE printer ADD CONSTRAINT CK_printer_type CHECK (type IN ('esc/pos', 'zpl', 'ipp'));
`);
console.log('MSSQL migration completed successfully.');
await provider.close();
} catch (err) {
console.warn('MSSQL migration skipped/failed (possibly server not running):', err.message);
}
}
async function run() {
await migrateSqlite('/home/seb/src/dashback/data/dash.db');
await migrateMssql();
}
run().catch((err) => {
console.error('Migration failed:', err);
process.exit(1);
});

View File

@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs'; import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import sharp from 'sharp'; import { optimizeImage } from 'wasm-image-optimization';
import LibDashStore from '../src/index.js'; import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
@@ -42,16 +42,9 @@ async function run() {
assert.ok(val.id > 0); assert.ok(val.id > 0);
// Create a 100x100 test image // Create a 100x100 test image
const imageBuffer = await sharp({ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="rgb(25,25,112)"/></svg>`;
create: { const encoded = await optimizeImage({ image: svg, format: 'avif' });
width: 100, const imageBuffer = Buffer.from(encoded.data);
height: 100,
channels: 4,
background: { r: 25, g: 25, b: 112, alpha: 1 },
},
})
.avif()
.toBuffer();
console.log('1. Adding image to attribute value...'); console.log('1. Adding image to attribute value...');
const addResult = await store.attributevalue.addImage(googleAuthId, val.id, imageBuffer); const addResult = await store.attributevalue.addImage(googleAuthId, val.id, imageBuffer);

142
scripts/test-carrier.js Normal file
View File

@@ -0,0 +1,142 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing Carrier Store API ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-carrier-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('carrier-test@example.com');
const company = await store.company.create({
name: 'Carrier Test Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// 1. Create carrier with defaults
const c1 = await store.carrier.create(googleAuthId, { name: 'DHL' });
assert.equal(c1.name, 'DHL');
assert.equal(c1.sort, 0);
assert.equal(c1.hasPackages, false);
assert.equal(c1.isReverse, false);
console.log('PASS: Carrier 1 created with defaults');
// 2. Create carrier with full flags
const c2 = await store.carrier.create(googleAuthId, {
name: 'UPS',
sort: 5,
hasPackages: true,
isReverse: true,
});
assert.equal(c2.name, 'UPS');
assert.equal(c2.sort, 5);
assert.equal(c2.hasPackages, true);
assert.equal(c2.isReverse, true);
console.log('PASS: Carrier 2 created with custom flags');
// 3. Auto-increment sort
const c3 = await store.carrier.create(googleAuthId, {
name: 'DPD',
hasPackages: true,
});
assert.equal(c3.name, 'DPD');
assert.equal(c3.sort, 6);
assert.equal(c3.hasPackages, true);
assert.equal(c3.isReverse, false);
console.log('PASS: Carrier 3 auto-incremented sort');
// 4. Duplicate name validation
await assert.rejects(
() => store.carrier.create(googleAuthId, { name: 'DHL' }),
{ message: 'Carrier already exists' },
);
console.log('PASS: Duplicate carrier rejected');
// 5. Empty name validation
await assert.rejects(
() => store.carrier.create(googleAuthId, { name: ' ' }),
{ message: 'Name required' },
);
console.log('PASS: Empty carrier name rejected');
// 6. getById
const fetched = await store.carrier.getById(googleAuthId, c2.id);
assert.deepEqual(fetched, c2);
const notFound = await store.carrier.getById(googleAuthId, 9999);
assert.equal(notFound, undefined);
console.log('PASS: getById fetched carrier correctly');
// 7. list
const list = await store.carrier.list(googleAuthId);
assert.equal(list.length, 3);
assert.equal(list[0].name, 'DHL');
assert.equal(list[1].name, 'UPS');
assert.equal(list[2].name, 'DPD');
console.log('PASS: list returned sorted carriers');
// 8. update
const updated = await store.carrier.update(googleAuthId, c1.id, {
name: 'DHL Express',
hasPackages: true,
isReverse: false,
});
assert.equal(updated.name, 'DHL Express');
assert.equal(updated.hasPackages, true);
console.log('PASS: update modified carrier');
// 9. reorder
const ordered = await store.carrier.order(googleAuthId, {
carrierIds: [c3.id, c2.id, c1.id],
});
assert.deepEqual(ordered.carrierIds, [c3.id, c2.id, c1.id]);
const listAfterOrder = await store.carrier.list(googleAuthId);
assert.equal(listAfterOrder[0].id, c3.id);
assert.equal(listAfterOrder[0].sort, 0);
assert.equal(listAfterOrder[1].id, c2.id);
assert.equal(listAfterOrder[1].sort, 1);
assert.equal(listAfterOrder[2].id, c1.id);
assert.equal(listAfterOrder[2].sort, 2);
console.log('PASS: order successfully reordered carriers');
// 10. delete
await store.carrier.delete(googleAuthId, c3.id);
const listAfterDelete = await store.carrier.list(googleAuthId);
assert.equal(listAfterDelete.length, 2);
console.log('PASS: delete removed carrier');
await assert.rejects(
() => store.carrier.delete(googleAuthId, c3.id),
{ message: 'Carrier not found' },
);
console.log('PASS: delete non-existent carrier rejected');
console.log('--- ALL CARRIER TESTS PASSED! ---');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -127,6 +127,11 @@ async function run() {
name: 'POS Register 1', name: 'POS Register 1',
pairingpin: '1234', pairingpin: '1234',
}); });
await store.posterminal.pair(posTerminal.id, {
token: 'pos-token-1',
clientserial: 'SN1',
pairedAt: new Date().toISOString(),
});
await store.posterminal.setOn(googleAuthId, posTerminal.id, subCat2.id); await store.posterminal.setOn(googleAuthId, posTerminal.id, subCat2.id);
await store.posterminal.setOn(googleAuthId, posTerminal.id, outsideCat.id); await store.posterminal.setOn(googleAuthId, posTerminal.id, outsideCat.id);
@@ -169,8 +174,8 @@ async function run() {
() => store.article.getBySku(googleAuthId, artOnlySub.sku), () => store.article.getBySku(googleAuthId, artOnlySub.sku),
/Article not found/ /Article not found/
); );
// tombstone for artOnlySub exists // obituary for artOnlySub exists
const t1 = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(artOnlySub.sku); const t1 = provider.db.prepare("SELECT * FROM posterminalobituary WHERE posterminal_id = ? AND entity_type = 'article' AND entity_id = ?").all(posTerminal.id, artOnlySub.id);
assert.equal(t1.length, 1); assert.equal(t1.length, 1);
// artParent and its child artChild: both deleted // artParent and its child artChild: both deleted
@@ -182,8 +187,8 @@ async function run() {
() => store.article.getBySku(googleAuthId, artChild.article.sku), () => store.article.getBySku(googleAuthId, artChild.article.sku),
/Article not found/ /Article not found/
); );
const tParent = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(artParent.sku); const tParent = provider.db.prepare("SELECT * FROM posterminalobituary WHERE posterminal_id = ? AND entity_type = 'article' AND entity_id = ?").all(posTerminal.id, artParent.id);
const tChild = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(artChild.article.sku); const tChild = provider.db.prepare("SELECT * FROM posterminalobituary WHERE posterminal_id = ? AND entity_type = 'article' AND entity_id = ?").all(posTerminal.id, artChild.article.id);
assert.equal(tParent.length, 1); assert.equal(tParent.length, 1);
assert.equal(tChild.length, 1); assert.equal(tChild.length, 1);

View File

@@ -0,0 +1,92 @@
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function open(tempDir) {
const provider = new BetterSqlite3Provider({ filename: join(tempDir, 'test.db'), storageDir: join(tempDir, 'storage') });
await provider.initialize();
return { provider, store: await new LibDashStore(provider) };
}
const tempDir = mkdtempSync(join(tmpdir(), 'test-cu-roles-'));
try {
console.log('--- Testing companyuser roles + autoinvite ---');
let { provider, store } = await open(tempDir);
const adminId = await store.google_auth.add('admin@example.com');
const userId = await store.google_auth.add('user@example.com');
const company = await store.company.create({ name: 'Roles Corp', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId: adminId });
await store.google_auth.setSelectedCompany(adminId, company.id);
// creator is admin
let members = await store.companyuser.list(adminId);
assert.deepEqual(members.map((m) => [m.email, m.role]), [['admin@example.com', 'admin']]);
assert.equal((await store.company.list(adminId))[0].role, 'admin');
// non-member cannot select the company
await assert.rejects(store.google_auth.setSelectedCompany(userId, company.id), /Not authorized/);
// admin adds existing user
await assert.rejects(store.companyuser.addByEmail(adminId, { email: 'nobody@example.com' }), /User not found/);
await assert.rejects(store.companyuser.addByEmail(adminId, { email: 'user@example.com', role: 'root' }), /Invalid role/);
const added = await store.companyuser.addByEmail(adminId, { email: 'User@Example.com' });
assert.equal(added.role, 'user');
await assert.rejects(store.companyuser.addByEmail(adminId, { email: 'user@example.com' }), /already in company/);
// user can now select it, but is not admin
await store.google_auth.setSelectedCompany(userId, company.id);
await assert.rejects(store.companyuser.list(userId), /Not authorized/);
await assert.rejects(store.companyuser.addByEmail(userId, { email: 'admin@example.com' }), /Not authorized/);
await assert.rejects(store.companyuser.setRole(userId, added.id, 'admin'), /Not authorized/);
await assert.rejects(store.companyuser.remove(userId, added.id), /Not authorized/);
await assert.rejects(store.autoinvite.add(userId, { email: 'x@example.com' }), /Not authorized/);
// last admin guard
const adminCu = members[0].id;
await assert.rejects(store.companyuser.setRole(adminId, adminCu, 'user'), /last admin/);
await assert.rejects(store.companyuser.remove(adminId, adminCu), /last admin/);
await store.companyuser.setRole(adminId, added.id, 'admin');
await store.companyuser.setRole(adminId, adminCu, 'user');
await store.companyuser.setRole(userId, adminCu, 'admin');
await store.companyuser.setRole(userId, added.id, 'user');
await assert.rejects(store.companyuser.setRole(adminId, 99999, 'user'), /Company user not found/);
// autoinvite
await assert.rejects(store.autoinvite.add(adminId, { email: 'user@example.com' }), /already registered/);
await assert.rejects(store.autoinvite.add(adminId, { email: 'bad' }), /Invalid email/);
await store.autoinvite.add(adminId, { email: 'New@Example.com', role: 'admin' });
await assert.rejects(store.autoinvite.add(adminId, { email: 'new@example.com' }), /already exists/);
await store.autoinvite.add(adminId, { email: 'gone@example.com' });
assert.equal((await store.autoinvite.list(adminId)).length, 2);
await store.autoinvite.remove(adminId, (await store.autoinvite.list(adminId)).find((i) => i.email === 'gone@example.com').id);
await assert.rejects(store.autoinvite.remove(adminId, 99999), /Invitation not found/);
const newId = await store.google_auth.add('new@example.com');
assert.deepEqual(await store.autoinvite.list(adminId), []);
await store.google_auth.setSelectedCompany(newId, company.id);
members = await store.companyuser.list(adminId);
assert.equal(members.find((m) => m.email === 'new@example.com').role, 'admin');
// remove clears selection
const userCu = members.find((m) => m.email === 'user@example.com');
await store.companyuser.remove(adminId, userCu.id);
assert.equal((await store.google_auth.getById(userId)).selectedCompanyId, null);
// downgrade → upgrade through the migration engine
await provider.close?.();
const env = { ...process.env, DB_PROVIDER: 'sqlite', SQLITE_PATH: join(tempDir, 'test.db') };
execFileSync('node', ['scripts/downgrade.js', '5'], { env, stdio: 'inherit' });
({ provider, store } = await open(tempDir).catch((e) => { throw e; }));
// opening upgrades to latest again; roles reset to admin for all members
const rows = (await provider.query('SELECT role FROM companyuser')).rows;
assert.ok(rows.length > 0 && rows.every((r) => r.role === 'admin'));
assert.deepEqual((await provider.query('SELECT * FROM autoinvite')).rows, []);
await provider.close?.();
console.log('OK');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}

View File

@@ -0,0 +1,145 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing Delivery Link Check & Sync in dashdbschema ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-delivery-link-check-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('link-check-test@example.com');
const company = await store.company.create({
name: 'Link Check Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
const supplier = await store.supplier.create(googleAuthId, {
name: 'eks',
});
const art = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Club Mate 0,5l',
track_stock: 1,
});
// Assign supplier with default pu=1, without EK
await store.articlesupplier.create(googleAuthId, art.sku, {
entityId: supplier.id,
sku: 'mate',
pu: 1,
});
let asup = await store.articlesupplier.listByArticleSku(googleAuthId, art.sku);
assert.equal(asup.length, 1);
assert.equal(asup[0].pu, 1);
assert.equal(asup[0].price, null);
// Create delivery with manifest line: sku: 'mate', pu: 20, net: 12
const delivery = await store.delivery.create(googleAuthId, {
date: '2026-10-25T10:00:00Z',
externalNumber: 'LS-000001',
supplierId: supplier.id,
lines: [
{ sku: 'mate', name: 'Club Mate 0,5l', pu: 20, amount: 2, net: 12, taxrate: 19 },
],
});
assert(delivery.id > 0);
assert.equal(delivery.lines[0].pu, 20);
// Verify articlesupplier link was updated!
asup = await store.articlesupplier.listByArticleSku(googleAuthId, art.sku);
assert.equal(asup.length, 1);
assert.equal(asup[0].pu, 20, 'VPE in articlesupplier must be synced to 20');
assert.equal(asup[0].price.net, 12, 'Net price in articlesupplier must be synced to 12');
assert.equal(asup[0].price.taxRate, 19, 'Tax rate in articlesupplier must be synced to 19');
// Verify article history
const history = await store.article.getHistoryBySku(googleAuthId, art.sku);
const historyTexts = (Array.isArray(history) ? history : history.history).map((h) => h.text);
console.log('Article history entries:', historyTexts);
assert(
historyTexts.some((t) => t.includes('Lieferant "eks" aktualisiert (VPE: 20, EK: € 12,00, MwSt.: 19%)')),
'Must record history entry for supplier update with VPE, EK, and MwSt'
);
// Now update delivery with new line pu: 24, net: 11.50, and taxrate: 7
await store.delivery.update(googleAuthId, delivery.id, {
lines: [
{ sku: 'mate', name: 'Club Mate 0,5l', pu: 24, amount: 3, net: 11.5, taxrate: 7 },
],
});
asup = await store.articlesupplier.listByArticleSku(googleAuthId, art.sku);
assert.equal(asup[0].pu, 24, 'VPE in articlesupplier must be synced to 24 on update');
assert.equal(asup[0].price.net, 11.5, 'Net price in articlesupplier must be synced to 11.5 on update');
assert.equal(asup[0].price.taxRate, 7, 'Tax rate in articlesupplier must be synced to 7 on update');
const historyAfterUpdate = await store.article.getHistoryBySku(googleAuthId, art.sku);
const historyAfterTexts = (Array.isArray(historyAfterUpdate) ? historyAfterUpdate : historyAfterUpdate.history).map((h) => h.text);
console.log('Article history entries after delivery update:', historyAfterTexts);
assert(
historyAfterTexts.some((t) => t.includes('Lieferant "eks" aktualisiert (VPE: 24, EK: € 11,50, MwSt.: 7%)')),
'Must record history entry for supplier update on delivery update'
);
// Now test booking the delivery with amount: 3, pu: 24
// Should add 3 * 24 = 72 to stock and record clear history!
await store.delivery.book(googleAuthId, delivery.id);
const artDetail = await store.article.getBySku(googleAuthId, art.sku);
const totalStock = (artDetail.article.warehouses || []).reduce((acc, w) => acc + (w.stock || 0), 0);
console.log('Article stock after booking delivery with amount 3, pu 24:', totalStock);
assert.equal(totalStock, 72, 'Physical warehouse stock must be amount * pu (3 * 24 = 72)');
const historyAfterBooking = await store.article.getHistoryBySku(googleAuthId, art.sku);
const bookingTexts = (Array.isArray(historyAfterBooking) ? historyAfterBooking : historyAfterBooking.history).map((h) => h.text);
console.log('Article history entries after delivery booking:', bookingTexts);
assert(
bookingTexts.some((t) => t.includes('Lager: "Hauptlager"') && t.includes('Menge: 72 (3 x VPE 24)')),
'Warehouse history must clearly show Menge: 72 (3 x VPE 24)'
);
assert(
bookingTexts.some((t) => t.includes('Lieferant: "eks"') && t.includes('Menge: 3, VPE: 24')),
'Acceptance history must clearly show Menge: 3, VPE: 24'
);
console.log('✓ Delivery link check and sync passed successfully!');
} finally {
try {
provider.close();
rmSync(tempDir, { recursive: true, force: true });
} catch {
// ignore cleanup errors
}
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,231 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing Delivery Summary, Ledger Sync & Filtering in dashdbschema ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-delivery-summary-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('delivery-summary-test@example.com');
const company = await store.company.create({
name: 'Delivery Summary Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
const warehouse = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
await store.unit.upsert(googleAuthId, {
si: 'stk',
basesi: 'stk',
basesifactor: 1,
symbol: 'Stk',
name: 'Stück',
});
const supplierAlpha = await store.supplier.create(googleAuthId, {
name: 'Alpha Supplier GmbH',
});
const supplierBeta = await store.supplier.create(googleAuthId, {
name: 'Beta Logistics Inc',
});
// 1. Create articles:
// Art1: track_stock = 1 (Stock item)
const artStock1 = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Stock Item 1',
track_stock: 1,
});
// Art2: track_stock = 1 (Stock item)
const artStock2 = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Stock Item 2',
track_stock: 1,
});
// Art3: track_stock = 0 (Service / Non-stock item)
const artService = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Shipping Freight Fee',
track_stock: 0,
});
// 2. Create Delivery 1:
// Has 2 lines: artStock1 (qty 10), artService (qty 1)
const delivery1 = await store.delivery.create(googleAuthId, {
date: '2026-08-01T10:00:00Z',
externalNumber: 'DEL-ALPHA-1001',
supplierId: supplierAlpha.id,
lines: [
{ sku: artStock1.sku, name: 'Stock Item 1', amount: 10, net: 50, taxrate: 19 },
{ sku: artService.sku, name: 'Shipping Freight Fee', amount: 1, net: 10, taxrate: 19 },
],
});
console.log('Delivery 1 created with id:', delivery1.id);
// Verify deliverysummary row for delivery 1:
// positions = 2, total_quantity = 11, stock_positions = 1, booked_positions = 0, unbooked_positions = 1, is_booked = 0
let sum1 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery1.id);
assert(sum1, 'deliverysummary row for delivery 1 must exist');
assert.equal(sum1.positions, 2, 'Total positions should be 2');
assert.equal(sum1.stock_positions, 1, 'Stock positions should be 1');
assert.equal(sum1.booked_positions, 0, 'Booked positions should initially be 0');
assert.equal(sum1.unbooked_positions, 1, 'Unbooked positions should initially be 1');
assert.equal(sum1.stock_quantity, 10, 'Stock quantity should be 10');
assert.equal(sum1.booked_quantity, 0, 'Booked quantity should initially be 0');
assert.equal(sum1.unbooked_quantity, 10, 'Unbooked quantity should initially be 10');
assert.equal(sum1.is_booked, 0, 'is_booked should be 0');
assert.equal(sum1.supplier_name, 'Alpha Supplier GmbH');
// 3. Create Delivery 2:
// Has 1 line: artStock2 (qty 5), Supplier: Beta
const delivery2 = await store.delivery.create(googleAuthId, {
date: '2026-10-01T10:00:00Z',
externalNumber: 'DEL-BETA-2002',
supplierId: supplierBeta.id,
lines: [
{ sku: artStock2.sku, name: 'Stock Item 2', amount: 5, net: 20, taxrate: 19 },
],
});
let sum2 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery2.id);
assert(sum2, 'deliverysummary row for delivery 2 must exist');
assert.equal(sum2.stock_positions, 1);
assert.equal(sum2.unbooked_positions, 1);
assert.equal(sum2.is_booked, 0);
// 4. Book Delivery 1 partially:
// Book 4 units of artStock1 in warehouse
const lineStock1 = delivery1.lines.find((l) => l.sku === artStock1.sku);
assert(lineStock1, 'lineStock1 must exist');
await store.ledger.add(googleAuthId, {
manifestarticle_id: lineStock1.id,
warehouse_id: warehouse.id,
transaction_type: 'DELIVERY',
amount: 4,
transaction_text: 'Partial goods receipt',
});
sum1 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery1.id);
assert.equal(sum1.booked_positions, 0, 'Booked positions should still be 0 since 4 < 10');
assert.equal(sum1.unbooked_positions, 1);
assert.equal(sum1.booked_quantity, 4);
assert.equal(sum1.unbooked_quantity, 6);
assert.equal(sum1.is_booked, 0);
console.log('✓ Partial booking properly updated deliverysummary');
// 5. Book remaining 6 units of artStock1
await store.ledger.add(googleAuthId, {
manifestarticle_id: lineStock1.id,
warehouse_id: warehouse.id,
transaction_type: 'DELIVERY',
amount: 6,
transaction_text: 'Final goods receipt',
});
sum1 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery1.id);
assert.equal(sum1.booked_positions, 1, 'Booked positions should now be 1');
assert.equal(sum1.unbooked_positions, 0, 'Unbooked positions should now be 0');
assert.equal(sum1.booked_quantity, 10);
assert.equal(sum1.unbooked_quantity, 0);
assert.equal(sum1.is_booked, 1, 'Delivery 1 should now be fully booked (is_booked = 1)');
console.log('✓ Full booking properly updated deliverysummary (is_booked = 1)');
// 6. Test windowed / paged list:
// Both deliveries present
const pagedAll = await store.delivery.list(googleAuthId, { offset: 0, limit: 10 });
assert.equal(pagedAll.total, 2);
assert.equal(pagedAll.unfilteredTotal, 2);
assert.equal(pagedAll.deliveries.length, 2);
assert.equal(pagedAll.offset, 0);
assert.equal(pagedAll.limit, 10);
// Filter onlyUnbooked (Delivery 1 is fully booked, Delivery 2 is unbooked)
const pagedUnbooked = await store.delivery.list(googleAuthId, { onlyUnbooked: true });
assert.equal(pagedUnbooked.total, 1, 'Only 1 delivery is unbooked');
assert.equal(pagedUnbooked.unfilteredTotal, 2, 'Unfiltered total remains 2');
assert.equal(pagedUnbooked.deliveries[0].id, delivery2.id);
console.log('✓ onlyUnbooked filter returned only unbooked deliveries');
// Search filter: 'Beta'
const searchBeta = await store.delivery.list(googleAuthId, { search: 'Beta' });
assert.equal(searchBeta.total, 1);
assert.equal(searchBeta.unfilteredTotal, 2);
assert.equal(searchBeta.deliveries[0].id, delivery2.id);
// Search filter: 'ALPHA-1001'
const searchNum = await store.delivery.list(googleAuthId, { search: 'ALPHA-1001' });
assert.equal(searchNum.total, 1);
assert.equal(searchNum.deliveries[0].id, delivery1.id);
// Test unpaged backwards compatibility
const unpaged = await store.delivery.list(googleAuthId);
assert(Array.isArray(unpaged), 'Unpaged list should return Array');
assert.equal(unpaged.length, 2);
const d1 = unpaged.find((d) => d.id === delivery1.id);
assert(d1.ledger, 'd1 should contain ledger summary object');
assert.equal(d1.ledger.totalPositions, 1);
assert.equal(d1.ledger.bookedPositions, 1);
assert.equal(d1.ledger.unbookedPositions, 0);
assert.equal(d1.ledger.isBooked, true);
const d2 = unpaged.find((d) => d.id === delivery2.id);
assert.equal(d2.ledger.unbookedPositions, 1);
assert.equal(d2.ledger.isBooked, false);
console.log('✓ Unpaged list returns enriched deliveries with ledger summary');
// 7. Test rebuildSummaries
provider.db.prepare('DELETE FROM deliverysummary').run();
const countEmpty = provider.db.prepare('SELECT COUNT(*) AS cnt FROM deliverysummary').get().cnt;
assert.equal(countEmpty, 0);
await store.delivery.rebuildSummaries(googleAuthId);
const countRebuilt = provider.db.prepare('SELECT COUNT(*) AS cnt FROM deliverysummary').get().cnt;
assert.equal(countRebuilt, 2, 'Rebuilt deliverysummary should have 2 rows');
const rebuiltD1 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery1.id);
assert.equal(rebuiltD1.is_booked, 1);
assert.equal(rebuiltD1.booked_quantity, 10);
assert.equal(rebuiltD1.unbooked_quantity, 0);
const rebuiltD2 = provider.db.prepare('SELECT * FROM deliverysummary WHERE manifest_id = ?').get(delivery2.id);
assert.equal(rebuiltD2.is_booked, 0);
assert.equal(rebuiltD2.unbooked_positions, 1);
console.log('✓ rebuildSummaries restored all deliverysummary rows accurately');
console.log('--- ALL DELIVERY SUMMARY & FILTER TESTS PASSED ---');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,45 @@
import assert from 'node:assert';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function main() {
console.log('Testing healthcheck store method on in-memory SQLite...');
const provider = new BetterSqlite3Provider({ path: ':memory:' });
await provider.initialize();
const store = await new LibDashStore(provider);
assert(store.healthcheck, 'store.healthcheck should exist');
assert.strictEqual(typeof store.healthcheck.check, 'function', 'store.healthcheck.check should be a function');
// First call: table is empty, should ensure row (0), select for update, increment to 1 and update
const res1 = await store.healthcheck.check();
console.log('Healthcheck call 1 result:', res1);
assert.strictEqual(res1.ok, true, 'res1.ok should be true');
assert.strictEqual(res1.sequence, 1, 'res1.sequence should be 1');
// Second call: row exists with 1, should increment to 2 and update
const res2 = await store.healthcheck.check();
console.log('Healthcheck call 2 result:', res2);
assert.strictEqual(res2.ok, true, 'res2.ok should be true');
assert.strictEqual(res2.sequence, 2, 'res2.sequence should be 2');
// Third call: row exists with 2, should increment to 3 and update
const res3 = await store.healthcheck.check();
console.log('Healthcheck call 3 result:', res3);
assert.strictEqual(res3.ok, true, 'res3.ok should be true');
assert.strictEqual(res3.sequence, 3, 'res3.sequence should be 3');
// Verify DB state directly
const rows = await provider.query('SELECT * FROM healthcheck');
console.log('Healthcheck DB rows:', rows.rows);
assert.strictEqual(rows.rows.length, 1, 'healthcheck should have exactly 1 row');
assert.strictEqual(Number(rows.rows[0].sequence), 3, 'sequence in DB should be 3');
await provider.close();
console.log('✓ All healthcheck store tests passed successfully!');
}
main().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -5,12 +5,10 @@ import { join } from 'node:path';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import LibDashStore from '../src/index.js'; import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
import { INIT_HASH, computePrevHash } from '../src/store/ledger/add.js'; import { computePrevHash, INIT_HASH } from '../src/store/ledger/add.js';
async function run() { async function run() {
console.log('--- Testing store.ledger (add, created_at, list queries) ---'); const tempDir = mkdtempSync(join(tmpdir(), 'dash-ledger-test-'));
const tempDir = mkdtempSync(join(tmpdir(), 'test-ledger-'));
const dbFile = join(tempDir, 'test.db'); const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage'); const storageDir = join(tempDir, 'storage');
@@ -18,42 +16,85 @@ async function run() {
filename: dbFile, filename: dbFile,
storageDir, storageDir,
}); });
await provider.initialize(); await provider.initialize();
const store = await new LibDashStore(provider); const store = await new LibDashStore(provider);
try { try {
// 1. Setup user and company console.log('--- Testing store.ledger (acceptance, warehouse, shipment, guards) ---');
const googleAuthId = await store.google_auth.add('ledger-test@example.com'); const googleAuthId = await store.google_auth.add('ledger-tester@example.com');
const company = await store.company.create({ const company = await store.company.create({
name: 'Ledger Test Corp', name: 'Ledger Test GmbH',
iso639: 'de', iso639: 'de',
taxrate: 19, taxrate: 19,
reducedtaxrate: 7, reducedtaxrate: 7,
googleAuthId, googleAuthId,
}); });
await store.google_auth.setSelectedCompany(googleAuthId, company.id); await store.google_auth.setSelectedCompany(googleAuthId, company.id);
await store.unit.upsert(googleAuthId, { const supplier = await store.supplier.create(googleAuthId, {
si: 'stk', name: 'Supplier X',
basesi: 'stk',
basesifactor: 1,
symbol: 'Stk',
name: 'Stück',
}); });
const supplier = await store.supplier.create(googleAuthId, { name: 'Supplier X' });
// Create an article with track_stock = false initially
const article = await store.article.createUnderParent({ const article = await store.article.createUnderParent({
googleAuthId, googleAuthId,
parentId: company.rootCategoryId, parentId: company.rootCategoryId,
name: 'Ledger Linked Article', name: 'Ledger Linked Article',
track_stock: false,
}); });
assert.equal(article.track_stock, false, 'Newly created article must have track_stock disabled');
await store.articlesupplier.create(googleAuthId, article.sku, { await store.articlesupplier.create(googleAuthId, article.sku, {
entityId: supplier.id, entityId: supplier.id,
sku: 'SKU-ABC', sku: 'SKU-ABC',
pu: 1, pu: 1,
}); });
// Create a delivery manifest with lines const customer1 = await store.entity.create(googleAuthId, {
name: 'Customer Quote Test',
isCustomer: true,
customergroupId: company.rootCategoryId,
});
// Create a quote and verify ledger bookings are rejected
const quote = await store.quote.create(googleAuthId, {
number: 'Q-1',
entityId: customer1.id,
date: new Date().toISOString(),
lines: [{ sku: 'SKU-ABC', name: 'Quote item', pu: 1, amount: 2, net: 10, taxrate: 19 }],
});
await assert.rejects(
async () => store.ledger.add(googleAuthId, {
manifestarticle_id: quote.lines[0].id,
transaction_type: 'ACCEPTANCE',
amount: 2,
transaction_text: 'Should fail for quote',
}),
/Cannot book ledger entries for quote/,
'Should reject booking ledger entries for quote manifest',
);
// Create an order and verify ledger bookings are rejected
const order = await store.order.create(googleAuthId, {
number: 'O-1',
entityId: customer1.id,
date: new Date().toISOString(),
lines: [{ sku: 'SKU-ABC', name: 'Order item', pu: 1, amount: 2, net: 10, taxrate: 19 }],
});
await assert.rejects(
async () => store.ledger.add(googleAuthId, {
manifestarticle_id: order.lines[0].id,
transaction_type: 'ACCEPTANCE',
amount: 2,
transaction_text: 'Should fail for order',
}),
/Cannot book ledger entries for order/,
'Should reject booking ledger entries for order manifest',
);
// 1. Create a delivery manifest with lines
const delivery = await store.delivery.create(googleAuthId, { const delivery = await store.delivery.create(googleAuthId, {
supplierId: supplier.id, supplierId: supplier.id,
externalNumber: 'DEL-1', externalNumber: 'DEL-1',
@@ -70,7 +111,7 @@ async function run() {
}, },
{ {
sku: 'SKU-ABC', sku: 'SKU-ABC',
name: 'Ledger Item 1 Batch 2', name: 'Ledger Item 2',
pu: 1, pu: 1,
amount: 10, amount: 10,
net: 20, net: 20,
@@ -82,243 +123,351 @@ async function run() {
const manifestarticleId1 = delivery.lines[0].id; const manifestarticleId1 = delivery.lines[0].id;
const manifestarticleId2 = delivery.lines[1].id; const manifestarticleId2 = delivery.lines[1].id;
assert(manifestarticleId1, 'manifestarticle line 1 should have an id');
assert(manifestarticleId2, 'manifestarticle line 2 should have an id');
// 2. Test first row (genesis row) // 2. Book line 1 for untracked article (acceptance only, warehouse_id is null)
console.log('1. Inserting genesis ledger row...'); console.log('1. Booking acceptance for untracked article line 1...');
const row1 = await store.ledger.add(googleAuthId, { const row1 = await store.ledger.add(googleAuthId, {
manifestarticle_id: manifestarticleId1, manifestarticle_id: manifestarticleId1,
transaction_type: 'GOODS_RECEIPT', transaction_type: 'ACCEPTANCE',
amount: 5, amount: 5,
transaction_text: 'Initial delivery movement', transaction_text: `Lieferung ${delivery.number}`,
}); });
assert.equal(row1.id, 1); assert.equal(row1.id, 1);
assert.equal(row1.manifestarticle_id, manifestarticleId1); assert.equal(row1.manifestarticle_id, manifestarticleId1);
assert.equal(row1.transaction_type, 'GOODS_RECEIPT'); assert.equal(row1.warehouse_id, null);
assert.equal(row1.transaction_type, 'ACCEPTANCE');
assert.equal(row1.amount, 5); assert.equal(row1.amount, 5);
assert.equal(row1.transaction_text, 'Initial delivery movement');
assert.equal(row1.hash_version, 1); assert.equal(row1.hash_version, 1);
assert.equal(row1.prev_hash, INIT_HASH); assert.equal(row1.prev_hash, INIT_HASH);
assert(row1.created_at, 'created_at should exist'); assert(row1.created_at, 'created_at should exist');
assert(!Number.isNaN(new Date(row1.created_at).getTime()), 'created_at should be valid date string');
console.log('✓ Row 1 created_at:', row1.created_at);
// Verify manual hash computation of row 1 (including created_at) // Attempting to book line 1 again must be rejected
const expectedHash2 = computePrevHash(row1); await assert.rejects(
const manualHash2 = createHash('sha512') async () => store.ledger.add(googleAuthId, {
.update(
JSON.stringify({
id: 1,
manifestarticle_id: manifestarticleId1, manifestarticle_id: manifestarticleId1,
warehouse_id: null, transaction_type: 'ACCEPTANCE',
transaction_type: 'GOODS_RECEIPT',
amount: 5, amount: 5,
transaction_text: 'Initial delivery movement', transaction_text: 'Double booking attempt',
hash_version: 1,
prev_hash: INIT_HASH,
created_at: new Date(row1.created_at).toISOString(),
}), }),
'utf8', /Delivery line already booked on ledger/,
) 'Should reject booking an already booked delivery line',
.digest('hex'); );
assert.equal(expectedHash2, manualHash2);
assert.equal(manualHash2.length, 128);
// 3. Test second row (line 1, different transaction type) // 3. Create warehouse and enable tracking on the article
console.log('2. Inserting second ledger row...'); console.log('2. Creating warehouse and enabling stock tracking...');
const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
await store.article.setTrackStockBySku(googleAuthId, article.sku, true);
// Even though tracking is now enabled, line 1 is already accepted so it cannot be booked again
await assert.rejects(
async () => store.ledger.add(googleAuthId, {
manifestarticle_id: manifestarticleId1,
warehouse_id: wh1.id,
transaction_type: 'ACCEPTANCE',
amount: 5,
transaction_text: 'Late booking attempt after tracking enabled',
}),
/Delivery line already booked on ledger/,
'Cannot book a delivery line later once stock tracking is enabled if already accepted',
);
// 4. Book line 2 (now tracked): books ACCEPTANCE (row 2) + WAREHOUSE (row 3) in one transaction
console.log('3. Booking line 2 with tracking enabled (dual entries: ACCEPTANCE + WAREHOUSE)...');
const expectedHash2 = computePrevHash(row1);
const row2 = await store.ledger.add(googleAuthId, { const row2 = await store.ledger.add(googleAuthId, {
manifestarticleId: manifestarticleId1, manifestarticle_id: manifestarticleId2,
transactionType: 'INVENTORY_ADJUST', warehouse_id: wh1.id,
amount: -1, transaction_type: 'ACCEPTANCE',
transactionText: 'Damaged item written off', amount: 10,
transaction_text: `Lieferung ${delivery.number}`,
}); });
assert.equal(row2.id, 2); assert.equal(row2.id, 2);
assert.equal(row2.hash_version, 1); assert.equal(row2.manifestarticle_id, manifestarticleId2);
assert.equal(row2.warehouse_id, null);
assert.equal(row2.transaction_type, 'ACCEPTANCE');
assert.equal(row2.amount, 10);
assert.equal(row2.prev_hash, expectedHash2); assert.equal(row2.prev_hash, expectedHash2);
assert(row2.created_at, 'row 2 created_at should exist');
console.log('✓ Row 2 prev_hash matches SHA-512 of Row 1 with created_at');
// 4. Test third row (line 2, same SKU SKU-ABC) // Query ledger lines for manifestarticle 2 to verify row 3 (WAREHOUSE entry)
console.log('3. Inserting third ledger row on line 2...'); const line2Entries = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId2);
const expectedHash3 = computePrevHash(row2); assert.equal(line2Entries.length, 2, 'Line 2 must have 2 ledger entries (ACCEPTANCE + WAREHOUSE)');
const row3 = await store.ledger.add(googleAuthId, { assert.equal(line2Entries[0].transaction_type, 'ACCEPTANCE');
manifestarticle_id: manifestarticleId2, assert.equal(line2Entries[0].warehouse_id, null);
transaction_type: 'GOODS_RECEIPT', assert.equal(line2Entries[1].transaction_type, 'WAREHOUSE');
amount: 10, assert.equal(line2Entries[1].warehouse_id, wh1.id);
transaction_text: 'Second line goods receipt', assert.equal(line2Entries[1].amount, 10);
}); assert.equal(line2Entries[1].id, 3);
assert.equal(line2Entries[1].prev_hash, computePrevHash(line2Entries[0]));
assert.equal(row3.id, 3); // 5. Check warehouse stock for article
assert.equal(row3.hash_version, 1); console.log('4. Checking warehouse stock calculation...');
assert.equal(row3.prev_hash, expectedHash3); const artDetail = await store.article.getBySku(googleAuthId, article.sku);
const whStock = artDetail.article.warehouses.find((w) => w.id === wh1.id)?.stock;
assert.equal(whStock, 10, 'Warehouse stock must be 10 (from line 2 WAREHOUSE ledger entry only)');
// 5. Test query by manifestarticle_id // 6. Check deliverysummary is_booked
console.log('4. Testing listByManifestarticleId...'); const updatedDelivery = await store.delivery.getById(googleAuthId, delivery.id);
const listLine1All = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId1); assert.equal(updatedDelivery.ledger.isBooked, true, 'Delivery should be fully booked');
assert.equal(listLine1All.length, 2); assert.equal(updatedDelivery.ledger.completelyOnLedgerPositions, 2);
assert.equal(listLine1All[0].id, 1);
assert.equal(listLine1All[1].id, 2);
assert.equal(listLine1All[0].sku, 'SKU-ABC');
const listLine1GoodsReceipt = await store.ledger.listByManifestarticleId( // 7. Check article history
googleAuthId, console.log('5. Verifying article history...');
manifestarticleId1,
'GOODS_RECEIPT',
);
assert.equal(listLine1GoodsReceipt.length, 1);
assert.equal(listLine1GoodsReceipt[0].id, 1);
const listLine1Adjust = await store.ledger.listByManifestarticleId(
googleAuthId,
manifestarticleId1,
{ transaction_type: 'INVENTORY_ADJUST' },
);
assert.equal(listLine1Adjust.length, 1);
assert.equal(listLine1Adjust[0].id, 2);
const listLine1None = await store.ledger.listByManifestarticleId(
googleAuthId,
manifestarticleId1,
'NON_EXISTENT',
);
assert.equal(listLine1None.length, 0);
// 6. Test query by SKU
console.log('5. Testing listBySku...');
const listSkuAll = await store.ledger.listBySku(googleAuthId, 'SKU-ABC');
assert.equal(listSkuAll.length, 3);
assert.equal(listSkuAll[0].id, 1);
assert.equal(listSkuAll[1].id, 2);
assert.equal(listSkuAll[2].id, 3);
const listSkuGoodsReceipt = await store.ledger.listBySku(
googleAuthId,
'SKU-ABC',
'GOODS_RECEIPT',
);
assert.equal(listSkuGoodsReceipt.length, 2);
assert.equal(listSkuGoodsReceipt[0].id, 1);
assert.equal(listSkuGoodsReceipt[1].id, 3);
const listSkuAdjust = await store.ledger.listBySku(
googleAuthId,
'SKU-ABC',
'INVENTORY_ADJUST',
);
assert.equal(listSkuAdjust.length, 1);
assert.equal(listSkuAdjust[0].id, 2);
const listUnknownSku = await store.ledger.listBySku(googleAuthId, 'UNKNOWN-SKU');
assert.equal(listUnknownSku.length, 0);
// 8. Test article history created from ledger
console.log('7. Testing article history entries from ledger rows...');
const history = await store.article.getHistoryBySku(googleAuthId, article.sku); const history = await store.article.getHistoryBySku(googleAuthId, article.sku);
const historyTexts = history.map((h) => h.text); const historyTexts = history.map((h) => h.text);
console.log('Article history texts:', historyTexts); console.log('History entries:', historyTexts);
assert( assert(
historyTexts.some((t) => t.includes('Initial delivery movement') && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 11.90') && t.includes('Menge: 5')), historyTexts.some((t) => t.includes(`Lieferung ${delivery.number}`) && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 11.90') && t.includes('Menge: 5')),
'History should include initial delivery movement with supplier, EK brutto, and amount', 'History must include acceptance of line 1 with Lieferung number, supplier, EK brutto, amount',
); );
assert(historyTexts.includes('Damaged item written off'), 'History should include undamaged adjustment memo as is');
assert( assert(
historyTexts.some((t) => t.includes('Second line goods receipt') && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 23.80') && t.includes('Menge: 10')), historyTexts.some((t) => t.includes(`Lieferung ${delivery.number}`) && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 23.80') && t.includes('Menge: 10')),
'History should include second line goods receipt with details', 'History must include acceptance of line 2',
);
assert(
historyTexts.some((t) => t.includes(`Lieferung ${delivery.number}`) && t.includes('Lager: "Hauptlager"') && t.includes('Menge: 10')),
'History must include warehouse entry for tracked line 2',
); );
// 9. Test warehouse CRUD, sort, reorder, and linking with ledger // 8. Test store.delivery.book on a fresh delivery
console.log('8. Testing warehouse CRUD, sort, reorder, and ledger association...'); console.log('6. Testing store.delivery.book batch booking...');
const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); const delivery2 = await store.delivery.create(googleAuthId, {
assert.equal(wh1.name, 'Hauptlager'); supplierId: supplier.id,
assert.equal(wh1.sort, 0); externalNumber: 'DEL-2',
assert(wh1.id > 0); date: new Date().toISOString(),
lines: [
const wh2 = await store.warehouse.create(googleAuthId, { name: 'Filiale Nord' }); { sku: 'SKU-ABC', name: 'Batch Item 1', pu: 1, amount: 4, net: 15, taxrate: 19 },
assert.equal(wh2.name, 'Filiale Nord'); ],
assert.equal(wh2.sort, 1);
const whList = await store.warehouse.list(googleAuthId);
assert.equal(whList.length, 2);
assert.equal(whList[0].id, wh1.id);
assert.equal(whList[0].sort, 0);
assert.equal(whList[1].id, wh2.id);
assert.equal(whList[1].sort, 1);
const whGet = await store.warehouse.getById(googleAuthId, wh1.id);
assert.equal(whGet.name, 'Hauptlager');
assert.equal(whGet.sort, 0);
const whUpdated = await store.warehouse.update(googleAuthId, wh2.id, { name: 'Filiale Süd', sort: 5 });
assert.equal(whUpdated.name, 'Filiale Süd');
assert.equal(whUpdated.sort, 5);
// Test warehouse order
await store.warehouse.order(googleAuthId, { warehouseIds: [wh2.id, wh1.id] });
const whListReordered = await store.warehouse.list(googleAuthId);
assert.equal(whListReordered[0].id, wh2.id);
assert.equal(whListReordered[0].sort, 0);
assert.equal(whListReordered[1].id, wh1.id);
assert.equal(whListReordered[1].sort, 1);
// Test invalid warehouse order
await assert.rejects(
async () => store.warehouse.order(googleAuthId, { warehouseIds: [wh1.id] }),
/Invalid warehouse order/,
);
// Verify article detail includes warehouses with sort
const articleDetail = await store.article.getBySku(googleAuthId, article.sku);
assert(Array.isArray(articleDetail.article.warehouses), 'Article should have warehouses array');
assert.equal(articleDetail.article.warehouses.length, 2);
assert.equal(articleDetail.article.warehouses[0].id, wh2.id);
assert.equal(articleDetail.article.warehouses[0].sort, 0);
assert.equal(articleDetail.article.warehouses[1].id, wh1.id);
assert.equal(articleDetail.article.warehouses[1].sort, 1);
// Add ledger row with warehouse
const rowWithWh = await store.ledger.add(googleAuthId, {
manifestarticle_id: manifestarticleId1,
warehouse_id: wh1.id,
transaction_type: 'DELIVERY',
amount: 25,
transaction_text: 'Delivered directly to Hauptlager',
}); });
assert.equal(rowWithWh.warehouse_id, wh1.id); const bookedDelivery2 = await store.delivery.book(googleAuthId, { id: delivery2.id, warehouseId: wh1.id });
assert.equal(rowWithWh.amount, 25); assert.equal(bookedDelivery2.ledger.isBooked, true);
assert.equal(bookedDelivery2.ledger.completelyOnLedgerPositions, 1);
// Filter by warehouse in listBySku and listByManifestarticleId // Double booking delivery2 must reject
const whRowsBySku = await store.ledger.listBySku(googleAuthId, 'SKU-ABC', { warehouse_id: wh1.id });
assert.equal(whRowsBySku.length, 1);
assert.equal(whRowsBySku[0].id, rowWithWh.id);
assert.equal(whRowsBySku[0].warehouse_id, wh1.id);
const whRowsByLine = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId1, { warehouse_id: wh1.id });
assert.equal(whRowsByLine.length, 1);
assert.equal(whRowsByLine[0].id, rowWithWh.id);
// Verify warehouse is recorded in article history
const historyWithWh = await store.article.getHistoryBySku(googleAuthId, article.sku);
assert(
historyWithWh.some((h) => h.text.includes('Lager: "Hauptlager"') && h.text.includes('Lieferant: "Supplier X"')),
'History should include warehouse name (Lager: "Hauptlager")',
);
// Trying to delete wh1 (used in ledger) must fail
await assert.rejects( await assert.rejects(
async () => store.warehouse.delete(googleAuthId, wh1.id), async () => store.delivery.book(googleAuthId, { id: delivery2.id }),
/Warehouse in use in ledger/, /Delivery line(s)? already (accepted|booked) on ledger/,
'Should reject deleting warehouse referenced in ledger', 'Should reject booking already booked delivery',
); );
// Deleting wh2 (not used in ledger) must succeed // 9. Test shipment booking
await store.warehouse.delete(googleAuthId, wh2.id); console.log('7. Testing shipment booking (SHIPMENT + WAREHOUSE dual entries)...');
const whListAfterDelete = await store.warehouse.list(googleAuthId); const customer = await store.entity.create(googleAuthId, {
assert.equal(whListAfterDelete.length, 1); name: 'Customer Y',
assert.equal(whListAfterDelete[0].id, wh1.id); isCustomer: true,
console.log('✓ Warehouse CRUD and ledger enforcement passed'); customergroupId: company.rootCategoryId,
});
const order2 = await store.order.create(googleAuthId, {
number: 'O-2',
entityId: customer.id,
date: new Date().toISOString(),
lines: [
{ sku: 'SKU-ABC', name: 'Ordered Item', pu: 1, amount: 3, net: 25, taxrate: 19 },
],
});
const testCarrier = await store.carrier.create(googleAuthId, { name: 'Test Carrier', hasPackages: true });
const testShipping = { carrierId: testCarrier.id, type: 'parcel', state: 'draft' };
const shipment = await store.shipment.create(googleAuthId, {
orderId: order2.id,
shipping: testShipping,
lines: [{ sku: 'SKU-ABC', amount: 3 }],
});
const shipmentLineId = shipment.lines[0].id;
const shipRow = await store.ledger.add(googleAuthId, {
manifestarticle_id: shipmentLineId,
warehouse_id: wh1.id,
transaction_type: 'SHIPMENT',
amount: 3,
transaction_text: `Lieferschein ${shipment.number}`,
});
assert.equal(shipRow.transaction_type, 'SHIPMENT');
assert.equal(shipRow.warehouse_id, null);
assert.equal(shipRow.amount, -3);
const shipEntries = await store.ledger.listByManifestarticleId(googleAuthId, shipmentLineId);
assert.equal(shipEntries.length, 2, 'Shipment line must have 2 entries (SHIPMENT + WAREHOUSE)');
assert.equal(shipEntries[0].transaction_type, 'SHIPMENT');
assert.equal(shipEntries[0].warehouse_id, null);
assert.equal(shipEntries[0].amount, -3);
assert.equal(shipEntries[1].transaction_type, 'WAREHOUSE');
assert.equal(shipEntries[1].warehouse_id, wh1.id);
assert.equal(shipEntries[1].amount, -3);
// Stock should now be: 10 (line 2) + 4 (delivery2) - 3 (shipment) = 11
const artDetailAfterShip = await store.article.getBySku(googleAuthId, article.sku);
const whStockAfterShip = artDetailAfterShip.article.warehouses.find((w) => w.id === wh1.id)?.stock;
assert.equal(whStockAfterShip, 11, 'Warehouse stock must be 11 (10 + 4 - 3)');
// Verify shipment history
const historyAfterShip = await store.article.getHistoryBySku(googleAuthId, article.sku);
const shipHistoryTexts = historyAfterShip.map((h) => h.text);
assert(
shipHistoryTexts.some((t) => t.includes(`Lieferschein ${shipment.number}`) && t.includes('Menge: 3')),
'Article history should record outgoing shipment',
);
// 8. Test store.stock.accept with deliverymanifestitem
console.log('8. Testing store.stock.accept (positive acceptance & negative return)...');
const delivery3 = await store.delivery.create(googleAuthId, {
supplierId: supplier.id,
externalNumber: 'DEL-3',
date: new Date().toISOString(),
lines: [
{ sku: 'SKU-ABC', name: 'Positive Delivery Line', pu: 1, amount: 6, net: 10, taxrate: 19 },
{ sku: 'SKU-ABC', name: 'Negative Delivery Line (Return)', pu: 1, amount: -2, net: 10, taxrate: 19 },
],
});
const posDeliveryLineId = delivery3.lines[0].id;
const negDeliveryLineId = delivery3.lines[1].id;
const acceptPosResult = await store.stock.accept(googleAuthId, {
manifestarticleId: posDeliveryLineId,
warehouseId: wh1.id,
});
assert.equal(acceptPosResult.document_ledger.transaction_type, 'ACCEPTANCE');
assert.equal(acceptPosResult.document_ledger.amount, 6);
assert.equal(acceptPosResult.document_ledger.warehouse_id, null);
assert.equal(acceptPosResult.adjust_ledger.amount, 6);
assert.equal(acceptPosResult.adjust_ledger.warehouse_id, wh1.id);
// Negative delivery line: return of a delivery (outgoing)
const acceptNegResult = await store.stock.accept(googleAuthId, {
manifestarticleId: negDeliveryLineId,
warehouseId: wh1.id,
});
assert.equal(acceptNegResult.document_ledger.transaction_type, 'RETURN');
assert.equal(acceptNegResult.document_ledger.amount, -2);
assert.equal(acceptNegResult.document_ledger.warehouse_id, null);
assert.equal(acceptNegResult.adjust_ledger.amount, -2);
assert.equal(acceptNegResult.adjust_ledger.warehouse_id, wh1.id);
// 9. Test store.stock.ship with shipmentmanifestitem
console.log('9. Testing store.stock.ship (positive shipment & rejection of negative line)...');
const order3 = await store.order.create(googleAuthId, {
number: 'O-3',
entityId: customer.id,
date: new Date().toISOString(),
lines: [
{ sku: 'SKU-ABC', name: 'Shipment Item Normal', pu: 1, amount: 5, net: 10, taxrate: 19 },
],
});
const shipment3 = await store.shipment.create(googleAuthId, {
orderId: order3.id,
shipping: testShipping,
lines: [
{ sku: 'SKU-ABC', amount: 4 },
{ sku: 'SKU-ABC', amount: -1 },
],
});
const posShipLineId = shipment3.lines[0].id;
const negShipLineId = shipment3.lines[1].id;
const shipPosResult = await store.stock.ship(googleAuthId, {
manifestarticleId: posShipLineId,
warehouseId: wh1.id,
});
assert.equal(shipPosResult.document_ledger.transaction_type, 'SHIPMENT');
assert.equal(shipPosResult.document_ledger.amount, -4);
assert.equal(shipPosResult.adjust_ledger.amount, -4);
// Negative shipment line: shipping is rejected
await assert.rejects(
store.stock.ship(googleAuthId, {
manifestarticleId: negShipLineId,
warehouseId: wh1.id,
}),
/Amount must be positive/,
);
// 10. Test store.stock.adjust
console.log('10. Testing store.stock.adjust failure on untracked article...');
const untrackedArt = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Pure Untracked Article',
track_stock: false,
});
await assert.rejects(
async () => store.stock.adjust(googleAuthId, {
sku: untrackedArt.sku,
warehouseId: wh1.id,
amount: 5,
}),
/Stock tracking is disabled for article/,
'Adjust must fail when article has no stock tracking',
);
// 10b. Test prevention of negative stock on shipment creation and stock adjust
console.log('10b. Testing prevention of negative stock on shipment creation and adjust...');
const guardOrder = await store.order.create(googleAuthId, {
number: 'ORD-GUARD-1',
date: '2026-10-20',
entityId: customer.id,
lines: [{ sku: article.sku, name: 'Order Line', pu: 1, amount: 9999, net: 10, taxrate: 19 }],
});
const currentArt = await store.article.getBySku(googleAuthId, article.sku);
const curStock = currentArt.article.warehouses.find((w) => w.id === wh1.id)?.stock ?? 0;
// Explicit shipment requesting more than available stock must be rejected
await assert.rejects(
async () => store.shipment.create(googleAuthId, {
orderId: guardOrder.id,
shipping: testShipping,
warehouseId: wh1.id,
lines: [{ sku: article.sku, amount: curStock + 10 }],
}),
/Insufficient stock for article/,
'Shipment creation must fail when requested amount exceeds warehouse stock',
);
// Manual stock adjust attempting to deduct more than current stock must be rejected
await assert.rejects(
async () => store.stock.adjust(googleAuthId, {
sku: article.sku,
warehouseId: wh1.id,
amount: -(curStock + 10),
}),
/Insufficient stock for article/,
'Stock adjust must fail when deduction exceeds available stock',
);
// 11. Test before stock tracking is disabled, adjust to 0 has to be ledgerized
console.log('11. Testing zero-adjustment before stock tracking is disabled...');
const artBeforeDisable = await store.article.getBySku(googleAuthId, article.sku);
const stockBeforeDisable = artBeforeDisable.article.warehouses.find((w) => w.id === wh1.id)?.stock;
console.log('Current stock before disabling tracking:', stockBeforeDisable);
assert(stockBeforeDisable > 0, 'Article must have non-zero stock before disabling');
// Disable stock tracking
await store.article.setTrackStockBySku(googleAuthId, article.sku, false);
const artAfterDisable = await store.article.getBySku(googleAuthId, article.sku);
assert.equal(artAfterDisable.article.track_stock, false, 'Article track_stock must now be false');
const stockAfterDisable = artAfterDisable.article.warehouses.find((w) => w.id === wh1.id)?.stock;
assert.equal(stockAfterDisable, 0, 'Warehouse stock must be adjusted to 0 on the ledger');
// Calling adjust now must fail
await assert.rejects(
async () => store.stock.adjust(googleAuthId, {
sku: article.sku,
warehouseId: wh1.id,
amount: 1,
}),
/Stock tracking is disabled for article/,
'Subsequent adjust must fail once tracking is disabled',
);
console.log('✓ All ledger tests passed successfully!'); console.log('✓ All ledger tests passed successfully!');
} finally { } finally {

View File

@@ -21,6 +21,15 @@ async function testProvider(name, provider) {
}); });
await store.google_auth.setSelectedCompany(googleAuthId, company.id); await store.google_auth.setSelectedCompany(googleAuthId, company.id);
const supplier = await store.supplier.create(googleAuthId, {
name: 'Test Supplier',
});
const groups = await store.customergroup.list(googleAuthId);
const customer = await store.entity.create(googleAuthId, {
name: 'Test Customer',
customergroupId: groups[0].id,
});
// 2. Test delivery with both dates // 2. Test delivery with both dates
console.log('Testing delivery creation with date and createdAt...'); console.log('Testing delivery creation with date and createdAt...');
const deliveryDate = '2026-10-15T12:00:00.000Z'; const deliveryDate = '2026-10-15T12:00:00.000Z';
@@ -28,6 +37,7 @@ async function testProvider(name, provider) {
const createdDelivery = await store.delivery.create(googleAuthId, { const createdDelivery = await store.delivery.create(googleAuthId, {
externalNumber: 'DEL-2DATES-1', externalNumber: 'DEL-2DATES-1',
supplierId: supplier.id,
date: deliveryDate, date: deliveryDate,
createdAt: customCreatedAt, createdAt: customCreatedAt,
lines: [ lines: [
@@ -98,6 +108,7 @@ async function testProvider(name, provider) {
const order = await store.order.create(googleAuthId, { const order = await store.order.create(googleAuthId, {
number: `ORD-${Date.now()}`, number: `ORD-${Date.now()}`,
date: '2026-12-15T00:00:00.000Z', date: '2026-12-15T00:00:00.000Z',
entityId: customer.id,
lines: [{ name: 'Order item', amount: 3, net: 25, taxrate: 19 }], lines: [{ name: 'Order item', amount: 3, net: 25, taxrate: 19 }],
}); });
assert(order.createdAt, 'Order should have default createdAt populated'); assert(order.createdAt, 'Order should have default createdAt populated');

295
scripts/test-obituary.js Normal file
View File

@@ -0,0 +1,295 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing posterminalobituary table and deletion flow ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-obituary-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
// 1. Verify posterminalobituary table exists and has expected columns
console.log('1. Checking posterminalobituary schema...');
const tableInfo = provider.db.pragma('table_info("posterminalobituary")');
const colNames = tableInfo.map((c) => c.name);
console.log('posterminalobituary columns:', colNames);
assert.deepEqual(colNames, [
'id',
'posterminal_id',
'entity_type',
'entity_id',
'created_at',
]);
// Setup company first
console.log('2. Setting up base data...');
const googleAuthId = await store.google_auth.add('obituary-tester@example.com');
const company = await store.company.create({
name: 'Obituary Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// Setup active paired POS terminal
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS Terminal 1',
pairingpin: '1234',
});
await store.posterminal.pair(terminal.id, {
token: 'pos-token-123',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
});
// 3. Test article deletion flow
console.log('3. Testing article deletion obituary...');
const article = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Coffee Mug',
});
await store.article.delete(googleAuthId, article.sku);
// 4. Test category deletion flow
console.log('4. Testing category deletion obituary...');
const cat = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Snacks',
});
await store.category.delete({ googleAuthId, id: cat.id });
// 5. Test customer deletion flow
console.log('5. Testing customer deletion obituary...');
const customer = await store.entity.create(googleAuthId, {
name: 'Mustermann',
firstName: 'Max',
customergroupId: company.posCustomerGroupId,
});
await store.entity.delete(googleAuthId, customer.id);
// 6. Test customer group deletion flow
console.log('6. Testing customer group deletion obituary...');
const cg = await store.customergroup.create(googleAuthId, {
name: 'VIP Guests',
});
await store.customergroup.delete(googleAuthId, cg.id);
// 7. Verify all obituaries exist in posterminalobituary
console.log('7. Verifying obituaries in database...');
const allObituaries = provider.db
.prepare('SELECT * FROM posterminalobituary WHERE posterminal_id = ? ORDER BY id ASC')
.all(terminal.id);
console.log('Created obituaries:', allObituaries);
assert.equal(allObituaries.length, 4);
assert.equal(allObituaries[0].entity_type, 'article');
assert.equal(allObituaries[0].entity_id, article.id);
assert.equal(allObituaries[1].entity_type, 'category');
assert.equal(allObituaries[1].entity_id, cat.id);
assert.equal(allObituaries[2].entity_type, 'customer');
assert.equal(allObituaries[2].entity_id, customer.id);
assert.equal(allObituaries[3].entity_type, 'customergroup');
assert.equal(allObituaries[3].entity_id, cg.id);
// 8. Test getDeletedEntityCount & getDeletedEntityList APIs
console.log('8. Testing posterminal.getDeletedEntityCount & getDeletedEntityList...');
const initialCount = await store.posterminal.getDeletedEntityCount({
posterminalId: terminal.id,
cursor: 0,
});
assert.equal(initialCount, 4);
const list = await store.posterminal.getDeletedEntityList({
posterminalId: terminal.id,
cursor: 0,
limit: 10,
});
assert.equal(list.length, 4);
assert.equal(list[0].entityType, 'article');
assert.equal(list[0].entityId, article.id);
assert.equal(list[1].entityType, 'category');
assert.equal(list[2].entityType, 'customer');
assert.equal(list[3].entityType, 'customergroup');
// Test cursor pagination
const cursor = allObituaries[1].id;
const countAfterCursor = await store.posterminal.getDeletedEntityCount({
posterminalId: terminal.id,
cursor,
});
assert.equal(countAfterCursor, 2);
const listAfterCursor = await store.posterminal.getDeletedEntityList({
posterminalId: terminal.id,
cursor,
limit: 10,
});
assert.equal(listAfterCursor.length, 2);
assert.equal(listAfterCursor[0].id, allObituaries[2].id);
assert.equal(listAfterCursor[1].id, allObituaries[3].id);
// 9. Test maxknownobituaryid
console.log('9. Testing maxknownobituaryid...');
await store.posterminal.setMaxKnownObituaryId(terminal.id, allObituaries[3].id);
const updatedTerminal = await store.posterminal.findByToken('pos-token-123');
assert.equal(updatedTerminal.maxknownobituaryid, allObituaries[3].id);
// 10. Test setOff category & orphaned article obituary generation
console.log('10. Testing setOff with orphaned and non-orphaned articles...');
// Create Cat A and Cat B
const catA = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Category A',
});
const catB = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Category B',
});
// Art A only in Cat A
const artA = await store.article.createUnderParent({
googleAuthId,
parentId: catA.id,
name: 'Article A (only in Cat A)',
});
// Art AB in both Cat A and Cat B
const artAB = await store.article.createUnderParent({
googleAuthId,
parentId: catA.id,
name: 'Article AB (in Cat A and B)',
});
await store.article.setCategoryIdsBySku(googleAuthId, artAB.sku, [catA.id, catB.id]);
// Turn both categories on for the terminal
await store.posterminal.setOn(googleAuthId, terminal.id, catA.id);
await store.posterminal.setOn(googleAuthId, terminal.id, catB.id);
// Clear previous obituaries to test cleanly
provider.db.prepare('DELETE FROM posterminalobituary WHERE posterminal_id = ?').run(terminal.id);
// Turn off Cat A:
// - Cat A should get an obituary
// - Art A should get an obituary (orphaned on this terminal)
// - Art AB should NOT get an obituary (still has active Cat B on this terminal)
await store.posterminal.setOff(googleAuthId, terminal.id, catA.id);
const obituariesAfterSetOff = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ? ORDER BY id ASC')
.all(terminal.id);
console.log('Obituaries after setOff(Cat A):', obituariesAfterSetOff);
assert.equal(obituariesAfterSetOff.length, 2);
assert.deepEqual(obituariesAfterSetOff, [
{ entity_type: 'category', entity_id: catA.id },
{ entity_type: 'article', entity_id: artA.id },
]);
// 11. Test setOn category clears pending obituaries for that category and its articles
console.log('11. Testing setOn clears pending category and article obituaries...');
await store.posterminal.setOn(googleAuthId, terminal.id, catA.id);
const obituariesAfterSetOn = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
.all(terminal.id);
console.log('Obituaries after setOn(Cat A):', obituariesAfterSetOn);
assert.equal(obituariesAfterSetOn.length, 0);
// 12. Test setOffAll generates obituaries for all active categories and all their articles
console.log('12. Testing setOffAll obituaries...');
await store.posterminal.setOffAll(googleAuthId, terminal.id);
const obituariesAfterSetOffAll = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ? ORDER BY id ASC')
.all(terminal.id);
console.log('Obituaries after setOffAll:', obituariesAfterSetOffAll);
// Cat A, Cat B, Art A, Art AB
const catObituaries = obituariesAfterSetOffAll.filter((t) => t.entity_type === 'category').map((t) => t.entity_id);
const artObituaries = obituariesAfterSetOffAll.filter((t) => t.entity_type === 'article').map((t) => t.entity_id);
assert(catObituaries.includes(catA.id));
assert(catObituaries.includes(catB.id));
assert(artObituaries.includes(artA.id));
assert(artObituaries.includes(artAB.id));
// 13. Test setOnAll clears category and article obituaries
console.log('13. Testing setOnAll clears all category & article obituaries...');
await store.posterminal.setOnAll(googleAuthId, terminal.id);
const obituariesAfterSetOnAll = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
.all(terminal.id);
console.log('Obituaries after setOnAll:', obituariesAfterSetOnAll);
assert.equal(obituariesAfterSetOnAll.length, 0);
// 14. Test setCategoryIdsBySku generates obituary when removed from POS-enabled category
console.log('14. Testing setCategoryIdsBySku obituary generation and clearing...');
// Setup terminal: only has Cat A enabled
await store.posterminal.setOffAll(googleAuthId, terminal.id);
// Clear obituaries from setOffAll to start clean
provider.db.prepare('DELETE FROM posterminalobituary WHERE posterminal_id = ?').run(terminal.id);
await store.posterminal.setOn(googleAuthId, terminal.id, catA.id);
// Create article in Cat A
const artMoving = await store.article.createUnderParent({
googleAuthId,
parentId: catA.id,
name: 'Moving Article',
});
// Should have 0 obituaries on terminal
let obits = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
.all(terminal.id);
assert.equal(obits.length, 0);
// Move article from Cat A to Cat B (which is not enabled on terminal)
console.log('Moving article to Cat B (not on terminal)...');
await store.article.setCategoryIdsBySku(googleAuthId, artMoving.sku, [catB.id]);
obits = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
.all(terminal.id);
console.log('Obituaries after moving to Cat B:', obits);
assert.equal(obits.length, 1);
assert.deepEqual(obits[0], { entity_type: 'article', entity_id: artMoving.id });
// Move article back to Cat A (enabled on terminal)
console.log('Moving article back to Cat A (on terminal)...');
await store.article.setCategoryIdsBySku(googleAuthId, artMoving.sku, [catA.id]);
obits = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
.all(terminal.id);
console.log('Obituaries after moving back to Cat A:', obits);
assert.equal(obits.length, 0);
console.log('✅ ALL POSTERMINALOBITUARY TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,151 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing store.order.createFromQuote in dashdbschema ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-order-create-from-quote-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('order-from-quote-test@example.com');
const company = await store.company.create({
name: 'Order Quote Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
const groups = await store.customergroup.list(googleAuthId);
const customer = await store.entity.create(googleAuthId, {
customergroupId: groups[0].id,
firstName: 'Alice',
name: 'Wonderland',
});
const art = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Gadget',
track_stock: 0,
});
// 1. Create a quote
const quote = await store.quote.create(googleAuthId, {
number: 'Q-2026-001',
date: '2026-09-01T10:00:00Z',
entityId: customer.id,
externalNumber: 'EXT-QUOTE-123',
lines: [
{ sku: art.sku, name: 'Gadget', pu: 1, amount: 5, net: 20, rebate: 10, taxrate: 19, comment: 'test item' },
],
});
assert.equal(quote.number, 'Q-2026-001');
assert.equal(quote.lines.length, 1);
// 2. Create order from quote with default auto-allocated number & inherited fields
const order1 = await store.order.createFromQuote(googleAuthId, quote.id);
assert.ok(order1.id, 'Order must have an id');
assert.equal(order1.type, 'order');
assert.equal(order1.entityId, customer.id);
assert.equal(order1.externalNumber, quote.number);
assert.equal(order1.number, 'AUF-000001');
assert.equal(order1.lines.length, 1);
assert.equal(order1.lines[0].sku, art.sku);
assert.equal(order1.lines[0].amount, 5);
assert.equal(order1.lines[0].net, 20);
assert.equal(order1.lines[0].rebate, 10);
assert.equal(order1.lines[0].taxrate, 19);
assert.equal(order1.lines[0].comment, 'test item');
// Verify ordersummary row
const summaryRows = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').all(order1.id);
assert.equal(summaryRows.length, 1);
assert.equal(summaryRows[0].customer_name, 'Alice Wonderland');
assert.equal(summaryRows[0].positions, 1);
assert.equal(summaryRows[0].ordered_quantity, 5);
// 3. Create order from quote with custom overrides (number, externalNumber, date)
const order2 = await store.order.createFromQuote(googleAuthId, quote.id, {
number: 'ORD-CUSTOM-999',
date: '2026-09-15T12:00:00Z',
externalNumber: 'CUSTOM-EXT',
});
assert.equal(order2.number, 'ORD-CUSTOM-999');
assert.equal(order2.externalNumber, 'CUSTOM-EXT');
assert.equal(order2.lines.length, 1);
// 4. Non-existent quote should throw 'Quote not found'
await assert.rejects(
async () => {
await store.order.createFromQuote(googleAuthId, 999999);
},
{ message: 'Quote not found' }
);
// 5. Invalid quoteId should throw 'Quote not found'
await assert.rejects(
async () => {
await store.order.createFromQuote(googleAuthId, 'invalid');
},
{ message: 'Quote not found' }
);
// 6. Quote without entity should throw 'No entity selected' if no entity passed
const quoteWithoutEntity = await store.quote.create(googleAuthId, {
number: 'Q-NO-ENT',
date: '2026-09-01T10:00:00Z',
lines: [{ sku: art.sku, name: 'Gadget', amount: 1, net: 10, taxrate: 19 }],
});
assert.equal(quoteWithoutEntity.entityId, null);
await assert.rejects(
async () => {
await store.order.createFromQuote(googleAuthId, quoteWithoutEntity.id);
},
{ message: 'No entity selected' }
);
// 7. Explicitly overriding with entityId: null should also throw 'No entity selected'
await assert.rejects(
async () => {
await store.order.createFromQuote(googleAuthId, quote.id, { entityId: null });
},
{ message: 'No entity selected' }
);
// 8. Passing entityId for quote without entity should succeed
const orderFromNoEnt = await store.order.createFromQuote(googleAuthId, quoteWithoutEntity.id, {
entityId: customer.id,
});
assert.equal(orderFromNoEnt.entityId, customer.id);
console.log('PASS: All order.createFromQuote tests passed successfully!');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {
// ignore cleanup errors
}
}
}
run().catch((err) => {
console.error('FAIL: Test failed:', err);
process.exit(1);
});

View File

@@ -95,14 +95,11 @@ async function run() {
entityId: customerBob.id, entityId: customerBob.id,
lines: [{ sku: art.sku, name: 'Widget', amount: 2, net: 100, taxrate: 19 }], lines: [{ sku: art.sku, name: 'Widget', amount: 2, net: 100, taxrate: 19 }],
}); });
const testCarrier = await store.carrier.create(googleAuthId, { name: 'Test Carrier', hasPackages: true });
const shipment2 = await store.shipment.create(googleAuthId, { const shipment2 = await store.shipment.create(googleAuthId, {
orderId: order2.id, orderId: order2.id,
lines: [{ sku: art.sku, amount: 2 }], lines: [{ sku: art.sku, amount: 2 }],
}); shipping: { carrierId: testCarrier.id, type: 'parcel', state: 'shipped' },
await store.shipping.create(googleAuthId, {
manifestId: shipment2.id,
type: 'parcel',
state: 'shipped',
}); });
// Order 3: Alice, O-1003, €59.50, Date: today (2026-10-13) // Order 3: Alice, O-1003, €59.50, Date: today (2026-10-13)
@@ -128,14 +125,10 @@ async function run() {
type: 'bank_transfer', type: 'bank_transfer',
amount: 150, // Overpaid! amount: 150, // Overpaid!
}); });
const shipment4 = await store.shipment.create(googleAuthId, { await store.shipment.create(googleAuthId, {
orderId: order4.id, orderId: order4.id,
lines: [{ sku: art.sku, amount: 1 }], lines: [{ sku: art.sku, amount: 1 }],
}); shipping: { carrierId: testCarrier.id, type: 'parcel', state: 'shipped' },
await store.shipping.create(googleAuthId, {
manifestId: shipment4.id,
type: 'parcel',
state: 'shipped',
}); });
console.log('Orders created successfully.'); console.log('Orders created successfully.');

View File

@@ -54,6 +54,10 @@ async function run() {
name: 'Max Mustermann', name: 'Max Mustermann',
}); });
const carrier = await store.carrier.create(googleAuthId, {
name: 'Standard Carrier',
});
// 2. Setup tracked article and untracked article // 2. Setup tracked article and untracked article
const trackedArt = await store.article.createUnderParent({ const trackedArt = await store.article.createUnderParent({
googleAuthId, googleAuthId,
@@ -114,12 +118,9 @@ async function run() {
{ sku: trackedArt.sku, amount: 3 }, { sku: trackedArt.sku, amount: 3 },
{ sku: untrackedArt.sku, amount: 2 }, { sku: untrackedArt.sku, amount: 2 },
], ],
shipping: { carrierId: carrier.id, type: 'parcel', state: 'open' },
}); });
const shippingOpen = await store.shipping.create(googleAuthId, { const shippingOpen = shipment.shippings[0];
manifestId: shipment.id,
type: 'parcel',
state: 'open',
});
assert.equal(shippingOpen.state, 'open'); assert.equal(shippingOpen.state, 'open');
// Assert stock is still 10 // Assert stock is still 10
@@ -140,16 +141,22 @@ async function run() {
// Assert ledger entries // Assert ledger entries
const ledgerRows = await store.ledger.listBySku(googleAuthId, trackedArt.sku); const ledgerRows = await store.ledger.listBySku(googleAuthId, trackedArt.sku);
assert.equal(ledgerRows.length, 2, 'Should have 2 ledger entries for tracked article (adjust + shipping)'); assert.equal(ledgerRows.length, 3, 'Should have 3 ledger entries for tracked article (adjust + shipment doc + warehouse)');
const shippingLedger = ledgerRows.find((l) => l.transaction_type === 'SHIPPING'); const shipmentDocLedger = ledgerRows.find((l) => l.transaction_type === 'SHIPMENT' && l.warehouse_id == null);
assert(shippingLedger, 'Shipping ledger entry should exist'); assert(shipmentDocLedger, 'Shipment document ledger entry should exist with warehouse_id null');
assert.equal(shippingLedger.amount, -3, 'Shipping ledger entry should be -3'); assert.equal(shipmentDocLedger.amount, -3, 'Shipment document ledger entry should be -3');
const shipmentWhLedger = ledgerRows.find((l) => l.transaction_type === 'WAREHOUSE' && l.warehouse_id === warehouse.id);
assert(shipmentWhLedger, 'Shipment warehouse ledger entry should exist with warehouse_id set');
assert.equal(shipmentWhLedger.amount, -3, 'Shipment warehouse ledger entry should be -3');
// Assert untracked article has 0 warehouse ledger entries // Assert untracked article has 1 document ledger entry and 0 warehouse ledger entries
const untrackedLedger = await store.ledger.listBySku(googleAuthId, untrackedArt.sku); const untrackedLedger = await store.ledger.listBySku(googleAuthId, untrackedArt.sku);
assert.equal(untrackedLedger.length, 0, 'Untracked article should not have warehouse ledger entries'); assert.equal(untrackedLedger.length, 1, 'Untracked article should have 1 shipment document ledger entry');
assert.equal(untrackedLedger[0].transaction_type, 'SHIPMENT');
assert.equal(untrackedLedger[0].warehouse_id, null, 'Untracked article should not have warehouse ledger entry');
assert.equal(untrackedLedger[0].amount, -2);
// Assert article history: order goes into history, shipment does NOT // Assert article history: order goes into history, and shipment produces history
const history = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku); const history = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku);
console.log('Tracked article history:', history.map((h) => h.text)); console.log('Tracked article history:', history.map((h) => h.text));
const orderHistory = history.find((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 3, € 59,50)'); const orderHistory = history.find((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 3, € 59,50)');
@@ -159,10 +166,10 @@ async function run() {
assert.equal(parsedMeta.type, 'order'); assert.equal(parsedMeta.type, 'order');
assert.equal(parsedMeta.orderId, order.id); assert.equal(parsedMeta.orderId, order.id);
assert.equal(parsedMeta.gross, 59.5); assert.equal(parsedMeta.gross, 59.5);
const shippingHistory = history.find((h) => h.text.includes('Versand Auftrag')); const shipmentHistory = history.find((h) => h.text.startsWith(`Lieferschein ${shipment.number}`) && h.text.includes('Kunde: "Max Mustermann"'));
assert(!shippingHistory, 'Article history should NOT report the shipment'); assert(shipmentHistory, 'Article history should report the shipment document');
// Untracked article also has the order in history, but not the shipment // Untracked article also has the order in history and shipment in history
const untrackedHistory = await store.article.getHistoryBySku(googleAuthId, untrackedArt.sku); const untrackedHistory = await store.article.getHistoryBySku(googleAuthId, untrackedArt.sku);
console.log('Untracked article history:', untrackedHistory.map((h) => h.text)); console.log('Untracked article history:', untrackedHistory.map((h) => h.text));
assert( assert(
@@ -170,8 +177,8 @@ async function run() {
'Untracked article history should contain order creation entry with formatted gross price', 'Untracked article history should contain order creation entry with formatted gross price',
); );
assert( assert(
!untrackedHistory.some((h) => h.text.includes('Versand Auftrag')), untrackedHistory.some((h) => h.text.startsWith(`Lieferschein ${shipment.number}`)),
'Untracked article history should NOT report the shipment', 'Untracked article history should report the shipment',
); );
// Test order import with historical date preserves date in article history // Test order import with historical date preserves date in article history
@@ -180,6 +187,7 @@ async function run() {
number: 'O-HIST-001', number: 'O-HIST-001',
date: historicalDate, date: historicalDate,
createdAt: historicalDate, createdAt: historicalDate,
entityId: customer.id,
lines: [{ sku: trackedArt.sku, amount: 1, net: 10, taxrate: 19 }], lines: [{ sku: trackedArt.sku, amount: 1, net: 10, taxrate: 19 }],
}); });
const histWithOld = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku); const histWithOld = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku);
@@ -213,6 +221,7 @@ async function run() {
async () => { async () => {
await store.shipping.create(googleAuthId, { await store.shipping.create(googleAuthId, {
manifestId: order.id, manifestId: order.id,
carrierId: carrier.id,
type: 'express', type: 'express',
state: 'shipped', state: 'shipped',
}); });
@@ -241,17 +250,28 @@ async function run() {
const shipment2 = await store.shipment.create(googleAuthId, { const shipment2 = await store.shipment.create(googleAuthId, {
orderId: order2.id, orderId: order2.id,
lines: [{ sku: trackedArt.sku, amount: 2 }], lines: [{ sku: trackedArt.sku, amount: 2 }],
shipping: { carrierId: carrier.id, type: 'parcel', state: 'shipped' },
}); });
const directShipped = shipment2.shippings[0];
const directShipped = await store.shipping.create(googleAuthId, { // 9b. Import-style: delivered shipment with shipped shipping created atomically
manifestId: shipment2.id, const order3 = await store.order.create(googleAuthId, {
type: 'parcel', number: 'O-2026-003',
state: 'shipped', date: new Date().toISOString(),
entityId: customer.id,
lines: [{ sku: trackedArt.sku, name: 'Tracked Article', pu: 1, amount: 1, net: 50, taxrate: 19 }],
}); });
const shipment3 = await store.shipment.create(googleAuthId, {
orderId: order3.id,
state: 'delivered',
lines: [{ sku: trackedArt.sku, amount: 1 }],
shipping: { carrierId: carrier.id, type: 'parcel', state: 'shipped' },
});
assert.equal(shipment3.shippings[0].state, 'shipped');
assert.equal(directShipped.state, 'shipped'); assert.equal(directShipped.state, 'shipped');
const artFinal = await store.article.getBySku(googleAuthId, trackedArt.sku); const artFinal = await store.article.getBySku(googleAuthId, trackedArt.sku);
assert.equal(artFinal.article.warehouses.reduce((sum, w) => sum + w.stock, 0), 5, 'Stock should now be 5 (7 - 2)'); assert.equal(artFinal.article.warehouses.reduce((sum, w) => sum + w.stock, 0), 4, 'Stock should now be 4 (7 - 2 - 1)');
console.log('✓ All order shipping ledger tests passed successfully!'); console.log('✓ All order shipping ledger tests passed successfully!');
} finally { } finally {

View File

@@ -0,0 +1,118 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
const tempDir = mkdtempSync(join(tmpdir(), 'dash-printeragent-test-'));
const provider = new BetterSqlite3Provider({ filename: join(tempDir, 'test.db'), storageDir: join(tempDir, 'storage') });
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('printeragent-tester@example.com');
const company = await store.company.create({ name: 'PA GmbH', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId });
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
assert.deepEqual(await store.printeragent.list(googleAuthId), []);
await assert.rejects(store.printeragent.create(googleAuthId, { name: ' ', pairingpin: '1' }), /Invalid printeragent/);
const a = await store.printeragent.create(googleAuthId, { name: ' Bar ', pairingpin: '123456', sort: 5 });
assert.equal(a.name, 'Bar');
assert.equal(a.pairingpin, '123456');
assert.equal(a.token, null);
assert.equal(a.sort, 5);
assert.equal(a.companyId, company.id);
assert.ok(a.updatedAt);
// findByPairingpin
const foundByPin = await store.printeragent.findByPairingpin('123456');
assert.equal(foundByPin.id, a.id);
assert.equal(await store.printeragent.findByPairingpin('999999'), null);
// pair
const paired = await store.printeragent.pair(a.id, {
token: 'test-token-123',
agentversion: '1.0.0',
});
assert.equal(paired.token, 'test-token-123');
assert.equal(paired.agentversion, '1.0.0');
assert.equal(paired.pairingpin, null);
assert.equal(paired.enabled, true);
assert.ok(paired.pairedAt);
// findByToken
const foundByToken = await store.printeragent.findByToken('test-token-123');
assert.equal(foundByToken.id, a.id);
assert.equal(foundByToken.enabled, true);
assert.equal(await store.printeragent.findByToken('invalid-token'), null);
// touchLastseen
await store.printeragent.touchLastseen(a.id, '1.0.1');
const afterTouch = await store.printeragent.findByToken('test-token-123');
assert.equal(afterTouch.agentversion, '1.0.1');
// update name, sort, and enabled
const b = await store.printeragent.update(googleAuthId, { id: a.id, name: 'Kitchen', sort: 1, enabled: false });
assert.equal(b.name, 'Kitchen');
assert.equal(b.sort, 1);
assert.equal(b.enabled, false);
// list
const list = await store.printeragent.list(googleAuthId);
assert.equal(list.length, 1);
assert.equal(list[0].name, 'Kitchen');
assert.equal(list[0].sort, 1);
assert.equal(list[0].enabled, false);
// test agentprinter findOrCreate
const ap1 = await store.agentprinter.findOrCreate(a.id, { type: 'zpl', model: 'ZD410-200dpi', serial: '50J203002218' });
assert.equal(ap1.created, true);
assert.equal(ap1.agentprinter.type, 'zpl');
assert.equal(ap1.agentprinter.model, 'ZD410-200dpi');
assert.equal(ap1.agentprinter.serial, '50J203002218');
assert.equal(ap1.agentprinter.enabled, true);
// second call with same combo returns existing
const ap1Again = await store.agentprinter.findOrCreate(a.id, { type: 'zpl', model: 'ZD410-200dpi', serial: '50J203002218' });
assert.equal(ap1Again.created, false);
assert.equal(ap1Again.agentprinter.id, ap1.agentprinter.id);
// create second printer
const ap2 = await store.agentprinter.findOrCreate(a.id, { type: 'esc/pos', model: 'TM-T88VI', serial: 'SN999' });
assert.equal(ap2.created, true);
// list agentprinters
const apList = await store.agentprinter.list(googleAuthId, a.id);
assert.equal(apList.length, 2);
// getById
const apGet = await store.agentprinter.getById(googleAuthId, ap1.agentprinter.id);
assert.equal(apGet.serial, '50J203002218');
// update agentprinter
const apUpdated = await store.agentprinter.update(googleAuthId, { id: ap1.agentprinter.id, enabled: false, sort: 10 });
assert.equal(apUpdated.enabled, false);
assert.equal(apUpdated.sort, 10);
// order agentprinters
const ordered = await store.agentprinter.order(googleAuthId, {
printeragentId: a.id,
agentprinterIds: [ap2.agentprinter.id, ap1.agentprinter.id],
});
assert.deepEqual(ordered.agentprinterIds, [ap2.agentprinter.id, ap1.agentprinter.id]);
// delete agentprinter
await store.agentprinter.delete(googleAuthId, ap2.agentprinter.id);
assert.equal((await store.agentprinter.list(googleAuthId, a.id)).length, 1);
// delete
await store.printeragent.delete(googleAuthId, a.id);
assert.equal(await store.printeragent.findByToken('test-token-123'), null);
assert.deepEqual(await store.printeragent.list(googleAuthId), []);
console.log('printeragent full test OK');
} finally {
await provider.close?.();
rmSync(tempDir, { recursive: true, force: true });
}

View File

@@ -47,12 +47,14 @@ async function run() {
parentId: company.rootCategoryId, parentId: company.rootCategoryId,
name: 'Item A', name: 'Item A',
sku: 'ITEM-A', sku: 'ITEM-A',
track_stock: true,
}); });
const itemB = await store.article.createUnderParent({ const itemB = await store.article.createUnderParent({
googleAuthId, googleAuthId,
parentId: company.rootCategoryId, parentId: company.rootCategoryId,
name: 'Item B', name: 'Item B',
sku: 'ITEM-B', sku: 'ITEM-B',
track_stock: true,
}); });
// Stock up Item A (20) and Item B (20) // Stock up Item A (20) and Item B (20)
@@ -82,8 +84,26 @@ async function run() {
assert.equal(order.number, 'AU-1001'); assert.equal(order.number, 'AU-1001');
console.log('Order created:', order.number); console.log('Order created:', order.number);
const carrier = await store.carrier.create(googleAuthId, {
name: 'DHL',
hasPackages: true,
isReverse: false,
});
assert.equal(carrier.name, 'DHL');
assert.equal(carrier.hasPackages, true);
assert.equal(carrier.isReverse, false);
const shipping = { carrierId: carrier.id, type: 'parcel', state: 'draft' };
// 0. Shipment without shipping is rejected
await assert.rejects(
() => store.shipment.create(googleAuthId, { orderId: order.id, lines: [{ sku: 'ITEM-A', amount: 1 }] }),
{ message: 'Shipping is required' },
);
console.log('PASS: Shipment without shipping rejected');
// 1. Create Shipment 1: 6x Item A, 3x Item B -> AU-1001-L-1 // 1. Create Shipment 1: 6x Item A, 3x Item B -> AU-1001-L-1
const shipment1 = await store.shipment.create(googleAuthId, { const shipment1 = await store.shipment.create(googleAuthId, {
shipping,
orderId: order.id, orderId: order.id,
lines: [ lines: [
{ sku: 'ITEM-A', amount: 6 }, { sku: 'ITEM-A', amount: 6 },
@@ -98,6 +118,7 @@ async function run() {
let ceilingErrorThrown = false; let ceilingErrorThrown = false;
try { try {
await store.shipment.create(googleAuthId, { await store.shipment.create(googleAuthId, {
shipping,
orderId: order.id, orderId: order.id,
lines: [{ sku: 'ITEM-A', amount: 5 }], lines: [{ sku: 'ITEM-A', amount: 5 }],
}); });
@@ -112,6 +133,7 @@ async function run() {
let foreignArticleErrorThrown = false; let foreignArticleErrorThrown = false;
try { try {
await store.shipment.create(googleAuthId, { await store.shipment.create(googleAuthId, {
shipping,
orderId: order.id, orderId: order.id,
lines: [{ sku: 'ITEM-UNKNOWN', amount: 1 }], lines: [{ sku: 'ITEM-UNKNOWN', amount: 1 }],
}); });
@@ -124,6 +146,7 @@ async function run() {
// 3. Create Shipment 2: 4x Item A, 2x Item B -> AU-1001-L-2 // 3. Create Shipment 2: 4x Item A, 2x Item B -> AU-1001-L-2
const shipment2 = await store.shipment.create(googleAuthId, { const shipment2 = await store.shipment.create(googleAuthId, {
shipping,
orderId: order.id, orderId: order.id,
lines: [ lines: [
{ sku: 'ITEM-A', amount: 4 }, { sku: 'ITEM-A', amount: 4 },
@@ -138,6 +161,7 @@ async function run() {
let exhaustedErrorThrown = false; let exhaustedErrorThrown = false;
try { try {
await store.shipment.create(googleAuthId, { await store.shipment.create(googleAuthId, {
shipping,
orderId: order.id, orderId: order.id,
lines: [{ sku: 'ITEM-A', amount: 1 }], lines: [{ sku: 'ITEM-A', amount: 1 }],
}); });
@@ -148,39 +172,48 @@ async function run() {
} }
assert.ok(exhaustedErrorThrown, 'Expected exhausted error to be thrown'); assert.ok(exhaustedErrorThrown, 'Expected exhausted error to be thrown');
// 5. Add packages to Shipment 1 with tracking codes // 5. Add further packages to Shipment 1 with tracking codes
const pkg1 = await store.shipping.create(googleAuthId, { const pkg1 = await store.shipping.create(googleAuthId, {
manifestId: shipment1.id, manifestId: shipment1.id,
carrierId: carrier.id,
type: 'parcel', type: 'parcel',
state: 'draft', state: 'draft',
trackingCode: 'TRACK-111', trackingCode: 'TRACK-111',
carrier: 'DHL',
}); });
assert.equal(pkg1.trackingCode, 'TRACK-111'); assert.equal(pkg1.trackingCode, 'TRACK-111');
assert.equal(pkg1.carrierId, carrier.id);
assert.equal(pkg1.carrier, 'DHL'); assert.equal(pkg1.carrier, 'DHL');
assert.equal(pkg1.state, 'draft'); assert.equal(pkg1.state, 'draft');
console.log('PASS: Package 1 created for Shipment 1:', pkg1.number, pkg1.trackingCode); console.log('PASS: Package 1 created for Shipment 1:', pkg1.number, pkg1.trackingCode);
const pkg2 = await store.shipping.create(googleAuthId, { const pkg2 = await store.shipping.create(googleAuthId, {
manifestId: shipment1.id, manifestId: shipment1.id,
carrierId: carrier.id,
type: 'parcel', type: 'parcel',
state: 'draft', state: 'draft',
trackingCode: 'TRACK-222', trackingCode: 'TRACK-222',
carrier: 'DHL',
}); });
assert.equal(pkg2.trackingCode, 'TRACK-222'); assert.equal(pkg2.trackingCode, 'TRACK-222');
assert.equal(pkg2.carrierId, carrier.id);
console.log('PASS: Package 2 created for Shipment 1:', pkg2.number, pkg2.trackingCode); console.log('PASS: Package 2 created for Shipment 1:', pkg2.number, pkg2.trackingCode);
// Verify carrier in use prevents deletion
await assert.rejects(
() => store.carrier.delete(googleAuthId, carrier.id),
{ message: 'Carrier in use' },
);
console.log('PASS: Carrier delete rejected while referenced in shipping');
// Check listByOrderId // Check listByOrderId
const orderShipments = await store.shipment.listByOrderId(googleAuthId, order.id); const orderShipments = await store.shipment.listByOrderId(googleAuthId, order.id);
assert.equal(orderShipments.length, 2); assert.equal(orderShipments.length, 2);
assert.equal(orderShipments[0].shippings.length, 2); assert.equal(orderShipments[0].shippings.length, 3);
console.log('PASS: store.shipment.listByOrderId returned both shipments'); console.log('PASS: store.shipment.listByOrderId returned both shipments');
// Check getById // Check getById
const fetchedShipment1 = await store.shipment.getById(googleAuthId, shipment1.id); const fetchedShipment1 = await store.shipment.getById(googleAuthId, shipment1.id);
assert.equal(fetchedShipment1.id, shipment1.id); assert.equal(fetchedShipment1.id, shipment1.id);
assert.equal(fetchedShipment1.shippings.length, 2); assert.equal(fetchedShipment1.shippings.length, 3);
console.log('PASS: store.shipment.getById returned shipment with shippings'); console.log('PASS: store.shipment.getById returned shipment with shippings');
// 6. Transition Package 1 to shipped (should deduct ledger stock for Shipment 1 lines: 6 of Item A, 3 of Item B) // 6. Transition Package 1 to shipped (should deduct ledger stock for Shipment 1 lines: 6 of Item A, 3 of Item B)
@@ -214,7 +247,78 @@ async function run() {
assert.equal(shipmentsAfterDel.length, 1); assert.equal(shipmentsAfterDel.length, 1);
console.log('PASS: Unshipped shipment 2 successfully deleted'); console.log('PASS: Unshipped shipment 2 successfully deleted');
// 9. Verify peek next number works for entity and other types without error // 9. Test order2: One item, some items, all items, and automatic ordersummary updates
const order2 = await store.order.create(googleAuthId, {
number: 'AU-1002',
date: '2026-10-15',
entityId: customer.id,
lines: [
{ sku: 'ITEM-A', amount: 10, net: 100, taxrate: 19 },
{ sku: 'ITEM-B', amount: 5, net: 50, taxrate: 19 },
],
});
// Check initial ordersummary for order2
let sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id);
assert.equal(sum2.ordered_quantity, 15);
assert.equal(sum2.shipped_quantity, 0);
assert.equal(sum2.is_shipped, 0);
// 9a. Ship ONE item (3x ITEM-A) via store.order.createShipment
const shOne = await store.order.createShipment(googleAuthId, order2.id, {
shipping,
lines: [{ sku: 'ITEM-A', amount: 3 }],
});
assert.equal(shOne.number, 'AU-1002-L-1');
assert.equal(shOne.lines.length, 1);
assert.equal(shOne.lines[0].sku, 'ITEM-A');
assert.equal(shOne.lines[0].amount, 3);
// Verify ordersummary updated: shipped_quantity = 3, is_shipped = 0
sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id);
assert.equal(sum2.shipped_quantity, 3);
assert.equal(sum2.is_shipped, 0);
console.log('PASS: 9a. Shipped ONE item; ordersummary updated with shipped_quantity=3, is_shipped=0');
// 9b. Ship SOME items with omitted amount (e.g. ITEM-A with no amount -> defaults to remaining 7)
const shSome = await store.order.createShipment(googleAuthId, order2.id, {
shipping,
lines: [{ sku: 'ITEM-A' }],
});
assert.equal(shSome.number, 'AU-1002-L-2');
assert.equal(shSome.lines.length, 1);
assert.equal(shSome.lines[0].sku, 'ITEM-A');
assert.equal(shSome.lines[0].amount, 7);
// Verify ordersummary updated: shipped_quantity = 10 (3 + 7), is_shipped = 0
sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id);
assert.equal(sum2.shipped_quantity, 10);
assert.equal(sum2.is_shipped, 0);
console.log('PASS: 9b. Shipped SOME items with omitted amount; ordersummary updated with shipped_quantity=10, is_shipped=0');
// 9c. Ship remaining items with explicit lines
const shAll = await store.order.createShipment(googleAuthId, order2.id, { shipping, lines: [{ sku: 'ITEM-B', amount: 5 }] });
assert.equal(shAll.number, 'AU-1002-L-3');
assert.equal(shAll.lines.length, 1);
assert.equal(shAll.lines[0].sku, 'ITEM-B');
assert.equal(shAll.lines[0].amount, 5);
// Verify ordersummary updated: shipped_quantity = 15, is_shipped = 1 (order is fully shipped!)
sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id);
assert.equal(sum2.shipped_quantity, 15);
assert.equal(sum2.is_shipped, 1);
console.log('PASS: 9c. Shipped remaining items with explicit lines; ordersummary updated with shipped_quantity=15, is_shipped=1');
// 9d. Calling createShipment without lines is rejected with "Invalid lines"
await assert.rejects(
async () => {
await store.order.createShipment(googleAuthId, order2.id);
},
{ message: 'Invalid lines' }
);
console.log('PASS: 9d. Creating shipment without lines rejected with "Invalid lines"');
// 10. Verify peek next number works for entity and other types without error
const nextEntity = await store.entity.peekNextNumber(googleAuthId); const nextEntity = await store.entity.peekNextNumber(googleAuthId);
assert.ok(typeof nextEntity === 'string' && nextEntity.length > 0); assert.ok(typeof nextEntity === 'string' && nextEntity.length > 0);
console.log('PASS: store.entity.peekNextNumber succeeded:', nextEntity); console.log('PASS: store.entity.peekNextNumber succeeded:', nextEntity);
@@ -223,8 +327,41 @@ async function run() {
assert.ok(Array.isArray(numList) && numList.length > 0); assert.ok(Array.isArray(numList) && numList.length > 0);
const shipmentSeq = numList.find((n) => n.type === 'shipment'); const shipmentSeq = numList.find((n) => n.type === 'shipment');
assert.ok(shipmentSeq, 'Expected shipment sequence in numbers.list'); assert.ok(shipmentSeq, 'Expected shipment sequence in numbers.list');
assert.equal(shipmentSeq.prefix, '-L-'); // 11. Test store.shipment.list, store.shipment.update, and rebuildSummaries
console.log('PASS: store.numbers.list includes shipment with prefix -L-'); const listAll = await store.shipment.list(googleAuthId, { offset: 0, limit: 10 });
assert.ok(listAll.total >= 3, `Expected at least 3 shipments, got ${listAll.total}`);
assert.equal(listAll.limit, 10);
assert.equal(listAll.offset, 0);
const s1 = listAll.shipments.find((s) => s.id === shOne.id);
assert.ok(s1, 'Expected shOne in shipment list');
assert.equal(s1.orderNumber, 'AU-1002');
assert.equal(s1.orderId, order2.id);
assert.equal(s1.state, 'new');
assert.equal(s1.positions, 1);
assert.equal(s1.totalQuantity, 3);
console.log('PASS: store.shipment.list returned shipments with order info and state');
// 11a. Search by order number
const listSearchOrder = await store.shipment.list(googleAuthId, { search: 'AU-1002', offset: 0, limit: 10 });
assert.equal(listSearchOrder.total, 3);
console.log('PASS: store.shipment.list filtered by orderNumber search');
// 11b. Update shipment state to 'delivered'
const updatedSh = await store.shipment.update(googleAuthId, shOne.id, { state: 'delivered' });
assert.equal(updatedSh.state, 'delivered');
// 11c. Filter by state = 'delivered'
const listDelivered = await store.shipment.list(googleAuthId, { state: 'delivered', offset: 0, limit: 10 });
assert.equal(listDelivered.total, 1);
assert.equal(listDelivered.shipments[0].id, shOne.id);
console.log('PASS: store.shipment.update set state="delivered" and filtered correctly');
// 11d. Rebuild shipment summaries
await store.shipment.rebuildSummaries(googleAuthId);
const listAfterRebuild = await store.shipment.list(googleAuthId, { state: 'delivered', offset: 0, limit: 10 });
assert.equal(listAfterRebuild.total, 1);
console.log('PASS: store.shipment.rebuildSummaries preserved delivered state and rebuilt correctly');
console.log('--- ALL TESTS PASSED SUCCESSFULLY! ---'); console.log('--- ALL TESTS PASSED SUCCESSFULLY! ---');
} finally { } finally {

View File

@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs'; import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
import sharp from 'sharp'; import { optimizeImage } from 'wasm-image-optimization';
import LibDashStore from '../src/index.js'; import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
@@ -72,16 +72,9 @@ async function runTests() {
const manifestId = delivery.id; const manifestId = delivery.id;
const testContent = Buffer.from('Hello SQLite Storage Engine! PDF or text content test.'); const testContent = Buffer.from('Hello SQLite Storage Engine! PDF or text content test.');
const testThumb = await sharp({ const thumbSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="rgb(255,0,0)"/></svg>`;
create: { const testThumbRes = await optimizeImage({ image: thumbSvg, format: 'avif' });
width: 64, const testThumb = Buffer.from(testThumbRes.data);
height: 64,
channels: 4,
background: { r: 255, g: 0, b: 0, alpha: 1 },
},
})
.avif()
.toBuffer();
const attachment = await store.manifestattachment.add(googleAuthId, manifestId, { const attachment = await store.manifestattachment.add(googleAuthId, manifestId, {
name: 'test-doc.txt', name: 'test-doc.txt',
@@ -157,14 +150,27 @@ async function runTests() {
parentId: cat.id, parentId: cat.id,
name: 'Test Article', name: 'Test Article',
}); });
assert.equal(article.track_stock, true, 'Default track_stock must be true'); assert.equal(article.track_stock, false, 'Default track_stock must be false');
const fetchedArt = await store.article.getBySku(googleAuthId, article.sku); const fetchedArt = await store.article.getBySku(googleAuthId, article.sku);
assert.equal(fetchedArt.article.track_stock, true, 'getBySku must return track_stock: true'); assert.equal(fetchedArt.article.track_stock, false, 'getBySku must return track_stock: false');
await store.article.setTrackStockBySku(googleAuthId, article.sku, false); // Enabling track_stock without a warehouse must fail
await assert.rejects(
async () => store.article.setTrackStockBySku(googleAuthId, article.sku, true),
/At least one warehouse is required to enable stock tracking/,
);
// Create warehouse to allow enabling track_stock
await store.warehouse.create(googleAuthId, { name: 'Storage WH' });
await store.article.setTrackStockBySku(googleAuthId, article.sku, true);
const afterSet = await store.article.getBySku(googleAuthId, article.sku); const afterSet = await store.article.getBySku(googleAuthId, article.sku);
assert.equal(afterSet.article.track_stock, false, 'setTrackStockBySku false must update track_stock'); assert.equal(afterSet.article.track_stock, true, 'setTrackStockBySku true must update track_stock');
await store.article.updateBySku(googleAuthId, article.sku, { track_stock: false });
const afterDisable = await store.article.getBySku(googleAuthId, article.sku);
assert.equal(afterDisable.article.track_stock, false, 'updateBySku track_stock: false must update track_stock');
await store.article.updateBySku(googleAuthId, article.sku, { track_stock: true }); await store.article.updateBySku(googleAuthId, article.sku, { track_stock: true });
const afterUpdate = await store.article.getBySku(googleAuthId, article.sku); const afterUpdate = await store.article.getBySku(googleAuthId, article.sku);
@@ -189,31 +195,24 @@ async function runTests() {
console.log('Article track_stock tests passed!'); console.log('Article track_stock tests passed!');
const sampleImage = await sharp({ const sampleSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200"><rect width="300" height="200" fill="rgb(0,128,255)"/></svg>`;
create: { const sampleImageRes = await optimizeImage({ image: sampleSvg, format: 'png' });
width: 300, const sampleImage = Buffer.from(sampleImageRes.data);
height: 200,
channels: 4,
background: { r: 0, g: 128, b: 255, alpha: 1 },
},
})
.png()
.toBuffer();
const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage); const picResult = await store.article.addImage(googleAuthId, article.sku, sampleImage);
const fullHash = picResult.fullHash; const fullHash = picResult.fullHash;
assert.equal(picResult.smallThumb.w, 40); assert.equal(picResult.smallThumb.w, 32);
assert.equal(picResult.smallThumb.h, 27); assert.equal(picResult.smallThumb.h, 21);
const smallThumbRow = provider.db.prepare( const smallThumbRow = provider.db.prepare(
'SELECT smallthumbw, smallthumbh, smallthumbbinary FROM pic WHERE hash = ?', 'SELECT smallthumbw, smallthumbh, smallthumbbinary FROM pic WHERE hash = ?',
).get(fullHash); ).get(fullHash);
assert.equal(smallThumbRow.smallthumbw, 40); assert.equal(smallThumbRow.smallthumbw, 32);
assert.equal(smallThumbRow.smallthumbh, 27); assert.equal(smallThumbRow.smallthumbh, 21);
assert(Buffer.isBuffer(smallThumbRow.smallthumbbinary), 'Small thumb AVIF must be stored in the pic row'); assert(Buffer.isBuffer(smallThumbRow.smallthumbbinary), 'Small thumb AVIF must be stored in the pic row');
const smallThumbMetadata = await sharp(smallThumbRow.smallthumbbinary).metadata(); const smallThumbMetadata = await optimizeImage({ image: smallThumbRow.smallthumbbinary, format: 'none' });
assert.equal(smallThumbMetadata.format, 'heif'); assert.equal(smallThumbMetadata.originalFormat, 'avif');
assert.equal(smallThumbMetadata.width, 40); assert.equal(smallThumbMetadata.originalWidth, 32);
assert.equal(smallThumbMetadata.height, 27); assert.equal(smallThumbMetadata.originalHeight, 21);
const brand = await store.brand.create(googleAuthId, { name: 'Small Thumb Test Brand' }); const brand = await store.brand.create(googleAuthId, { name: 'Small Thumb Test Brand' });
await store.brand.addImage(googleAuthId, brand.id, sampleImage); await store.brand.addImage(googleAuthId, brand.id, sampleImage);
@@ -232,8 +231,8 @@ async function runTests() {
assert(listedBrandWithSmallThumbs, 'Brand list with thumbnails should include the created brand'); assert(listedBrandWithSmallThumbs, 'Brand list with thumbnails should include the created brand');
assert.equal(listedBrandWithSmallThumbs.pics.length, 1); assert.equal(listedBrandWithSmallThumbs.pics.length, 1);
const listedPic = listedBrandWithSmallThumbs.pics[0]; const listedPic = listedBrandWithSmallThumbs.pics[0];
assert.equal(listedPic.smallThumbW, 40); assert.equal(listedPic.smallThumbW, 32);
assert.equal(listedPic.smallThumbH, 27); assert.equal(listedPic.smallThumbH, 21);
assert.equal(typeof listedPic.smallThumbBinary, 'string'); assert.equal(typeof listedPic.smallThumbBinary, 'string');
const listedSmallThumb = Buffer.from(listedPic.smallThumbBinary, 'base64'); const listedSmallThumb = Buffer.from(listedPic.smallThumbBinary, 'base64');
assert.deepEqual(listedSmallThumb, smallThumbRow.smallthumbbinary); assert.deepEqual(listedSmallThumb, smallThumbRow.smallthumbbinary);

View File

@@ -40,11 +40,13 @@ async function run() {
googleAuthId, googleAuthId,
parentId: company.rootCategoryId, parentId: company.rootCategoryId,
name: 'Component Article 1', name: 'Component Article 1',
track_stock: true,
}); });
const comp2 = await store.article.createUnderParent({ const comp2 = await store.article.createUnderParent({
googleAuthId, googleAuthId,
parentId: company.rootCategoryId, parentId: company.rootCategoryId,
name: 'Component Article 2', name: 'Component Article 2',
track_stock: true,
}); });
// Create a bundle article // Create a bundle article

436
scripts/test-syncqueue.js Normal file
View File

@@ -0,0 +1,436 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing posterminalsyncqueue table and sync queue flow ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-syncqueue-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
const clearQueue = () => {
provider.db.prepare('DELETE FROM posterminalsyncqueue').run();
};
const getQueue = () =>
provider.db
.prepare('SELECT posterminal_id, entity_type, entity_id FROM posterminalsyncqueue ORDER BY id ASC')
.all();
try {
// 1. Verify posterminalsyncqueue table exists and has expected columns
console.log('1. Checking posterminalsyncqueue schema...');
const tableInfo = provider.db.pragma('table_info("posterminalsyncqueue")');
const colNames = tableInfo.map((c) => c.name);
console.log('posterminalsyncqueue columns:', colNames);
assert.deepEqual(colNames, [
'id',
'posterminal_id',
'entity_type',
'entity_id',
]);
// 2. Setup company & 2 POS terminals + another company with 1 terminal (to test company scoping)
console.log('2. Setting up base data with 2 terminals in company 1 and 1 terminal in company 2...');
const googleAuthId = await store.google_auth.add('syncqueue-tester@example.com');
const company1 = await store.company.create({
name: 'SyncQueue Corp 1',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
const company2 = await store.company.create({
name: 'SyncQueue Corp 2',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company2.id);
const otherTerminal = await store.posterminal.upsert(googleAuthId, {
name: 'Other Corp Terminal',
pairingpin: '9999',
});
await store.google_auth.setSelectedCompany(googleAuthId, company1.id);
const term1 = await store.posterminal.upsert(googleAuthId, {
name: 'POS Terminal 1',
pairingpin: '1111',
});
const term2 = await store.posterminal.upsert(googleAuthId, {
name: 'POS Terminal 2',
pairingpin: '2222',
});
// Helper to assert that both term1 and term2 received the expected entity_type + entity_id, and otherTerminal did not
const assertQueuedForBothTerminals = (entityType, entityId, label) => {
const rows = getQueue();
const matching = rows.filter((r) => r.entity_type === entityType && r.entity_id === entityId);
const termIds = matching.map((r) => r.posterminal_id).sort((a, b) => a - b);
assert.deepEqual(
termIds,
[term1.id, term2.id],
`Expected [${term1.id}, ${term2.id}] for ${label} (${entityType}:${entityId}), got ${JSON.stringify(rows)}`,
);
const otherRows = rows.filter((r) => r.posterminal_id === otherTerminal.id);
assert.equal(otherRows.length, 0, `Other company terminal should not receive rows (${label})`);
};
// 3. Test Category operations
console.log('3. Testing category operations...');
clearQueue();
const catA = await store.category.createUnderParent({
googleAuthId,
parentId: company1.rootCategoryId,
name: 'Category A',
});
assertQueuedForBothTerminals('category', catA.id, 'category.createUnderParent');
clearQueue();
await store.category.rename({
googleAuthId,
id: catA.id,
name: 'Category A Renamed',
});
assertQueuedForBothTerminals('category', catA.id, 'category.rename');
clearQueue();
const catLeafId = await store.category.ensureByPath({
googleAuthId,
parentId: company1.rootCategoryId,
path: 'Category B | Subcategory B1',
});
assertQueuedForBothTerminals('category', catLeafId, 'category.ensureByPath');
const catBRow = provider.db
.prepare('SELECT c.id FROM category c WHERE c.parent_id = ? AND c.id != ?')
.get(company1.rootCategoryId, catA.id);
clearQueue();
await store.category.order({
googleAuthId,
parentId: company1.rootCategoryId,
categoryIds: [catBRow.id, catA.id],
});
assertQueuedForBothTerminals('category', catA.id, 'category.order');
assertQueuedForBothTerminals('category', catBRow.id, 'category.order (sibling)');
clearQueue();
await store.category.move({
googleAuthId,
id: catLeafId,
parentId: catA.id,
});
assertQueuedForBothTerminals('category', catLeafId, 'category.move');
const prop = await store.propertyname.create(googleAuthId, { name: 'Color', exclusive: false });
clearQueue();
await store.categoryproperty.setByCategoryId(googleAuthId, catA.id, [
{ propertyId: prop.id, value: 'Red' },
]);
assertQueuedForBothTerminals('category', catA.id, 'categoryproperty.setByCategoryId');
clearQueue();
await store.categoryproperty.delete(googleAuthId, catA.id, prop.id);
assertQueuedForBothTerminals('category', catA.id, 'categoryproperty.delete');
const attr = await store.attributename.create(googleAuthId, { name: 'Size', exclusive: true });
const attrVal = await store.attributevalue.create(googleAuthId, attr.id, { value: 'XL' });
clearQueue();
await store.categoryattributevalue.setByCategoryId(googleAuthId, catA.id, [attrVal.id]);
assertQueuedForBothTerminals('category', catA.id, 'categoryattributevalue.setByCategoryId');
// 4. Test Article operations
console.log('4. Testing article operations...');
clearQueue();
const art1 = await store.article.createUnderParent({
googleAuthId,
parentId: catA.id,
name: 'Cola 0.33L',
});
assertQueuedForBothTerminals('article', art1.id, 'article.createUnderParent');
clearQueue();
await store.article.updateBySku(googleAuthId, art1.sku, { name: 'Cola Classic 0.33L' });
assertQueuedForBothTerminals('article', art1.id, 'article.updateBySku');
clearQueue();
await store.article.setPricesBySku(googleAuthId, art1.sku, [{ net: 1.5, taxRate: 19 }]);
assertQueuedForBothTerminals('article', art1.id, 'article.setPricesBySku');
clearQueue();
await store.article.setEansBySku(googleAuthId, art1.sku, ['4001234567890']);
assertQueuedForBothTerminals('article', art1.id, 'article.setEansBySku');
const brand = await store.brand.create(googleAuthId, { name: 'ColaBrand' });
clearQueue();
await store.article.setBrandIdBySku(googleAuthId, art1.sku, brand.id);
assertQueuedForBothTerminals('article', art1.id, 'article.setBrandIdBySku');
clearQueue();
await store.brand.update(googleAuthId, brand.id, { name: 'ColaBrand Updated' });
assertQueuedForBothTerminals('article', art1.id, 'brand.update');
clearQueue();
await store.article.setWeightBySku(googleAuthId, art1.sku, 0.35);
assertQueuedForBothTerminals('article', art1.id, 'article.setWeightBySku');
const wh = await store.warehouse.create(googleAuthId, { name: 'Main Warehouse' });
clearQueue();
await store.article.setTrackStockBySku(googleAuthId, art1.sku, true);
assertQueuedForBothTerminals('article', art1.id, 'article.setTrackStockBySku');
clearQueue();
await store.stock.adjust(googleAuthId, {
sku: art1.sku,
warehouseId: wh.id,
amount: 10,
text: 'Initial stock',
});
assertQueuedForBothTerminals('article', art1.id, 'stock.adjust');
clearQueue();
await store.article.setCategoryIdsBySku(googleAuthId, art1.sku, [catA.id, catLeafId]);
assertQueuedForBothTerminals('article', art1.id, 'article.setCategoryIdsBySku');
clearQueue();
await store.articleproperty.setByArticleSku(googleAuthId, art1.sku, [
{ propertyId: prop.id, value: 'Dark' },
]);
assertQueuedForBothTerminals('article', art1.id, 'articleproperty.setByArticleSku');
clearQueue();
await store.articleproperty.delete(googleAuthId, art1.sku, prop.id);
assertQueuedForBothTerminals('article', art1.id, 'articleproperty.delete');
clearQueue();
await store.articleattributevalue.setByArticleSku(googleAuthId, art1.sku, [attrVal.id]);
assertQueuedForBothTerminals('article', art1.id, 'articleattributevalue.setByArticleSku');
clearQueue();
await store.attributevalue.update(googleAuthId, attrVal.id, { value: 'XXL' });
assertQueuedForBothTerminals('article', art1.id, 'attributevalue.update');
// Optionslist & Stückliste
const pfand = await store.article.createUnderParent({
googleAuthId,
parentId: catA.id,
name: 'Pfand 0.25',
});
clearQueue();
await store.optionslist.setOptionsByArticleSku(googleAuthId, art1.sku, [
{ sku: pfand.sku, count: 1, mandated: true },
]);
assertQueuedForBothTerminals('article', art1.sku ? art1.id : 0, 'optionslist.setOptionsByArticleSku');
// Updating option article price should also enqueue parent article!
clearQueue();
await store.article.setPricesBySku(googleAuthId, pfand.sku, [{ net: 0.25, taxRate: 19 }]);
assertQueuedForBothTerminals('article', pfand.id, 'setPricesBySku (option itself)');
assertQueuedForBothTerminals('article', art1.id, 'setPricesBySku (option parent)');
const bundle = await store.article.createUnderParent({
googleAuthId,
parentId: catA.id,
name: 'Cola Crate',
});
clearQueue();
await store.articlelist.setItemsByArticleSku(googleAuthId, bundle.sku, [
{ sku: art1.sku, count: 6 },
]);
assertQueuedForBothTerminals('article', bundle.id, 'articlelist.setItemsByArticleSku');
// Adjusting component stock should also enqueue Stückliste parent!
clearQueue();
await store.stock.adjust(googleAuthId, {
sku: art1.sku,
warehouseId: wh.id,
amount: 2,
text: 'Restock component',
});
assertQueuedForBothTerminals('article', art1.id, 'stock.adjust (component)');
assertQueuedForBothTerminals('article', bundle.id, 'stock.adjust (Stückliste parent)');
// 5. Test Customer Group operations
console.log('5. Testing customergroup operations...');
clearQueue();
const cg1 = await store.customergroup.create(googleAuthId, { name: 'B2B' });
assertQueuedForBothTerminals('customergroup', cg1.id, 'customergroup.create');
clearQueue();
await store.customergroup.update(googleAuthId, cg1.id, { name: 'B2B Gold' });
assertQueuedForBothTerminals('customergroup', cg1.id, 'customergroup.update');
const allGroups = await store.customergroup.list(googleAuthId);
clearQueue();
await store.customergroup.order(googleAuthId, {
customergroupIds: allGroups.map((g) => g.id).reverse(),
});
assertQueuedForBothTerminals('customergroup', cg1.id, 'customergroup.order');
// 6. Test Customer (Entity + Address + Contact) operations
console.log('6. Testing customer (entity, entityaddress, entitycontact) operations...');
clearQueue();
const cust = await store.entity.create(googleAuthId, {
customergroupId: cg1.id,
name: 'Doe',
firstName: 'John',
});
assertQueuedForBothTerminals('customer', cust.id, 'entity.create');
clearQueue();
await store.entity.update(googleAuthId, cust.id, { firstName: 'Johnny' });
assertQueuedForBothTerminals('customer', cust.id, 'entity.update');
clearQueue();
const addr = await store.entityaddress.create(googleAuthId, cust.id, {
addressType: 'billing',
street: 'Main St 1',
city: 'Berlin',
});
assertQueuedForBothTerminals('customer', cust.id, 'entityaddress.create');
clearQueue();
await store.entityaddress.update(googleAuthId, cust.id, addr.id, {
city: 'Munich',
});
assertQueuedForBothTerminals('customer', cust.id, 'entityaddress.update');
clearQueue();
await store.entityaddress.delete(googleAuthId, cust.id, addr.id);
assertQueuedForBothTerminals('customer', cust.id, 'entityaddress.delete');
clearQueue();
const contact = await store.entitycontact.create(googleAuthId, cust.id, {
contactType: 'email',
contact: 'john@example.com',
});
assertQueuedForBothTerminals('customer', cust.id, 'entitycontact.create');
clearQueue();
await store.entitycontact.update(googleAuthId, cust.id, contact.id, {
contact: 'johnny@example.com',
});
assertQueuedForBothTerminals('customer', cust.id, 'entitycontact.update');
clearQueue();
await store.entitycontact.delete(googleAuthId, cust.id, contact.id);
assertQueuedForBothTerminals('customer', cust.id, 'entitycontact.delete');
// 7. Test CHECK constraint on entity_type
console.log('7. Testing entity_type CHECK constraint...');
assert.throws(() => {
provider.db
.prepare('INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id) VALUES (?, ?, ?)')
.run(term1.id, 'invalid_type', 1);
}, /CHECK constraint failed/);
// 8. Test POS Sync Cursor (posterminalsyncqueue.id) across counts and lists
console.log('8. Testing POS Sync Cursor (posterminalsyncqueue.id) for counts & lists...');
// Create a fresh terminal (term3) and enable all categories on it to verify full initial sync + delta cursor
const term3 = await store.posterminal.upsert(googleAuthId, {
name: 'POS Terminal 3',
pairingpin: '3333',
});
await store.posterminal.setOnAll(googleAuthId, term3.id);
// Initial sync with cursor = 0
const initProductCount = await store.posterminal.getProductCount({ posterminalId: term3.id, cursor: 0 });
const initCategoryCount = await store.posterminal.getCategoryCount({ posterminalId: term3.id, cursor: 0 });
const initCustomerCount = await store.posterminal.getCustomerCount({ posterminalId: term3.id, cursor: 0 });
const initCustomerGroupCount = await store.posterminal.getCustomerGroupCount({ posterminalId: term3.id, cursor: 0 });
const initCompositeCount = await store.posterminal.getCompositeProductCount({ posterminalId: term3.id, cursor: 0 });
assert.ok(initProductCount > 0, `Expected initProductCount > 0, got ${initProductCount}`);
assert.ok(initCategoryCount > 0, `Expected initCategoryCount > 0, got ${initCategoryCount}`);
assert.ok(initCustomerCount > 0, `Expected initCustomerCount > 0, got ${initCustomerCount}`);
assert.ok(initCustomerGroupCount > 0, `Expected initCustomerGroupCount > 0, got ${initCustomerGroupCount}`);
assert.ok(initCompositeCount > 0, `Expected initCompositeCount > 0, got ${initCompositeCount}`);
const products = await store.posterminal.getProductList({ posterminalId: term3.id, cursor: 0, limit: 100 });
const categories = await store.posterminal.getCategoryList({ posterminalId: term3.id, cursor: 0, limit: 100 });
const customers = await store.posterminal.getCustomerList({ posterminalId: term3.id, cursor: 0, limit: 100 });
const customerGroups = await store.posterminal.getCustomerGroupList({ posterminalId: term3.id, cursor: 0, limit: 100 });
assert.equal(products.length, initProductCount);
assert.equal(categories.length, initCategoryCount);
assert.equal(customers.length, initCustomerCount);
assert.equal(customerGroups.length, initCustomerGroupCount);
// Advance cursors to max lastChanged
const maxProductCursor = Math.max(...products.map((p) => Number(p.lastChanged)));
const maxCategoryCursor = Math.max(...categories.map((c) => Number(c.lastChanged)));
const maxCustomerCursor = Math.max(...customers.map((c) => Number(c.lastChanged)));
const maxCustomerGroupCursor = Math.max(...customerGroups.map((cg) => Number(cg.lastChanged)));
// Verify all counts and lists are 0 at the current cursors
assert.equal(await store.posterminal.getProductCount({ posterminalId: term3.id, cursor: maxProductCursor }), 0);
assert.equal(await store.posterminal.getCategoryCount({ posterminalId: term3.id, cursor: maxCategoryCursor }), 0);
assert.equal(await store.posterminal.getCustomerCount({ posterminalId: term3.id, cursor: maxCustomerCursor }), 0);
assert.equal(await store.posterminal.getCustomerGroupCount({ posterminalId: term3.id, cursor: maxCustomerGroupCursor }), 0);
assert.equal(await store.posterminal.getCompositeProductCount({ posterminalId: term3.id, cursor: maxProductCursor }), 0);
assert.equal((await store.posterminal.getProductList({ posterminalId: term3.id, cursor: maxProductCursor })).length, 0);
assert.equal((await store.posterminal.getCategoryList({ posterminalId: term3.id, cursor: maxCategoryCursor })).length, 0);
assert.equal((await store.posterminal.getCustomerList({ posterminalId: term3.id, cursor: maxCustomerCursor })).length, 0);
assert.equal((await store.posterminal.getCustomerGroupList({ posterminalId: term3.id, cursor: maxCustomerGroupCursor })).length, 0);
// Now touch one article twice, one category, one customer, one customergroup
await store.article.updateBySku(googleAuthId, art1.sku, { name: 'Cola Classic v2' });
await store.article.updateBySku(googleAuthId, art1.sku, { name: 'Cola Classic v3' });
await store.category.rename({ googleAuthId, id: catA.id, name: 'Category A v2' });
await store.entity.update(googleAuthId, cust.id, { firstName: 'John v2' });
await store.customergroup.update(googleAuthId, cg1.id, { name: 'B2B Platinum' });
// Even though art1 was updated twice, count should be 1 (deduplicated by entity_id with MAX(id) > cursor)
assert.equal(await store.posterminal.getProductCount({ posterminalId: term3.id, cursor: maxProductCursor }), 1);
assert.equal(await store.posterminal.getCategoryCount({ posterminalId: term3.id, cursor: maxCategoryCursor }), 1);
assert.equal(await store.posterminal.getCustomerCount({ posterminalId: term3.id, cursor: maxCustomerCursor }), 1);
assert.equal(await store.posterminal.getCustomerGroupCount({ posterminalId: term3.id, cursor: maxCustomerGroupCursor }), 1);
const deltaProducts = await store.posterminal.getProductList({ posterminalId: term3.id, cursor: maxProductCursor });
const deltaCategories = await store.posterminal.getCategoryList({ posterminalId: term3.id, cursor: maxCategoryCursor });
const deltaCustomers = await store.posterminal.getCustomerList({ posterminalId: term3.id, cursor: maxCustomerCursor });
const deltaCustomerGroups = await store.posterminal.getCustomerGroupList({ posterminalId: term3.id, cursor: maxCustomerGroupCursor });
assert.equal(deltaProducts.length, 1);
assert.equal(deltaProducts[0]._id, String(art1.id));
assert.ok(Number(deltaProducts[0].lastChanged) > maxProductCursor);
assert.equal(deltaCategories.length, 1);
assert.equal(deltaCategories[0]._id, String(catA.id));
assert.ok(Number(deltaCategories[0].lastChanged) > maxCategoryCursor);
assert.equal(deltaCustomers.length, 1);
assert.equal(deltaCustomers[0].id, String(cust.id));
assert.ok(Number(deltaCustomers[0].lastChanged) > maxCustomerCursor);
assert.equal(deltaCustomerGroups.length, 1);
assert.equal(deltaCustomerGroups[0].customerGroupId, String(cg1.id));
assert.ok(Number(deltaCustomerGroups[0].lastChanged) > maxCustomerGroupCursor);
console.log('✅ ALL POSTERMINALSYNCQUEUE TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -1,210 +0,0 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing articletombstone table and deletion flow ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-tombstone-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
// 1. Verify table exists and has expected columns
console.log('1. Checking articletombstone schema...');
const tableInfo = provider.db.pragma('table_info("articletombstone")');
const colNames = tableInfo.map((c) => c.name);
console.log('articletombstone columns:', colNames);
assert.deepEqual(colNames, [
'id',
'company_id',
'article_id',
'sku',
'name',
'taxrate',
'net',
'deleted_at',
]);
// Setup company first
console.log('2. Setting up base data...');
const googleAuthId = await store.google_auth.add('tombstone-tester@example.com');
const company = await store.company.create({
name: 'Tombstone Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// 3. Test CHECK constraint (article_id IS NOT NULL OR sku IS NOT NULL)
console.log('3. Testing CHECK constraint...');
// Setting both article_id and sku to NULL should fail
assert.throws(() => {
provider.db.prepare(`
INSERT INTO articletombstone (company_id, article_id, sku, name)
VALUES (?, NULL, NULL, 'Test')
`).run(company.id);
}, /CHECK constraint failed/);
// article_id present, sku null -> should succeed
const r1 = provider.db.prepare(`
INSERT INTO articletombstone (company_id, article_id, sku, name)
VALUES (?, 999, NULL, 'Only ID')
`).run(company.id);
assert.ok(r1.lastInsertRowid);
// sku present, article_id null -> should succeed
const r2 = provider.db.prepare(`
INSERT INTO articletombstone (company_id, article_id, sku, name)
VALUES (?, NULL, 'SKU-ONLY', 'Only SKU')
`).run(company.id);
assert.ok(r2.lastInsertRowid);
// Clean up manual test entries
provider.db.prepare('DELETE FROM articletombstone').run();
// 4. Test article deletion flow
console.log('4. Creating test article...');
const article = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Coffee Mug',
});
// Set prices for the article
await store.article.setPricesBySku(googleAuthId, article.sku, [
{ price: 12.50, tax: 19, minmultiplier: 1 },
]);
// Verify article exists
const detailBefore = await store.article.getBySku(googleAuthId, article.sku);
assert.equal(detailBefore.article.name, 'Coffee Mug');
assert.equal(detailBefore.article.prices[0].net, 12.50);
assert.equal(detailBefore.article.prices[0].taxRate, 19);
// Delete the article
console.log('4. Deleting article and checking tombstone creation...');
await store.article.delete(googleAuthId, article.sku);
// Verify article is deleted
await assert.rejects(
() => store.article.getBySku(googleAuthId, article.sku),
/Article not found/
);
// Verify tombstone record
const tombstones = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(article.sku);
assert.equal(tombstones.length, 1);
const tomb = tombstones[0];
console.log('Created tombstone:', tomb);
assert.equal(tomb.company_id, company.id);
assert.equal(tomb.article_id, article.id);
assert.equal(tomb.sku, article.sku);
assert.equal(tomb.name, 'Coffee Mug');
assert.equal(tomb.net, 12.50);
assert.equal(tomb.taxrate, 19);
assert.ok(tomb.deleted_at);
// 5. Test cascading deletion with parent and variants
console.log('5. Testing parent with variant child deletion...');
const parent = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Pants',
});
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
await store.article.toggleParent(googleAuthId, parent.sku, {
isParent: true,
attributeNameId: attrName.id,
});
const child = await store.article.addChild(googleAuthId, parent.sku, { value: 'M' });
await store.article.setPricesBySku(googleAuthId, child.article.sku, [
{ price: 49.99, tax: 19, minmultiplier: 1 },
]);
// Delete child via removeChild with deleteArticle
console.log('Deleting child via removeChild({ deleteArticle: true })...');
await store.article.removeChild(googleAuthId, parent.sku, child.article.sku, { deleteArticle: true });
// Add another child and delete parent with cascade: true
const child2 = await store.article.addChild(googleAuthId, parent.sku, { value: 'L' });
console.log('Deleting parent with cascade: true...');
await store.article.delete(googleAuthId, parent.sku, { cascade: true });
const allTombstones = provider.db.prepare('SELECT * FROM articletombstone ORDER BY id ASC').all();
assert.equal(allTombstones.length, 4);
// 1st tombstone: Coffee Mug
assert.equal(allTombstones[0].name, 'Coffee Mug');
assert.equal(allTombstones[0].sku, '1');
assert.equal(allTombstones[0].net, 12.5);
assert.equal(allTombstones[0].taxrate, 19);
// 2nd tombstone: Pants - M (deleted via removeChild)
assert.equal(allTombstones[1].name, 'Pants - M');
assert.equal(allTombstones[1].sku, child.article.sku);
assert.equal(allTombstones[1].net, 49.99);
assert.equal(allTombstones[1].taxrate, 19);
// 3rd tombstone: Pants - L (child deleted in cascade)
assert.equal(allTombstones[2].name, 'Pants - L');
assert.equal(allTombstones[2].sku, child2.article.sku);
// 4th tombstone: Pants (parent deleted in cascade)
assert.equal(allTombstones[3].name, 'Pants');
assert.equal(allTombstones[3].sku, parent.sku);
// 6. Test posterminal maxknowntombstoneid column and updates
console.log('6. Testing posterminal maxknowntombstoneid column...');
const posterminalCols = provider.db.pragma('table_info("posterminal")').map((c) => c.name);
assert.ok(posterminalCols.includes('maxknowntombstoneid'), 'posterminal should have maxknowntombstoneid column');
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS Terminal 1',
pairingpin: '1234',
});
assert.equal(terminal.maxknowntombstoneid, null);
// Pair terminal
await store.posterminal.pair(terminal.id, {
token: 'tok-123',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
});
const foundByToken = await store.posterminal.findByToken('tok-123');
assert.equal(foundByToken.id, terminal.id);
assert.equal(foundByToken.maxknowntombstoneid, null);
// Update maxknowntombstoneid
await store.posterminal.setMaxKnownTombstoneId(terminal.id, 4);
const foundAfterUpdate = await store.posterminal.findByToken('tok-123');
assert.equal(foundAfterUpdate.maxknowntombstoneid, 4);
console.log('✅ ALL TOMBSTONE TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -1,6 +1,13 @@
import companyuserGetByEmail from './store/companyuser/getByEmail.js'; import companyuserGetByEmail from './store/companyuser/getByEmail.js';
import companyuserAdd from './store/companyuser/add.js'; import companyuserAdd from './store/companyuser/add.js';
import companyuserDelete from './store/companyuser/delete.js'; import companyuserDelete from './store/companyuser/delete.js';
import companyuserList from './store/companyuser/list.js';
import companyuserAddByEmail from './store/companyuser/addByEmail.js';
import companyuserSetRole from './store/companyuser/setRole.js';
import companyuserRemove from './store/companyuser/remove.js';
import autoinviteAdd from './store/autoinvite/add.js';
import autoinviteList from './store/autoinvite/list.js';
import autoinviteRemove from './store/autoinvite/remove.js';
import companyCreate from './store/company/create.js'; import companyCreate from './store/company/create.js';
import companyUpdate from './store/company/update.js'; import companyUpdate from './store/company/update.js';
import companyDelete from './store/company/delete.js'; import companyDelete from './store/company/delete.js';
@@ -18,6 +25,7 @@ import categoryOrder from './store/category/order.js';
import categoryDelete from './store/category/delete.js'; import categoryDelete from './store/category/delete.js';
import categoryAddImage from './store/category/addImage.js'; import categoryAddImage from './store/category/addImage.js';
import categoryDeleteImage from './store/category/deleteImage.js'; import categoryDeleteImage from './store/category/deleteImage.js';
import categoryGenerateImage from './store/category/generateImage.js';
import articleDelete from './store/article/delete.js'; import articleDelete from './store/article/delete.js';
import articleDeleteImage from './store/article/deleteImage.js'; import articleDeleteImage from './store/article/deleteImage.js';
import articleOrderImages from './store/article/orderImages.js'; import articleOrderImages from './store/article/orderImages.js';
@@ -25,6 +33,7 @@ import articleAddImage from './store/article/addImage.js';
import articleCreateUnderParent from './store/article/createUnderParent.js'; import articleCreateUnderParent from './store/article/createUnderParent.js';
import articleGetBySku from './store/article/getBySku.js'; import articleGetBySku from './store/article/getBySku.js';
import articleGetHistoryBySku from './store/article/getHistoryBySku.js'; import articleGetHistoryBySku from './store/article/getHistoryBySku.js';
import articleListHistory from './store/article/listHistory.js';
import articleListSearchIndex from './store/article/listSearchIndex.js'; import articleListSearchIndex from './store/article/listSearchIndex.js';
import articleListExport from './store/article/listExport.js'; import articleListExport from './store/article/listExport.js';
import articleUpdateBySku from './store/article/updateBySku.js'; import articleUpdateBySku from './store/article/updateBySku.js';
@@ -90,6 +99,8 @@ import deliveryGetById from './store/delivery/getById.js';
import deliveryCreate from './store/delivery/create.js'; import deliveryCreate from './store/delivery/create.js';
import deliveryUpdate from './store/delivery/update.js'; import deliveryUpdate from './store/delivery/update.js';
import deliveryDelete from './store/delivery/delete.js'; import deliveryDelete from './store/delivery/delete.js';
import deliveryBook from './store/delivery/book.js';
import deliveryRebuildSummaries from './store/delivery/rebuildSummaries.js';
import quoteList from './store/quote/list.js'; import quoteList from './store/quote/list.js';
import quoteGetById from './store/quote/getById.js'; import quoteGetById from './store/quote/getById.js';
import quoteCreate from './store/quote/create.js'; import quoteCreate from './store/quote/create.js';
@@ -99,9 +110,25 @@ import quoteRebuildSummaries from './store/quote/rebuildSummaries.js';
import orderList from './store/order/list.js'; import orderList from './store/order/list.js';
import orderGetById from './store/order/getById.js'; import orderGetById from './store/order/getById.js';
import orderCreate from './store/order/create.js'; import orderCreate from './store/order/create.js';
import orderCreateFromQuote from './store/order/createFromQuote.js';
import orderCreateShipment from './store/order/createShipment.js';
import orderUpdate from './store/order/update.js'; import orderUpdate from './store/order/update.js';
import orderDelete from './store/order/delete.js'; import orderDelete from './store/order/delete.js';
import orderRebuildSummaries from './store/order/rebuildSummaries.js'; import orderRebuildSummaries from './store/order/rebuildSummaries.js';
import returnList from './store/return/list.js';
import returnGetById from './store/return/getById.js';
import returnCreate from './store/return/create.js';
import returnUpdate from './store/return/update.js';
import returnDelete from './store/return/delete.js';
import returnBook from './store/return/book.js';
import returnRebuildSummaries from './store/return/rebuildSummaries.js';
import retoureList from './store/retoure/list.js';
import retoureGetById from './store/retoure/getById.js';
import retoureCreate from './store/retoure/create.js';
import retoureCreateShipment from './store/retoure/createShipment.js';
import retoureUpdate from './store/retoure/update.js';
import retoureDelete from './store/retoure/delete.js';
import retoureRebuildSummaries from './store/retoure/rebuildSummaries.js';
import posterminalList from './store/posterminal/list.js'; import posterminalList from './store/posterminal/list.js';
import posterminalUpsert from './store/posterminal/upsert.js'; import posterminalUpsert from './store/posterminal/upsert.js';
import posterminalDelete from './store/posterminal/delete.js'; import posterminalDelete from './store/posterminal/delete.js';
@@ -116,14 +143,20 @@ import posterminalPair from './store/posterminal/pair.js';
import posterminalGetProductCount from './store/posterminal/getProductCount.js'; import posterminalGetProductCount from './store/posterminal/getProductCount.js';
import posterminalGetCategoryCount from './store/posterminal/getCategoryCount.js'; import posterminalGetCategoryCount from './store/posterminal/getCategoryCount.js';
import posterminalGetCustomerCount from './store/posterminal/getCustomerCount.js'; import posterminalGetCustomerCount from './store/posterminal/getCustomerCount.js';
import posterminalGetCustomerList from './store/posterminal/getCustomerList.js';
import posterminalGetCustomerGroupCount from './store/posterminal/getCustomerGroupCount.js'; import posterminalGetCustomerGroupCount from './store/posterminal/getCustomerGroupCount.js';
import posterminalGetCustomerGroupList from './store/posterminal/getCustomerGroupList.js';
import posterminalGetCompositeProductCount from './store/posterminal/getCompositeProductCount.js'; import posterminalGetCompositeProductCount from './store/posterminal/getCompositeProductCount.js';
import posterminalGetDeletedEntityCount from './store/posterminal/getDeletedEntityCount.js'; import posterminalGetDeletedEntityCount from './store/posterminal/getDeletedEntityCount.js';
import posterminalGetDeletedEntityList from './store/posterminal/getDeletedEntityList.js';
import posterminalRecordObituary from './store/posterminal/recordObituary.js';
import posterminalRecordSyncQueue from './store/posterminal/recordSyncQueue.js';
import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js'; import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js';
import posterminalGetCategoryList from './store/posterminal/getCategoryList.js'; import posterminalGetCategoryList from './store/posterminal/getCategoryList.js';
import posterminalGetProductList from './store/posterminal/getProductList.js'; import posterminalGetProductList from './store/posterminal/getProductList.js';
import posterminalSetMaxKnownTombstoneId from './store/posterminal/setMaxKnownTombstoneId.js'; import posterminalSetMaxKnownObituaryId from './store/posterminal/setMaxKnownObituaryId.js';
import posterminalTouchLastseen from './store/posterminal/touchLastseen.js'; import posterminalTouchLastseen from './store/posterminal/touchLastseen.js';
import posterminalSetClientversion from './store/posterminal/setClientversion.js';
import langCreate from './store/lang/create.js'; import langCreate from './store/lang/create.js';
import langDelete from './store/lang/delete.js'; import langDelete from './store/lang/delete.js';
import langList from './store/lang/list.js'; import langList from './store/lang/list.js';
@@ -155,18 +188,45 @@ import manifestattachmentOrder from './store/manifestattachment/order.js';
import ledgerAdd from './store/ledger/add.js'; import ledgerAdd from './store/ledger/add.js';
import ledgerListByManifestarticleId from './store/ledger/listByManifestarticleId.js'; import ledgerListByManifestarticleId from './store/ledger/listByManifestarticleId.js';
import ledgerListBySku from './store/ledger/listBySku.js'; import ledgerListBySku from './store/ledger/listBySku.js';
import ledgerListHistory from './store/ledger/listHistory.js';
import warehouseCreate from './store/warehouse/create.js'; import warehouseCreate from './store/warehouse/create.js';
import warehouseGetById from './store/warehouse/getById.js'; import warehouseGetById from './store/warehouse/getById.js';
import warehouseList from './store/warehouse/list.js'; import warehouseList from './store/warehouse/list.js';
import warehouseUpdate from './store/warehouse/update.js'; import warehouseUpdate from './store/warehouse/update.js';
import warehouseDelete from './store/warehouse/delete.js'; import warehouseDelete from './store/warehouse/delete.js';
import warehouseOrder from './store/warehouse/order.js'; import warehouseOrder from './store/warehouse/order.js';
import carrierCreate from './store/carrier/create.js';
import carrierGetById from './store/carrier/getById.js';
import carrierList from './store/carrier/list.js';
import carrierUpdate from './store/carrier/update.js';
import carrierDelete from './store/carrier/delete.js';
import carrierOrder from './store/carrier/order.js';
import printerCreate from './store/printer/create.js'; import printerCreate from './store/printer/create.js';
import printerGetById from './store/printer/getById.js'; import printerGetById from './store/printer/getById.js';
import printerList from './store/printer/list.js'; import printerList from './store/printer/list.js';
import printerUpdate from './store/printer/update.js'; import printerUpdate from './store/printer/update.js';
import printerDelete from './store/printer/delete.js'; import printerDelete from './store/printer/delete.js';
import printerOrder from './store/printer/order.js'; import printerOrder from './store/printer/order.js';
import printeragentList from './store/printeragent/list.js';
import printeragentCreate from './store/printeragent/create.js';
import printeragentUpdate from './store/printeragent/update.js';
import printeragentDelete from './store/printeragent/delete.js';
import printeragentFindByPairingpin from './store/printeragent/findByPairingpin.js';
import printeragentFindByToken from './store/printeragent/findByToken.js';
import printeragentPair from './store/printeragent/pair.js';
import printeragentUpdateState from './store/printeragent/updateState.js';
import printeragentTouchLastseen from './store/printeragent/touchLastseen.js';
import agentprinterList from './store/agentprinter/list.js';
import agentprinterGetById from './store/agentprinter/getById.js';
import agentprinterFindOrCreate from './store/agentprinter/findOrCreate.js';
import agentprinterUpdate from './store/agentprinter/update.js';
import agentprinterOrder from './store/agentprinter/order.js';
import agentprinterDelete from './store/agentprinter/delete.js';
import printerjobCreate from './store/printerjob/create.js';
import printerjobGetById from './store/printerjob/getById.js';
import printerjobList from './store/printerjob/list.js';
import printerjobUpdateState from './store/printerjob/updateState.js';
import printerjobDelete from './store/printerjob/delete.js';
import modelconversationCreate from './store/modelconversation/create.js'; import modelconversationCreate from './store/modelconversation/create.js';
import modelconversationGet from './store/modelconversation/get.js'; import modelconversationGet from './store/modelconversation/get.js';
import modelconversationList from './store/modelconversation/list.js'; import modelconversationList from './store/modelconversation/list.js';
@@ -179,6 +239,11 @@ import modelvfsRead from './store/modelvfs/read.js';
import modelvfsList from './store/modelvfs/list.js'; import modelvfsList from './store/modelvfs/list.js';
import modelvfsDelete from './store/modelvfs/delete.js'; import modelvfsDelete from './store/modelvfs/delete.js';
import modelvfsReset from './store/modelvfs/reset.js'; import modelvfsReset from './store/modelvfs/reset.js';
import subagentscheduleCreate from './store/subagentschedule/create.js';
import subagentscheduleGetById from './store/subagentschedule/getById.js';
import subagentscheduleList from './store/subagentschedule/list.js';
import subagentscheduleUpdate from './store/subagentschedule/update.js';
import subagentscheduleDelete from './store/subagentschedule/delete.js';
import accountCreate from './store/account/create.js'; import accountCreate from './store/account/create.js';
import accountList from './store/account/list.js'; import accountList from './store/account/list.js';
import accountDelete from './store/account/delete.js'; import accountDelete from './store/account/delete.js';
@@ -191,8 +256,11 @@ import shippingListByManifestId from './store/shipping/listByManifestId.js';
import shippingDelete from './store/shipping/delete.js'; import shippingDelete from './store/shipping/delete.js';
import shipmentCreate from './store/shipment/create.js'; import shipmentCreate from './store/shipment/create.js';
import shipmentGetById from './store/shipment/getById.js'; import shipmentGetById from './store/shipment/getById.js';
import shipmentList from './store/shipment/list.js';
import shipmentListByOrderId from './store/shipment/listByOrderId.js'; import shipmentListByOrderId from './store/shipment/listByOrderId.js';
import shipmentUpdate from './store/shipment/update.js';
import shipmentDelete from './store/shipment/delete.js'; import shipmentDelete from './store/shipment/delete.js';
import shipmentRebuildSummaries from './store/shipment/rebuildSummaries.js';
import packageCreate from './store/package/create.js'; import packageCreate from './store/package/create.js';
import packageListByShippingId from './store/package/listByShippingId.js'; import packageListByShippingId from './store/package/listByShippingId.js';
import packageDelete from './store/package/delete.js'; import packageDelete from './store/package/delete.js';
@@ -203,6 +271,9 @@ import labeltemplateList from './store/labeltemplate/list.js';
import labeltemplateUpdate from './store/labeltemplate/update.js'; import labeltemplateUpdate from './store/labeltemplate/update.js';
import labeltemplateDelete from './store/labeltemplate/delete.js'; import labeltemplateDelete from './store/labeltemplate/delete.js';
import stockAdjust from './store/stock/adjust.js'; import stockAdjust from './store/stock/adjust.js';
import stockAccept from './store/stock/accept.js';
import stockShip from './store/stock/ship.js';
import stockRebuildSummaries from './store/stock/rebuildSummaries.js';
import propertynameList from './store/propertyname/list.js'; import propertynameList from './store/propertyname/list.js';
import propertynameCreate from './store/propertyname/create.js'; import propertynameCreate from './store/propertyname/create.js';
import propertynameUpdate from './store/propertyname/update.js'; import propertynameUpdate from './store/propertyname/update.js';
@@ -246,6 +317,7 @@ import mcpGetRefreshToken from './store/mcp/getRefreshToken.js';
import mcpRevokeRefreshToken from './store/mcp/revokeRefreshToken.js'; import mcpRevokeRefreshToken from './store/mcp/revokeRefreshToken.js';
import mcpListAuthorizations from './store/mcp/listAuthorizations.js'; import mcpListAuthorizations from './store/mcp/listAuthorizations.js';
import mcpRevokeAuthorizations from './store/mcp/revokeAuthorizations.js'; import mcpRevokeAuthorizations from './store/mcp/revokeAuthorizations.js';
import healthcheckCheck from './store/healthcheck/check.js';
export default class LibDashStore { export default class LibDashStore {
/** /**
@@ -269,6 +341,16 @@ export default class LibDashStore {
getByEmail: companyuserGetByEmail(provider), getByEmail: companyuserGetByEmail(provider),
add: companyuserAdd(provider), add: companyuserAdd(provider),
delete: companyuserDelete(provider), delete: companyuserDelete(provider),
list: companyuserList(provider),
addByEmail: companyuserAddByEmail(provider),
setRole: companyuserSetRole(provider),
remove: companyuserRemove(provider),
};
this.autoinvite = {
add: autoinviteAdd(provider),
list: autoinviteList(provider),
remove: autoinviteRemove(provider),
}; };
this.company = { this.company = {
@@ -293,12 +375,14 @@ export default class LibDashStore {
delete: categoryDelete(provider), delete: categoryDelete(provider),
addImage: categoryAddImage(provider), addImage: categoryAddImage(provider),
deleteImage: categoryDeleteImage(provider), deleteImage: categoryDeleteImage(provider),
generateImage: categoryGenerateImage(provider),
}; };
this.article = { this.article = {
createUnderParent: articleCreateUnderParent(provider), createUnderParent: articleCreateUnderParent(provider),
getBySku: articleGetBySku(provider), getBySku: articleGetBySku(provider),
getHistoryBySku: articleGetHistoryBySku(provider), getHistoryBySku: articleGetHistoryBySku(provider),
listHistory: articleListHistory(provider),
listSearchIndex: articleListSearchIndex(provider), listSearchIndex: articleListSearchIndex(provider),
listExport: articleListExport(provider), listExport: articleListExport(provider),
updateBySku: articleUpdateBySku(provider), updateBySku: articleUpdateBySku(provider),
@@ -417,6 +501,8 @@ export default class LibDashStore {
create: deliveryCreate(provider), create: deliveryCreate(provider),
update: deliveryUpdate(provider), update: deliveryUpdate(provider),
delete: deliveryDelete(provider), delete: deliveryDelete(provider),
book: deliveryBook(provider),
rebuildSummaries: deliveryRebuildSummaries(provider),
}; };
this.quote = { this.quote = {
@@ -432,11 +518,33 @@ export default class LibDashStore {
list: orderList(provider), list: orderList(provider),
getById: orderGetById(provider), getById: orderGetById(provider),
create: orderCreate(provider), create: orderCreate(provider),
createFromQuote: orderCreateFromQuote(provider),
createShipment: orderCreateShipment(provider),
update: orderUpdate(provider), update: orderUpdate(provider),
delete: orderDelete(provider), delete: orderDelete(provider),
rebuildSummaries: orderRebuildSummaries(provider), rebuildSummaries: orderRebuildSummaries(provider),
}; };
this.return = {
list: returnList(provider),
getById: returnGetById(provider),
create: returnCreate(provider),
update: returnUpdate(provider),
delete: returnDelete(provider),
book: returnBook(provider),
rebuildSummaries: returnRebuildSummaries(provider),
};
this.retoure = {
list: retoureList(provider),
getById: retoureGetById(provider),
create: retoureCreate(provider),
createShipment: retoureCreateShipment(provider),
update: retoureUpdate(provider),
delete: retoureDelete(provider),
rebuildSummaries: retoureRebuildSummaries(provider),
};
this.posterminal = { this.posterminal = {
list: posterminalList(provider), list: posterminalList(provider),
upsert: posterminalUpsert(provider), upsert: posterminalUpsert(provider),
@@ -452,14 +560,20 @@ export default class LibDashStore {
getProductCount: posterminalGetProductCount(provider), getProductCount: posterminalGetProductCount(provider),
getCategoryCount: posterminalGetCategoryCount(provider), getCategoryCount: posterminalGetCategoryCount(provider),
getCustomerCount: posterminalGetCustomerCount(provider), getCustomerCount: posterminalGetCustomerCount(provider),
getCustomerList: posterminalGetCustomerList(provider),
getCustomerGroupCount: posterminalGetCustomerGroupCount(provider), getCustomerGroupCount: posterminalGetCustomerGroupCount(provider),
getCustomerGroupList: posterminalGetCustomerGroupList(provider),
getCompositeProductCount: posterminalGetCompositeProductCount(provider), getCompositeProductCount: posterminalGetCompositeProductCount(provider),
getDeletedEntityCount: posterminalGetDeletedEntityCount(provider), getDeletedEntityCount: posterminalGetDeletedEntityCount(provider),
getDeletedEntityList: posterminalGetDeletedEntityList(provider),
recordObituary: posterminalRecordObituary(provider),
recordSyncQueue: posterminalRecordSyncQueue(provider),
getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider), getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider),
getCategoryList: posterminalGetCategoryList(provider), getCategoryList: posterminalGetCategoryList(provider),
getProductList: posterminalGetProductList(provider), getProductList: posterminalGetProductList(provider),
setMaxKnownTombstoneId: posterminalSetMaxKnownTombstoneId(provider), setMaxKnownObituaryId: posterminalSetMaxKnownObituaryId(provider),
touchLastseen: posterminalTouchLastseen(provider), touchLastseen: posterminalTouchLastseen(provider),
setClientversion: posterminalSetClientversion(provider),
}; };
this.account = { this.account = {
@@ -484,8 +598,11 @@ export default class LibDashStore {
this.shipment = { this.shipment = {
create: shipmentCreate(provider), create: shipmentCreate(provider),
getById: shipmentGetById(provider), getById: shipmentGetById(provider),
list: shipmentList(provider),
listByOrderId: shipmentListByOrderId(provider), listByOrderId: shipmentListByOrderId(provider),
update: shipmentUpdate(provider),
delete: shipmentDelete(provider), delete: shipmentDelete(provider),
rebuildSummaries: shipmentRebuildSummaries(provider),
}; };
this.package = { this.package = {
@@ -527,6 +644,7 @@ export default class LibDashStore {
add: ledgerAdd(provider), add: ledgerAdd(provider),
listByManifestarticleId: ledgerListByManifestarticleId(provider), listByManifestarticleId: ledgerListByManifestarticleId(provider),
listBySku: ledgerListBySku(provider), listBySku: ledgerListBySku(provider),
listHistory: ledgerListHistory(provider),
}; };
this.warehouse = { this.warehouse = {
@@ -538,6 +656,15 @@ export default class LibDashStore {
order: warehouseOrder(provider), order: warehouseOrder(provider),
}; };
this.carrier = {
create: carrierCreate(provider),
getById: carrierGetById(provider),
list: carrierList(provider),
update: carrierUpdate(provider),
delete: carrierDelete(provider),
order: carrierOrder(provider),
};
this.printer = { this.printer = {
create: printerCreate(provider), create: printerCreate(provider),
getById: printerGetById(provider), getById: printerGetById(provider),
@@ -547,6 +674,35 @@ export default class LibDashStore {
order: printerOrder(provider), order: printerOrder(provider),
}; };
this.printeragent = {
list: printeragentList(provider),
create: printeragentCreate(provider),
update: printeragentUpdate(provider),
delete: printeragentDelete(provider),
findByPairingpin: printeragentFindByPairingpin(provider),
findByToken: printeragentFindByToken(provider),
pair: printeragentPair(provider),
updateState: printeragentUpdateState(provider),
touchLastseen: printeragentTouchLastseen(provider),
};
this.agentprinter = {
list: agentprinterList(provider),
getById: agentprinterGetById(provider),
findOrCreate: agentprinterFindOrCreate(provider),
update: agentprinterUpdate(provider),
order: agentprinterOrder(provider),
delete: agentprinterDelete(provider),
};
this.printerjob = {
create: printerjobCreate(provider),
getById: printerjobGetById(provider),
list: printerjobList(provider),
updateState: printerjobUpdateState(provider),
delete: printerjobDelete(provider),
};
this.modelconversation = { this.modelconversation = {
create: modelconversationCreate(provider), create: modelconversationCreate(provider),
get: modelconversationGet(provider), get: modelconversationGet(provider),
@@ -568,6 +724,14 @@ export default class LibDashStore {
add: modelmessageAdd(provider), add: modelmessageAdd(provider),
}; };
this.subagentschedule = {
create: subagentscheduleCreate(provider),
getById: subagentscheduleGetById(provider),
list: subagentscheduleList(provider),
update: subagentscheduleUpdate(provider),
delete: subagentscheduleDelete(provider),
};
this.labeltemplate = { this.labeltemplate = {
create: labeltemplateCreate(provider), create: labeltemplateCreate(provider),
getById: labeltemplateGetById(provider), getById: labeltemplateGetById(provider),
@@ -578,6 +742,9 @@ export default class LibDashStore {
this.stock = { this.stock = {
adjust: stockAdjust(provider), adjust: stockAdjust(provider),
accept: stockAccept(provider),
ship: stockShip(provider),
rebuildSummaries: stockRebuildSummaries(provider),
}; };
this.adjustment = this.stock; this.adjustment = this.stock;
@@ -654,8 +821,13 @@ export default class LibDashStore {
revokeAuthorizations: mcpRevokeAuthorizations(provider), revokeAuthorizations: mcpRevokeAuthorizations(provider),
}; };
this.healthcheck = {
check: healthcheckCheck(provider),
};
this.purge = storePurge(provider); this.purge = storePurge(provider);
this.initialize = storeInitialize(provider); this.initialize = storeInitialize(provider);
this.transaction = (fn) => provider.transaction(fn);
return this.initialize().then(() => this); return this.initialize().then(() => this);
} }

View File

@@ -0,0 +1,15 @@
/**
* Schema 2 → 1 (MSSQL): drops posterminal `sort`, `enabled`, `clientversion`.
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
*/
export default async function downgrade({ query, assertVersion, setVersion }) {
await assertVersion(2);
await query(`
ALTER TABLE posterminal DROP CONSTRAINT DF_posterminal_sort, DF_posterminal_enabled;
ALTER TABLE posterminal DROP COLUMN sort, enabled, clientversion;
`);
await setVersion(1);
}

View File

@@ -0,0 +1,11 @@
/**
* Schema 3 → 2 (MSSQL): drops the `printeragent` table.
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
*/
export default async function downgrade({ query, assertVersion, setVersion }) {
await assertVersion(3);
await query(`DROP TABLE IF EXISTS printeragent;`);
await setVersion(2);
}

View File

@@ -0,0 +1,38 @@
/**
* Schema 4 → 3 (MSSQL):
* - Drops agentprinter table
* - Removes enabled from printeragent
* - Restores clientserial, clientmodel, clientstate in printeragent
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
*/
export default async function downgrade({ query, assertVersion, setVersion }) {
await assertVersion(4);
await query(`DROP TABLE IF EXISTS agentprinter;`);
await query(`
IF OBJECT_ID(N'dbo.printeragent', N'U') IS NOT NULL
BEGIN
IF COL_LENGTH('printeragent', 'enabled') IS NOT NULL
BEGIN
DECLARE @defName NVARCHAR(255);
SELECT @defName = dc.name
FROM sys.default_constraints dc
JOIN sys.columns c ON c.default_object_id = dc.object_id
WHERE dc.parent_object_id = OBJECT_ID(N'dbo.printeragent') AND c.name = 'enabled';
IF @defName IS NOT NULL
EXEC('ALTER TABLE printeragent DROP CONSTRAINT ' + @defName);
ALTER TABLE printeragent DROP COLUMN enabled;
END
IF COL_LENGTH('printeragent', 'clientserial') IS NULL
ALTER TABLE printeragent ADD clientserial NVARCHAR(255) NULL;
IF COL_LENGTH('printeragent', 'clientmodel') IS NULL
ALTER TABLE printeragent ADD clientmodel NVARCHAR(255) NULL;
IF COL_LENGTH('printeragent', 'clientstate') IS NULL
ALTER TABLE printeragent ADD clientstate NVARCHAR(255) NULL;
END
`);
await setVersion(3);
}

View File

@@ -0,0 +1,13 @@
/**
* Schema 5 → 4 (MSSQL):
* - Drops printerjob table
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
*/
export default async function downgrade({ query, assertVersion, setVersion }) {
await assertVersion(5);
await query(`DROP TABLE IF EXISTS printerjob;`);
await setVersion(4);
}

View File

@@ -0,0 +1,18 @@
/**
* Schema 6 → 5 (MSSQL):
* - Drops autoinvite table
* - Drops companyuser.role
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
*/
export default async function downgrade({ query, assertVersion, setVersion }) {
await assertVersion(6);
await query(`DROP TABLE IF EXISTS autoinvite;`);
await query(`
ALTER TABLE companyuser DROP CONSTRAINT CK_companyuser_role, DF_companyuser_role;
`);
await query(`ALTER TABLE companyuser DROP COLUMN role;`);
await setVersion(5);
}

View File

@@ -4,8 +4,12 @@
* { rows, rowsAffected, insertId } * { rows, rowsAffected, insertId }
*/ */
import { join } from 'node:path';
import { mkdirSync, promises as fsPromises } from 'node:fs';
import sql from 'mssql'; import sql from 'mssql';
import { logSlowQuery } from '../provider.js'; import { logSlowQuery } from '../provider.js';
import { safeWrite, safeReadByHash, safeDeleteByHash } from '../storage.js';
import { upgradeSchema, downgradeSchema, readSchemaVersion } from './migrate.js';
function normalizeRows(result) { function normalizeRows(result) {
const recordset = result?.recordset ?? []; const recordset = result?.recordset ?? [];
@@ -16,13 +20,57 @@ function normalizeRows(result) {
} }
export class MssqlExecutor { export class MssqlExecutor {
/** @param {() => import('mssql').Request} requestFactory */ /**
constructor(requestFactory) { * @param {() => import('mssql').Request} requestFactory
* @param {string|null} [storageDir=null]
*/
constructor(requestFactory, storageDir = null) {
this.#requestFactory = requestFactory; this.#requestFactory = requestFactory;
this.storageDir = storageDir;
this.dialect = 'mssql';
} }
#requestFactory; #requestFactory;
async writePicFull(hash, buffer) {
if (!this.storageDir || !hash) return;
await safeWrite(join(this.storageDir, 'fullbinary', hash + '.avif'), buffer);
}
async readPicFull(hash) {
if (!this.storageDir || !hash) return null;
return safeReadByHash(join(this.storageDir, 'fullbinary'), hash, '.avif');
}
async writePicThumb(hash, buffer) {
if (!this.storageDir || !hash) return;
await safeWrite(join(this.storageDir, 'thumbbinary', hash + '.avif'), buffer);
}
async readPicThumb(hash) {
if (!this.storageDir || !hash) return null;
return safeReadByHash(join(this.storageDir, 'thumbbinary'), hash, '.avif');
}
async deletePic(hash) {
if (!this.storageDir || !hash) return;
await safeDeleteByHash(join(this.storageDir, 'fullbinary'), hash);
await safeDeleteByHash(join(this.storageDir, 'thumbbinary'), hash);
}
async purgeFiles() {
if (!this.storageDir) return;
for (const sub of ['fullbinary', 'thumbbinary']) {
const dir = join(this.storageDir, sub);
try {
const files = await fsPromises.readdir(dir);
await Promise.all(files.map((f) => fsPromises.unlink(join(dir, f))));
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
}
}
/** @returns {Promise<import('../provider.js').QueryResult>} */ /** @returns {Promise<import('../provider.js').QueryResult>} */
async query(sql, params = {}) { async query(sql, params = {}) {
const start = performance.now(); const start = performance.now();
@@ -73,8 +121,9 @@ export class MssqlProvider {
* Either an already-connected mssql ConnectionPool, or a plain mssql * Either an already-connected mssql ConnectionPool, or a plain mssql
* config object — in which case the pool is created lazily on first use * config object — in which case the pool is created lazily on first use
* (and closed by {@link MssqlProvider#close}). * (and closed by {@link MssqlProvider#close}).
* @param {{ storageDir?: string|null }} [options]
*/ */
constructor(poolOrConfig) { constructor(poolOrConfig, { storageDir = null } = {}) {
const isPool = const isPool =
poolOrConfig && typeof poolOrConfig.request === 'function'; poolOrConfig && typeof poolOrConfig.request === 'function';
if (!poolOrConfig || (!isPool && !poolOrConfig.server)) { if (!poolOrConfig || (!isPool && !poolOrConfig.server)) {
@@ -87,12 +136,50 @@ export class MssqlProvider {
// a different object after a lazy connection would leave them with null. // a different object after a lazy connection would leave them with null.
this.pool = isPool ? poolOrConfig : new sql.ConnectionPool(poolOrConfig); this.pool = isPool ? poolOrConfig : new sql.ConnectionPool(poolOrConfig);
this.#config = isPool ? null : poolOrConfig; this.#config = isPool ? null : poolOrConfig;
this.storageDir = storageDir || poolOrConfig?.storageDir || null;
this.dialect = 'mssql'; this.dialect = 'mssql';
} }
#config; #config;
#connecting; #connecting;
async #tryCreateDatabaseIfMissing() {
if (!this.#config?.database) return false;
const targetDb = String(this.#config.database);
if (targetDb.toLowerCase() === 'master') return false;
const masterConfig = {
...this.#config,
database: 'master',
pool: { min: 0, max: 1, idleTimeoutMillis: 5000 },
};
let masterPool;
try {
masterPool = await new sql.ConnectionPool(masterConfig).connect();
} catch {
return false;
}
try {
const checkReq = masterPool.request();
checkReq.input('dbname', sql.NVarChar, targetDb);
const res = await checkReq.query('SELECT DB_ID(@dbname) AS dbid');
if (res.recordset[0]?.dbid != null) {
return false;
}
const createReq = masterPool.request();
createReq.input('dbname', sql.NVarChar, targetDb);
await createReq.query(
"DECLARE @sql NVARCHAR(MAX) = N'CREATE DATABASE ' + QUOTENAME(@dbname); EXEC sp_executesql @sql;"
);
return true;
} finally {
await masterPool.close();
}
}
/** /**
* Returns the underlying pool, creating and connecting it on first use * Returns the underlying pool, creating and connecting it on first use
* when the provider was constructed from a config object. * when the provider was constructed from a config object.
@@ -101,12 +188,23 @@ export class MssqlProvider {
ensurePool() { ensurePool() {
if (this.pool.connected) return Promise.resolve(this.pool); if (this.pool.connected) return Promise.resolve(this.pool);
if (!this.#connecting) { if (!this.#connecting) {
this.#connecting = this.pool this.#connecting = (async () => {
.connect() try {
.then((pool) => { return await this.pool.connect();
return pool; } catch (err) {
}) if (err.code === 'ELOGIN') {
.catch((err) => { try {
const created = await this.#tryCreateDatabaseIfMissing();
if (created) {
return await this.pool.connect();
}
} catch {
// ignore creation error and fall through to rethrow original error
}
}
throw err;
}
})().catch((err) => {
this.#connecting = undefined; this.#connecting = undefined;
throw err; throw err;
}); });
@@ -115,12 +213,12 @@ export class MssqlProvider {
} }
makeExecutor(requestFactory = () => this.pool.request()) { makeExecutor(requestFactory = () => this.pool.request()) {
return new MssqlExecutor(requestFactory); return new MssqlExecutor(requestFactory, this.storageDir);
} }
async #withPool(fn) { async #withPool(fn) {
const pool = await this.ensurePool(); const pool = await this.ensurePool();
return fn(new MssqlExecutor(() => pool.request())); return fn(new MssqlExecutor(() => pool.request(), this.storageDir));
} }
async query(sql, params = {}) { async query(sql, params = {}) {
@@ -131,6 +229,45 @@ export class MssqlProvider {
return this.#withPool((executor) => executor.exec(procName, options)); return this.#withPool((executor) => executor.exec(procName, options));
} }
async writePicFull(hash, buffer) {
if (!this.storageDir || !hash) return;
await safeWrite(join(this.storageDir, 'fullbinary', hash + '.avif'), buffer);
}
async readPicFull(hash) {
if (!this.storageDir || !hash) return null;
return safeReadByHash(join(this.storageDir, 'fullbinary'), hash, '.avif');
}
async writePicThumb(hash, buffer) {
if (!this.storageDir || !hash) return;
await safeWrite(join(this.storageDir, 'thumbbinary', hash + '.avif'), buffer);
}
async readPicThumb(hash) {
if (!this.storageDir || !hash) return null;
return safeReadByHash(join(this.storageDir, 'thumbbinary'), hash, '.avif');
}
async deletePic(hash) {
if (!this.storageDir || !hash) return;
await safeDeleteByHash(join(this.storageDir, 'fullbinary'), hash);
await safeDeleteByHash(join(this.storageDir, 'thumbbinary'), hash);
}
async purgeFiles() {
if (!this.storageDir) return;
for (const sub of ['fullbinary', 'thumbbinary']) {
const dir = join(this.storageDir, sub);
try {
const files = await fsPromises.readdir(dir);
await Promise.all(files.map((f) => fsPromises.unlink(join(dir, f))));
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
}
}
/** /**
* Runs `fn(runner)` inside a transaction. Commit on success, * Runs `fn(runner)` inside a transaction. Commit on success,
* rollback on error. `runner` has query/exec like the provider. * rollback on error. `runner` has query/exec like the provider.
@@ -149,6 +286,12 @@ export class MssqlProvider {
runner.query = runner; runner.query = runner;
runner.exec = (proc, opts) => executor.exec(proc, opts); runner.exec = (proc, opts) => executor.exec(proc, opts);
runner.dialect = 'mssql'; runner.dialect = 'mssql';
runner.writePicFull = executor.writePicFull.bind(executor);
runner.readPicFull = executor.readPicFull.bind(executor);
runner.writePicThumb = executor.writePicThumb.bind(executor);
runner.readPicThumb = executor.readPicThumb.bind(executor);
runner.deletePic = executor.deletePic.bind(executor);
runner.purgeFiles = executor.purgeFiles.bind(executor);
const result = await fn(runner, tx); const result = await fn(runner, tx);
await tx.commit(); await tx.commit();
return result; return result;
@@ -162,8 +305,17 @@ export class MssqlProvider {
} }
} }
async initialize() { /**
* Initializes the MSSQL schema.
* @param {((message: string) => void) | { log?: (message: string) => void }} [options]
* @returns {Promise<void>}
*/
async initialize(options = {}) {
const pool = await this.ensurePool(); const pool = await this.ensurePool();
const log = typeof options === 'function' ? options : options?.log;
// Versioned upgrades first, so schema.js never meets an outdated table shape.
await upgradeSchema(this, log);
const { SCHEMA_SCRIPTS } = await import('./schema.js'); const { SCHEMA_SCRIPTS } = await import('./schema.js');
for (const script of SCHEMA_SCRIPTS) { for (const script of SCHEMA_SCRIPTS) {
if (script && script.trim()) { if (script && script.trim()) {
@@ -199,20 +351,57 @@ export class MssqlProvider {
ALTER TABLE category ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_category_updated_at DEFAULT SYSUTCDATETIME(); ALTER TABLE category ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_category_updated_at DEFAULT SYSUTCDATETIME();
END END
IF OBJECT_ID(N'dbo.payment', N'U') IS NOT NULL AND COL_LENGTH('payment', 'created_at') IS NULL
BEGIN
ALTER TABLE payment ADD created_at DATETIME2 NOT NULL CONSTRAINT DF_payment_created_at DEFAULT SYSUTCDATETIME();
END
IF OBJECT_ID(N'dbo.customergroup', N'U') IS NOT NULL AND COL_LENGTH('customergroup', 'updated_at') IS NULL IF OBJECT_ID(N'dbo.customergroup', N'U') IS NOT NULL AND COL_LENGTH('customergroup', 'updated_at') IS NULL
BEGIN BEGIN
ALTER TABLE customergroup ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_customergroup_updated_at DEFAULT SYSUTCDATETIME(); ALTER TABLE customergroup ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_customergroup_updated_at DEFAULT SYSUTCDATETIME();
END END
IF OBJECT_ID(N'dbo.printeragent', N'U') IS NOT NULL AND COL_LENGTH('printeragent', 'sort') IS NULL
BEGIN
ALTER TABLE printeragent ADD sort INT NOT NULL CONSTRAINT DF_printeragent_sort DEFAULT 0;
END
`); `);
} catch { } catch {
// ignore // ignore
} }
if (this.storageDir) {
for (const sub of ['fullbinary', 'thumbbinary']) {
mkdirSync(join(this.storageDir, sub), { recursive: true });
}
}
} }
async purge() { async purge() {
const pool = await this.ensurePool(); const pool = await this.ensurePool();
const { buildPurgeSql } = await import('./purgeSql.js'); const { buildPurgeSql } = await import('./purgeSql.js');
await pool.request().query(buildPurgeSql()); await pool.request().query(buildPurgeSql());
if (this.storageDir) {
await this.purgeFiles();
}
}
/**
* Current `meta.version`, or `null` if the database has none.
* @returns {Promise<number|null>}
*/
async getSchemaVersion() {
return readSchemaVersion((sql, params) => this.query(sql, params));
}
/**
* Runs downgrade steps down to `target`. CLI only (`npm run downgrade`).
* @param {number} target
* @param {(message: string) => void} [log]
* @returns {Promise<number>} Steps applied.
*/
async downgradeSchema(target, log) {
return downgradeSchema(this, target, log);
} }
close() { close() {

View File

@@ -0,0 +1,126 @@
/**
* MSSQL schema versioning (see ../schemaMigrations.js).
*
* Step context: `{ query, getVersion, assertVersion, setVersion }` where
* `query(sql, params)` runs inside the step's transaction (same contract as
* `provider.query`). Note: a column added by `ALTER TABLE` is not visible to
* later statements of the SAME batch — use separate `query()` calls.
*/
import {
BASE_SCHEMA_VERSION,
assertStepResult,
latestSchemaVersion,
makeVersionContext,
migrateSchema,
parseSchemaVersion,
} from '../schemaMigrations.js';
export const UPGRADE_DIR = new URL('./upgrade/', import.meta.url);
export const DOWNGRADE_DIR = new URL('./downgrade/', import.meta.url);
/** Schema version a fresh database is created at. */
export const SCHEMA_VERSION = latestSchemaVersion(UPGRADE_DIR);
/**
* @typedef {(sql: string, params?: object) => Promise<{ rows: any[] }>} QueryFn
*/
/**
* Reads `meta.version`, or `null` when the meta table/row is missing.
* @param {QueryFn} query
* @returns {Promise<number|null>}
*/
export async function readSchemaVersion(query) {
const result = await query(`
IF OBJECT_ID(N'dbo.meta', N'U') IS NOT NULL
SELECT value FROM meta WHERE [key] = N'version'
ELSE
SELECT CAST(NULL AS NVARCHAR(MAX)) AS value WHERE 1 = 0
`);
const row = result.rows[0];
return row ? parseSchemaVersion(row.value) : null;
}
async function writeSchemaVersion(query, version) {
await query(`
UPDATE meta SET value = @version WHERE [key] = N'version';
IF @@ROWCOUNT = 0 INSERT INTO meta ([key], value) VALUES (N'version', @version);
`, { version: String(version) });
}
async function hasAnyTable(query) {
const result = await query(`SELECT TOP 1 1 AS found FROM sys.tables WHERE is_ms_shipped = 0`);
return result.rows.length > 0;
}
/**
* @param {import('./index.js').MssqlProvider} provider
*/
function makeRunStep(provider) {
return async (step, fn) => {
await provider.transaction(async (runner) => {
const query = (sql, params) => runner.query(sql, params);
const ctx = {
query,
...makeVersionContext(step.file, () => readSchemaVersion(query), (v) => writeSchemaVersion(query, v)),
};
await fn(ctx);
assertStepResult(step, await readSchemaVersion(query));
});
};
}
/**
* Upgrades an existing database to `SCHEMA_VERSION`. Must run BEFORE the
* `schema.js` scripts so they never meet an outdated table shape.
* Fresh (empty) databases are skipped — `INIT_META` stamps them.
* A non-empty database without a version row is treated as version 1.
* @param {import('./index.js').MssqlProvider} provider
* @param {(message: string) => void} [log]
*/
export async function upgradeSchema(provider, log) {
const query = (sql, params) => provider.query(sql, params);
if (!(await hasAnyTable(query))) {
log?.(`Fresh database detected; schema will initialize directly at version ${SCHEMA_VERSION}`);
return 0;
}
let current = await readSchemaVersion(query);
if (current === null) {
log?.(`Existing database has no version row; setting baseline version ${BASE_SCHEMA_VERSION}`);
await query(`
IF OBJECT_ID(N'dbo.meta', N'U') IS NULL
CREATE TABLE meta ([key] NVARCHAR(255) NOT NULL, value NVARCHAR(MAX) NOT NULL, CONSTRAINT PK_meta PRIMARY KEY ([key]));
`);
await writeSchemaVersion(query, BASE_SCHEMA_VERSION);
current = BASE_SCHEMA_VERSION;
}
return migrateSchema({
direction: 'upgrade',
dirUrl: UPGRADE_DIR,
current,
target: SCHEMA_VERSION,
runStep: makeRunStep(provider),
log,
});
}
/**
* Downgrades the database to `target` (CLI only, never at server start).
* @param {import('./index.js').MssqlProvider} provider
* @param {number} target
* @param {(message: string) => void} [log]
*/
export async function downgradeSchema(provider, target, log) {
const current = await readSchemaVersion((sql, params) => provider.query(sql, params));
if (current === null) throw new Error('Database has no schema version (meta table missing)');
return migrateSchema({
direction: 'downgrade',
dirUrl: DOWNGRADE_DIR,
current,
target,
runStep: makeRunStep(provider),
log,
});
}

View File

@@ -1,9 +1,14 @@
/** Drop order = reverse of schema create dependency order. */ /** Drop order = reverse of schema create dependency order. */
export const TABLES = [ export const TABLES = [
'healthcheck',
'articlewarehousesummary',
'deliverysummary',
'shipmentsummary',
'quotesummary', 'quotesummary',
'ordersummary', 'ordersummary',
'package', 'package',
'shipping', 'shipping',
'carrier',
'payment', 'payment',
'account', 'account',
'articleattributevalue', 'articleattributevalue',
@@ -19,8 +24,13 @@ export const TABLES = [
'modelconversation', 'modelconversation',
'companyuser', 'companyuser',
'companysetting', 'companysetting',
'posterminalsyncqueue',
'posterminalobituary',
'posterminalcategory', 'posterminalcategory',
'posterminal', 'posterminal',
'printerjob',
'agentprinter',
'printeragent',
'printer', 'printer',
'numbers', 'numbers',
'ledger', 'ledger',

View File

@@ -0,0 +1,69 @@
export const AGENTPRINTER_COLUMNS = `
ap.id,
ap.printeragent_id AS printeragentId,
ap.type,
ap.model,
ap.serial,
ap.enabled,
ap.sort
`;
export const LIST_BY_COMPANY = `
SELECT
${AGENTPRINTER_COLUMNS}
FROM agentprinter ap
JOIN printeragent pa ON pa.id = ap.printeragent_id
WHERE pa.company_id = @companyId
ORDER BY ap.sort, ap.id
`;
export const LIST_BY_PRINTERAGENT = `
SELECT
${AGENTPRINTER_COLUMNS}
FROM agentprinter ap
JOIN printeragent pa ON pa.id = ap.printeragent_id
WHERE ap.printeragent_id = @printeragentId AND pa.company_id = @companyId
ORDER BY ap.sort, ap.id
`;
export const GET_BY_ID = `
SELECT
${AGENTPRINTER_COLUMNS}
FROM agentprinter ap
JOIN printeragent pa ON pa.id = ap.printeragent_id
WHERE ap.id = @id AND pa.company_id = @companyId
`;
export const FIND_BY_COMBO = `
SELECT
${AGENTPRINTER_COLUMNS}
FROM agentprinter ap
WHERE ap.printeragent_id = @printeragentId
AND ap.type = @type
AND (ap.serial = @serial OR (ap.serial IS NULL AND @serial IS NULL))
AND (ap.model = @model OR (ap.model IS NULL AND @model IS NULL))
`;
export const CREATE = `
INSERT INTO agentprinter (printeragent_id, type, model, serial, enabled, sort)
OUTPUT
INSERTED.id,
INSERTED.printeragent_id AS printeragentId,
INSERTED.type,
INSERTED.model,
INSERTED.serial,
INSERTED.enabled,
INSERTED.sort
VALUES (@printeragentId, @type, @model, @serial, @enabled, @sort)
`;
export const UPDATE = `
UPDATE agentprinter
SET enabled = @enabled, sort = @sort
WHERE id = @id AND printeragent_id IN (SELECT id FROM printeragent WHERE company_id = @companyId)
`;
export const DELETE_BY_COMPANY_AND_ID = `
DELETE FROM agentprinter
WHERE id = @id AND printeragent_id IN (SELECT id FROM printeragent WHERE company_id = @companyId)
`;

View File

@@ -29,6 +29,8 @@ export const ARTICLE_LIST_COLUMNS = `
a.sku, a.sku,
at.name, at.name,
at.slug, at.slug,
ISNULL(LEN(at.[desc]), 0) AS descLength,
at.seo AS seo,
pr.net, pr.net,
t.rate AS taxRate, t.rate AS taxRate,
a.track_stock, a.track_stock,
@@ -60,6 +62,9 @@ export const ARTICLE_LIST_COLUMNS = `
pic.thumbw AS thumbW, pic.thumbw AS thumbW,
pic.thumbh AS thumbH, pic.thumbh AS thumbH,
pic.thumbhash AS thumbHash, pic.thumbhash AS thumbHash,
pic.smallthumbw AS smallThumbW,
pic.smallthumbh AS smallThumbH,
pic.smallthumbbinary AS smallThumbBinary,
b.id AS brandId, b.id AS brandId,
b.name AS brandName, b.name AS brandName,
brand_pic.hash AS brandFullHash, brand_pic.hash AS brandFullHash,
@@ -114,7 +119,8 @@ WHERE id = @articleId
export const TOGGLE_PARENT = ` export const TOGGLE_PARENT = `
UPDATE article UPDATE article
SET is_parent = @isParent, SET is_parent = @isParent,
parentattributename_id = @parentAttributeNameId parentattributename_id = @parentAttributeNameId,
updated_at = SYSUTCDATETIME()
WHERE id = @articleId WHERE id = @articleId
`; `;
@@ -219,6 +225,65 @@ SET unit_id = @unitId, unitamount = @unitamount, updated_at = SYSUTCDATETIME()
WHERE id = @articleId WHERE id = @articleId
`; `;
export const TOUCH_UPDATED_AT = `
UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE id = @articleId
`;
export const TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT = `
UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE optionslist_id IN (
SELECT ola.optionslist_id
FROM optionslistarticle ola
WHERE ola.article_id = @articleId
)
`;
export const TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT = `
UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE articlelist_id IN (
SELECT ala.articlelist_id
FROM articlelistarticle ala
WHERE ala.article_id = @articleId
)
`;
export const ENQUEUE_SYNC_BY_ARTICLE = `
INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id)
SELECT pt.id, 'article', a.id
FROM article a
INNER JOIN posterminal pt ON pt.company_id = a.company_id
WHERE a.id = @articleId
`;
export const ENQUEUE_OPTION_PARENT_ARTICLES_SYNC = `
INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id)
SELECT pt.id, 'article', a.id
FROM article a
INNER JOIN posterminal pt ON pt.company_id = a.company_id
WHERE a.optionslist_id IN (
SELECT ola.optionslist_id
FROM optionslistarticle ola
WHERE ola.article_id = @articleId
)
`;
export const ENQUEUE_STUECKLISTE_ARTICLES_SYNC = `
INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id)
SELECT pt.id, 'article', a.id
FROM article a
INNER JOIN posterminal pt ON pt.company_id = a.company_id
WHERE a.articlelist_id IN (
SELECT ala.articlelist_id
FROM articlelistarticle ala
WHERE ala.article_id = @articleId
)
`;
export const CREATE = ` export const CREATE = `
INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock) INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock)
OUTPUT INSERTED.id OUTPUT INSERTED.id
@@ -245,6 +310,28 @@ INSERT INTO categoryarticle (category_id, article_id)
VALUES (@categoryId, @articleId) VALUES (@categoryId, @articleId)
`; `;
export const LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE = `
SELECT DISTINCT pt.id AS posterminal_id
FROM posterminal pt
INNER JOIN posterminalcategory pc ON pc.posterminal_id = pt.id
INNER JOIN categoryarticle ca ON ca.category_id = pc.category_id
WHERE ca.article_id = @articleId
AND pt.company_id = @companyId
AND pt.token IS NOT NULL
`;
export const INSERT_POSTERMINAL_OBITUARY = `
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
VALUES (@posterminalId, @entityType, @entityId)
`;
export const CLEAR_POSTERMINAL_OBITUARY = `
DELETE FROM posterminalobituary
WHERE posterminal_id = @posterminalId
AND entity_type = @entityType
AND entity_id = @entityId
`;
export const GET_BRAND_BY_COMPANY_AND_ID = ` export const GET_BRAND_BY_COMPANY_AND_ID = `
SELECT id SELECT id
FROM brand FROM brand
@@ -409,12 +496,12 @@ export const GET_ARTICLE_BY_SKU_FULL = `
co.default_language_id AS languageId, co.default_language_id AS languageId,
co.poscustomergroup_id AS posCustomerGroupId, co.poscustomergroup_id AS posCustomerGroupId,
cu.id AS companyuserId cu.id AS companyuserId
FROM article a FROM article a WITH (NOLOCK)
INNER JOIN company co ON co.id = a.company_id INNER JOIN company co WITH (NOLOCK) ON co.id = a.company_id
INNER JOIN google_auth ga ON ga.id = @googleAuthId INNER JOIN google_auth ga WITH (NOLOCK) ON ga.id = @googleAuthId
INNER JOIN companyuser cu INNER JOIN companyuser cu WITH (NOLOCK)
ON cu.company_id = a.company_id AND cu.google_auth_id = @googleAuthId ON cu.company_id = a.company_id AND cu.google_auth_id = @googleAuthId
LEFT JOIN categoryarticle ca ON ca.article_id = a.id LEFT JOIN categoryarticle ca WITH (NOLOCK) ON ca.article_id = a.id
WHERE a.sku = @sku WHERE a.sku = @sku
AND (ga.selected_company IS NULL OR a.company_id = ga.selected_company) AND (ga.selected_company IS NULL OR a.company_id = ga.selected_company)
) )
@@ -450,7 +537,7 @@ SELECT
bp.thumbhash AS brandThumbHash, bp.thumbhash AS brandThumbHash,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM optionslistarticle ola FROM optionslistarticle ola WITH (NOLOCK)
WHERE ola.optionslist_id = a.optionslist_id WHERE ola.optionslist_id = a.optionslist_id
) AS optionsCount, ) AS optionsCount,
a.optionslist_id AS optionslistId, a.optionslist_id AS optionslistId,
@@ -463,16 +550,16 @@ SELECT
ola.count, ola.count,
ola.sort, ola.sort,
ola.mandated ola.mandated
FROM optionslistarticle ola FROM optionslistarticle ola WITH (NOLOCK)
INNER JOIN article a_opt ON a_opt.id = ola.article_id INNER JOIN article a_opt WITH (NOLOCK) ON a_opt.id = ola.article_id
LEFT JOIN articletext at_opt ON at_opt.article_id = a_opt.id AND at_opt.language_id = auth.languageId LEFT JOIN articletext at_opt WITH (NOLOCK) ON at_opt.article_id = a_opt.id AND at_opt.language_id = auth.languageId
WHERE ola.optionslist_id = a.optionslist_id WHERE ola.optionslist_id = a.optionslist_id
ORDER BY ola.sort ASC, ola.id ASC ORDER BY ola.sort ASC, ola.id ASC
FOR JSON PATH FOR JSON PATH
) AS optionslistItemsJson, ) AS optionslistItemsJson,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM articlelistarticle ala FROM articlelistarticle ala WITH (NOLOCK)
WHERE ala.articlelist_id = a.articlelist_id WHERE ala.articlelist_id = a.articlelist_id
) AS articleListCount, ) AS articleListCount,
a.articlelist_id AS articlelistId, a.articlelist_id AS articlelistId,
@@ -484,16 +571,16 @@ SELECT
at_item.name, at_item.name,
ala.count, ala.count,
ala.sort ala.sort
FROM articlelistarticle ala FROM articlelistarticle ala WITH (NOLOCK)
INNER JOIN article a_item ON a_item.id = ala.article_id INNER JOIN article a_item WITH (NOLOCK) ON a_item.id = ala.article_id
LEFT JOIN articletext at_item ON at_item.article_id = a_item.id AND at_item.language_id = auth.languageId LEFT JOIN articletext at_item WITH (NOLOCK) ON at_item.article_id = a_item.id AND at_item.language_id = auth.languageId
WHERE ala.articlelist_id = a.articlelist_id WHERE ala.articlelist_id = a.articlelist_id
ORDER BY ala.sort ASC, ala.id ASC ORDER BY ala.sort ASC, ala.id ASC
FOR JSON PATH FOR JSON PATH
) AS articlelistItemsJson, ) AS articlelistItemsJson,
( (
SELECT COUNT(*) SELECT COUNT(*)
FROM article child FROM article child WITH (NOLOCK)
WHERE child.parentarticle_id = a.id WHERE child.parentarticle_id = a.id
) AS childCount, ) AS childCount,
a.is_parent AS isParent, a.is_parent AS isParent,
@@ -510,11 +597,11 @@ SELECT
at_child.name, at_child.name,
av.id AS attributeValueId, av.id AS attributeValueId,
av.value AS attributeValue av.value AS attributeValue
FROM article child FROM article child WITH (NOLOCK)
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId LEFT JOIN articletext at_child WITH (NOLOCK) ON at_child.article_id = child.id AND at_child.language_id = auth.languageId
LEFT JOIN ( LEFT JOIN (
articleattributevalue aav articleattributevalue aav WITH (NOLOCK)
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.parentattributename_id INNER JOIN attributevalue av WITH (NOLOCK) ON av.id = aav.attributevalue_id AND av.attribute_id = a.parentattributename_id
) ON aav.article_id = child.id ) ON aav.article_id = child.id
WHERE child.parentarticle_id = a.id WHERE child.parentarticle_id = a.id
ORDER BY child.parentsort ASC, child.id ASC ORDER BY child.parentsort ASC, child.id ASC
@@ -522,7 +609,7 @@ SELECT
) AS childrenJson, ) AS childrenJson,
( (
SELECT ca2.category_id SELECT ca2.category_id
FROM categoryarticle ca2 FROM categoryarticle ca2 WITH (NOLOCK)
WHERE ca2.article_id = a.id WHERE ca2.article_id = a.id
ORDER BY ca2.category_id ORDER BY ca2.category_id
FOR JSON PATH FOR JSON PATH
@@ -537,15 +624,15 @@ SELECT
p.thumbw AS thumbW, p.thumbw AS thumbW,
p.thumbh AS thumbH, p.thumbh AS thumbH,
p.thumbhash AS thumbHash p.thumbhash AS thumbHash
FROM articlepic ap FROM articlepic ap WITH (NOLOCK)
INNER JOIN pic p ON p.id = ap.pic_id INNER JOIN pic p WITH (NOLOCK) ON p.id = ap.pic_id
WHERE ap.article_id = a.id WHERE ap.article_id = a.id
ORDER BY ap.sort ASC, ap.id ASC ORDER BY ap.sort ASC, ap.id ASC
FOR JSON PATH FOR JSON PATH
) AS photosJson, ) AS photosJson,
( (
SELECT ae.ean SELECT ae.ean
FROM articleean ae FROM articleean ae WITH (NOLOCK)
WHERE ae.article_id = a.id WHERE ae.article_id = a.id
ORDER BY ae.ean ORDER BY ae.ean
FOR JSON PATH FOR JSON PATH
@@ -556,8 +643,8 @@ SELECT
pr.net, pr.net,
t.rate AS taxRate, t.rate AS taxRate,
pr.customergroup_id AS customergroupId pr.customergroup_id AS customergroupId
FROM price pr FROM price pr WITH (NOLOCK)
INNER JOIN tax t ON t.id = pr.tax_id INNER JOIN tax t WITH (NOLOCK) ON t.id = pr.tax_id
WHERE pr.article_id = a.id WHERE pr.article_id = a.id
ORDER BY pr.customergroup_id, pr.minmultiplier ORDER BY pr.customergroup_id, pr.minmultiplier
FOR JSON PATH FOR JSON PATH
@@ -574,10 +661,33 @@ SELECT
asup.sort, asup.sort,
asup.net AS [price.net], asup.net AS [price.net],
asup.tax_id AS [price.taxId], asup.tax_id AS [price.taxId],
t.rate AS [price.taxRate] t.rate AS [price.taxRate],
FROM articlesupplier asup (
INNER JOIN entity e ON e.id = asup.entity_id SELECT
LEFT JOIN tax t ON t.id = asup.tax_id m.[date] AS [date],
SUM(ma.amount - COALESCE(ma_ledger.booked_amount, 0)) AS amount
FROM manifest m WITH (NOLOCK)
INNER JOIN manifestarticle ma WITH (NOLOCK) ON ma.manifest_id = m.id
LEFT JOIN (
SELECT manifestarticle_id, SUM(amount) AS booked_amount
FROM ledger WITH (NOLOCK)
WHERE warehouse_id IS NULL
GROUP BY manifestarticle_id
) ma_ledger ON ma_ledger.manifestarticle_id = ma.id
WHERE m.company_id = a.company_id
AND m.[type] = 'delivery'
AND m.entity_id = asup.entity_id
AND asup.sku IS NOT NULL
AND asup.sku <> ''
AND ma.sku = asup.sku
AND (ma.amount - COALESCE(ma_ledger.booked_amount, 0)) > 0
GROUP BY m.id, m.[date]
ORDER BY m.[date] ASC, m.id ASC
FOR JSON PATH
) AS incomingJson
FROM articlesupplier asup WITH (NOLOCK)
INNER JOIN entity e WITH (NOLOCK) ON e.id = asup.entity_id
LEFT JOIN tax t WITH (NOLOCK) ON t.id = asup.tax_id
WHERE asup.article_id = a.id WHERE asup.article_id = a.id
ORDER BY asup.sort ASC, asup.id ASC ORDER BY asup.sort ASC, asup.id ASC
FOR JSON PATH FOR JSON PATH
@@ -589,66 +699,105 @@ SELECT
w.sort, w.sort,
CASE CASE
WHEN a.articlelist_id IS NOT NULL AND EXISTS ( WHEN a.articlelist_id IS NOT NULL AND EXISTS (
SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id SELECT 1 FROM articlelistarticle ala WITH (NOLOCK) WHERE ala.articlelist_id = a.articlelist_id
) THEN COALESCE(( ) THEN COALESCE((
SELECT CASE SELECT CASE
WHEN MIN(COALESCE(comp_stock.stock, 0) / ala.count) < 0 THEN 0 WHEN MIN(COALESCE(aws_comp.stock, 0) / ala.count) < 0 THEN 0
ELSE FLOOR(MIN(COALESCE(comp_stock.stock, 0) / ala.count)) ELSE FLOOR(MIN(COALESCE(aws_comp.stock, 0) / ala.count))
END END
FROM articlelistarticle ala FROM articlelistarticle ala WITH (NOLOCK)
LEFT JOIN ( LEFT JOIN articlewarehousesummary aws_comp WITH (NOLOCK)
SELECT ca.id AS comp_id, SUM(l.amount) AS stock ON aws_comp.article_id = ala.article_id AND aws_comp.warehouse_id = w.id
FROM ledger l
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id
INNER JOIN article ca ON (
ma.sku = ca.sku
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = ca.id)
)
WHERE m.company_id = a.company_id
AND l.warehouse_id = w.id
GROUP BY ca.id
) comp_stock ON comp_stock.comp_id = ala.article_id
WHERE ala.articlelist_id = a.articlelist_id WHERE ala.articlelist_id = a.articlelist_id
), 0) ), 0)
WHEN a.articlelist_id IS NOT NULL THEN 0 WHEN a.articlelist_id IS NOT NULL THEN 0
ELSE COALESCE(stock_calc.stock, 0) ELSE COALESCE(aws.stock, 0)
END AS stock END AS stock
FROM warehouse w WITH (NOLOCK)
LEFT JOIN articlewarehousesummary aws WITH (NOLOCK)
ON aws.article_id = a.id AND aws.warehouse_id = w.id
WHERE w.company_id = a.company_id
ORDER BY w.sort ASC, w.name ASC
FOR JSON PATH
) AS warehousesJson,
(
SELECT
m.id AS orderId,
m.[number] AS orderNumber,
m.[date] AS [date],
COALESCE(os.customer_name, LTRIM(RTRIM(ISNULL(e.first_name + ' ', '') + ISNULL(e.name, '')))) AS customerName,
ROUND(SUM(ma.amount) - COALESCE((
SELECT SUM(ma_s.amount)
FROM shipmentsummary ss WITH (NOLOCK)
INNER JOIN manifestarticle ma_s WITH (NOLOCK) ON ma_s.manifest_id = ss.manifest_id
WHERE ss.company_id = m.company_id
AND ss.order_id = m.id
AND ISNULL(ss.state, 'new') != 'cancelled'
AND ma_s.sku = a.sku
), 0), 3) AS amount
FROM manifest m WITH (NOLOCK)
INNER JOIN manifestarticle ma WITH (NOLOCK) ON ma.manifest_id = m.id
LEFT JOIN ordersummary os WITH (NOLOCK) ON os.manifest_id = m.id
LEFT JOIN entity e WITH (NOLOCK) ON e.id = m.entity_id
WHERE m.company_id = a.company_id
AND m.[type] = 'order'
AND ma.sku = a.sku
AND COALESCE(os.is_shipped, 0) = 0
GROUP BY m.id, m.[number], m.[date], m.company_id, os.customer_name, e.first_name, e.name
HAVING (SUM(ma.amount) - COALESCE((
SELECT SUM(ma_s.amount)
FROM shipmentsummary ss WITH (NOLOCK)
INNER JOIN manifestarticle ma_s WITH (NOLOCK) ON ma_s.manifest_id = ss.manifest_id
WHERE ss.company_id = m.company_id
AND ss.order_id = m.id
AND ISNULL(ss.state, 'new') != 'cancelled'
AND ma_s.sku = a.sku
), 0)) > 0
ORDER BY m.[date] ASC, m.id ASC
FOR JSON PATH
) AS undeliveredOrdersJson
FROM article a WITH (NOLOCK)
LEFT JOIN articletext at WITH (NOLOCK)
ON at.article_id = a.id AND at.language_id = auth.languageId
LEFT JOIN unit u WITH (NOLOCK) ON u.id = a.unit_id
LEFT JOIN unittext ut WITH (NOLOCK)
ON ut.unit_id = u.id AND ut.language_id = auth.languageId
LEFT JOIN brand b WITH (NOLOCK) ON b.id = a.brand_id
OUTER APPLY (
SELECT TOP 1 bp2.pic_id
FROM brandpic bp2 WITH (NOLOCK)
WHERE bp2.brand_id = b.id
ORDER BY bp2.sort ASC, bp2.id ASC
) first_brand_pic
LEFT JOIN pic bp WITH (NOLOCK) ON bp.id = first_brand_pic.pic_id
LEFT JOIN article fa WITH (NOLOCK) ON fa.id = a.parentarticle_id
LEFT JOIN attributename fan WITH (NOLOCK) ON fan.id = a.parentattributename_id
WHERE a.id = auth.articleId
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
) AS articleJson
FROM auth
`;
export const CHECK_HAS_WAREHOUSE = `
SELECT TOP 1 id
FROM warehouse
WHERE company_id = @companyId;
`;
export const CHECK_ARTICLE_WAREHOUSE_STOCKS = `
SELECT w.id AS warehouseId, w.name AS warehouseName, COALESCE(stock_calc.stock, 0) AS stock
FROM warehouse w FROM warehouse w
LEFT JOIN ( LEFT JOIN (
SELECT l.warehouse_id, SUM(l.amount) AS stock SELECT l.warehouse_id, SUM(l.amount) AS stock
FROM ledger l FROM ledger l
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id INNER JOIN manifest m ON m.id = ma.manifest_id
WHERE m.company_id = a.company_id WHERE m.company_id = @companyId
AND ( AND (
ma.sku = a.sku ma.sku = @sku
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id) OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = @articleId)
) )
GROUP BY l.warehouse_id GROUP BY l.warehouse_id
) stock_calc ON stock_calc.warehouse_id = w.id ) stock_calc ON stock_calc.warehouse_id = w.id
WHERE w.company_id = a.company_id WHERE w.company_id = @companyId;
ORDER BY w.sort ASC, w.name ASC
FOR JSON PATH
) AS warehousesJson
FROM article a
LEFT JOIN articletext at
ON at.article_id = a.id AND at.language_id = auth.languageId
LEFT JOIN unit u ON u.id = a.unit_id
LEFT JOIN unittext ut
ON ut.unit_id = u.id AND ut.language_id = auth.languageId
LEFT JOIN brand b ON b.id = a.brand_id
OUTER APPLY (
SELECT TOP 1 bp2.pic_id
FROM brandpic bp2
WHERE bp2.brand_id = b.id
ORDER BY bp2.sort ASC, bp2.id ASC
) first_brand_pic
LEFT JOIN pic bp ON bp.id = first_brand_pic.pic_id
LEFT JOIN article fa ON fa.id = a.parentarticle_id
LEFT JOIN attributename fan ON fan.id = a.parentattributename_id
WHERE a.id = auth.articleId
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
) AS articleJson
FROM auth
`; `;

View File

@@ -17,29 +17,6 @@ WHERE id = @articleId
`; `;
export const CREATE_TOMBSTONE = `
INSERT INTO articletombstone (company_id, article_id, sku, name, taxrate, net)
SELECT
a.company_id,
a.id AS article_id,
a.sku,
COALESCE(
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id ORDER BY at.id ASC)
) AS name,
COALESCE(
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.id ASC)
) AS taxrate,
COALESCE(
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id ORDER BY pr.id ASC)
) AS net
FROM article a
INNER JOIN company co ON co.id = a.company_id
WHERE a.id = @articleId
`;
export const DELETE_PICS_BY_ARTICLE = ` export const DELETE_PICS_BY_ARTICLE = `
DELETE p DELETE p
FROM pic p FROM pic p

View File

@@ -0,0 +1,43 @@
export const ADD = `
INSERT INTO autoinvite (company_id, email, role)
OUTPUT INSERTED.id
VALUES (@companyId, @email, @role)
`;
export const LIST_BY_COMPANY = `
SELECT
id,
email,
role,
created_at AS createdAt
FROM autoinvite
WHERE company_id = @companyId
ORDER BY email
`;
export const LIST_BY_EMAIL = `
SELECT
id,
company_id AS companyId,
role
FROM autoinvite
WHERE email = @email
ORDER BY id
`;
export const GET_IN_COMPANY = `
SELECT id
FROM autoinvite
WHERE id = @id AND company_id = @companyId
`;
export const GET_BY_COMPANY_EMAIL = `
SELECT id
FROM autoinvite
WHERE company_id = @companyId AND email = @email
`;
export const DELETE = `
DELETE FROM autoinvite
WHERE id = @id
`;

View File

@@ -0,0 +1,78 @@
export const LIST_BY_COMPANY = `
SELECT
id,
company_id,
name,
sort,
has_packages,
is_reverse
FROM carrier
WHERE company_id = @companyId
ORDER BY sort ASC, name ASC, id ASC;
`;
export const GET_BY_COMPANY_AND_ID = `
SELECT
id,
company_id,
name,
sort,
has_packages,
is_reverse
FROM carrier
WHERE id = @id AND company_id = @companyId;
`;
export const GET_BY_COMPANY_AND_NAME = `
SELECT
id,
company_id,
name,
sort,
has_packages,
is_reverse
FROM carrier
WHERE company_id = @companyId AND name = @name;
`;
export const GET_NEXT_SORT = `
SELECT ISNULL(MAX(sort), -1) + 1 AS sort
FROM carrier
WHERE company_id = @companyId;
`;
export const CREATE = `
INSERT INTO carrier (company_id, name, sort, has_packages, is_reverse)
OUTPUT INSERTED.id, INSERTED.company_id, INSERTED.name, INSERTED.sort, INSERTED.has_packages, INSERTED.is_reverse
VALUES (@companyId, @name, @sort, @hasPackages, @isReverse);
`;
export const UPDATE = `
UPDATE carrier
SET name = @name, sort = @sort, has_packages = @hasPackages, is_reverse = @isReverse
OUTPUT INSERTED.id, INSERTED.company_id, INSERTED.name, INSERTED.sort, INSERTED.has_packages, INSERTED.is_reverse
WHERE id = @id AND company_id = @companyId;
`;
export const LIST_IDS_BY_COMPANY = `
SELECT id
FROM carrier
WHERE company_id = @companyId;
`;
export const UPDATE_SORT = `
UPDATE carrier
SET sort = @sort
WHERE id = @id AND company_id = @companyId;
`;
export const DELETE_BY_COMPANY_AND_ID = `
DELETE FROM carrier
WHERE id = @id AND company_id = @companyId;
`;
export const CHECK_SHIPPING_USAGE = `
SELECT TOP 1 id
FROM shipping
WHERE carrier_id = @carrierId;
`;

View File

@@ -64,10 +64,34 @@ FROM categoryarticle ca
INNER JOIN article a ON a.id = ca.article_id INNER JOIN article a ON a.id = ca.article_id
${ARTICLE_LIST_JOINS} ${ARTICLE_LIST_JOINS}
WHERE ca.category_id = @categoryId WHERE ca.category_id = @categoryId
AND a.parentarticle_id IS NULL
ORDER BY a.id ORDER BY a.id
`; `;
export const GET_CATEGORY_ARTICLE_WAREHOUSE_SUMMARIES = `
SELECT
aws.id,
aws.article_id AS articleId,
aws.warehouse_id AS warehouseId,
w.name AS warehouseName,
aws.stock
FROM articlewarehousesummary aws
INNER JOIN warehouse w ON w.id = aws.warehouse_id
INNER JOIN categoryarticle ca ON ca.article_id = aws.article_id
INNER JOIN article a ON a.id = ca.article_id
WHERE ca.category_id = @categoryId
ORDER BY w.sort ASC, w.name ASC, aws.id ASC
`;
export const GET_CATEGORY_ARTICLE_EANS = `
SELECT
ae.article_id AS articleId,
ae.ean
FROM articleean ae
INNER JOIN categoryarticle ca ON ca.article_id = ae.article_id
WHERE ca.category_id = @categoryId
ORDER BY ae.ean ASC
`;
export const CREATE = ` export const CREATE = `
INSERT INTO category (parent_id, sort) INSERT INTO category (parent_id, sort)
OUTPUT INSERTED.id, INSERTED.parent_id AS parentId, INSERTED.sort OUTPUT INSERTED.id, INSERTED.parent_id AS parentId, INSERTED.sort
@@ -162,3 +186,102 @@ export const DELETE = `
DELETE FROM category DELETE FROM category
WHERE id = @id WHERE id = @id
`; `;
export const LIST_CHILD_CATEGORIES = `
SELECT id
FROM category
WHERE parent_id = @parentId
ORDER BY sort ASC, id ASC
`;
export const GET_CATEGORY_ARTICLE_PICS = `
SELECT
a.id AS articleId,
p.id AS picId,
p.hash,
NULL AS buffer
FROM categoryarticle ca
INNER JOIN article a ON a.id = ca.article_id
OUTER APPLY (
SELECT TOP 1 ap.pic_id
FROM articlepic ap
WHERE ap.article_id = a.id
ORDER BY ap.sort ASC, ap.id ASC
) first_pic
INNER JOIN pic p ON p.id = first_pic.pic_id
WHERE ca.category_id = @categoryId
AND a.parentarticle_id IS NULL
ORDER BY a.id ASC
`;
export const TOUCH_SUBTREE_CATEGORIES = `
;WITH cats AS (
SELECT id FROM category WHERE id = @id
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
UPDATE category
SET updated_at = SYSUTCDATETIME()
WHERE id IN (SELECT id FROM cats)
`;
export const TOUCH_SUBTREE_ARTICLES = `
;WITH cats AS (
SELECT id FROM category WHERE id = @id
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
UNION
SELECT child.id FROM article child
WHERE child.parentarticle_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
)
UNION
SELECT parent.parentarticle_id FROM article parent
WHERE parent.id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
) AND parent.parentarticle_id IS NOT NULL
)
`;
export const ENQUEUE_SUBTREE_CATEGORIES_SYNC = `
;WITH cats AS (
SELECT id FROM category WHERE id = @id
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id)
SELECT pt.id, 'category', cats.id
FROM cats
INNER JOIN posterminal pt ON pt.company_id = @companyId
`;
export const ENQUEUE_SUBTREE_ARTICLES_SYNC = `
;WITH cats AS (
SELECT id FROM category WHERE id = @id
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
),
arts AS (
SELECT ca.article_id AS id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
UNION
SELECT child.id FROM article child
WHERE child.parentarticle_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
)
UNION
SELECT parent.parentarticle_id AS id FROM article parent
WHERE parent.id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
) AND parent.parentarticle_id IS NOT NULL
)
INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id)
SELECT pt.id, 'article', arts.id
FROM arts
INNER JOIN posterminal pt ON pt.company_id = @companyId
`;

View File

@@ -55,7 +55,8 @@ SELECT
l.iso639, l.iso639,
tr.rate AS taxrate, tr.rate AS taxrate,
rtr.rate AS reducedtaxrate, rtr.rate AS reducedtaxrate,
tfr.rate AS taxfreerate tfr.rate AS taxfreerate,
cu.role AS role
FROM company c FROM company c
INNER JOIN companyuser cu ON cu.company_id = c.id INNER JOIN companyuser cu ON cu.company_id = c.id
LEFT JOIN language l ON l.id = c.default_language_id LEFT JOIN language l ON l.id = c.default_language_id

View File

@@ -10,7 +10,8 @@ SELECT
c.root_category_id AS rootCategoryId, c.root_category_id AS rootCategoryId,
c.default_language_id AS languageId, c.default_language_id AS languageId,
c.poscustomergroup_id AS posCustomerGroupId, c.poscustomergroup_id AS posCustomerGroupId,
cu.id AS companyuserId cu.id AS companyuserId,
cu.role AS role
FROM company c FROM company c
LEFT JOIN companyuser cu LEFT JOIN companyuser cu
ON cu.company_id = c.id AND cu.google_auth_id = @googleAuthId ON cu.company_id = c.id AND cu.google_auth_id = @googleAuthId
@@ -18,12 +19,75 @@ WHERE c.id = @companyId
`; `;
export const ADD = ` export const ADD = `
INSERT INTO companyuser (google_auth_id, company_id) INSERT INTO companyuser (google_auth_id, company_id, role)
OUTPUT INSERTED.id OUTPUT INSERTED.id
VALUES (@googleAuthId, @companyId) VALUES (@googleAuthId, @companyId, @role)
`; `;
export const DELETE = ` export const DELETE = `
DELETE FROM companyuser DELETE FROM companyuser
WHERE id = @id WHERE id = @id
`; `;
export const LIST_BY_COMPANY = `
SELECT
cu.id,
cu.google_auth_id AS googleAuthId,
ga.email,
ga.picture,
cu.role
FROM companyuser cu
INNER JOIN google_auth ga ON ga.id = cu.google_auth_id
WHERE cu.company_id = @companyId
ORDER BY ga.email
`;
export const GET_IN_COMPANY = `
SELECT
cu.id,
cu.google_auth_id AS googleAuthId,
ga.email,
ga.picture,
cu.role
FROM companyuser cu
INNER JOIN google_auth ga ON ga.id = cu.google_auth_id
WHERE cu.id = @id AND cu.company_id = @companyId
`;
export const GET_MEMBERSHIP = `
SELECT id, role
FROM companyuser
WHERE google_auth_id = @googleAuthId AND company_id = @companyId
`;
export const COUNT_ADMINS = `
SELECT COUNT(*) AS count
FROM companyuser
WHERE company_id = @companyId AND role = 'admin'
`;
export const SET_ROLE = `
UPDATE companyuser
SET role = @role
WHERE id = @id
`;
export const CLEAR_ENTITYHISTORY_USER = `
UPDATE entityhistory SET companyuser_id = NULL WHERE companyuser_id = @id
`;
export const CLEAR_COMPANYHISTORY_USER = `
UPDATE companyhistory SET companyuser_id = NULL WHERE companyuser_id = @id
`;
export const IS_MEMBER = `
SELECT id
FROM companyuser
WHERE google_auth_id = @googleAuthId AND company_id = @companyId
`;
export const CLEAR_SELECTED_COMPANY = `
UPDATE google_auth
SET selected_company = NULL
WHERE id = @googleAuthId AND selected_company = @companyId
`;

View File

@@ -43,29 +43,23 @@ SELECT
m.external_number AS externalNumber, m.external_number AS externalNumber,
m.[date], m.[date],
m.created_at AS createdAt, m.created_at AS createdAt,
COUNT(ma.id) AS totalPositions, ds.positions,
COUNT(CASE WHEN ma.id IS NOT NULL AND ( ds.gross_total AS grossTotal,
(ma.amount > 0 AND COALESCE(ma_ledger.booked_amount, 0) >= ma.amount) OR ds.net_total AS netTotal,
(ma.amount <= 0 AND COALESCE(ma_ledger.booked_amount, 0) > 0) ds.total_quantity AS totalQuantity,
) THEN 1 END) AS completelyOnLedgerPositions ds.stock_positions AS stockPositions,
ds.booked_positions AS bookedPositions,
ds.unbooked_positions AS unbookedPositions,
ds.stock_quantity AS stockQuantity,
ds.booked_quantity AS bookedQuantity,
ds.unbooked_quantity AS unbookedQuantity,
ds.is_booked AS isBooked,
ds.supplier_name AS supplierName
FROM manifest m FROM manifest m
LEFT JOIN manifestarticle ma ON ma.manifest_id = m.id LEFT JOIN deliverysummary ds ON ds.manifest_id = m.id
LEFT JOIN (
SELECT manifestarticle_id, SUM(amount) AS booked_amount
FROM ledger
GROUP BY manifestarticle_id
) ma_ledger ON ma_ledger.manifestarticle_id = ma.id
WHERE m.id = @id WHERE m.id = @id
AND m.company_id = @companyId AND m.company_id = @companyId
AND m.[type] = @type AND m.[type] = @type
GROUP BY
m.id,
m.[number],
m.[type],
m.entity_id,
m.external_number,
m.[date],
m.created_at
`; `;
export const GET_DELIVERY_LINES = ` export const GET_DELIVERY_LINES = `

View File

@@ -136,6 +136,12 @@ WHERE id = @id
AND customergroup_id = @customergroupId AND customergroup_id = @customergroupId
`; `;
export const TOUCH_UPDATED_AT = `
UPDATE entity
SET updated_at = SYSUTCDATETIME()
WHERE id = @id
`;
export const DELETE_BY_COMPANY_AND_ID = ` export const DELETE_BY_COMPANY_AND_ID = `
DELETE e DELETE e
FROM entity e FROM entity e
@@ -161,3 +167,14 @@ WHERE e.id = @id
AND cg.company_id = @companyId AND cg.company_id = @companyId
AND cg.name = @groupName AND cg.name = @groupName
`; `;
export const CHECK_SUPPLIER_IN_USE = `
SELECT TOP 1 1 AS inUse
FROM manifest m
WHERE m.entity_id = @id AND m.company_id = @companyId
UNION ALL
SELECT TOP 1 1 AS inUse
FROM articlesupplier asup
INNER JOIN article a ON a.id = asup.article_id
WHERE asup.entity_id = @id AND a.company_id = @companyId;
`;

View File

@@ -0,0 +1,19 @@
export const ENSURE_ROW = `
IF NOT EXISTS (SELECT 1 FROM healthcheck)
BEGIN
INSERT INTO healthcheck ([sequence]) VALUES (0);
END
`;
export const SELECT_FOR_UPDATE = `
SELECT TOP 1 id, [sequence]
FROM healthcheck WITH (UPDLOCK, ROWLOCK)
ORDER BY id ASC;
`;
export const UPDATE_SEQUENCE = `
UPDATE healthcheck
SET [sequence] = @sequence,
updated_at = SYSUTCDATETIME()
WHERE id = @id;
`;

View File

@@ -3,6 +3,7 @@ SELECT
ma.id, ma.id,
ma.sku, ma.sku,
ma.amount AS lineAmount, ma.amount AS lineAmount,
ma.pu AS linePu,
ma.net, ma.net,
ma.rebate, ma.rebate,
ma.taxrate, ma.taxrate,
@@ -23,6 +24,20 @@ FROM warehouse
WHERE id = @warehouseId AND company_id = @companyId; WHERE id = @warehouseId AND company_id = @companyId;
`; `;
export const GET_DEFAULT_WAREHOUSE = `
SELECT TOP 1 id, name
FROM warehouse
WHERE company_id = @companyId
ORDER BY sort ASC, name ASC, id ASC;
`;
export const CHECK_MANIFESTARTICLE_BOOKED = `
SELECT COALESCE(SUM(amount), 0) AS bookedAmount
FROM ledger
WHERE manifestarticle_id = @manifestarticleId
AND warehouse_id IS NULL;
`;
export const FIND_ARTICLES_FOR_HISTORY = ` export const FIND_ARTICLES_FOR_HISTORY = `
SELECT DISTINCT asup.article_id AS articleId SELECT DISTINCT asup.article_id AS articleId
FROM articlesupplier asup FROM articlesupplier asup
@@ -53,7 +68,7 @@ SELECT TOP 1
hash_version, hash_version,
prev_hash, prev_hash,
created_at created_at
FROM ledger WITH (UPDLOCK, HOLDLOCK) FROM ledger WITH (UPDLOCK, ROWLOCK)
ORDER BY id DESC; ORDER BY id DESC;
`; `;
@@ -131,3 +146,26 @@ WHERE ma.sku = @sku
AND (@warehouseId IS NULL OR l.warehouse_id = @warehouseId) AND (@warehouseId IS NULL OR l.warehouse_id = @warehouseId)
ORDER BY l.id ASC; ORDER BY l.id ASC;
`; `;
export const GET_ARTICLE_TRACK_STOCK = `
SELECT TOP 1 a.id, a.sku, a.track_stock
FROM article a
WHERE a.company_id = @companyId
AND (
a.sku = @sku
OR EXISTS (SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku)
);
`;
export const GET_ARTICLE_WAREHOUSE_STOCK = `
SELECT COALESCE(SUM(l.amount), 0) AS stock
FROM ledger l
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id
WHERE l.warehouse_id = @warehouseId
AND m.company_id = @companyId
AND (
ma.sku = @sku
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = @articleId)
);
`;

View File

@@ -39,12 +39,12 @@ WHERE id = @id AND company_id = @companyId
export const PAYMENT_CREATE = ` export const PAYMENT_CREATE = `
INSERT INTO payment (account_id, manifest_id, [type], amount) INSERT INTO payment (account_id, manifest_id, [type], amount)
OUTPUT INSERTED.id, INSERTED.account_id, INSERTED.manifest_id, INSERTED.[type], INSERTED.amount OUTPUT INSERTED.id, INSERTED.account_id, INSERTED.manifest_id, INSERTED.[type], INSERTED.amount, INSERTED.created_at AS createdAt
VALUES (@accountId, @manifestId, @type, @amount) VALUES (@accountId, @manifestId, @type, @amount)
`; `;
export const PAYMENT_LIST_BY_MANIFEST = ` export const PAYMENT_LIST_BY_MANIFEST = `
SELECT id, account_id, manifest_id, [type], amount SELECT id, account_id, manifest_id, [type], amount, created_at AS createdAt
FROM payment FROM payment
WHERE manifest_id = @manifestId WHERE manifest_id = @manifestId
ORDER BY id ASC ORDER BY id ASC
@@ -56,17 +56,24 @@ WHERE id = @id
AND manifest_id IN (SELECT m.id FROM manifest m WHERE m.company_id = @companyId) AND manifest_id IN (SELECT m.id FROM manifest m WHERE m.company_id = @companyId)
`; `;
export const CHECK_CARRIER = `
SELECT TOP 1 id, name
FROM carrier
WHERE id = @carrierId AND company_id = @companyId;
`;
export const SHIPPING_CREATE = ` export const SHIPPING_CREATE = `
INSERT INTO shipping (manifest_id, [number], tracking_code, carrier, [type], [state]) INSERT INTO shipping (manifest_id, [number], tracking_code, carrier_id, [type], [state])
OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier, INSERTED.[type], INSERTED.[state] OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier_id, INSERTED.[type], INSERTED.[state]
VALUES (@manifestId, @number, @trackingCode, @carrier, @type, @state) VALUES (@manifestId, @number, @trackingCode, @carrierId, @type, @state)
`; `;
export const SHIPPING_LIST_BY_MANIFEST = ` export const SHIPPING_LIST_BY_MANIFEST = `
SELECT id, manifest_id, [number], tracking_code, carrier, [type], [state] SELECT s.id, s.manifest_id, s.[number], s.tracking_code, s.carrier_id, c.name AS carrier, s.[type], s.[state]
FROM shipping FROM shipping s
WHERE manifest_id = @manifestId LEFT JOIN carrier c ON c.id = s.carrier_id
ORDER BY id ASC WHERE s.manifest_id = @manifestId
ORDER BY s.id ASC
`; `;
export const SHIPPING_DELETE = ` export const SHIPPING_DELETE = `
@@ -76,25 +83,43 @@ WHERE id = @id
`; `;
export const GET_SHIPPING_BY_ID = ` export const GET_SHIPPING_BY_ID = `
SELECT s.id, s.manifest_id AS manifestId, s.[number], s.tracking_code, s.carrier, s.[type], s.[state] SELECT s.id, s.manifest_id AS manifestId, s.[number], s.tracking_code, s.carrier_id AS carrierId, c.name AS carrier, s.[type], s.[state]
FROM shipping s FROM shipping s
INNER JOIN manifest m ON m.id = s.manifest_id INNER JOIN manifest m ON m.id = s.manifest_id
LEFT JOIN carrier c ON c.id = s.carrier_id
WHERE s.id = @id AND m.company_id = @companyId; WHERE s.id = @id AND m.company_id = @companyId;
`; `;
export const SHIPPING_UPDATE = ` export const SHIPPING_UPDATE = `
UPDATE shipping UPDATE shipping
SET [type] = @type, [state] = @state, tracking_code = @trackingCode, carrier = @carrier SET [type] = @type, [state] = @state, tracking_code = @trackingCode, carrier_id = @carrierId
OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier, INSERTED.[type], INSERTED.[state] OUTPUT INSERTED.id, INSERTED.manifest_id, INSERTED.[number], INSERTED.tracking_code, INSERTED.carrier_id, INSERTED.[type], INSERTED.[state]
WHERE id = @id; WHERE id = @id;
`; `;
export const CHECK_SHIPMENT_ALREADY_SHIPPED = `
SELECT TOP 1 1
FROM manifest m
WHERE m.id = @manifestId
AND (
EXISTS (
SELECT 1 FROM shipping s
WHERE s.manifest_id = m.id AND LOWER(s.[state]) = 'shipped'
)
OR EXISTS (
SELECT 1 FROM shipmentsummary ss
WHERE ss.manifest_id = m.id AND LOWER(ss.[state]) = 'delivered'
)
);
`;
export const CHECK_ORDER_SHIPPED = ` export const CHECK_ORDER_SHIPPED = `
SELECT TOP 1 s.id SELECT TOP 1 s.id
FROM shipping s FROM shipping s
WHERE s.manifest_id = @manifestId AND LOWER(s.[state]) = 'shipped'; WHERE s.manifest_id = @manifestId AND LOWER(s.[state]) = 'shipped';
`; `;
export const GET_ORDER_LINES_FOR_SHIPPING = ` export const GET_ORDER_LINES_FOR_SHIPPING = `
SELECT id, sku, name, amount SELECT id, sku, name, amount
FROM manifestarticle FROM manifestarticle

View File

@@ -5,6 +5,8 @@
export const QUOTE_TYPE = 'quote'; export const QUOTE_TYPE = 'quote';
export const ORDER_TYPE = 'order'; export const ORDER_TYPE = 'order';
export const RETOURE_TYPE = 'retoure';
export const RETURN_TYPE = 'return';
export const LIST_MANIFESTS = ` export const LIST_MANIFESTS = `
SELECT SELECT
@@ -55,6 +57,7 @@ SELECT
ma.net, ma.net,
ma.rebate, ma.rebate,
ma.taxrate, ma.taxrate,
ma.shippable,
ma.comment ma.comment
FROM manifestarticle ma FROM manifestarticle ma
WHERE ma.manifest_id IN (SELECT CAST(value AS INT) FROM OPENJSON(@manifestIdsJson)) WHERE ma.manifest_id IN (SELECT CAST(value AS INT) FROM OPENJSON(@manifestIdsJson))
@@ -86,6 +89,7 @@ SELECT
ma.net, ma.net,
ma.rebate, ma.rebate,
ma.taxrate, ma.taxrate,
ma.shippable,
ma.comment ma.comment
FROM manifestarticle ma FROM manifestarticle ma
WHERE ma.manifest_id = @manifestId WHERE ma.manifest_id = @manifestId
@@ -133,7 +137,8 @@ INSERT INTO manifestarticle (
net, net,
comment, comment,
rebate, rebate,
taxrate taxrate,
shippable
) )
OUTPUT INSERTED.id OUTPUT INSERTED.id
VALUES ( VALUES (
@@ -145,7 +150,8 @@ VALUES (
@net, @net,
@comment, @comment,
@rebate, @rebate,
@taxrate @taxrate,
@shippable
) )
`; `;
@@ -154,22 +160,61 @@ SELECT
a.id, a.id,
a.is_parent AS isParent, a.is_parent AS isParent,
COALESCE( COALESCE(
(SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id), (SELECT at.name FROM articletext at WITH (NOLOCK) WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
(SELECT MIN(at2.name) FROM articletext at2 WHERE at2.article_id = a.id), (SELECT MIN(at2.name) FROM articletext at2 WITH (NOLOCK) WHERE at2.article_id = a.id),
a.sku a.sku
) AS articleName, ) AS articleName,
COALESCE( COALESCE(
(SELECT MIN(asup.pu) FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku), (SELECT MIN(asup.pu) FROM articlesupplier asup WITH (NOLOCK) WHERE asup.article_id = a.id AND asup.sku = @sku),
(SELECT MIN(asup2.pu) FROM articlesupplier asup2 WHERE asup2.article_id = a.id), (SELECT MIN(asup2.pu) FROM articlesupplier asup2 WITH (NOLOCK) WHERE asup2.article_id = a.id),
a.unitamount, a.unitamount,
1 1
) AS pu ) AS pu
FROM article a FROM article a WITH (NOLOCK)
INNER JOIN manifest m ON m.company_id = a.company_id INNER JOIN manifest m WITH (NOLOCK) ON m.company_id = a.company_id
INNER JOIN company co ON co.id = m.company_id INNER JOIN company co WITH (NOLOCK) ON co.id = m.company_id
WHERE m.id = @manifestId WHERE m.id = @manifestId
AND (a.sku = @sku OR EXISTS ( AND (a.sku = @sku OR EXISTS (
SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku SELECT 1 FROM articlesupplier asup WITH (NOLOCK) WHERE asup.article_id = a.id AND asup.sku = @sku
)) ))
`; `;
export const GET_MANIFEST_CONTEXT = `
SELECT
m.id,
m.company_id AS companyId,
m.[type] AS [type],
m.entity_id AS entityId,
e.name AS entityName
FROM manifest m
LEFT JOIN entity e ON e.id = m.entity_id
WHERE m.id = @manifestId
`;
export const GET_ARTICLE_SUPPLIER_LINK = `
SELECT
asup.id,
asup.article_id AS articleId,
asup.entity_id AS entityId,
asup.sku,
asup.pu,
asup.net,
asup.tax_id AS taxId,
t.rate AS taxRate,
e.name AS supplierName
FROM articlesupplier asup
LEFT JOIN entity e ON e.id = asup.entity_id
LEFT JOIN tax t ON t.id = asup.tax_id
WHERE asup.article_id = @articleId
AND asup.entity_id = @entityId
`;
export const UPDATE_ARTICLE_SUPPLIER_LINK = `
UPDATE articlesupplier
SET
pu = @pu,
net = @net,
tax_id = @taxId
WHERE id = @id
`;

View File

@@ -5,8 +5,8 @@ IF NOT EXISTS (
WHERE company_id = @companyId AND [type] = @type WHERE company_id = @companyId AND [type] = @type
) )
BEGIN BEGIN
INSERT INTO numbers (company_id, [type], prefix) INSERT INTO numbers (company_id, [type], prefix, padding, [next])
VALUES (@companyId, @type, ISNULL(@prefix, '')); VALUES (@companyId, @type, ISNULL(@prefix, ''), ISNULL(@padding, 0), ISNULL(@next, 1));
END END
`; `;

View File

@@ -1,14 +1,12 @@
export const CREATE = ` export const CREATE = `
INSERT INTO pic ( INSERT INTO pic (
w, h, size, hash, thumbw, thumbh, thumbhash, w, h, size, hash, thumbw, thumbh, thumbhash,
smallthumbw, smallthumbh, smallthumbbinary, smallthumbw, smallthumbh, smallthumbbinary, source
source, fullbinary, thumbbinary
) )
OUTPUT INSERTED.id OUTPUT INSERTED.id
VALUES ( VALUES (
@w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash, @w, @h, @size, @hash, @thumbw, @thumbh, @thumbhash,
@smallthumbw, @smallthumbh, @smallthumbbinary, @smallthumbw, @smallthumbh, @smallthumbbinary, @source
@source, @fullbinary, @thumbbinary
) )
`; `;
@@ -60,13 +58,13 @@ WHERE categoryId IS NOT NULL
`; `;
export const GET_FULL_BY_HASH = ` export const GET_FULL_BY_HASH = `
SELECT w, h, size, fullbinary AS buffer SELECT w, h, size, hash
FROM pic FROM pic
WHERE hash = @hash WHERE hash = @hash
`; `;
export const GET_THUMB_BY_HASH = ` export const GET_THUMB_BY_HASH = `
SELECT thumbw AS w, thumbh AS h, thumbbinary AS buffer SELECT thumbw AS w, thumbh AS h, hash
FROM pic FROM pic
WHERE hash = @hash WHERE hash = @hash
`; `;

View File

@@ -6,14 +6,17 @@ SELECT
pairingpin, pairingpin,
paired_at AS pairedAt, paired_at AS pairedAt,
clientserial AS clientserial, clientserial AS clientserial,
maxknowntombstoneid AS maxknowntombstoneid, maxknownobituaryid AS maxknownobituaryid,
root_category_id AS rootCategoryId, root_category_id AS rootCategoryId,
sync_root_articles AS syncRootArticles, sync_root_articles AS syncRootArticles,
lastseen_at AS lastseenAt, lastseen_at AS lastseenAt,
updated_at AS updatedAt updated_at AS updatedAt,
sort,
enabled,
clientversion
FROM posterminal FROM posterminal
WHERE company_id = @companyId WHERE company_id = @companyId
ORDER BY id ORDER BY sort, id
`; `;
export const GET_BY_ID = ` export const GET_BY_ID = `
@@ -25,11 +28,14 @@ SELECT
pairingpin, pairingpin,
paired_at AS pairedAt, paired_at AS pairedAt,
clientserial AS clientserial, clientserial AS clientserial,
maxknowntombstoneid AS maxknowntombstoneid, maxknownobituaryid AS maxknownobituaryid,
root_category_id AS rootCategoryId, root_category_id AS rootCategoryId,
sync_root_articles AS syncRootArticles, sync_root_articles AS syncRootArticles,
lastseen_at AS lastseenAt, lastseen_at AS lastseenAt,
updated_at AS updatedAt updated_at AS updatedAt,
sort,
enabled,
clientversion
FROM posterminal FROM posterminal
WHERE id = @id WHERE id = @id
`; `;
@@ -43,42 +49,51 @@ SELECT
pairingpin, pairingpin,
paired_at AS pairedAt, paired_at AS pairedAt,
clientserial AS clientserial, clientserial AS clientserial,
maxknowntombstoneid AS maxknowntombstoneid, maxknownobituaryid AS maxknownobituaryid,
root_category_id AS rootCategoryId, root_category_id AS rootCategoryId,
sync_root_articles AS syncRootArticles, sync_root_articles AS syncRootArticles,
lastseen_at AS lastseenAt, lastseen_at AS lastseenAt,
updated_at AS updatedAt updated_at AS updatedAt,
sort,
enabled,
clientversion
FROM posterminal FROM posterminal
WHERE pairingpin = @pairingpin WHERE pairingpin = @pairingpin
`; `;
export const PAIR = ` export const PAIR = `
UPDATE posterminal UPDATE posterminal
SET token = @token, paired_at = @pairedAt, clientserial = @clientserial, pairingpin = NULL, lastseen_at = @pairedAt, updated_at = SYSUTCDATETIME() SET token = @token, paired_at = @pairedAt, clientserial = @clientserial, clientversion = @clientversion, pairingpin = NULL, lastseen_at = @pairedAt, updated_at = SYSUTCDATETIME()
WHERE id = @id WHERE id = @id
`; `;
export const CREATE = ` export const CREATE = `
INSERT INTO posterminal (company_id, name, token, pairingpin, paired_at, clientserial, maxknowntombstoneid, root_category_id, sync_root_articles, lastseen_at, updated_at) INSERT INTO posterminal (company_id, name, token, pairingpin, paired_at, clientserial, maxknownobituaryid, root_category_id, sync_root_articles, lastseen_at, sort, enabled, updated_at)
OUTPUT INSERTED.id, INSERTED.name, INSERTED.token, INSERTED.pairingpin, INSERTED.paired_at AS pairedAt, INSERTED.clientserial AS clientserial, INSERTED.maxknowntombstoneid AS maxknowntombstoneid, INSERTED.root_category_id AS rootCategoryId, INSERTED.sync_root_articles AS syncRootArticles, INSERTED.lastseen_at AS lastseenAt, INSERTED.updated_at AS updatedAt OUTPUT INSERTED.id, INSERTED.name, INSERTED.token, INSERTED.pairingpin, INSERTED.paired_at AS pairedAt, INSERTED.clientserial AS clientserial, INSERTED.maxknownobituaryid AS maxknownobituaryid, INSERTED.root_category_id AS rootCategoryId, INSERTED.sync_root_articles AS syncRootArticles, INSERTED.lastseen_at AS lastseenAt, INSERTED.updated_at AS updatedAt, INSERTED.sort, INSERTED.enabled, INSERTED.clientversion
VALUES (@companyId, @name, @token, @pairingpin, @pairedAt, @clientserial, @maxknowntombstoneid, @rootCategoryId, @syncRootArticles, @lastseenAt, SYSUTCDATETIME()) VALUES (@companyId, @name, @token, @pairingpin, @pairedAt, @clientserial, @maxknownobituaryid, @rootCategoryId, @syncRootArticles, @lastseenAt, @sort, @enabled, SYSUTCDATETIME())
`; `;
export const UPDATE = ` export const UPDATE = `
UPDATE posterminal UPDATE posterminal
SET name = @name, token = @token, pairingpin = @pairingpin, paired_at = @pairedAt, clientserial = @clientserial, maxknowntombstoneid = @maxknowntombstoneid, root_category_id = @rootCategoryId, sync_root_articles = @syncRootArticles, lastseen_at = @lastseenAt, updated_at = SYSUTCDATETIME() SET name = @name, token = @token, pairingpin = @pairingpin, paired_at = @pairedAt, clientserial = @clientserial, maxknownobituaryid = @maxknownobituaryid, root_category_id = @rootCategoryId, sync_root_articles = @syncRootArticles, lastseen_at = @lastseenAt, sort = @sort, enabled = @enabled, clientversion = @clientversion, updated_at = SYSUTCDATETIME()
WHERE id = @id AND company_id = @companyId WHERE id = @id AND company_id = @companyId
`; `;
export const SET_CLIENTVERSION = `
UPDATE posterminal
SET clientversion = @clientversion, updated_at = SYSUTCDATETIME()
WHERE id = @id
`;
export const TOUCH_LASTSEEN = ` export const TOUCH_LASTSEEN = `
UPDATE posterminal UPDATE posterminal
SET lastseen_at = SYSUTCDATETIME() SET lastseen_at = SYSUTCDATETIME()
WHERE id = @id WHERE id = @id
`; `;
export const SET_MAX_KNOWN_TOMBSTONE_ID = ` export const SET_MAX_KNOWN_OBITUARY_ID = `
UPDATE posterminal UPDATE posterminal
SET maxknowntombstoneid = @maxknowntombstoneid SET maxknownobituaryid = @maxknownobituaryid
WHERE id = @id WHERE id = @id
`; `;
@@ -93,20 +108,32 @@ FROM posterminalcategory
WHERE posterminal_id = @posterminalId WHERE posterminal_id = @posterminalId
`; `;
/** setOn: insert if not exists (single terminal + single category). */ /** setOn: enable a category and all its subcategories on a terminal. */
export const SET_ON = ` export const SET_ON = `
IF NOT EXISTS ( ;WITH cats AS (
SELECT 1 FROM posterminalcategory SELECT id FROM category WHERE id = @categoryId
WHERE posterminal_id = @posterminalId AND category_id = @categoryId UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
) )
INSERT INTO posterminalcategory (posterminal_id, category_id) INSERT INTO posterminalcategory (posterminal_id, category_id)
VALUES (@posterminalId, @categoryId) SELECT @posterminalId, cats.id
FROM cats
WHERE NOT EXISTS (
SELECT 1 FROM posterminalcategory pc
WHERE pc.posterminal_id = @posterminalId AND pc.category_id = cats.id
)
`; `;
/** setOff: remove a single category from a single terminal. */ /** setOff: remove a category and all its subcategories from a terminal. */
export const SET_OFF = ` export const SET_OFF = `
;WITH cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
DELETE FROM posterminalcategory DELETE FROM posterminalcategory
WHERE posterminal_id = @posterminalId AND category_id = @categoryId WHERE posterminal_id = @posterminalId
AND category_id IN (SELECT id FROM cats)
`; `;
/** setOnAll: enable ALL company categories on one terminal. */ /** setOnAll: enable ALL company categories on one terminal. */
@@ -150,62 +177,98 @@ SELECT
pairingpin, pairingpin,
paired_at AS pairedAt, paired_at AS pairedAt,
clientserial AS clientserial, clientserial AS clientserial,
maxknowntombstoneid AS maxknowntombstoneid, maxknownobituaryid AS maxknownobituaryid,
root_category_id AS rootCategoryId, root_category_id AS rootCategoryId,
sync_root_articles AS syncRootArticles, sync_root_articles AS syncRootArticles,
lastseen_at AS lastseenAt, lastseen_at AS lastseenAt,
updated_at AS updatedAt updated_at AS updatedAt,
sort,
enabled,
clientversion
FROM posterminal FROM posterminal
WHERE token = @token WHERE token = @token
`; `;
export const COUNT_PRODUCTS = ` export const COUNT_PRODUCTS = `
SELECT COUNT(DISTINCT a.id) AS count SELECT COUNT(*) AS count
FROM article a FROM (
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id SELECT MAX(q.id) AS syncId, a.id
WHERE EXISTS ( FROM posterminalsyncqueue q
INNER JOIN article a ON a.id = q.entity_id
INNER JOIN posterminal pt ON pt.id = q.posterminal_id AND pt.company_id = a.company_id
WHERE q.posterminal_id = @posterminalId
AND q.entity_type = 'article'
AND EXISTS (
SELECT 1 SELECT 1
FROM categoryarticle ca FROM categoryarticle ca
INNER JOIN posterminalcategory pc ON pc.category_id = ca.category_id INNER JOIN posterminalcategory pc ON pc.category_id = ca.category_id
WHERE ca.article_id = a.id WHERE ca.article_id = a.id
AND pc.posterminal_id = @posterminalId AND pc.posterminal_id = @posterminalId
) )
AND DATEDIFF_BIG(second, '1970-01-01', a.updated_at) > @cursor GROUP BY a.id
HAVING MAX(q.id) > @cursor
) t
`; `;
export const COUNT_CATEGORIES = ` export const COUNT_CATEGORIES = `
SELECT COUNT(*) AS count SELECT COUNT(*) AS count
FROM category c FROM (
INNER JOIN posterminalcategory pc ON pc.category_id = c.id SELECT MAX(q.id) AS syncId, c.id
FROM posterminalsyncqueue q
INNER JOIN category c ON c.id = q.entity_id
INNER JOIN posterminalcategory pc ON pc.category_id = c.id AND pc.posterminal_id = q.posterminal_id
INNER JOIN posterminal pt ON pt.id = pc.posterminal_id INNER JOIN posterminal pt ON pt.id = pc.posterminal_id
INNER JOIN company co ON co.id = pt.company_id INNER JOIN company co ON co.id = pt.company_id
WHERE pc.posterminal_id = @posterminalId WHERE q.posterminal_id = @posterminalId
AND q.entity_type = 'category'
AND (co.root_category_id IS NULL OR c.id <> co.root_category_id) AND (co.root_category_id IS NULL OR c.id <> co.root_category_id)
AND DATEDIFF_BIG(second, '1970-01-01', c.updated_at) > @cursor GROUP BY c.id
HAVING MAX(q.id) > @cursor
) t
`; `;
export const COUNT_CUSTOMERS = ` export const COUNT_CUSTOMERS = `
SELECT COUNT(*) AS count SELECT COUNT(*) AS count
FROM entity e FROM (
SELECT MAX(q.id) AS syncId, e.id
FROM posterminalsyncqueue q
INNER JOIN entity e ON e.id = q.entity_id
INNER JOIN customergroup cg ON cg.id = e.customergroup_id INNER JOIN customergroup cg ON cg.id = e.customergroup_id
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id INNER JOIN posterminal pt ON pt.id = q.posterminal_id AND pt.company_id = cg.company_id
WHERE cg.name NOT IN ('Lieferanten', 'POS') WHERE q.posterminal_id = @posterminalId
AND DATEDIFF_BIG(second, '1970-01-01', e.updated_at) > @cursor AND q.entity_type = 'customer'
AND cg.name <> 'Lieferanten'
GROUP BY e.id
HAVING MAX(q.id) > @cursor
) t
`; `;
export const COUNT_CUSTOMER_GROUPS = ` export const COUNT_CUSTOMER_GROUPS = `
SELECT COUNT(*) AS count SELECT COUNT(*) AS count
FROM customergroup cg FROM (
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id SELECT MAX(q.id) AS syncId, cg.id
WHERE cg.name <> 'Lieferanten' FROM posterminalsyncqueue q
AND DATEDIFF_BIG(second, '1970-01-01', cg.updated_at) > @cursor INNER JOIN customergroup cg ON cg.id = q.entity_id
INNER JOIN posterminal pt ON pt.id = q.posterminal_id AND pt.company_id = cg.company_id
WHERE q.posterminal_id = @posterminalId
AND q.entity_type = 'customergroup'
AND cg.name <> 'Lieferanten'
GROUP BY cg.id
HAVING MAX(q.id) > @cursor
) t
`; `;
export const COUNT_COMPOSITE_PRODUCTS = ` export const COUNT_COMPOSITE_PRODUCTS = `
SELECT COUNT(DISTINCT a.id) AS count SELECT COUNT(*) AS count
FROM article a FROM (
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id SELECT MAX(q.id) AS syncId, a.id
WHERE a.articlelist_id IS NOT NULL FROM posterminalsyncqueue q
INNER JOIN article a ON a.id = q.entity_id
INNER JOIN posterminal pt ON pt.id = q.posterminal_id AND pt.company_id = a.company_id
WHERE q.posterminal_id = @posterminalId
AND q.entity_type = 'article'
AND a.articlelist_id IS NOT NULL
AND EXISTS ( AND EXISTS (
SELECT 1 SELECT 1
FROM articlelistarticle ala FROM articlelistarticle ala
@@ -218,7 +281,9 @@ WHERE a.articlelist_id IS NOT NULL
WHERE ca.article_id = a.id WHERE ca.article_id = a.id
AND pc.posterminal_id = @posterminalId AND pc.posterminal_id = @posterminalId
) )
AND DATEDIFF_BIG(second, '1970-01-01', a.updated_at) > @cursor GROUP BY a.id
HAVING MAX(q.id) > @cursor
) t
`; `;
export const MAX_ORDER_ID = ` export const MAX_ORDER_ID = `
@@ -242,16 +307,21 @@ SELECT TOP (@limit)
WHERE cp.category_id = c.id WHERE cp.category_id = c.id
ORDER BY cp.sort ASC, cp.id ASC ORDER BY cp.sort ASC, cp.id ASC
) AS imgHash, ) AS imgHash,
DATEDIFF_BIG(second, '1970-01-01', c.updated_at) AS lastChanged sq.syncId AS lastChanged
FROM category c FROM (
INNER JOIN posterminalcategory pc ON pc.category_id = c.id SELECT entity_id, MAX(id) AS syncId
FROM posterminalsyncqueue
WHERE posterminal_id = @posterminalId AND entity_type = 'category'
GROUP BY entity_id
HAVING MAX(id) > @cursor
) sq
INNER JOIN category c ON c.id = sq.entity_id
INNER JOIN posterminalcategory pc ON pc.category_id = c.id AND pc.posterminal_id = @posterminalId
INNER JOIN posterminal pt ON pt.id = pc.posterminal_id INNER JOIN posterminal pt ON pt.id = pc.posterminal_id
INNER JOIN company co ON co.id = pt.company_id INNER JOIN company co ON co.id = pt.company_id
LEFT JOIN categorytext ct ON ct.category_id = c.id AND ct.language_id = co.default_language_id LEFT JOIN categorytext ct ON ct.category_id = c.id AND ct.language_id = co.default_language_id
WHERE pc.posterminal_id = @posterminalId WHERE (co.root_category_id IS NULL OR c.id <> co.root_category_id)
AND (co.root_category_id IS NULL OR c.id <> co.root_category_id) ORDER BY sq.syncId ASC
AND DATEDIFF_BIG(second, '1970-01-01', c.updated_at) > @cursor
ORDER BY DATEDIFF_BIG(second, '1970-01-01', c.updated_at) ASC, c.id ASC
`; `;
export const LIST_POS_PRODUCTS = ` export const LIST_POS_PRODUCTS = `
@@ -262,7 +332,7 @@ SELECT TOP (@limit)
COALESCE(pr.net, 0) AS netPrice, COALESCE(pr.net, 0) AS netPrice,
COALESCE(ptax.rate, ctax.rate, 0) AS taxRate, COALESCE(ptax.rate, ctax.rate, 0) AS taxRate,
a.created_at AS createdAt, a.created_at AS createdAt,
DATEDIFF_BIG(second, '1970-01-01', a.updated_at) AS lastChanged, sq.syncId AS lastChanged,
COALESCE( COALESCE(
( (
SELECT TOP 1 p.hash SELECT TOP 1 p.hash
@@ -287,13 +357,15 @@ SELECT TOP (@limit)
) AS barcode, ) AS barcode,
a.is_parent AS isParent, a.is_parent AS isParent,
a.parentarticle_id AS parentArticleId, a.parentarticle_id AS parentArticleId,
a.parentsort AS parentsort,
( (
SELECT TOP 1 CONCAT(fan.name, '|', av.value, '|', fan.sort, '|', a.parentsort) SELECT TOP 1 av.value
FROM article fa FROM articleattributevalue aav
INNER JOIN attributename fan ON fan.id = fa.parentattributename_id INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
INNER JOIN articleattributevalue aav ON aav.article_id = a.id LEFT JOIN article fa ON fa.id = a.parentarticle_id
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.id WHERE aav.article_id = a.id
WHERE a.parentarticle_id IS NOT NULL AND fa.id = a.parentarticle_id AND (fa.parentattributename_id IS NULL OR av.attribute_id = fa.parentattributename_id)
ORDER BY CASE WHEN av.attribute_id = fa.parentattributename_id THEN 0 ELSE 1 END, aav.sort ASC, aav.id ASC
) AS variantName, ) AS variantName,
CASE CASE
WHEN a.articlelist_id IS NOT NULL AND EXISTS ( WHEN a.articlelist_id IS NOT NULL AND EXISTS (
@@ -318,6 +390,7 @@ SELECT TOP (@limit)
) AS sort, ) AS sort,
a.track_stock AS trackStock, a.track_stock AS trackStock,
CASE CASE
WHEN a.track_stock = 0 THEN 0
WHEN a.articlelist_id IS NOT NULL AND EXISTS ( WHEN a.articlelist_id IS NOT NULL AND EXISTS (
SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id
) THEN COALESCE(( ) THEN COALESCE((
@@ -336,6 +409,7 @@ SELECT TOP (@limit)
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = ca.id) OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = ca.id)
) )
WHERE m.company_id = a.company_id WHERE m.company_id = a.company_id
AND l.warehouse_id IS NOT NULL
GROUP BY ca.id GROUP BY ca.id
) comp_stock ON comp_stock.comp_id = ala.article_id ) comp_stock ON comp_stock.comp_id = ala.article_id
WHERE ala.articlelist_id = a.articlelist_id WHERE ala.articlelist_id = a.articlelist_id
@@ -347,6 +421,7 @@ SELECT TOP (@limit)
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id INNER JOIN manifest m ON m.id = ma.manifest_id
WHERE m.company_id = a.company_id WHERE m.company_id = a.company_id
AND l.warehouse_id IS NOT NULL
AND ( AND (
ma.sku = a.sku ma.sku = a.sku
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id) OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id)
@@ -356,7 +431,14 @@ SELECT TOP (@limit)
co.id AS companyId, co.id AS companyId,
co.root_category_id AS rootCategoryId, co.root_category_id AS rootCategoryId,
co.poscustomergroup_id AS posCustomerGroupId co.poscustomergroup_id AS posCustomerGroupId
FROM article a FROM (
SELECT entity_id, MAX(id) AS syncId
FROM posterminalsyncqueue
WHERE posterminal_id = @posterminalId AND entity_type = 'article'
GROUP BY entity_id
HAVING MAX(id) > @cursor
) sq
INNER JOIN article a ON a.id = sq.entity_id
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id
INNER JOIN company co ON co.id = a.company_id INNER JOIN company co ON co.id = a.company_id
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = co.default_language_id LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = co.default_language_id
@@ -370,8 +452,7 @@ WHERE EXISTS (
WHERE ca.article_id = a.id WHERE ca.article_id = a.id
AND pc.posterminal_id = @posterminalId AND pc.posterminal_id = @posterminalId
) )
AND DATEDIFF_BIG(second, '1970-01-01', a.updated_at) > @cursor ORDER BY sq.syncId ASC
ORDER BY DATEDIFF_BIG(second, '1970-01-01', a.updated_at) ASC, a.id ASC
`; `;
export const LIST_POS_PRODUCT_CATEGORIES = ` export const LIST_POS_PRODUCT_CATEGORIES = `
@@ -387,6 +468,92 @@ WHERE pc.posterminal_id = @posterminalId
ORDER BY ca.article_id ASC, ca.category_id ASC ORDER BY ca.article_id ASC, ca.category_id ASC
`; `;
export const LIST_POS_CUSTOMER_GROUPS = `
SELECT TOP (@limit)
cg.id,
cg.name,
CASE WHEN cg.id = co.poscustomergroup_id THEN '1' ELSE '0' END AS standard,
'0.000000' AS discountPercent,
sq.syncId AS lastChanged
FROM (
SELECT entity_id, MAX(id) AS syncId
FROM posterminalsyncqueue
WHERE posterminal_id = @posterminalId AND entity_type = 'customergroup'
GROUP BY entity_id
HAVING MAX(id) > @cursor
) sq
INNER JOIN customergroup cg ON cg.id = sq.entity_id
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id
INNER JOIN company co ON co.id = pt.company_id
WHERE cg.name <> 'Lieferanten'
ORDER BY sq.syncId ASC
`;
export const LIST_POS_CUSTOMERS = `
SELECT TOP (@limit)
e.id,
e.customergroup_id AS customergroupId,
e.first_name AS firstName,
e.name AS lastName,
e.[number] AS customerNumber,
(
SELECT TOP 1 ea.street
FROM entityaddress ea
WHERE ea.entity_id = e.id
ORDER BY CASE WHEN ea.addresstype = 'billing' THEN 0 ELSE 1 END, ea.sort ASC, ea.id ASC
) AS street,
(
SELECT TOP 1 ea.streetnumber
FROM entityaddress ea
WHERE ea.entity_id = e.id
ORDER BY CASE WHEN ea.addresstype = 'billing' THEN 0 ELSE 1 END, ea.sort ASC, ea.id ASC
) AS streetNumber,
(
SELECT TOP 1 ea.city
FROM entityaddress ea
WHERE ea.entity_id = e.id
ORDER BY CASE WHEN ea.addresstype = 'billing' THEN 0 ELSE 1 END, ea.sort ASC, ea.id ASC
) AS city,
(
SELECT TOP 1 ea.zip
FROM entityaddress ea
WHERE ea.entity_id = e.id
ORDER BY CASE WHEN ea.addresstype = 'billing' THEN 0 ELSE 1 END, ea.sort ASC, ea.id ASC
) AS postalCode,
(
SELECT TOP 1 ea.county
FROM entityaddress ea
WHERE ea.entity_id = e.id
ORDER BY CASE WHEN ea.addresstype = 'billing' THEN 0 ELSE 1 END, ea.sort ASC, ea.id ASC
) AS [state],
(
SELECT TOP 1 ec.contact
FROM entitycontact ec
WHERE ec.entity_id = e.id AND ec.contacttype IN ('phone', 'mobile', 'tel')
ORDER BY ec.sort ASC, ec.id ASC
) AS phone,
(
SELECT TOP 1 ec.contact
FROM entitycontact ec
WHERE ec.entity_id = e.id AND ec.contacttype IN ('email', 'mail')
ORDER BY ec.sort ASC, ec.id ASC
) AS email,
sq.syncId AS lastChanged
FROM (
SELECT entity_id, MAX(id) AS syncId
FROM posterminalsyncqueue
WHERE posterminal_id = @posterminalId AND entity_type = 'customer'
GROUP BY entity_id
HAVING MAX(id) > @cursor
) sq
INNER JOIN entity e ON e.id = sq.entity_id
INNER JOIN customergroup cg ON cg.id = e.customergroup_id
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id
WHERE cg.name <> 'Lieferanten'
ORDER BY sq.syncId ASC
`;
export const LIST_POS_CUSTOMER_GROUP_IDS = ` export const LIST_POS_CUSTOMER_GROUP_IDS = `
SELECT cg.id SELECT cg.id
FROM customergroup cg FROM customergroup cg
@@ -395,6 +562,7 @@ WHERE cg.name <> 'Lieferanten'
ORDER BY cg.id ASC ORDER BY cg.id ASC
`; `;
export const LIST_POS_PRODUCT_PRICES = ` export const LIST_POS_PRODUCT_PRICES = `
SELECT SELECT
pr.article_id AS articleId, pr.article_id AS articleId,
@@ -406,3 +574,89 @@ INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.compan
WHERE pr.minmultiplier = 1 WHERE pr.minmultiplier = 1
GROUP BY pr.article_id, pr.customergroup_id GROUP BY pr.article_id, pr.customergroup_id
`; `;
export const LIST_POS_MANDATED_OPTIONS = `
SELECT
a.id AS articleId,
ola.count,
COALESCE(opt_at.name, opt_a.sku) AS depositName,
opt_pr.net AS depositNetPrice,
COALESCE(opt_ptax.rate, opt_ctax.rate, 0) AS depositTaxRate
FROM article a
INNER JOIN optionslistarticle ola ON ola.optionslist_id = a.optionslist_id AND ola.mandated = 1
INNER JOIN article opt_a ON opt_a.id = ola.article_id
INNER JOIN company co ON co.id = a.company_id
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id
LEFT JOIN articletext opt_at ON opt_at.article_id = opt_a.id AND opt_at.language_id = co.default_language_id
LEFT JOIN price opt_pr ON opt_pr.article_id = opt_a.id AND opt_pr.customergroup_id = co.poscustomergroup_id AND opt_pr.minmultiplier = 1
LEFT JOIN tax opt_ptax ON opt_ptax.id = opt_pr.tax_id
LEFT JOIN tax opt_ctax ON opt_ctax.id = co.taxrate
ORDER BY ola.sort ASC, ola.id ASC
`;
export const LIST_POS_PARENT_OPTIONS = `
SELECT
fa.id AS parentArticleId,
COALESCE(fan.name, 'Variante') AS attributeName,
av.value AS variantValue
FROM article fa
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = fa.company_id
INNER JOIN article child ON child.parentarticle_id = fa.id
LEFT JOIN attributename fan ON fan.id = fa.parentattributename_id
LEFT JOIN articleattributevalue aav ON aav.article_id = child.id
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND (fa.parentattributename_id IS NULL OR av.attribute_id = fa.parentattributename_id)
WHERE fa.is_parent = 1
ORDER BY fa.id ASC, child.parentsort ASC, child.id ASC
`;
export const RECORD_OBITUARY = `
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
SELECT pt.id, @entityType, @entityId
FROM posterminal pt
WHERE pt.company_id = @companyId AND pt.token IS NOT NULL
`;
export const RECORD_SYNC_QUEUE = `
INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id)
SELECT pt.id, @entityType, @entityId
FROM posterminal pt
WHERE pt.company_id = @companyId
`;
export const SEED_SYNC_QUEUE_FOR_TERMINAL = `
INSERT INTO posterminalsyncqueue (posterminal_id, entity_type, entity_id)
SELECT @posterminalId, 'customergroup', cg.id
FROM customergroup cg
WHERE cg.company_id = @companyId
UNION ALL
SELECT @posterminalId, 'customer', e.id
FROM entity e
INNER JOIN customergroup cg ON cg.id = e.customergroup_id
WHERE cg.company_id = @companyId
UNION ALL
SELECT @posterminalId, 'category', c.id
FROM category c
INNER JOIN posterminalcategory pc ON pc.category_id = c.id
WHERE pc.posterminal_id = @posterminalId
UNION ALL
SELECT @posterminalId, 'article', a.id
FROM article a
WHERE a.company_id = @companyId
`;
export const COUNT_DELETED_ENTITIES = `
SELECT COUNT(*) AS count
FROM posterminalobituary
WHERE posterminal_id = @posterminalId AND id > @cursor
`;
export const LIST_DELETED_ENTITIES = `
SELECT TOP (@limit)
id,
entity_type AS entityType,
entity_id AS entityId
FROM posterminalobituary
WHERE posterminal_id = @posterminalId AND id > @cursor
ORDER BY id ASC
`;

View File

@@ -0,0 +1,109 @@
export const PRINTERAGENT_COLUMNS = `
id,
company_id AS companyId,
name,
token,
pairingpin,
paired_at AS pairedAt,
agentversion,
lastseen_at AS lastseenAt,
updated_at AS updatedAt,
sort,
enabled
`;
export const LIST_BY_COMPANY = `
SELECT
${PRINTERAGENT_COLUMNS}
FROM printeragent
WHERE company_id = @companyId
ORDER BY sort, id
`;
export const GET_BY_COMPANY_AND_ID = `
SELECT
${PRINTERAGENT_COLUMNS}
FROM printeragent
WHERE id = @id AND company_id = @companyId
`;
export const GET_BY_ID = `
SELECT
${PRINTERAGENT_COLUMNS}
FROM printeragent
WHERE id = @id
`;
export const FIND_BY_PAIRINGPIN = `
SELECT
${PRINTERAGENT_COLUMNS}
FROM printeragent
WHERE pairingpin = @pairingpin
`;
export const FIND_BY_TOKEN = `
SELECT
${PRINTERAGENT_COLUMNS}
FROM printeragent
WHERE token = @token
`;
export const PAIR = `
UPDATE printeragent
SET token = @token,
pairingpin = NULL,
paired_at = @pairedAt,
agentversion = @agentversion,
lastseen_at = SYSUTCDATETIME(),
updated_at = SYSUTCDATETIME()
WHERE id = @id
`;
export const UPDATE_STATE = `
UPDATE printeragent
SET clientstate = @clientstate,
lastseen_at = SYSUTCDATETIME(),
updated_at = SYSUTCDATETIME()
WHERE id = @id
`;
export const TOUCH_LASTSEEN = `
UPDATE printeragent
SET lastseen_at = SYSUTCDATETIME()
WHERE id = @id
`;
export const TOUCH_LASTSEEN_AND_VERSION = `
UPDATE printeragent
SET lastseen_at = SYSUTCDATETIME(),
agentversion = @agentversion
WHERE id = @id
`;
export const CREATE = `
INSERT INTO printeragent (company_id, name, pairingpin, sort, enabled)
OUTPUT
INSERTED.id,
INSERTED.company_id AS companyId,
INSERTED.name,
INSERTED.token,
INSERTED.pairingpin,
INSERTED.paired_at AS pairedAt,
INSERTED.agentversion,
INSERTED.lastseen_at AS lastseenAt,
INSERTED.updated_at AS updatedAt,
INSERTED.sort,
INSERTED.enabled
VALUES (@companyId, @name, @pairingpin, @sort, @enabled)
`;
export const UPDATE = `
UPDATE printeragent
SET name = @name, sort = @sort, enabled = @enabled, updated_at = SYSUTCDATETIME()
WHERE id = @id AND company_id = @companyId
`;
export const DELETE_BY_COMPANY_AND_ID = `
DELETE FROM printeragent
WHERE id = @id AND company_id = @companyId
`;

View File

@@ -0,0 +1,115 @@
export const PRINTERJOB_COLUMNS = `
pj.id,
pj.agentprinter_id AS agentprinterId,
pj.type,
pj.state,
pj.data,
pj.created_at AS createdAt,
pj.completed_at AS completedAt
`;
export const CREATE = `
INSERT INTO printerjob (agentprinter_id, type, state, data, created_at)
OUTPUT
INSERTED.id,
INSERTED.agentprinter_id AS agentprinterId,
INSERTED.type,
INSERTED.state,
INSERTED.data,
INSERTED.created_at AS createdAt,
INSERTED.completed_at AS completedAt
VALUES (@agentprinterId, @type, @state, @data, SYSUTCDATETIME())
`;
export const GET_BY_ID_FOR_COMPANY = `
SELECT
${PRINTERJOB_COLUMNS},
ap.printeragent_id AS printeragentId
FROM printerjob pj
JOIN agentprinter ap ON ap.id = pj.agentprinter_id
JOIN printeragent pa ON pa.id = ap.printeragent_id
WHERE pj.id = @id AND pa.company_id = @companyId
`;
export const GET_BY_ID_FOR_AGENT = `
SELECT
${PRINTERJOB_COLUMNS},
ap.printeragent_id AS printeragentId
FROM printerjob pj
JOIN agentprinter ap ON ap.id = pj.agentprinter_id
WHERE pj.id = @id AND ap.printeragent_id = @agentId
`;
export const LIST_BY_COMPANY = `
SELECT
${PRINTERJOB_COLUMNS},
ap.printeragent_id AS printeragentId
FROM printerjob pj
JOIN agentprinter ap ON ap.id = pj.agentprinter_id
JOIN printeragent pa ON pa.id = ap.printeragent_id
WHERE pa.company_id = @companyId
ORDER BY pj.id DESC
`;
export const LIST_BY_COMPANY_AND_AGENTPRINTER = `
SELECT
${PRINTERJOB_COLUMNS},
ap.printeragent_id AS printeragentId
FROM printerjob pj
JOIN agentprinter ap ON ap.id = pj.agentprinter_id
JOIN printeragent pa ON pa.id = ap.printeragent_id
WHERE pa.company_id = @companyId AND pj.agentprinter_id = @agentprinterId
ORDER BY pj.id DESC
`;
export const LIST_BY_AGENT = `
SELECT
${PRINTERJOB_COLUMNS},
ap.printeragent_id AS printeragentId
FROM printerjob pj
JOIN agentprinter ap ON ap.id = pj.agentprinter_id
WHERE ap.printeragent_id = @agentId
ORDER BY pj.id ASC
`;
export const LIST_BY_AGENT_AND_STATE = `
SELECT
${PRINTERJOB_COLUMNS},
ap.printeragent_id AS printeragentId
FROM printerjob pj
JOIN agentprinter ap ON ap.id = pj.agentprinter_id
WHERE ap.printeragent_id = @agentId AND pj.state = @state
ORDER BY pj.id ASC
`;
export const UPDATE_STATE_FOR_AGENT = `
UPDATE printerjob
SET
state = @state,
completed_at = CASE WHEN @state IN ('completed', 'cancelled') THEN SYSUTCDATETIME() ELSE completed_at END
WHERE id = @id AND agentprinter_id IN (
SELECT ap.id FROM agentprinter ap WHERE ap.printeragent_id = @agentId
)
`;
export const UPDATE_STATE_FOR_COMPANY = `
UPDATE printerjob
SET
state = @state,
completed_at = CASE WHEN @state IN ('completed', 'cancelled') THEN SYSUTCDATETIME() ELSE completed_at END
WHERE id = @id AND agentprinter_id IN (
SELECT ap.id FROM agentprinter ap
JOIN printeragent pa ON pa.id = ap.printeragent_id
WHERE pa.company_id = @companyId
)
`;
export const DELETE_BY_COMPANY_AND_ID = `
DELETE FROM printerjob
WHERE id = @id AND agentprinter_id IN (
SELECT ap.id FROM agentprinter ap
JOIN printeragent pa ON pa.id = ap.printeragent_id
WHERE pa.company_id = @companyId
)
`;

View File

@@ -0,0 +1,25 @@
export const RETOURE_TYPE = 'retoure';
export const GET_RETOURE_HEADER = `
SELECT
m.id,
m.[number],
m.[type],
m.entity_id AS entityId,
m.external_number AS externalNumber,
m.[date],
m.created_at AS createdAt,
rs.positions,
rs.gross_total AS grossTotal,
rs.paid_total AS paidTotal,
rs.ordered_quantity AS orderedQuantity,
rs.shipped_quantity AS shippedQuantity,
rs.is_paid AS isPaid,
rs.is_shipped AS isShipped,
rs.customer_name AS customerName
FROM manifest m
LEFT JOIN retouresummary rs ON rs.manifest_id = m.id
WHERE m.id = @id
AND m.company_id = @companyId
AND m.[type] = @type
`;

View File

@@ -0,0 +1,79 @@
export const RETURN_TYPE = 'return';
export const GET_RETURN_HEADER = `
SELECT
m.id,
m.[number],
m.[type],
m.entity_id AS supplierId,
m.external_number AS externalNumber,
m.[date],
m.created_at AS createdAt,
rs.positions,
rs.gross_total AS grossTotal,
rs.net_total AS netTotal,
rs.total_quantity AS totalQuantity,
rs.stock_positions AS stockPositions,
rs.booked_positions AS bookedPositions,
rs.unbooked_positions AS unbookedPositions,
rs.stock_quantity AS stockQuantity,
rs.booked_quantity AS bookedQuantity,
rs.unbooked_quantity AS unbookedQuantity,
rs.is_booked AS isBooked,
rs.supplier_name AS supplierName
FROM manifest m
LEFT JOIN returnsummary rs ON rs.manifest_id = m.id
WHERE m.id = @id
AND m.company_id = @companyId
AND m.[type] = @type
`;
export const GET_RETURN_LINES = `
SELECT
ma.id,
ma.sku,
ma.name,
ma.pu,
ma.amount,
ma.net,
ma.rebate,
ma.taxrate,
ma.comment
FROM manifestarticle ma
WHERE ma.manifest_id = @manifestId
ORDER BY ma.id
`;
export const LIST_RETURNS = `
SELECT
m.id,
m.[number],
m.[type],
m.entity_id AS supplierId,
m.external_number AS externalNumber,
m.[date],
m.created_at AS createdAt,
COUNT(ma.id) AS totalPositions,
COUNT(CASE WHEN ma.id IS NOT NULL AND (
(ma.amount > 0 AND COALESCE(ma_ledger.booked_amount, 0) >= ma.amount) OR
(ma.amount <= 0 AND COALESCE(ma_ledger.booked_amount, 0) > 0)
) THEN 1 END) AS completelyOnLedgerPositions
FROM manifest m
LEFT JOIN manifestarticle ma ON ma.manifest_id = m.id
LEFT JOIN (
SELECT manifestarticle_id, SUM(amount) AS booked_amount
FROM ledger
GROUP BY manifestarticle_id
) ma_ledger ON ma_ledger.manifestarticle_id = ma.id
WHERE m.company_id = @companyId
AND m.[type] = @type
GROUP BY
m.id,
m.[number],
m.[type],
m.entity_id,
m.external_number,
m.[date],
m.created_at
ORDER BY m.[date] DESC, m.id DESC
`;

View File

@@ -3,11 +3,11 @@ export const SHIPMENT_TYPE = 'shipment';
export const GET_ORDER_HEADER = ` export const GET_ORDER_HEADER = `
SELECT id, company_id AS companyId, [number], [type], entity_id AS entityId, external_number AS externalNumber, [date] SELECT id, company_id AS companyId, [number], [type], entity_id AS entityId, external_number AS externalNumber, [date]
FROM manifest FROM manifest
WHERE id = @orderId AND company_id = @companyId AND [type] = 'order'; WHERE id = @orderId AND company_id = @companyId AND [type] IN ('order', 'retoure');
`; `;
export const GET_ORDER_LINES = ` export const GET_ORDER_LINES = `
SELECT id, sku, name, pu, amount, net, rebate, taxrate, comment SELECT id, sku, name, pu, amount, net, rebate, taxrate, shippable, comment
FROM manifestarticle FROM manifestarticle
WHERE manifest_id = @orderId WHERE manifest_id = @orderId
ORDER BY id ASC; ORDER BY id ASC;
@@ -22,21 +22,25 @@ WHERE company_id = @companyId AND [type] = 'shipment';
export const LIST_SHIPMENTS_BY_ORDER_NUMBER_PATTERN = ` export const LIST_SHIPMENTS_BY_ORDER_NUMBER_PATTERN = `
SELECT m.id, m.[number], m.[type], m.entity_id AS entityId, m.external_number AS externalNumber, m.[date], m.created_at AS createdAt SELECT m.id, m.[number], m.[type], m.entity_id AS entityId, m.external_number AS externalNumber, m.[date], m.created_at AS createdAt
FROM manifest m FROM manifest m
LEFT JOIN shipmentsummary ss ON ss.manifest_id = m.id
WHERE m.company_id = @companyId WHERE m.company_id = @companyId
AND m.[type] = 'shipment' AND m.[type] = 'shipment'
AND m.[number] LIKE @pattern AND m.[number] LIKE @pattern
AND (ss.state IS NULL OR ss.state != 'cancelled')
ORDER BY m.id ASC; ORDER BY m.id ASC;
`; `;
export const GET_SHIPPED_AMOUNTS_BY_ORDER_PATTERN = ` export const GET_SHIPPED_AMOUNTS_BY_ORDER_PATTERN = `
SELECT ma.sku, SUM(ma.amount) AS shippedAmount SELECT ma.sku, ma.name, SUM(ma.amount) AS shippedAmount
FROM manifest m FROM manifest m
INNER JOIN manifestarticle ma ON ma.manifest_id = m.id INNER JOIN manifestarticle ma ON ma.manifest_id = m.id
LEFT JOIN shipmentsummary ss ON ss.manifest_id = m.id
WHERE m.company_id = @companyId WHERE m.company_id = @companyId
AND m.[type] = 'shipment' AND m.[type] = 'shipment'
AND m.[number] LIKE @pattern AND m.[number] LIKE @pattern
AND (@excludeShipmentId IS NULL OR m.id != @excludeShipmentId) AND (@excludeShipmentId IS NULL OR m.id != @excludeShipmentId)
GROUP BY ma.sku; AND (ss.state IS NULL OR ss.state != 'cancelled')
GROUP BY ma.sku, ma.name;
`; `;
export const GET_SHIPMENT_HEADER = ` export const GET_SHIPMENT_HEADER = `
@@ -51,7 +55,52 @@ FROM shipping
WHERE manifest_id = @id AND LOWER([state]) = 'shipped'; WHERE manifest_id = @id AND LOWER([state]) = 'shipped';
`; `;
export const CHECK_ORDER_HAS_SHIPMENTS = `
SELECT TOP 1 m.id
FROM manifest m
LEFT JOIN shipmentsummary ss ON ss.manifest_id = m.id AND ss.company_id = @companyId
WHERE m.company_id = @companyId
AND m.[type] = 'shipment'
AND (ss.state IS NULL OR ss.state != 'cancelled')
AND (
m.[number] LIKE @pattern
OR ss.order_id = @orderId
OR ss.order_number = @orderNumber
);
`;
export const DELETE_SHIPMENT = ` export const DELETE_SHIPMENT = `
DELETE FROM manifest DELETE FROM manifest
WHERE id = @id AND company_id = @companyId AND [type] = 'shipment'; WHERE id = @id AND company_id = @companyId AND [type] = 'shipment';
`; `;
export const GET_DEFAULT_WAREHOUSE = `
SELECT TOP 1 id, name
FROM warehouse
WHERE company_id = @companyId
ORDER BY sort ASC, name ASC, id ASC;
`;
export const GET_ARTICLE_TRACK_STOCK = `
SELECT TOP 1 a.id, a.sku, a.track_stock
FROM article a
WHERE a.company_id = @companyId
AND (
a.sku = @sku
OR EXISTS (SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku)
);
`;
export const GET_ARTICLE_WAREHOUSE_STOCK = `
SELECT COALESCE(SUM(l.amount), 0) AS stock
FROM ledger l
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id
WHERE l.warehouse_id = @warehouseId
AND m.company_id = @companyId
AND (
ma.sku = @sku
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = @articleId)
);
`;

View File

@@ -0,0 +1,88 @@
export const SUBAGENTSCHEDULE_COLUMNS = `
id,
company_id,
[desc],
prompt,
skills,
lastagentnote,
max_itr,
cron,
created_at,
lastrun_at
`;
export const LIST_BY_COMPANY = `
SELECT
${SUBAGENTSCHEDULE_COLUMNS}
FROM subagentschedule
WHERE company_id = @companyId
ORDER BY id ASC
`;
export const GET_BY_COMPANY_AND_ID = `
SELECT
${SUBAGENTSCHEDULE_COLUMNS}
FROM subagentschedule
WHERE id = @id AND company_id = @companyId
`;
export const CREATE = `
INSERT INTO subagentschedule (
company_id,
[desc],
prompt,
skills,
lastagentnote,
max_itr,
cron
)
OUTPUT
INSERTED.id,
INSERTED.company_id,
INSERTED.[desc],
INSERTED.prompt,
INSERTED.skills,
INSERTED.lastagentnote,
INSERTED.max_itr,
INSERTED.cron,
INSERTED.created_at,
INSERTED.lastrun_at
VALUES (
@companyId,
@desc,
@prompt,
@skills,
@lastagentnote,
@maxItr,
@cron
)
`;
export const UPDATE = `
UPDATE subagentschedule
SET
[desc] = @desc,
prompt = @prompt,
skills = @skills,
lastagentnote = @lastagentnote,
max_itr = @maxItr,
cron = @cron,
lastrun_at = @lastrunAt
OUTPUT
INSERTED.id,
INSERTED.company_id,
INSERTED.[desc],
INSERTED.prompt,
INSERTED.skills,
INSERTED.lastagentnote,
INSERTED.max_itr,
INSERTED.cron,
INSERTED.created_at,
INSERTED.lastrun_at
WHERE id = @id AND company_id = @companyId
`;
export const DELETE_BY_COMPANY_AND_ID = `
DELETE FROM subagentschedule
WHERE id = @id AND company_id = @companyId
`;

View File

@@ -67,3 +67,16 @@ WHERE id = @id AND company_id = @companyId;
export const CHECK_LEDGER_USAGE = ` export const CHECK_LEDGER_USAGE = `
SELECT TOP 1 id FROM ledger WHERE warehouse_id = @warehouseId; SELECT TOP 1 id FROM ledger WHERE warehouse_id = @warehouseId;
`; `;
export const COUNT_BY_COMPANY = `
SELECT COUNT(1) AS count
FROM warehouse
WHERE company_id = @companyId;
`;
export const CHECK_TRACK_STOCK_ARTICLES = `
SELECT TOP 1 id
FROM article
WHERE company_id = @companyId AND track_stock = 1;
`;

View File

@@ -1,3 +1,5 @@
import { SCHEMA_VERSION } from './migrate.js';
export const INIT_META = ` export const INIT_META = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'meta') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'meta')
BEGIN BEGIN
@@ -11,7 +13,7 @@ END
IF NOT EXISTS (SELECT 1 FROM meta WHERE [key] = N'version') IF NOT EXISTS (SELECT 1 FROM meta WHERE [key] = N'version')
BEGIN BEGIN
INSERT INTO meta ([key], value) INSERT INTO meta ([key], value)
VALUES (N'version', N'1'); VALUES (N'version', N'${SCHEMA_VERSION}');
END END
`; `;
@@ -97,10 +99,8 @@ BEGIN
thumbhash NVARCHAR(64) NOT NULL, thumbhash NVARCHAR(64) NOT NULL,
smallthumbw INT NOT NULL, smallthumbw INT NOT NULL,
smallthumbh INT NOT NULL, smallthumbh INT NOT NULL,
source NVARCHAR(MAX) NOT NULL,
fullbinary VARBINARY(MAX) NOT NULL,
thumbbinary VARBINARY(MAX) NOT NULL,
smallthumbbinary VARBINARY(MAX) NOT NULL, smallthumbbinary VARBINARY(MAX) NOT NULL,
source NVARCHAR(MAX) NOT NULL,
CONSTRAINT UQ_pic_hash UNIQUE (hash) CONSTRAINT UQ_pic_hash UNIQUE (hash)
); );
END END
@@ -118,7 +118,6 @@ BEGIN
reducedtaxrate INT NULL, reducedtaxrate INT NULL,
taxfreerate INT NULL, taxfreerate INT NULL,
poscustomergroup_id INT NOT NULL, poscustomergroup_id INT NOT NULL,
CONSTRAINT UQ_company_name UNIQUE (name),
CONSTRAINT FK_company_root_category FOREIGN KEY (root_category_id) REFERENCES category(id) ON DELETE SET NULL, CONSTRAINT FK_company_root_category FOREIGN KEY (root_category_id) REFERENCES category(id) ON DELETE SET NULL,
CONSTRAINT FK_company_default_language FOREIGN KEY (default_language_id) REFERENCES language(id) ON DELETE SET NULL, CONSTRAINT FK_company_default_language FOREIGN KEY (default_language_id) REFERENCES language(id) ON DELETE SET NULL,
CONSTRAINT FK_company_taxrate FOREIGN KEY (taxrate) REFERENCES tax(id) ON DELETE NO ACTION, CONSTRAINT FK_company_taxrate FOREIGN KEY (taxrate) REFERENCES tax(id) ON DELETE NO ACTION,
@@ -322,7 +321,7 @@ BEGIN
unit_id INT NOT NULL, unit_id INT NOT NULL,
unitamount FLOAT NOT NULL, unitamount FLOAT NOT NULL,
weight FLOAT NOT NULL, weight FLOAT NOT NULL,
track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1, track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 0,
brand_id INT NULL, brand_id INT NULL,
articlelist_id INT NULL, articlelist_id INT NULL,
optionslist_id INT NULL, optionslist_id INT NULL,
@@ -358,9 +357,19 @@ BEGIN
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock') IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
BEGIN BEGIN
ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1; ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 0;
END END
END END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_article_sku' AND object_id = OBJECT_ID('article'))
BEGIN
CREATE NONCLUSTERED INDEX IX_article_sku ON article (sku);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_article_parentarticle_id' AND object_id = OBJECT_ID('article'))
BEGIN
CREATE NONCLUSTERED INDEX IX_article_parentarticle_id ON article (parentarticle_id);
END
`; `;
export const INIT_ARTICLEHISTORY = ` export const INIT_ARTICLEHISTORY = `
@@ -388,32 +397,46 @@ BEGIN
[date] DATETIME2 NOT NULL CONSTRAINT DF_companyhistory_date DEFAULT SYSUTCDATETIME(), [date] DATETIME2 NOT NULL CONSTRAINT DF_companyhistory_date DEFAULT SYSUTCDATETIME(),
[text] NVARCHAR(MAX) NOT NULL, [text] NVARCHAR(MAX) NOT NULL,
CONSTRAINT FK_companyhistory_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, CONSTRAINT FK_companyhistory_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
CONSTRAINT FK_companyhistory_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL CONSTRAINT FK_companyhistory_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE NO ACTION
); );
END END
`; `;
export const INIT_ARTICLETOMBSTONE = ` export const INIT_POSTERMINALOBITUARY = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articletombstone') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'posterminalobituary')
BEGIN BEGIN
CREATE TABLE articletombstone ( CREATE TABLE posterminalobituary (
id INT IDENTITY(1,1) PRIMARY KEY, id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL, posterminal_id INT NOT NULL,
article_id INT NULL, entity_type NVARCHAR(50) NOT NULL,
sku NVARCHAR(100) NULL, entity_id INT NOT NULL,
name NVARCHAR(255) NULL, created_at DATETIME2 NOT NULL CONSTRAINT DF_posterminalobituary_created_at DEFAULT SYSUTCDATETIME(),
taxrate DECIMAL(18,4) NULL, CONSTRAINT FK_posterminalobituary_posterminal FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE
net FLOAT NULL,
deleted_at DATETIME2 NOT NULL CONSTRAINT DF_articletombstone_deleted_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT CK_articletombstone_article_or_sku CHECK (article_id IS NOT NULL OR sku IS NOT NULL),
CONSTRAINT FK_articletombstone_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
); );
CREATE INDEX IX_articletombstone_company_deleted CREATE INDEX IX_posterminalobituary_posterminal_id
ON articletombstone (company_id, deleted_at); ON posterminalobituary (posterminal_id, id);
END END
`; `;
export const INIT_POSTERMINALSYNCQUEUE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'posterminalsyncqueue')
BEGIN
CREATE TABLE posterminalsyncqueue (
id INT IDENTITY(1,1) PRIMARY KEY,
posterminal_id INT NOT NULL,
entity_type NVARCHAR(50) NOT NULL,
entity_id INT NOT NULL,
CONSTRAINT CK_posterminalsyncqueue_entity_type CHECK (entity_type IN ('article', 'category', 'customer', 'customergroup')),
CONSTRAINT FK_posterminalsyncqueue_posterminal FOREIGN KEY (posterminal_id) REFERENCES posterminal(id) ON DELETE CASCADE
);
CREATE INDEX IX_posterminalsyncqueue_posterminal_id
ON posterminalsyncqueue (posterminal_id, id);
END
`;
export const INIT_ARTICLEEAN = ` export const INIT_ARTICLEEAN = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean')
BEGIN BEGIN
@@ -619,7 +642,7 @@ BEGIN
pu FLOAT NOT NULL CONSTRAINT DF_articlesupplier_pu DEFAULT 1, pu FLOAT NOT NULL CONSTRAINT DF_articlesupplier_pu DEFAULT 1,
sort INT NOT NULL CONSTRAINT DF_articlesupplier_sort DEFAULT 0, sort INT NOT NULL CONSTRAINT DF_articlesupplier_sort DEFAULT 0,
CONSTRAINT FK_articlesupplier_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE, CONSTRAINT FK_articlesupplier_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
CONSTRAINT FK_articlesupplier_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE, CONSTRAINT FK_articlesupplier_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE NO ACTION,
CONSTRAINT FK_articlesupplier_tax FOREIGN KEY (tax_id) REFERENCES tax(id) ON DELETE NO ACTION, CONSTRAINT FK_articlesupplier_tax FOREIGN KEY (tax_id) REFERENCES tax(id) ON DELETE NO ACTION,
CONSTRAINT UQ_articlesupplier_article_entity UNIQUE (article_id, entity_id), CONSTRAINT UQ_articlesupplier_article_entity UNIQUE (article_id, entity_id),
CONSTRAINT UQ_articlesupplier_article_sort UNIQUE (article_id, sort) CONSTRAINT UQ_articlesupplier_article_sort UNIQUE (article_id, sort)
@@ -628,6 +651,18 @@ BEGIN
CREATE INDEX IX_articlesupplier_article_entity CREATE INDEX IX_articlesupplier_article_entity
ON articlesupplier (article_id, entity_id); ON articlesupplier (article_id, entity_id);
END END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_articlesupplier_sku' AND object_id = OBJECT_ID('articlesupplier'))
BEGIN
CREATE NONCLUSTERED INDEX IX_articlesupplier_sku
ON articlesupplier (sku, article_id);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_articlesupplier_entity' AND object_id = OBJECT_ID('articlesupplier'))
BEGIN
CREATE NONCLUSTERED INDEX IX_articlesupplier_entity
ON articlesupplier (entity_id);
END
`; `;
export const INIT_MANIFEST = ` export const INIT_MANIFEST = `
@@ -643,9 +678,21 @@ BEGIN
[date] DATETIME2 NOT NULL CONSTRAINT DF_manifest_date DEFAULT SYSUTCDATETIME(), [date] DATETIME2 NOT NULL CONSTRAINT DF_manifest_date DEFAULT SYSUTCDATETIME(),
created_at DATETIME2 NOT NULL CONSTRAINT DF_manifest_created_at DEFAULT SYSUTCDATETIME(), created_at DATETIME2 NOT NULL CONSTRAINT DF_manifest_created_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_manifest_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION, CONSTRAINT FK_manifest_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
CONSTRAINT FK_manifest_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL CONSTRAINT FK_manifest_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE NO ACTION
); );
END END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UQ_manifest_company_type_number' AND object_id = OBJECT_ID('manifest'))
BEGIN
CREATE UNIQUE NONCLUSTERED INDEX UQ_manifest_company_type_number
ON manifest (company_id, [type], [number]);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_manifest_entity_id' AND object_id = OBJECT_ID('manifest'))
BEGIN
CREATE NONCLUSTERED INDEX IX_manifest_entity_id
ON manifest (entity_id);
END
`; `;
export const INIT_MANIFESTARTICLE = ` export const INIT_MANIFESTARTICLE = `
@@ -662,9 +709,22 @@ BEGIN
comment NVARCHAR(MAX) NULL, comment NVARCHAR(MAX) NULL,
rebate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_rebate DEFAULT 0, rebate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_rebate DEFAULT 0,
taxrate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_taxrate DEFAULT 0, taxrate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_taxrate DEFAULT 0,
shippable TINYINT NOT NULL CONSTRAINT DF_manifestarticle_shippable DEFAULT 1,
CONSTRAINT FK_manifestarticle_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE CONSTRAINT FK_manifestarticle_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE
); );
END END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_manifestarticle_manifest_id' AND object_id = OBJECT_ID('manifestarticle'))
BEGIN
CREATE NONCLUSTERED INDEX IX_manifestarticle_manifest_id
ON manifestarticle (manifest_id);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_manifestarticle_sku' AND object_id = OBJECT_ID('manifestarticle'))
BEGIN
CREATE NONCLUSTERED INDEX IX_manifestarticle_sku
ON manifestarticle (sku);
END
`; `;
@@ -703,6 +763,18 @@ BEGIN
CONSTRAINT FK_ledger_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE NO ACTION CONSTRAINT FK_ledger_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE NO ACTION
) WITH (LEDGER = ON (APPEND_ONLY = ON)); ) WITH (LEDGER = ON (APPEND_ONLY = ON));
END END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ledger_manifestarticle_warehouse' AND object_id = OBJECT_ID('ledger'))
BEGIN
CREATE NONCLUSTERED INDEX IX_ledger_manifestarticle_warehouse
ON ledger (manifestarticle_id, warehouse_id);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ledger_warehouse_id' AND object_id = OBJECT_ID('ledger'))
BEGIN
CREATE NONCLUSTERED INDEX IX_ledger_warehouse_id
ON ledger (warehouse_id);
END
`; `;
export const INIT_NUMBERS = ` export const INIT_NUMBERS = `
@@ -794,6 +866,22 @@ END
'); ');
`; `;
export const INIT_AUTOINVITE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'autoinvite')
BEGIN
CREATE TABLE autoinvite (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
email NVARCHAR(255) NOT NULL,
role NVARCHAR(10) NOT NULL CONSTRAINT DF_autoinvite_role DEFAULT 'user',
created_at DATETIME2 NOT NULL CONSTRAINT DF_autoinvite_created_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT CK_autoinvite_role CHECK (role IN ('admin', 'user')),
CONSTRAINT UQ_autoinvite_company_email UNIQUE (company_id, email),
CONSTRAINT FK_autoinvite_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
END
`;
export const INIT_COMPANYUSER = ` export const INIT_COMPANYUSER = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'companyuser') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'companyuser')
BEGIN BEGIN
@@ -801,6 +889,8 @@ BEGIN
id INT IDENTITY(1,1) PRIMARY KEY, id INT IDENTITY(1,1) PRIMARY KEY,
google_auth_id INT NOT NULL, google_auth_id INT NOT NULL,
company_id INT NOT NULL, company_id INT NOT NULL,
role NVARCHAR(10) NOT NULL CONSTRAINT DF_companyuser_role DEFAULT 'user',
CONSTRAINT CK_companyuser_role CHECK (role IN ('admin', 'user')),
CONSTRAINT UQ_companyuser_google_auth_company UNIQUE (google_auth_id, company_id), CONSTRAINT UQ_companyuser_google_auth_company UNIQUE (google_auth_id, company_id),
CONSTRAINT FK_companyuser_google_auth FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE, CONSTRAINT FK_companyuser_google_auth FOREIGN KEY (google_auth_id) REFERENCES google_auth(id) ON DELETE CASCADE,
CONSTRAINT FK_companyuser_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE CONSTRAINT FK_companyuser_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
@@ -891,11 +981,14 @@ BEGIN
pairingpin NVARCHAR(50) NULL, pairingpin NVARCHAR(50) NULL,
paired_at DATETIME2 NULL, paired_at DATETIME2 NULL,
clientserial NVARCHAR(255) NULL, clientserial NVARCHAR(255) NULL,
maxknowntombstoneid INT NULL, maxknownobituaryid INT NULL,
root_category_id INT NULL, root_category_id INT NULL,
sync_root_articles BIT NOT NULL CONSTRAINT DF_posterminal_sync_root_articles DEFAULT 1, sync_root_articles BIT NOT NULL CONSTRAINT DF_posterminal_sync_root_articles DEFAULT 1,
lastseen_at DATETIME2 NULL, lastseen_at DATETIME2 NULL,
updated_at DATETIME2 NOT NULL CONSTRAINT DF_posterminal_updated_at DEFAULT SYSUTCDATETIME(), updated_at DATETIME2 NOT NULL CONSTRAINT DF_posterminal_updated_at DEFAULT SYSUTCDATETIME(),
sort INT NOT NULL CONSTRAINT DF_posterminal_sort DEFAULT 0,
enabled BIT NOT NULL CONSTRAINT DF_posterminal_enabled DEFAULT 1,
clientversion NVARCHAR(255) NULL,
CONSTRAINT FK_posterminal_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, CONSTRAINT FK_posterminal_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
CONSTRAINT FK_posterminal_root_category FOREIGN KEY (root_category_id) REFERENCES category(id) ON DELETE SET NULL CONSTRAINT FK_posterminal_root_category FOREIGN KEY (root_category_id) REFERENCES category(id) ON DELETE SET NULL
); );
@@ -936,6 +1029,63 @@ END
`; `;
export const INIT_PRINTERAGENT = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'printeragent')
BEGIN
CREATE TABLE printeragent (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
name NVARCHAR(255) NOT NULL,
token NVARCHAR(255) NULL,
pairingpin NVARCHAR(50) NULL,
paired_at DATETIME2 NULL,
agentversion NVARCHAR(255) NULL,
lastseen_at DATETIME2 NULL,
updated_at DATETIME2 NOT NULL CONSTRAINT DF_printeragent_updated_at DEFAULT SYSUTCDATETIME(),
sort INT NOT NULL CONSTRAINT DF_printeragent_sort DEFAULT 0,
enabled BIT NOT NULL CONSTRAINT DF_printeragent_enabled DEFAULT 1,
CONSTRAINT FK_printeragent_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX UQ_printeragent_token ON printeragent(token) WHERE token IS NOT NULL;
CREATE UNIQUE INDEX UQ_printeragent_pairingpin ON printeragent(pairingpin) WHERE pairingpin IS NOT NULL;
END
`;
export const INIT_AGENTPRINTER = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'agentprinter')
BEGIN
CREATE TABLE agentprinter (
id INT IDENTITY(1,1) PRIMARY KEY,
printeragent_id INT NOT NULL,
type NVARCHAR(50) NOT NULL,
model NVARCHAR(255) NULL,
serial NVARCHAR(255) NULL,
enabled BIT NOT NULL CONSTRAINT DF_agentprinter_enabled DEFAULT 1,
sort INT NOT NULL CONSTRAINT DF_agentprinter_sort DEFAULT 0,
CONSTRAINT FK_agentprinter_printeragent FOREIGN KEY (printeragent_id) REFERENCES printeragent(id) ON DELETE CASCADE
);
CREATE INDEX IX_agentprinter_printeragent_id ON agentprinter(printeragent_id);
END
`;
export const INIT_PRINTERJOB = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'printerjob')
BEGIN
CREATE TABLE printerjob (
id INT IDENTITY(1,1) PRIMARY KEY,
agentprinter_id INT NOT NULL,
type NVARCHAR(50) NOT NULL,
state NVARCHAR(50) NOT NULL CONSTRAINT DF_printerjob_state DEFAULT 'pending',
data NVARCHAR(MAX) NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_printerjob_created_at DEFAULT SYSUTCDATETIME(),
completed_at DATETIME2 NULL,
CONSTRAINT FK_printerjob_agentprinter FOREIGN KEY (agentprinter_id) REFERENCES agentprinter(id) ON DELETE CASCADE
);
CREATE INDEX IX_printerjob_agentprinter_id ON printerjob(agentprinter_id);
CREATE INDEX IX_printerjob_state ON printerjob(state);
END
`;
export const INIT_LABELTEMPLATE = ` export const INIT_LABELTEMPLATE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'labeltemplate') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'labeltemplate')
BEGIN BEGIN
@@ -1138,10 +1288,33 @@ BEGIN
manifest_id INT NOT NULL, manifest_id INT NOT NULL,
[type] NVARCHAR(100) NOT NULL, [type] NVARCHAR(100) NOT NULL,
amount FLOAT NOT NULL, amount FLOAT NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_payment_created_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT FK_payment_account FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE, CONSTRAINT FK_payment_account FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE,
CONSTRAINT FK_payment_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE CONSTRAINT FK_payment_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE
); );
END END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_payment_manifest_id' AND object_id = OBJECT_ID('payment'))
BEGIN
CREATE NONCLUSTERED INDEX IX_payment_manifest_id
ON payment (manifest_id);
END
`;
export const INIT_CARRIER = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'carrier')
BEGIN
CREATE TABLE carrier (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
name NVARCHAR(255) NOT NULL,
sort INT NOT NULL CONSTRAINT DF_carrier_sort DEFAULT 0,
has_packages BIT NOT NULL CONSTRAINT DF_carrier_has_packages DEFAULT 0,
is_reverse BIT NOT NULL CONSTRAINT DF_carrier_is_reverse DEFAULT 0,
CONSTRAINT FK_carrier_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
CONSTRAINT UQ_carrier_company_name UNIQUE (company_id, name)
);
END
`; `;
export const INIT_SHIPPING = ` export const INIT_SHIPPING = `
@@ -1152,10 +1325,11 @@ BEGIN
manifest_id INT NOT NULL, manifest_id INT NOT NULL,
[number] NVARCHAR(100) NOT NULL, [number] NVARCHAR(100) NOT NULL,
tracking_code NVARCHAR(255) NULL, tracking_code NVARCHAR(255) NULL,
carrier NVARCHAR(100) NULL, carrier_id INT NOT NULL,
[type] NVARCHAR(100) NOT NULL, [type] NVARCHAR(100) NOT NULL,
[state] NVARCHAR(100) NOT NULL, [state] NVARCHAR(100) NOT NULL,
CONSTRAINT FK_shipping_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE CONSTRAINT FK_shipping_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
CONSTRAINT FK_shipping_carrier FOREIGN KEY (carrier_id) REFERENCES carrier(id) ON DELETE NO ACTION
); );
END END
`; `;
@@ -1243,6 +1417,183 @@ BEGIN
END END
`; `;
export const INIT_SHIPMENTSUMMARY = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'shipmentsummary')
BEGIN
CREATE TABLE shipmentsummary (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
manifest_id INT NOT NULL CONSTRAINT UQ_shipmentsummary_manifest UNIQUE,
shipment_number NVARCHAR(100) NOT NULL,
shipment_date DATETIME2 NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_shipmentsummary_created_at DEFAULT SYSUTCDATETIME(),
order_id INT NULL,
order_number NVARCHAR(100) NULL,
entity_id INT NULL,
customer_name NVARCHAR(255) NULL,
positions INT NOT NULL CONSTRAINT DF_shipmentsummary_positions DEFAULT 0,
total_quantity FLOAT NOT NULL CONSTRAINT DF_shipmentsummary_total_quantity DEFAULT 0,
package_count INT NOT NULL CONSTRAINT DF_shipmentsummary_package_count DEFAULT 0,
carrier NVARCHAR(100) NULL,
tracking_code NVARCHAR(255) NULL,
state NVARCHAR(50) NOT NULL CONSTRAINT DF_shipmentsummary_state DEFAULT 'new',
CONSTRAINT FK_shipmentsummary_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
CONSTRAINT FK_shipmentsummary_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
CONSTRAINT FK_shipmentsummary_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
);
CREATE INDEX IX_shipmentsummary_company_date
ON shipmentsummary (company_id, shipment_date DESC, manifest_id DESC);
CREATE INDEX IX_shipmentsummary_company_number
ON shipmentsummary (company_id, shipment_number);
CREATE INDEX IX_shipmentsummary_company_state
ON shipmentsummary (company_id, state);
CREATE INDEX IX_shipmentsummary_company_order
ON shipmentsummary (company_id, order_id);
END
`;
export const INIT_DELIVERYSUMMARY = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'deliverysummary')
BEGIN
CREATE TABLE deliverysummary (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
manifest_id INT NOT NULL CONSTRAINT UQ_deliverysummary_manifest UNIQUE,
delivery_number NVARCHAR(100) NOT NULL,
external_number NVARCHAR(100) NULL,
delivery_date DATETIME2 NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_deliverysummary_created_at DEFAULT SYSUTCDATETIME(),
supplier_id INT NULL,
supplier_name NVARCHAR(255) NULL,
positions INT NOT NULL CONSTRAINT DF_deliverysummary_positions DEFAULT 0,
gross_total FLOAT NOT NULL CONSTRAINT DF_deliverysummary_gross_total DEFAULT 0,
net_total FLOAT NOT NULL CONSTRAINT DF_deliverysummary_net_total DEFAULT 0,
total_quantity FLOAT NOT NULL CONSTRAINT DF_deliverysummary_total_quantity DEFAULT 0,
stock_positions INT NOT NULL CONSTRAINT DF_deliverysummary_stock_positions DEFAULT 0,
booked_positions INT NOT NULL CONSTRAINT DF_deliverysummary_booked_positions DEFAULT 0,
unbooked_positions INT NOT NULL CONSTRAINT DF_deliverysummary_unbooked_positions DEFAULT 0,
stock_quantity FLOAT NOT NULL CONSTRAINT DF_deliverysummary_stock_quantity DEFAULT 0,
booked_quantity FLOAT NOT NULL CONSTRAINT DF_deliverysummary_booked_quantity DEFAULT 0,
unbooked_quantity FLOAT NOT NULL CONSTRAINT DF_deliverysummary_unbooked_quantity DEFAULT 0,
is_booked TINYINT NOT NULL CONSTRAINT DF_deliverysummary_is_booked DEFAULT 0,
CONSTRAINT FK_deliverysummary_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
CONSTRAINT FK_deliverysummary_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
CONSTRAINT FK_deliverysummary_supplier FOREIGN KEY (supplier_id) REFERENCES entity(id) ON DELETE SET NULL
);
CREATE INDEX IX_deliverysummary_company_date
ON deliverysummary (company_id, delivery_date DESC, manifest_id DESC);
CREATE INDEX IX_deliverysummary_company_number
ON deliverysummary (company_id, delivery_number);
CREATE INDEX IX_deliverysummary_company_booked
ON deliverysummary (company_id, is_booked);
CREATE INDEX IX_deliverysummary_company_supplier
ON deliverysummary (company_id, supplier_id);
END
`;
export const INIT_RETURNSUMMARY = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'returnsummary')
BEGIN
CREATE TABLE returnsummary (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
manifest_id INT NOT NULL CONSTRAINT UQ_returnsummary_manifest UNIQUE,
return_number NVARCHAR(100) NOT NULL,
external_number NVARCHAR(100) NULL,
return_date DATETIME2 NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_returnsummary_created_at DEFAULT SYSUTCDATETIME(),
supplier_id INT NULL,
supplier_name NVARCHAR(255) NULL,
positions INT NOT NULL CONSTRAINT DF_returnsummary_positions DEFAULT 0,
gross_total FLOAT NOT NULL CONSTRAINT DF_returnsummary_gross_total DEFAULT 0,
net_total FLOAT NOT NULL CONSTRAINT DF_returnsummary_net_total DEFAULT 0,
total_quantity FLOAT NOT NULL CONSTRAINT DF_returnsummary_total_quantity DEFAULT 0,
stock_positions INT NOT NULL CONSTRAINT DF_returnsummary_stock_positions DEFAULT 0,
booked_positions INT NOT NULL CONSTRAINT DF_returnsummary_booked_positions DEFAULT 0,
unbooked_positions INT NOT NULL CONSTRAINT DF_returnsummary_unbooked_positions DEFAULT 0,
stock_quantity FLOAT NOT NULL CONSTRAINT DF_returnsummary_stock_quantity DEFAULT 0,
booked_quantity FLOAT NOT NULL CONSTRAINT DF_returnsummary_booked_quantity DEFAULT 0,
unbooked_quantity FLOAT NOT NULL CONSTRAINT DF_returnsummary_unbooked_quantity DEFAULT 0,
is_booked TINYINT NOT NULL CONSTRAINT DF_returnsummary_is_booked DEFAULT 0,
CONSTRAINT FK_returnsummary_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
CONSTRAINT FK_returnsummary_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
CONSTRAINT FK_returnsummary_supplier FOREIGN KEY (supplier_id) REFERENCES entity(id) ON DELETE SET NULL
);
CREATE INDEX IX_returnsummary_company_date
ON returnsummary (company_id, return_date DESC, manifest_id DESC);
CREATE INDEX IX_returnsummary_company_number
ON returnsummary (company_id, return_number);
CREATE INDEX IX_returnsummary_company_booked
ON returnsummary (company_id, is_booked);
CREATE INDEX IX_returnsummary_company_supplier
ON returnsummary (company_id, supplier_id);
END
`;
export const INIT_RETOURESUMMARY = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'retouresummary')
BEGIN
CREATE TABLE retouresummary (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
manifest_id INT NOT NULL CONSTRAINT UQ_retouresummary_manifest UNIQUE,
retoure_number NVARCHAR(100) NOT NULL,
retoure_date DATETIME2 NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_retouresummary_created_at DEFAULT SYSUTCDATETIME(),
entity_id INT NULL,
customer_name NVARCHAR(255) NULL,
positions INT NOT NULL CONSTRAINT DF_retouresummary_positions DEFAULT 0,
gross_total FLOAT NOT NULL CONSTRAINT DF_retouresummary_gross_total DEFAULT 0,
paid_total FLOAT NOT NULL CONSTRAINT DF_retouresummary_paid_total DEFAULT 0,
ordered_quantity FLOAT NOT NULL CONSTRAINT DF_retouresummary_ordered_quantity DEFAULT 0,
shipped_quantity FLOAT NOT NULL CONSTRAINT DF_retouresummary_shipped_quantity DEFAULT 0,
is_paid TINYINT NOT NULL CONSTRAINT DF_retouresummary_is_paid DEFAULT 0,
is_shipped TINYINT NOT NULL CONSTRAINT DF_retouresummary_is_shipped DEFAULT 0,
CONSTRAINT FK_retouresummary_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
CONSTRAINT FK_retouresummary_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
CONSTRAINT FK_retouresummary_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
);
CREATE INDEX IX_retouresummary_company_date
ON retouresummary (company_id, retoure_date DESC, manifest_id DESC);
CREATE INDEX IX_retouresummary_company_shipped
ON retouresummary (company_id, is_shipped);
CREATE INDEX IX_retouresummary_company_paid
ON retouresummary (company_id, is_paid);
CREATE INDEX IX_retouresummary_company_number
ON retouresummary (company_id, retoure_number);
END
`;
export const INIT_ARTICLEWAREHOUSESUMMARY = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlewarehousesummary')
BEGIN
CREATE TABLE articlewarehousesummary (
id INT IDENTITY(1,1) PRIMARY KEY,
article_id INT NOT NULL,
warehouse_id INT NOT NULL,
stock FLOAT NOT NULL CONSTRAINT DF_articlewarehousesummary_stock DEFAULT 0,
CONSTRAINT FK_articlewarehousesummary_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
CONSTRAINT FK_articlewarehousesummary_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE CASCADE,
CONSTRAINT UQ_articlewarehousesummary_article_warehouse UNIQUE (article_id, warehouse_id)
);
CREATE INDEX IX_articlewarehousesummary_warehouse
ON articlewarehousesummary (warehouse_id);
END
`;
export const INIT_MCP_CLIENT = ` export const INIT_MCP_CLIENT = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_client') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_client')
BEGIN BEGIN
@@ -1298,8 +1649,40 @@ BEGIN
END END
`; `;
export const INIT_SUBAGENTSCHEDULE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'subagentschedule')
BEGIN
CREATE TABLE subagentschedule (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
[desc] NVARCHAR(MAX) NULL,
prompt NVARCHAR(MAX) NOT NULL,
skills NVARCHAR(MAX) NULL,
lastagentnote NVARCHAR(MAX) NULL,
max_itr INT NOT NULL CONSTRAINT DF_subagentschedule_max_itr DEFAULT 15,
cron NVARCHAR(100) NOT NULL,
created_at DATETIME2 NOT NULL CONSTRAINT DF_subagentschedule_created_at DEFAULT SYSUTCDATETIME(),
lastrun_at DATETIME2 NULL,
CONSTRAINT FK_subagentschedule_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
CREATE INDEX IX_subagentschedule_company ON subagentschedule (company_id);
END
`;
export const INIT_HEALTHCHECK = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'healthcheck')
BEGIN
CREATE TABLE healthcheck (
id INT IDENTITY(1,1) PRIMARY KEY,
[sequence] BIGINT NOT NULL CONSTRAINT DF_healthcheck_sequence DEFAULT 0,
updated_at DATETIME2 NOT NULL CONSTRAINT DF_healthcheck_updated_at DEFAULT SYSUTCDATETIME()
);
END
`;
/** DDL scripts in dependency order for schema init. */ /** DDL scripts in dependency order for schema init. */
export const SCHEMA_SCRIPTS = [ export const SCHEMA_SCRIPTS = [
INIT_HEALTHCHECK,
INIT_META, INIT_META,
INIT_LANGUAGE, INIT_LANGUAGE,
INIT_TAX, INIT_TAX,
@@ -1346,28 +1729,40 @@ export const SCHEMA_SCRIPTS = [
INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
INIT_PEEK_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, INIT_PEEK_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
INIT_COMPANYUSER, INIT_COMPANYUSER,
INIT_AUTOINVITE,
INIT_MODELCONVERSATION, INIT_MODELCONVERSATION,
INIT_MODELMESSAGE, INIT_MODELMESSAGE,
INIT_MODELVFS, INIT_MODELVFS,
INIT_ARTICLEHISTORY, INIT_ARTICLEHISTORY,
INIT_ARTICLETOMBSTONE,
INIT_ENTITYHISTORY, INIT_ENTITYHISTORY,
INIT_COMPANYHISTORY, INIT_COMPANYHISTORY,
INIT_COMPANYSETTING, INIT_COMPANYSETTING,
INIT_POSTERMINAL, INIT_POSTERMINAL,
INIT_POSTERMINALCATEGORY, INIT_POSTERMINALCATEGORY,
INIT_POSTERMINALOBITUARY,
INIT_POSTERMINALSYNCQUEUE,
INIT_PRINTER, INIT_PRINTER,
INIT_PRINTERAGENT,
INIT_AGENTPRINTER,
INIT_PRINTERJOB,
INIT_LABELTEMPLATE, INIT_LABELTEMPLATE,
INIT_FILE, INIT_FILE,
INIT_MANIFESTATTACHMENT, INIT_MANIFESTATTACHMENT,
INIT_MANIFESTATTACHMENT_TRIGGER, INIT_MANIFESTATTACHMENT_TRIGGER,
INIT_ACCOUNT, INIT_ACCOUNT,
INIT_PAYMENT, INIT_PAYMENT,
INIT_CARRIER,
INIT_SHIPPING, INIT_SHIPPING,
INIT_PACKAGE, INIT_PACKAGE,
INIT_ORDERSUMMARY, INIT_ORDERSUMMARY,
INIT_QUOTESUMMARY, INIT_QUOTESUMMARY,
INIT_SHIPMENTSUMMARY,
INIT_DELIVERYSUMMARY,
INIT_RETURNSUMMARY,
INIT_RETOURESUMMARY,
INIT_ARTICLEWAREHOUSESUMMARY,
INIT_MCP_CLIENT, INIT_MCP_CLIENT,
INIT_MCP_AUTH_CODE, INIT_MCP_AUTH_CODE,
INIT_MCP_TOKEN, INIT_MCP_TOKEN,
INIT_SUBAGENTSCHEDULE,
]; ];

View File

@@ -0,0 +1,17 @@
/**
* Schema 1 → 2 (MSSQL): posterminal gets `sort`, `enabled`, `clientversion`.
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
*/
export default async function upgrade({ query, assertVersion, setVersion }) {
await assertVersion(1);
await query(`
ALTER TABLE posterminal ADD
sort INT NOT NULL CONSTRAINT DF_posterminal_sort DEFAULT 0,
enabled BIT NOT NULL CONSTRAINT DF_posterminal_enabled DEFAULT 1,
clientversion NVARCHAR(255) NULL;
`);
await setVersion(2);
}

View File

@@ -0,0 +1,20 @@
import { INIT_PRINTERAGENT } from '../schema.js';
/**
* Schema 2 → 3 (MSSQL): adds the `printeragent` table.
* @param {{ query: (sql: string, params?: object) => Promise<unknown>, assertVersion: (v: number) => Promise<void>, setVersion: (v: number) => Promise<void> }} ctx
*/
export default async function upgrade({ query, assertVersion, setVersion }) {
await assertVersion(2);
await query(INIT_PRINTERAGENT);
await query(`
IF OBJECT_ID(N'dbo.printeragent', N'U') IS NOT NULL AND COL_LENGTH('printeragent', 'sort') IS NULL
BEGIN
ALTER TABLE printeragent ADD sort INT NOT NULL CONSTRAINT DF_printeragent_sort DEFAULT 0;
END
`);
await setVersion(3);
}

Some files were not shown because too many files have changed in this diff Show More